blob: 72782a3e8c8c81ac2695ba6ac5977e91572c42a5 [file] [log] [blame]
/*
* Copyright (C) 2021 CESNET, https://photonics.cesnet.cz/
*
* Written by Václav Kubernát <kubernat@cesnet.cz>
*
*/
#include "trompeloeil_doctest.h"
#include <sysrepo-cpp/utils/exception.hpp>
#include "fs-helpers/FileInjector.h"
#include "fs-helpers/utils.h"
#include "mock/system.h"
#include "pretty_printers.h"
#include "system/Authentication.h"
#include "system_vars.h"
#include "test_log_setup.h"
#include "test_sysrepo_helpers.h"
#include "tests/configure.cmake.h"
#include "utils/io.h"
#include "utils/libyang.h"
using namespace std::string_literals;
TEST_CASE("Authentication")
{
FakeAuthentication mock;
trompeloeil::sequence seq1;
TEST_INIT_LOGS;
TEST_SYSREPO_INIT;
TEST_SYSREPO_INIT_CLIENT;
std::filesystem::path testDir = CMAKE_CURRENT_BINARY_DIR "/tests/authentication"s;
removeDirectoryTreeIfExists(testDir);
std::filesystem::create_directory(testDir);
std::filesystem::create_directory(testDir / "authorized_keys");
std::string authorized_keys_format = testDir / "authorized_keys/{USER}";
std::string etc_passwd = testDir / "etc_passwd";
std::string etc_shadow = testDir / "etc_shadow";
velia::system::Authentication auth(srSess, etc_passwd, etc_shadow, authorized_keys_format, [&mock] (const auto& user, const auto& password, const auto& etc_shadow) { mock.changePassword(user, password, etc_shadow); });
client.switchDatastore(sysrepo::Datastore::Operational);
FileInjector passwd(etc_passwd, std::filesystem::perms::owner_read,
"root:x:0:0::/root:/bin/bash\n"
"ci:x:1000:1000::/home/ci:/bin/bash\n"
);
FileInjector shadow(etc_shadow, std::filesystem::perms::owner_read,
"root::18514::::::\n"
"ci::20000::::::\n"
);
using velia::system::User;
SECTION("list users")
{
FileInjector rootKeys(testDir / "authorized_keys/root", std::filesystem::perms::owner_read,
"ssh-rsa SOME_KEY comment"
);
auto data = dataFromSysrepo(client, "/czechlight-system:authentication/users");
decltype(data) expected = {
{"[name='ci']", ""},
{"[name='ci']/name", "ci"},
{"[name='ci']/password-last-change", "2024-10-04T00:00:00-00:00"},
{"[name='root']", ""},
{"[name='root']/name", "root"},
{"[name='root']/password-last-change", "2020-09-09T00:00:00-00:00"},
{"[name='root']/authorized-keys[index='0']", ""},
{"[name='root']/authorized-keys[index='0']/index", "0"},
{"[name='root']/authorized-keys[index='0']/public-key", "ssh-rsa SOME_KEY comment"}
};
REQUIRE(data == expected);
}
SECTION("Password changes")
{
std::string rpcPath;
std::map<std::string, std::string> input;
std::map<std::string, std::string> expected;
std::unique_ptr<trompeloeil::expectation> expectation;
SECTION("changePassword is successful")
{
rpcPath = "/czechlight-system:authentication/users[name='root']/change-password";
expectation = NAMED_REQUIRE_CALL(mock, changePassword("root", "new-password", etc_shadow));
expected = {
{"/result", "success"}
};
input = {
{"password-cleartext", "new-password"}
};
}
SECTION("changePassword throws")
{
rpcPath = "/czechlight-system:authentication/users[name='root']/change-password";
expectation = NAMED_REQUIRE_CALL(mock, changePassword("root", "new-password", etc_shadow)).THROW(std::runtime_error("Task failed succesfully."));
expected = {
{"/result", "failure"},
{"/message", "Task failed succesfully."}
};
input = {
{"password-cleartext", "new-password"}
};
}
auto output = rpcFromSysrepo(client, rpcPath, input);
REQUIRE(output == expected);
}
SECTION("keys")
{
std::string rpcPath;
std::map<std::string, std::string> input;
std::map<std::string, std::string> expected;
std::filesystem::path fileToCheck;
std::string expectedContents;
FileInjector rootKeys(testDir / "authorized_keys/root", std::filesystem::perms::owner_read | std::filesystem::perms::owner_write,
"ssh-rsa SOME_KEY comment\n"
);
FileInjector ciKeys(testDir / "authorized_keys/ci", std::filesystem::perms::owner_read | std::filesystem::perms::owner_write,
"ssh-rsa ci1 comment\n"
"ssh-rsa ci2 comment\n"
);
SECTION("add a key")
{
SECTION("keyfile directory does not exist")
{
removeDirectoryTreeIfExists(testDir / "authorized_keys");
expectedContents = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDCiBEDq8VmzBcJ23q/5GjUy8Hc18Ib20cxGEdI8McjN66eeCPc8tkeji6KT1mx15UmaJ1y+8S8cPxKi2ycdUyFpuXijDkgpuwbd3XYsOQQvMarNhyzEP7SoK5xhMy0Rcgw0Ep57JMDCEaO/V7+4lK4Mu1e+e+CyR5gVg5anGnROlRElr7h18fqCMf1JNW1tZcK5xyfUqYqnkCMKrjIFCOKqZlSo1UVJaKgWNvMx+snrBAsCUvK4N7uKniDMGt4foJBfSNQ60T1UWREbeK5B/dRnmuWJB2P43oWZB0aeEbiBpM/kGh6TE22SmTutpAk/bsgfGd6TKyOuyhkyjITbixo3F5QJ7an8LtF4Uau8CLCs14lRORBeI7a5RpZnfD/TJJ+OvpDm1LKJO3ZlILO0achrkUT1O2urM4tc6O7Fik2QjGUC9QkL4AHXIDDGjpg1or56zoR8W9Tmng6/2+8SGm4n/qxtfoifYyxqPJVUya0zwmAjkoyofoyBtrktzlH4qk= comment\n";
}
SECTION("keyfile directory exists")
{
expectedContents = "ssh-rsa SOME_KEY comment\n"
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDCiBEDq8VmzBcJ23q/5GjUy8Hc18Ib20cxGEdI8McjN66eeCPc8tkeji6KT1mx15UmaJ1y+8S8cPxKi2ycdUyFpuXijDkgpuwbd3XYsOQQvMarNhyzEP7SoK5xhMy0Rcgw0Ep57JMDCEaO/V7+4lK4Mu1e+e+CyR5gVg5anGnROlRElr7h18fqCMf1JNW1tZcK5xyfUqYqnkCMKrjIFCOKqZlSo1UVJaKgWNvMx+snrBAsCUvK4N7uKniDMGt4foJBfSNQ60T1UWREbeK5B/dRnmuWJB2P43oWZB0aeEbiBpM/kGh6TE22SmTutpAk/bsgfGd6TKyOuyhkyjITbixo3F5QJ7an8LtF4Uau8CLCs14lRORBeI7a5RpZnfD/TJJ+OvpDm1LKJO3ZlILO0achrkUT1O2urM4tc6O7Fik2QjGUC9QkL4AHXIDDGjpg1or56zoR8W9Tmng6/2+8SGm4n/qxtfoifYyxqPJVUya0zwmAjkoyofoyBtrktzlH4qk= comment\n";
}
rpcPath = "/czechlight-system:authentication/users[name='root']/add-authorized-key";
expected = {
{"/result", "success"}
};
input = {
{"key", "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDCiBEDq8VmzBcJ23q/5GjUy8Hc18Ib20cxGEdI8McjN66eeCPc8tkeji6KT1mx15UmaJ1y+8S8cPxKi2ycdUyFpuXijDkgpuwbd3XYsOQQvMarNhyzEP7SoK5xhMy0Rcgw0Ep57JMDCEaO/V7+4lK4Mu1e+e+CyR5gVg5anGnROlRElr7h18fqCMf1JNW1tZcK5xyfUqYqnkCMKrjIFCOKqZlSo1UVJaKgWNvMx+snrBAsCUvK4N7uKniDMGt4foJBfSNQ60T1UWREbeK5B/dRnmuWJB2P43oWZB0aeEbiBpM/kGh6TE22SmTutpAk/bsgfGd6TKyOuyhkyjITbixo3F5QJ7an8LtF4Uau8CLCs14lRORBeI7a5RpZnfD/TJJ+OvpDm1LKJO3ZlILO0achrkUT1O2urM4tc6O7Fik2QjGUC9QkL4AHXIDDGjpg1or56zoR8W9Tmng6/2+8SGm4n/qxtfoifYyxqPJVUya0zwmAjkoyofoyBtrktzlH4qk= comment"}
};
fileToCheck = testDir / "authorized_keys/root";
auto result = rpcFromSysrepo(client, rpcPath, input);
REQUIRE(result == expected);
}
SECTION("adding invalid key")
{
rpcPath = "/czechlight-system:authentication/users[name='root']/add-authorized-key";
expected = {
{"/result", "failure"},
{"/message", "Key is not a valid SSH public key: " SSH_KEYGEN_EXECUTABLE " returned non-zero exit code 255\nssh-rsa INVALID comment"}
};
input = {
{"key", "ssh-rsa INVALID comment"}
};
expectedContents = "ssh-rsa SOME_KEY comment\n";
fileToCheck = testDir / "authorized_keys/root";
auto result = rpcFromSysrepo(client, rpcPath, input);
REQUIRE(result == expected);
}
SECTION("remove key")
{
rpcPath = "/czechlight-system:authentication/users[name='ci']/authorized-keys[index='0']/remove";
expected = {
{"/result", "success"},
};
expectedContents = "ssh-rsa ci2 comment\n";
fileToCheck = testDir / "authorized_keys/ci";
auto result = rpcFromSysrepo(client, rpcPath, input);
REQUIRE(result == expected);
}
SECTION("remove last key")
{
rpcPath = "/czechlight-system:authentication/users[name='root']/authorized-keys[index='0']/remove";
expected = {
{"/result", "failure"},
{"/message", "Can't remove last key."},
};
expectedContents = "ssh-rsa SOME_KEY comment\n";
fileToCheck = testDir / "authorized_keys/root";
auto result = rpcFromSysrepo(client, rpcPath, input);
REQUIRE(result == expected);
}
REQUIRE(velia::utils::readFileToString(fileToCheck) == expectedContents);
}
SECTION("NACM")
{
std::map<std::string, std::string> input;
const std::string prefix = "/czechlight-system:authentication/users[name='ci']";
auto sub = srSess.initNacm();
srSess.switchDatastore(sysrepo::Datastore::Running);
srSess.setItem("/ietf-netconf-acm:nacm/groups/group[name='users']/user-name[.='ci']", std::nullopt);
srSess.setItem("/ietf-netconf-acm:nacm/groups/group[name='tests']/user-name[.='test']", std::nullopt);
srSess.applyChanges();
FileInjector ciKeys(testDir / "authorized_keys/ci", std::filesystem::perms::owner_read | std::filesystem::perms::owner_write, "ssh-rsa ci1 comment\n"
"ssh-rsa ci2 comment\n");
SECTION("current users auth")
{
std::map<std::string, std::string> result;
client.setNacmUser("ci");
SECTION("actions")
{
SECTION("change password")
{
input = {{"password-cleartext", "blah"}};
REQUIRE_CALL(mock, changePassword("ci", "blah", etc_shadow)).IN_SEQUENCE(seq1);
result = rpcFromSysrepo(client, prefix + "/change-password", input);
waitForCompletionAndBitMore(seq1);
}
SECTION("add key")
{
input = {{"key", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAdKwJwhSfuBeve5UfVHm0cx/3Jk81Z5a/iNZadjymwl cement"}};
result = rpcFromSysrepo(client, prefix + "/add-authorized-key", input);
}
SECTION("remove key")
{
result = rpcFromSysrepo(client, prefix + "/authorized-keys[index='0']/remove", {});
}
std::map<std::string, std::string> expected = {{"/result", "success"}};
REQUIRE(result == expected);
}
SECTION("data")
{
auto data = dataFromSysrepo(client, prefix);
REQUIRE(data.contains("/password-last-change"));
data.erase("/password-last-change");
REQUIRE(data == std::map<std::string, std::string>{
{"/authorized-keys[index='0']", ""},
{"/authorized-keys[index='0']/index", "0"},
{"/authorized-keys[index='0']/public-key", "ssh-rsa ci1 comment"},
{"/authorized-keys[index='1']", ""},
{"/authorized-keys[index='1']/index", "1"},
{"/authorized-keys[index='1']/public-key", "ssh-rsa ci2 comment"},
{"/name", "ci"},
});
}
}
SECTION("different user's auth")
{
client.setNacmUser("test");
SECTION("change password")
{
input = {{"password-cleartext", "blah"}};
REQUIRE_THROWS_WITH_AS(rpcFromSysrepo(client, prefix + "/change-password", input),
"Couldn't send RPC: SR_ERR_UNAUTHORIZED\n"
" NACM access denied by \"change-password\" node extension \"default-deny-all\". (SR_ERR_UNAUTHORIZED)\n"
" NETCONF: protocol: access-denied: /czechlight-system:authentication/users[name='ci']/change-password: Executing the operation is denied because \"test\" NACM authorization failed.",
sysrepo::ErrorWithCode);
}
SECTION("add key")
{
input = {{"key", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAdKwJwhSfuBeve5UfVHm0cx/3Jk81Z5a/iNZadjymwl cement"}};
REQUIRE_THROWS_WITH_AS(rpcFromSysrepo(client, prefix + "/add-authorized-key", input),
"Couldn't send RPC: SR_ERR_UNAUTHORIZED\n"
" NACM access denied by \"add-authorized-key\" node extension \"default-deny-all\". (SR_ERR_UNAUTHORIZED)\n"
" NETCONF: protocol: access-denied: /czechlight-system:authentication/users[name='ci']/add-authorized-key: Executing the operation is denied because \"test\" NACM authorization failed.",
sysrepo::ErrorWithCode);
}
SECTION("remove key")
{
REQUIRE_THROWS_WITH_AS(rpcFromSysrepo(client, prefix + "/authorized-keys[index='0']/remove", {}),
"Couldn't send RPC: SR_ERR_UNAUTHORIZED\n"
" NACM access denied by \"remove\" node extension \"default-deny-all\". (SR_ERR_UNAUTHORIZED)\n"
" NETCONF: protocol: access-denied: /czechlight-system:authentication/users[name='ci']/authorized-keys[index='0']/remove: Executing the operation is denied because \"test\" NACM authorization failed.",
sysrepo::ErrorWithCode);
}
SECTION("data")
{
REQUIRE(dataFromSysrepo(client, prefix) == std::map<std::string, std::string>{{"/name", "ci"}});
}
}
}
}