blob: 72782a3e8c8c81ac2695ba6ac5977e91572c42a5 [file] [log] [blame]
Václav Kubernátbabbab92021-01-27 09:25:05 +01001/*
2 * Copyright (C) 2021 CESNET, https://photonics.cesnet.cz/
3 *
4 * Written by Václav Kubernát <kubernat@cesnet.cz>
5 *
6*/
7
8#include "trompeloeil_doctest.h"
Tomáš Pecka24b54832024-01-17 18:10:44 +01009#include <sysrepo-cpp/utils/exception.hpp>
Václav Kubernátbabbab92021-01-27 09:25:05 +010010#include "fs-helpers/FileInjector.h"
11#include "fs-helpers/utils.h"
12#include "mock/system.h"
13#include "pretty_printers.h"
14#include "system/Authentication.h"
15#include "system_vars.h"
16#include "test_log_setup.h"
17#include "test_sysrepo_helpers.h"
18#include "tests/configure.cmake.h"
19#include "utils/io.h"
20#include "utils/libyang.h"
21
22using namespace std::string_literals;
23
24TEST_CASE("Authentication")
25{
26 FakeAuthentication mock;
Tomáš Pecka24b54832024-01-17 18:10:44 +010027 trompeloeil::sequence seq1;
Václav Kubernátbabbab92021-01-27 09:25:05 +010028 TEST_INIT_LOGS;
Tomáš Peckab3f624e2024-01-17 14:07:25 +010029 TEST_SYSREPO_INIT;
30 TEST_SYSREPO_INIT_CLIENT;
Václav Kubernátbabbab92021-01-27 09:25:05 +010031 std::filesystem::path testDir = CMAKE_CURRENT_BINARY_DIR "/tests/authentication"s;
32 removeDirectoryTreeIfExists(testDir);
33 std::filesystem::create_directory(testDir);
34 std::filesystem::create_directory(testDir / "authorized_keys");
Václav Kubernátbabbab92021-01-27 09:25:05 +010035 std::string authorized_keys_format = testDir / "authorized_keys/{USER}";
36 std::string etc_passwd = testDir / "etc_passwd";
37 std::string etc_shadow = testDir / "etc_shadow";
Tomáš Peckad9e741f2021-02-10 15:51:17 +010038 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); });
Václav Kubernátbabbab92021-01-27 09:25:05 +010039
Tomáš Peckab3f624e2024-01-17 14:07:25 +010040 client.switchDatastore(sysrepo::Datastore::Operational);
Václav Kubernátbabbab92021-01-27 09:25:05 +010041
42 FileInjector passwd(etc_passwd, std::filesystem::perms::owner_read,
43 "root:x:0:0::/root:/bin/bash\n"
44 "ci:x:1000:1000::/home/ci:/bin/bash\n"
45
46 );
47 FileInjector shadow(etc_shadow, std::filesystem::perms::owner_read,
48 "root::18514::::::\n"
49 "ci::20000::::::\n"
50 );
51
52 using velia::system::User;
53 SECTION("list users")
54 {
55 FileInjector rootKeys(testDir / "authorized_keys/root", std::filesystem::perms::owner_read,
56 "ssh-rsa SOME_KEY comment"
57 );
Tomáš Peckab3f624e2024-01-17 14:07:25 +010058 auto data = dataFromSysrepo(client, "/czechlight-system:authentication/users");
Václav Kubernátbabbab92021-01-27 09:25:05 +010059 decltype(data) expected = {
Václav Kubernát7efd6d52021-11-09 01:31:11 +010060 {"[name='ci']", ""},
Václav Kubernátbabbab92021-01-27 09:25:05 +010061 {"[name='ci']/name", "ci"},
Václav Kubernát693c7952021-11-12 16:07:18 +010062 {"[name='ci']/password-last-change", "2024-10-04T00:00:00-00:00"},
Václav Kubernát7efd6d52021-11-09 01:31:11 +010063 {"[name='root']", ""},
Václav Kubernátbabbab92021-01-27 09:25:05 +010064 {"[name='root']/name", "root"},
Václav Kubernát693c7952021-11-12 16:07:18 +010065 {"[name='root']/password-last-change", "2020-09-09T00:00:00-00:00"},
Václav Kubernátbabbab92021-01-27 09:25:05 +010066 {"[name='root']/authorized-keys[index='0']", ""},
67 {"[name='root']/authorized-keys[index='0']/index", "0"},
68 {"[name='root']/authorized-keys[index='0']/public-key", "ssh-rsa SOME_KEY comment"}
69
70 };
71 REQUIRE(data == expected);
72 }
73
Tomáš Peckab151d2d2024-01-17 14:16:01 +010074 SECTION("Password changes")
Václav Kubernátbabbab92021-01-27 09:25:05 +010075 {
76 std::string rpcPath;
77 std::map<std::string, std::string> input;
78 std::map<std::string, std::string> expected;
79 std::unique_ptr<trompeloeil::expectation> expectation;
80
Tomáš Peckab151d2d2024-01-17 14:16:01 +010081 SECTION("changePassword is successful")
Václav Kubernátbabbab92021-01-27 09:25:05 +010082 {
Tomáš Peckab151d2d2024-01-17 14:16:01 +010083 rpcPath = "/czechlight-system:authentication/users[name='root']/change-password";
84 expectation = NAMED_REQUIRE_CALL(mock, changePassword("root", "new-password", etc_shadow));
85 expected = {
86 {"/result", "success"}
87 };
88 input = {
89 {"password-cleartext", "new-password"}
90 };
91 }
Václav Kubernátbabbab92021-01-27 09:25:05 +010092
Tomáš Peckab151d2d2024-01-17 14:16:01 +010093 SECTION("changePassword throws")
94 {
95 rpcPath = "/czechlight-system:authentication/users[name='root']/change-password";
96 expectation = NAMED_REQUIRE_CALL(mock, changePassword("root", "new-password", etc_shadow)).THROW(std::runtime_error("Task failed succesfully."));
97 expected = {
98 {"/result", "failure"},
99 {"/message", "Task failed succesfully."}
100 };
101 input = {
102 {"password-cleartext", "new-password"}
103 };
Václav Kubernátbabbab92021-01-27 09:25:05 +0100104 }
105
Tomáš Peckab3f624e2024-01-17 14:07:25 +0100106 auto output = rpcFromSysrepo(client, rpcPath, input);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100107 REQUIRE(output == expected);
108 }
109
110 SECTION("keys")
111 {
112 std::string rpcPath;
113 std::map<std::string, std::string> input;
114 std::map<std::string, std::string> expected;
115
116 std::filesystem::path fileToCheck;
117 std::string expectedContents;
118
119 FileInjector rootKeys(testDir / "authorized_keys/root", std::filesystem::perms::owner_read | std::filesystem::perms::owner_write,
120 "ssh-rsa SOME_KEY comment\n"
121 );
122
123 FileInjector ciKeys(testDir / "authorized_keys/ci", std::filesystem::perms::owner_read | std::filesystem::perms::owner_write,
124 "ssh-rsa ci1 comment\n"
125 "ssh-rsa ci2 comment\n"
126 );
127
128 SECTION("add a key")
129 {
Václav Kubernátb6adbde2021-10-11 22:06:06 +0200130 SECTION("keyfile directory does not exist")
131 {
132 removeDirectoryTreeIfExists(testDir / "authorized_keys");
133 expectedContents = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDCiBEDq8VmzBcJ23q/5GjUy8Hc18Ib20cxGEdI8McjN66eeCPc8tkeji6KT1mx15UmaJ1y+8S8cPxKi2ycdUyFpuXijDkgpuwbd3XYsOQQvMarNhyzEP7SoK5xhMy0Rcgw0Ep57JMDCEaO/V7+4lK4Mu1e+e+CyR5gVg5anGnROlRElr7h18fqCMf1JNW1tZcK5xyfUqYqnkCMKrjIFCOKqZlSo1UVJaKgWNvMx+snrBAsCUvK4N7uKniDMGt4foJBfSNQ60T1UWREbeK5B/dRnmuWJB2P43oWZB0aeEbiBpM/kGh6TE22SmTutpAk/bsgfGd6TKyOuyhkyjITbixo3F5QJ7an8LtF4Uau8CLCs14lRORBeI7a5RpZnfD/TJJ+OvpDm1LKJO3ZlILO0achrkUT1O2urM4tc6O7Fik2QjGUC9QkL4AHXIDDGjpg1or56zoR8W9Tmng6/2+8SGm4n/qxtfoifYyxqPJVUya0zwmAjkoyofoyBtrktzlH4qk= comment\n";
134
135 }
136
137 SECTION("keyfile directory exists")
138 {
139 expectedContents = "ssh-rsa SOME_KEY comment\n"
140 "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDCiBEDq8VmzBcJ23q/5GjUy8Hc18Ib20cxGEdI8McjN66eeCPc8tkeji6KT1mx15UmaJ1y+8S8cPxKi2ycdUyFpuXijDkgpuwbd3XYsOQQvMarNhyzEP7SoK5xhMy0Rcgw0Ep57JMDCEaO/V7+4lK4Mu1e+e+CyR5gVg5anGnROlRElr7h18fqCMf1JNW1tZcK5xyfUqYqnkCMKrjIFCOKqZlSo1UVJaKgWNvMx+snrBAsCUvK4N7uKniDMGt4foJBfSNQ60T1UWREbeK5B/dRnmuWJB2P43oWZB0aeEbiBpM/kGh6TE22SmTutpAk/bsgfGd6TKyOuyhkyjITbixo3F5QJ7an8LtF4Uau8CLCs14lRORBeI7a5RpZnfD/TJJ+OvpDm1LKJO3ZlILO0achrkUT1O2urM4tc6O7Fik2QjGUC9QkL4AHXIDDGjpg1or56zoR8W9Tmng6/2+8SGm4n/qxtfoifYyxqPJVUya0zwmAjkoyofoyBtrktzlH4qk= comment\n";
141 }
142
Václav Kubernátbabbab92021-01-27 09:25:05 +0100143 rpcPath = "/czechlight-system:authentication/users[name='root']/add-authorized-key";
144
145 expected = {
146 {"/result", "success"}
147 };
148 input = {
149 {"key", "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDCiBEDq8VmzBcJ23q/5GjUy8Hc18Ib20cxGEdI8McjN66eeCPc8tkeji6KT1mx15UmaJ1y+8S8cPxKi2ycdUyFpuXijDkgpuwbd3XYsOQQvMarNhyzEP7SoK5xhMy0Rcgw0Ep57JMDCEaO/V7+4lK4Mu1e+e+CyR5gVg5anGnROlRElr7h18fqCMf1JNW1tZcK5xyfUqYqnkCMKrjIFCOKqZlSo1UVJaKgWNvMx+snrBAsCUvK4N7uKniDMGt4foJBfSNQ60T1UWREbeK5B/dRnmuWJB2P43oWZB0aeEbiBpM/kGh6TE22SmTutpAk/bsgfGd6TKyOuyhkyjITbixo3F5QJ7an8LtF4Uau8CLCs14lRORBeI7a5RpZnfD/TJJ+OvpDm1LKJO3ZlILO0achrkUT1O2urM4tc6O7Fik2QjGUC9QkL4AHXIDDGjpg1or56zoR8W9Tmng6/2+8SGm4n/qxtfoifYyxqPJVUya0zwmAjkoyofoyBtrktzlH4qk= comment"}
150 };
151
Václav Kubernátbabbab92021-01-27 09:25:05 +0100152
153 fileToCheck = testDir / "authorized_keys/root";
154
Tomáš Peckab3f624e2024-01-17 14:07:25 +0100155 auto result = rpcFromSysrepo(client, rpcPath, input);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100156 REQUIRE(result == expected);
157 }
158
159 SECTION("adding invalid key")
160 {
161 rpcPath = "/czechlight-system:authentication/users[name='root']/add-authorized-key";
162
163 expected = {
164 {"/result", "failure"},
165 {"/message", "Key is not a valid SSH public key: " SSH_KEYGEN_EXECUTABLE " returned non-zero exit code 255\nssh-rsa INVALID comment"}
166 };
167 input = {
168 {"key", "ssh-rsa INVALID comment"}
169 };
170
171 expectedContents = "ssh-rsa SOME_KEY comment\n";
172
173 fileToCheck = testDir / "authorized_keys/root";
174
Tomáš Peckab3f624e2024-01-17 14:07:25 +0100175 auto result = rpcFromSysrepo(client, rpcPath, input);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100176 REQUIRE(result == expected);
177 }
178
179 SECTION("remove key")
180 {
181 rpcPath = "/czechlight-system:authentication/users[name='ci']/authorized-keys[index='0']/remove";
182
183 expected = {
184 {"/result", "success"},
185 };
186
187 expectedContents = "ssh-rsa ci2 comment\n";
188
189 fileToCheck = testDir / "authorized_keys/ci";
190
Tomáš Peckab3f624e2024-01-17 14:07:25 +0100191 auto result = rpcFromSysrepo(client, rpcPath, input);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100192 REQUIRE(result == expected);
193
194 }
195
196 SECTION("remove last key")
197 {
198 rpcPath = "/czechlight-system:authentication/users[name='root']/authorized-keys[index='0']/remove";
199
200 expected = {
201 {"/result", "failure"},
202 {"/message", "Can't remove last key."},
203 };
204
205 expectedContents = "ssh-rsa SOME_KEY comment\n";
206
207 fileToCheck = testDir / "authorized_keys/root";
208
Tomáš Peckab3f624e2024-01-17 14:07:25 +0100209 auto result = rpcFromSysrepo(client, rpcPath, input);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100210 REQUIRE(result == expected);
211 }
212
213
214 REQUIRE(velia::utils::readFileToString(fileToCheck) == expectedContents);
215 }
216
Tomáš Pecka24b54832024-01-17 18:10:44 +0100217 SECTION("NACM")
218 {
219 std::map<std::string, std::string> input;
220 const std::string prefix = "/czechlight-system:authentication/users[name='ci']";
221
222 auto sub = srSess.initNacm();
223
224 srSess.switchDatastore(sysrepo::Datastore::Running);
225 srSess.setItem("/ietf-netconf-acm:nacm/groups/group[name='users']/user-name[.='ci']", std::nullopt);
226 srSess.setItem("/ietf-netconf-acm:nacm/groups/group[name='tests']/user-name[.='test']", std::nullopt);
227 srSess.applyChanges();
228
229 FileInjector ciKeys(testDir / "authorized_keys/ci", std::filesystem::perms::owner_read | std::filesystem::perms::owner_write, "ssh-rsa ci1 comment\n"
230 "ssh-rsa ci2 comment\n");
231
232 SECTION("current users auth")
233 {
234 std::map<std::string, std::string> result;
235 client.setNacmUser("ci");
236
Tomáš Pecka3414e432024-01-17 19:11:39 +0100237 SECTION("actions")
Tomáš Pecka24b54832024-01-17 18:10:44 +0100238 {
Tomáš Pecka3414e432024-01-17 19:11:39 +0100239 SECTION("change password")
240 {
241 input = {{"password-cleartext", "blah"}};
242 REQUIRE_CALL(mock, changePassword("ci", "blah", etc_shadow)).IN_SEQUENCE(seq1);
243 result = rpcFromSysrepo(client, prefix + "/change-password", input);
244 waitForCompletionAndBitMore(seq1);
245 }
246
247 SECTION("add key")
248 {
249 input = {{"key", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAdKwJwhSfuBeve5UfVHm0cx/3Jk81Z5a/iNZadjymwl cement"}};
250 result = rpcFromSysrepo(client, prefix + "/add-authorized-key", input);
251 }
252
253 SECTION("remove key")
254 {
255 result = rpcFromSysrepo(client, prefix + "/authorized-keys[index='0']/remove", {});
256 }
257
258 std::map<std::string, std::string> expected = {{"/result", "success"}};
259 REQUIRE(result == expected);
Tomáš Pecka24b54832024-01-17 18:10:44 +0100260 }
261
Tomáš Pecka3414e432024-01-17 19:11:39 +0100262 SECTION("data")
Tomáš Pecka24b54832024-01-17 18:10:44 +0100263 {
Tomáš Pecka3414e432024-01-17 19:11:39 +0100264 auto data = dataFromSysrepo(client, prefix);
265 REQUIRE(data.contains("/password-last-change"));
266 data.erase("/password-last-change");
267 REQUIRE(data == std::map<std::string, std::string>{
268 {"/authorized-keys[index='0']", ""},
269 {"/authorized-keys[index='0']/index", "0"},
270 {"/authorized-keys[index='0']/public-key", "ssh-rsa ci1 comment"},
271 {"/authorized-keys[index='1']", ""},
272 {"/authorized-keys[index='1']/index", "1"},
273 {"/authorized-keys[index='1']/public-key", "ssh-rsa ci2 comment"},
274 {"/name", "ci"},
275 });
Tomáš Pecka24b54832024-01-17 18:10:44 +0100276 }
Tomáš Pecka24b54832024-01-17 18:10:44 +0100277 }
278
279 SECTION("different user's auth")
280 {
281 client.setNacmUser("test");
282
283 SECTION("change password")
284 {
285 input = {{"password-cleartext", "blah"}};
286 REQUIRE_THROWS_WITH_AS(rpcFromSysrepo(client, prefix + "/change-password", input),
287 "Couldn't send RPC: SR_ERR_UNAUTHORIZED\n"
288 " NACM access denied by \"change-password\" node extension \"default-deny-all\". (SR_ERR_UNAUTHORIZED)\n"
289 " NETCONF: protocol: access-denied: /czechlight-system:authentication/users[name='ci']/change-password: Executing the operation is denied because \"test\" NACM authorization failed.",
290 sysrepo::ErrorWithCode);
291 }
292
293 SECTION("add key")
294 {
295 input = {{"key", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAdKwJwhSfuBeve5UfVHm0cx/3Jk81Z5a/iNZadjymwl cement"}};
296 REQUIRE_THROWS_WITH_AS(rpcFromSysrepo(client, prefix + "/add-authorized-key", input),
297 "Couldn't send RPC: SR_ERR_UNAUTHORIZED\n"
298 " NACM access denied by \"add-authorized-key\" node extension \"default-deny-all\". (SR_ERR_UNAUTHORIZED)\n"
299 " NETCONF: protocol: access-denied: /czechlight-system:authentication/users[name='ci']/add-authorized-key: Executing the operation is denied because \"test\" NACM authorization failed.",
300 sysrepo::ErrorWithCode);
301 }
302
303 SECTION("remove key")
304 {
305 REQUIRE_THROWS_WITH_AS(rpcFromSysrepo(client, prefix + "/authorized-keys[index='0']/remove", {}),
306 "Couldn't send RPC: SR_ERR_UNAUTHORIZED\n"
307 " NACM access denied by \"remove\" node extension \"default-deny-all\". (SR_ERR_UNAUTHORIZED)\n"
308 " 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.",
309 sysrepo::ErrorWithCode);
310 }
Tomáš Pecka3414e432024-01-17 19:11:39 +0100311
312 SECTION("data")
313 {
314 REQUIRE(dataFromSysrepo(client, prefix) == std::map<std::string, std::string>{{"/name", "ci"}});
315 }
Tomáš Pecka24b54832024-01-17 18:10:44 +0100316 }
317 }
Václav Kubernátbabbab92021-01-27 09:25:05 +0100318}