blob: 15ed3889e2cca2453064813f8c001055431d46cc [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áš Pecka72f540e2024-07-08 13:45:22 +02009#include <chrono>
10#include <libyang-cpp/Time.hpp>
Tomáš Pecka24b54832024-01-17 18:10:44 +010011#include <sysrepo-cpp/utils/exception.hpp>
Václav Kubernátbabbab92021-01-27 09:25:05 +010012#include "fs-helpers/FileInjector.h"
13#include "fs-helpers/utils.h"
14#include "mock/system.h"
15#include "pretty_printers.h"
16#include "system/Authentication.h"
17#include "system_vars.h"
18#include "test_log_setup.h"
Václav Kubernátbabbab92021-01-27 09:25:05 +010019#include "tests/configure.cmake.h"
Tomáš Peckac164ca62024-01-24 13:38:03 +010020#include "tests/sysrepo-helpers/common.h"
Václav Kubernátbabbab92021-01-27 09:25:05 +010021#include "utils/io.h"
22#include "utils/libyang.h"
23
24using namespace std::string_literals;
25
26TEST_CASE("Authentication")
27{
28 FakeAuthentication mock;
Tomáš Pecka24b54832024-01-17 18:10:44 +010029 trompeloeil::sequence seq1;
Václav Kubernátbabbab92021-01-27 09:25:05 +010030 TEST_INIT_LOGS;
Tomáš Peckab3f624e2024-01-17 14:07:25 +010031 TEST_SYSREPO_INIT;
32 TEST_SYSREPO_INIT_CLIENT;
Václav Kubernátbabbab92021-01-27 09:25:05 +010033 std::filesystem::path testDir = CMAKE_CURRENT_BINARY_DIR "/tests/authentication"s;
34 removeDirectoryTreeIfExists(testDir);
35 std::filesystem::create_directory(testDir);
36 std::filesystem::create_directory(testDir / "authorized_keys");
Václav Kubernátbabbab92021-01-27 09:25:05 +010037 std::string authorized_keys_format = testDir / "authorized_keys/{USER}";
38 std::string etc_passwd = testDir / "etc_passwd";
39 std::string etc_shadow = testDir / "etc_shadow";
Tomáš Peckad9e741f2021-02-10 15:51:17 +010040 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 +010041
Tomáš Peckab3f624e2024-01-17 14:07:25 +010042 client.switchDatastore(sysrepo::Datastore::Operational);
Václav Kubernátbabbab92021-01-27 09:25:05 +010043
44 FileInjector passwd(etc_passwd, std::filesystem::perms::owner_read,
45 "root:x:0:0::/root:/bin/bash\n"
46 "ci:x:1000:1000::/home/ci:/bin/bash\n"
47
48 );
49 FileInjector shadow(etc_shadow, std::filesystem::perms::owner_read,
50 "root::18514::::::\n"
51 "ci::20000::::::\n"
52 );
53
54 using velia::system::User;
55 SECTION("list users")
56 {
57 FileInjector rootKeys(testDir / "authorized_keys/root", std::filesystem::perms::owner_read,
58 "ssh-rsa SOME_KEY comment"
59 );
Tomáš Pecka72f540e2024-07-08 13:45:22 +020060
61 using namespace std::chrono;
Tomáš Peckab3f624e2024-01-17 14:07:25 +010062 auto data = dataFromSysrepo(client, "/czechlight-system:authentication/users");
Václav Kubernátbabbab92021-01-27 09:25:05 +010063 decltype(data) expected = {
Václav Kubernát7efd6d52021-11-09 01:31:11 +010064 {"[name='ci']", ""},
Václav Kubernátbabbab92021-01-27 09:25:05 +010065 {"[name='ci']/name", "ci"},
Tomáš Pecka72f540e2024-07-08 13:45:22 +020066 {"[name='ci']/password-last-change", libyang::yangTimeFormat(sys_days(2024y/October/4), libyang::TimezoneInterpretation::Local)},
Václav Kubernát7efd6d52021-11-09 01:31:11 +010067 {"[name='root']", ""},
Václav Kubernátbabbab92021-01-27 09:25:05 +010068 {"[name='root']/name", "root"},
Tomáš Pecka72f540e2024-07-08 13:45:22 +020069 {"[name='root']/password-last-change", libyang::yangTimeFormat(sys_days(2020y/September/9), libyang::TimezoneInterpretation::Local)},
Václav Kubernátbabbab92021-01-27 09:25:05 +010070 {"[name='root']/authorized-keys[index='0']", ""},
71 {"[name='root']/authorized-keys[index='0']/index", "0"},
72 {"[name='root']/authorized-keys[index='0']/public-key", "ssh-rsa SOME_KEY comment"}
73
74 };
75 REQUIRE(data == expected);
76 }
77
Tomáš Peckab151d2d2024-01-17 14:16:01 +010078 SECTION("Password changes")
Václav Kubernátbabbab92021-01-27 09:25:05 +010079 {
80 std::string rpcPath;
81 std::map<std::string, std::string> input;
82 std::map<std::string, std::string> expected;
83 std::unique_ptr<trompeloeil::expectation> expectation;
84
Tomáš Peckab151d2d2024-01-17 14:16:01 +010085 SECTION("changePassword is successful")
Václav Kubernátbabbab92021-01-27 09:25:05 +010086 {
Tomáš Peckab151d2d2024-01-17 14:16:01 +010087 rpcPath = "/czechlight-system:authentication/users[name='root']/change-password";
88 expectation = NAMED_REQUIRE_CALL(mock, changePassword("root", "new-password", etc_shadow));
89 expected = {
90 {"/result", "success"}
91 };
92 input = {
93 {"password-cleartext", "new-password"}
94 };
95 }
Václav Kubernátbabbab92021-01-27 09:25:05 +010096
Tomáš Peckab151d2d2024-01-17 14:16:01 +010097 SECTION("changePassword throws")
98 {
99 rpcPath = "/czechlight-system:authentication/users[name='root']/change-password";
100 expectation = NAMED_REQUIRE_CALL(mock, changePassword("root", "new-password", etc_shadow)).THROW(std::runtime_error("Task failed succesfully."));
101 expected = {
102 {"/result", "failure"},
103 {"/message", "Task failed succesfully."}
104 };
105 input = {
106 {"password-cleartext", "new-password"}
107 };
Václav Kubernátbabbab92021-01-27 09:25:05 +0100108 }
109
Tomáš Peckab3f624e2024-01-17 14:07:25 +0100110 auto output = rpcFromSysrepo(client, rpcPath, input);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100111 REQUIRE(output == expected);
112 }
113
114 SECTION("keys")
115 {
116 std::string rpcPath;
117 std::map<std::string, std::string> input;
118 std::map<std::string, std::string> expected;
119
120 std::filesystem::path fileToCheck;
121 std::string expectedContents;
122
123 FileInjector rootKeys(testDir / "authorized_keys/root", std::filesystem::perms::owner_read | std::filesystem::perms::owner_write,
124 "ssh-rsa SOME_KEY comment\n"
125 );
126
127 FileInjector ciKeys(testDir / "authorized_keys/ci", std::filesystem::perms::owner_read | std::filesystem::perms::owner_write,
128 "ssh-rsa ci1 comment\n"
129 "ssh-rsa ci2 comment\n"
130 );
131
132 SECTION("add a key")
133 {
Václav Kubernátb6adbde2021-10-11 22:06:06 +0200134 SECTION("keyfile directory does not exist")
135 {
136 removeDirectoryTreeIfExists(testDir / "authorized_keys");
137 expectedContents = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDCiBEDq8VmzBcJ23q/5GjUy8Hc18Ib20cxGEdI8McjN66eeCPc8tkeji6KT1mx15UmaJ1y+8S8cPxKi2ycdUyFpuXijDkgpuwbd3XYsOQQvMarNhyzEP7SoK5xhMy0Rcgw0Ep57JMDCEaO/V7+4lK4Mu1e+e+CyR5gVg5anGnROlRElr7h18fqCMf1JNW1tZcK5xyfUqYqnkCMKrjIFCOKqZlSo1UVJaKgWNvMx+snrBAsCUvK4N7uKniDMGt4foJBfSNQ60T1UWREbeK5B/dRnmuWJB2P43oWZB0aeEbiBpM/kGh6TE22SmTutpAk/bsgfGd6TKyOuyhkyjITbixo3F5QJ7an8LtF4Uau8CLCs14lRORBeI7a5RpZnfD/TJJ+OvpDm1LKJO3ZlILO0achrkUT1O2urM4tc6O7Fik2QjGUC9QkL4AHXIDDGjpg1or56zoR8W9Tmng6/2+8SGm4n/qxtfoifYyxqPJVUya0zwmAjkoyofoyBtrktzlH4qk= comment\n";
138
139 }
140
141 SECTION("keyfile directory exists")
142 {
143 expectedContents = "ssh-rsa SOME_KEY comment\n"
144 "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDCiBEDq8VmzBcJ23q/5GjUy8Hc18Ib20cxGEdI8McjN66eeCPc8tkeji6KT1mx15UmaJ1y+8S8cPxKi2ycdUyFpuXijDkgpuwbd3XYsOQQvMarNhyzEP7SoK5xhMy0Rcgw0Ep57JMDCEaO/V7+4lK4Mu1e+e+CyR5gVg5anGnROlRElr7h18fqCMf1JNW1tZcK5xyfUqYqnkCMKrjIFCOKqZlSo1UVJaKgWNvMx+snrBAsCUvK4N7uKniDMGt4foJBfSNQ60T1UWREbeK5B/dRnmuWJB2P43oWZB0aeEbiBpM/kGh6TE22SmTutpAk/bsgfGd6TKyOuyhkyjITbixo3F5QJ7an8LtF4Uau8CLCs14lRORBeI7a5RpZnfD/TJJ+OvpDm1LKJO3ZlILO0achrkUT1O2urM4tc6O7Fik2QjGUC9QkL4AHXIDDGjpg1or56zoR8W9Tmng6/2+8SGm4n/qxtfoifYyxqPJVUya0zwmAjkoyofoyBtrktzlH4qk= comment\n";
145 }
146
Václav Kubernátbabbab92021-01-27 09:25:05 +0100147 rpcPath = "/czechlight-system:authentication/users[name='root']/add-authorized-key";
148
149 expected = {
150 {"/result", "success"}
151 };
152 input = {
153 {"key", "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDCiBEDq8VmzBcJ23q/5GjUy8Hc18Ib20cxGEdI8McjN66eeCPc8tkeji6KT1mx15UmaJ1y+8S8cPxKi2ycdUyFpuXijDkgpuwbd3XYsOQQvMarNhyzEP7SoK5xhMy0Rcgw0Ep57JMDCEaO/V7+4lK4Mu1e+e+CyR5gVg5anGnROlRElr7h18fqCMf1JNW1tZcK5xyfUqYqnkCMKrjIFCOKqZlSo1UVJaKgWNvMx+snrBAsCUvK4N7uKniDMGt4foJBfSNQ60T1UWREbeK5B/dRnmuWJB2P43oWZB0aeEbiBpM/kGh6TE22SmTutpAk/bsgfGd6TKyOuyhkyjITbixo3F5QJ7an8LtF4Uau8CLCs14lRORBeI7a5RpZnfD/TJJ+OvpDm1LKJO3ZlILO0achrkUT1O2urM4tc6O7Fik2QjGUC9QkL4AHXIDDGjpg1or56zoR8W9Tmng6/2+8SGm4n/qxtfoifYyxqPJVUya0zwmAjkoyofoyBtrktzlH4qk= comment"}
154 };
155
Václav Kubernátbabbab92021-01-27 09:25:05 +0100156
157 fileToCheck = testDir / "authorized_keys/root";
158
Tomáš Peckab3f624e2024-01-17 14:07:25 +0100159 auto result = rpcFromSysrepo(client, rpcPath, input);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100160 REQUIRE(result == expected);
161 }
162
163 SECTION("adding invalid key")
164 {
165 rpcPath = "/czechlight-system:authentication/users[name='root']/add-authorized-key";
166
167 expected = {
168 {"/result", "failure"},
169 {"/message", "Key is not a valid SSH public key: " SSH_KEYGEN_EXECUTABLE " returned non-zero exit code 255\nssh-rsa INVALID comment"}
170 };
171 input = {
172 {"key", "ssh-rsa INVALID comment"}
173 };
174
175 expectedContents = "ssh-rsa SOME_KEY comment\n";
176
177 fileToCheck = testDir / "authorized_keys/root";
178
Tomáš Peckab3f624e2024-01-17 14:07:25 +0100179 auto result = rpcFromSysrepo(client, rpcPath, input);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100180 REQUIRE(result == expected);
181 }
182
183 SECTION("remove key")
184 {
185 rpcPath = "/czechlight-system:authentication/users[name='ci']/authorized-keys[index='0']/remove";
186
187 expected = {
188 {"/result", "success"},
189 };
190
191 expectedContents = "ssh-rsa ci2 comment\n";
192
193 fileToCheck = testDir / "authorized_keys/ci";
194
Tomáš Peckab3f624e2024-01-17 14:07:25 +0100195 auto result = rpcFromSysrepo(client, rpcPath, input);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100196 REQUIRE(result == expected);
197
198 }
199
200 SECTION("remove last key")
201 {
202 rpcPath = "/czechlight-system:authentication/users[name='root']/authorized-keys[index='0']/remove";
203
204 expected = {
205 {"/result", "failure"},
206 {"/message", "Can't remove last key."},
207 };
208
209 expectedContents = "ssh-rsa SOME_KEY comment\n";
210
211 fileToCheck = testDir / "authorized_keys/root";
212
Tomáš Peckab3f624e2024-01-17 14:07:25 +0100213 auto result = rpcFromSysrepo(client, rpcPath, input);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100214 REQUIRE(result == expected);
215 }
216
217
218 REQUIRE(velia::utils::readFileToString(fileToCheck) == expectedContents);
219 }
220
Tomáš Pecka24b54832024-01-17 18:10:44 +0100221 SECTION("NACM")
222 {
223 std::map<std::string, std::string> input;
224 const std::string prefix = "/czechlight-system:authentication/users[name='ci']";
225
226 auto sub = srSess.initNacm();
227
228 srSess.switchDatastore(sysrepo::Datastore::Running);
229 srSess.setItem("/ietf-netconf-acm:nacm/groups/group[name='users']/user-name[.='ci']", std::nullopt);
230 srSess.setItem("/ietf-netconf-acm:nacm/groups/group[name='tests']/user-name[.='test']", std::nullopt);
231 srSess.applyChanges();
232
233 FileInjector ciKeys(testDir / "authorized_keys/ci", std::filesystem::perms::owner_read | std::filesystem::perms::owner_write, "ssh-rsa ci1 comment\n"
234 "ssh-rsa ci2 comment\n");
235
236 SECTION("current users auth")
237 {
238 std::map<std::string, std::string> result;
239 client.setNacmUser("ci");
240
Tomáš Pecka3414e432024-01-17 19:11:39 +0100241 SECTION("actions")
Tomáš Pecka24b54832024-01-17 18:10:44 +0100242 {
Tomáš Pecka3414e432024-01-17 19:11:39 +0100243 SECTION("change password")
244 {
245 input = {{"password-cleartext", "blah"}};
246 REQUIRE_CALL(mock, changePassword("ci", "blah", etc_shadow)).IN_SEQUENCE(seq1);
247 result = rpcFromSysrepo(client, prefix + "/change-password", input);
248 waitForCompletionAndBitMore(seq1);
249 }
250
251 SECTION("add key")
252 {
253 input = {{"key", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAdKwJwhSfuBeve5UfVHm0cx/3Jk81Z5a/iNZadjymwl cement"}};
254 result = rpcFromSysrepo(client, prefix + "/add-authorized-key", input);
255 }
256
257 SECTION("remove key")
258 {
259 result = rpcFromSysrepo(client, prefix + "/authorized-keys[index='0']/remove", {});
260 }
261
262 std::map<std::string, std::string> expected = {{"/result", "success"}};
263 REQUIRE(result == expected);
Tomáš Pecka24b54832024-01-17 18:10:44 +0100264 }
265
Tomáš Pecka3414e432024-01-17 19:11:39 +0100266 SECTION("data")
Tomáš Pecka24b54832024-01-17 18:10:44 +0100267 {
Tomáš Pecka3414e432024-01-17 19:11:39 +0100268 auto data = dataFromSysrepo(client, prefix);
269 REQUIRE(data.contains("/password-last-change"));
270 data.erase("/password-last-change");
271 REQUIRE(data == std::map<std::string, std::string>{
272 {"/authorized-keys[index='0']", ""},
273 {"/authorized-keys[index='0']/index", "0"},
274 {"/authorized-keys[index='0']/public-key", "ssh-rsa ci1 comment"},
275 {"/authorized-keys[index='1']", ""},
276 {"/authorized-keys[index='1']/index", "1"},
277 {"/authorized-keys[index='1']/public-key", "ssh-rsa ci2 comment"},
278 {"/name", "ci"},
279 });
Tomáš Pecka24b54832024-01-17 18:10:44 +0100280 }
Tomáš Pecka24b54832024-01-17 18:10:44 +0100281 }
282
283 SECTION("different user's auth")
284 {
285 client.setNacmUser("test");
286
287 SECTION("change password")
288 {
289 input = {{"password-cleartext", "blah"}};
290 REQUIRE_THROWS_WITH_AS(rpcFromSysrepo(client, prefix + "/change-password", input),
291 "Couldn't send RPC: SR_ERR_UNAUTHORIZED\n"
Jan Kundrát4a10d0a2024-04-10 03:33:43 +0200292 " Executing the operation is denied because \"test\" NACM authorization failed. (SR_ERR_UNAUTHORIZED)\n"
Tomáš Pecka24b54832024-01-17 18:10:44 +0100293 " NETCONF: protocol: access-denied: /czechlight-system:authentication/users[name='ci']/change-password: Executing the operation is denied because \"test\" NACM authorization failed.",
294 sysrepo::ErrorWithCode);
295 }
296
297 SECTION("add key")
298 {
299 input = {{"key", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAdKwJwhSfuBeve5UfVHm0cx/3Jk81Z5a/iNZadjymwl cement"}};
300 REQUIRE_THROWS_WITH_AS(rpcFromSysrepo(client, prefix + "/add-authorized-key", input),
301 "Couldn't send RPC: SR_ERR_UNAUTHORIZED\n"
Jan Kundrát4a10d0a2024-04-10 03:33:43 +0200302 " Executing the operation is denied because \"test\" NACM authorization failed. (SR_ERR_UNAUTHORIZED)\n"
Tomáš Pecka24b54832024-01-17 18:10:44 +0100303 " NETCONF: protocol: access-denied: /czechlight-system:authentication/users[name='ci']/add-authorized-key: Executing the operation is denied because \"test\" NACM authorization failed.",
304 sysrepo::ErrorWithCode);
305 }
306
307 SECTION("remove key")
308 {
309 REQUIRE_THROWS_WITH_AS(rpcFromSysrepo(client, prefix + "/authorized-keys[index='0']/remove", {}),
310 "Couldn't send RPC: SR_ERR_UNAUTHORIZED\n"
Jan Kundrát4a10d0a2024-04-10 03:33:43 +0200311 " Executing the operation is denied because \"test\" NACM authorization failed. (SR_ERR_UNAUTHORIZED)\n"
Tomáš Pecka24b54832024-01-17 18:10:44 +0100312 " 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.",
313 sysrepo::ErrorWithCode);
314 }
Tomáš Pecka3414e432024-01-17 19:11:39 +0100315
316 SECTION("data")
317 {
318 REQUIRE(dataFromSysrepo(client, prefix) == std::map<std::string, std::string>{{"/name", "ci"}});
319 }
Tomáš Pecka24b54832024-01-17 18:10:44 +0100320 }
321 }
Václav Kubernátbabbab92021-01-27 09:25:05 +0100322}