1import ./make-test-python.nix (
2 { pkgs, ... }:
3 let
4 certs = import ./common/acme/server/snakeoil-certs.nix;
5 serverDomain = certs.domain;
6
7 # copy certs to store to work around mount namespacing
8 certsPath = pkgs.runCommandNoCC "snakeoil-certs" { } ''
9 mkdir $out
10 cp ${certs."${serverDomain}".cert} $out/snakeoil.crt
11 cp ${certs."${serverDomain}".key} $out/snakeoil.key
12 '';
13
14 provisionAdminPassword = "very-strong-password-for-admin";
15 provisionIdmAdminPassword = "very-strong-password-for-idm-admin";
16 provisionIdmAdminPassword2 = "very-strong-alternative-password-for-idm-admin";
17 in
18 {
19 name = "kanidm-provisioning";
20 meta.maintainers = with pkgs.lib.maintainers; [ oddlama ];
21
22 nodes.provision =
23 { pkgs, lib, ... }:
24 {
25 services.kanidm = {
26 package = pkgs.kanidmWithSecretProvisioning;
27 enableServer = true;
28 serverSettings = {
29 origin = "https://${serverDomain}";
30 domain = serverDomain;
31 bindaddress = "[::]:443";
32 ldapbindaddress = "[::1]:636";
33 tls_chain = "${certsPath}/snakeoil.crt";
34 tls_key = "${certsPath}/snakeoil.key";
35 };
36 # So we can check whether provisioning did what we wanted
37 enableClient = true;
38 clientSettings = {
39 uri = "https://${serverDomain}";
40 verify_ca = true;
41 verify_hostnames = true;
42 };
43 };
44
45 specialisation.credentialProvision.configuration =
46 { ... }:
47 {
48 services.kanidm.provision = lib.mkForce {
49 enable = true;
50 adminPasswordFile = pkgs.writeText "admin-pw" provisionAdminPassword;
51 idmAdminPasswordFile = pkgs.writeText "idm-admin-pw" provisionIdmAdminPassword;
52 };
53 };
54
55 specialisation.changedCredential.configuration =
56 { ... }:
57 {
58 services.kanidm.provision = lib.mkForce {
59 enable = true;
60 idmAdminPasswordFile = pkgs.writeText "idm-admin-pw" provisionIdmAdminPassword2;
61 };
62 };
63
64 specialisation.addEntities.configuration =
65 { ... }:
66 {
67 services.kanidm.provision = lib.mkForce {
68 enable = true;
69 # Test whether credential recovery works without specific idmAdmin password
70 #idmAdminPasswordFile =
71
72 groups.supergroup1 = {
73 members = [ "testgroup1" ];
74 };
75
76 groups.testgroup1 = { };
77
78 persons.testuser1 = {
79 displayName = "Test User";
80 legalName = "Jane Doe";
81 mailAddresses = [ "jane.doe@example.com" ];
82 groups = [
83 "testgroup1"
84 "service1-access"
85 ];
86 };
87
88 persons.testuser2 = {
89 displayName = "Powerful Test User";
90 legalName = "Ryouiki Tenkai";
91 groups = [ "service1-admin" ];
92 };
93
94 groups.service1-access = { };
95 groups.service1-admin = { };
96 systems.oauth2.service1 = {
97 displayName = "Service One";
98 originUrl = "https://one.example.com/";
99 originLanding = "https://one.example.com/landing";
100 basicSecretFile = pkgs.writeText "bs-service1" "very-strong-secret-for-service1";
101 scopeMaps.service1-access = [
102 "openid"
103 "email"
104 "profile"
105 ];
106 supplementaryScopeMaps.service1-admin = [ "admin" ];
107 claimMaps.groups = {
108 valuesByGroup.service1-admin = [ "admin" ];
109 };
110 };
111
112 systems.oauth2.service2 = {
113 displayName = "Service Two";
114 originUrl = "https://two.example.com/";
115 originLanding = "https://landing2.example.com/";
116 # Test not setting secret
117 # basicSecretFile =
118 allowInsecureClientDisablePkce = true;
119 preferShortUsername = true;
120 };
121 };
122 };
123
124 specialisation.changeAttributes.configuration =
125 { ... }:
126 {
127 services.kanidm.provision = lib.mkForce {
128 enable = true;
129 # Changing admin credentials at any time should not be a problem:
130 idmAdminPasswordFile = pkgs.writeText "idm-admin-pw" provisionIdmAdminPassword;
131
132 groups.supergroup1 = {
133 #members = ["testgroup1"];
134 };
135
136 groups.testgroup1 = { };
137
138 persons.testuser1 = {
139 displayName = "Test User (changed)";
140 legalName = "Jane Doe (changed)";
141 mailAddresses = [
142 "jane.doe@example.com"
143 "second.doe@example.com"
144 ];
145 groups = [
146 #"testgroup1"
147 "service1-access"
148 ];
149 };
150
151 persons.testuser2 = {
152 displayName = "Powerful Test User (changed)";
153 legalName = "Ryouiki Tenkai (changed)";
154 groups = [ "service1-admin" ];
155 };
156
157 groups.service1-access = { };
158 groups.service1-admin = { };
159 systems.oauth2.service1 = {
160 displayName = "Service One (changed)";
161 # multiple origin urls
162 originUrl = [
163 "https://changed-one.example.com/"
164 "https://changed-one.example.org/"
165 ];
166 originLanding = "https://changed-one.example.com/landing-changed";
167 basicSecretFile = pkgs.writeText "bs-service1" "changed-very-strong-secret-for-service1";
168 scopeMaps.service1-access = [
169 "openid"
170 "email"
171 #"profile"
172 ];
173 supplementaryScopeMaps.service1-admin = [ "adminchanged" ];
174 claimMaps.groups = {
175 valuesByGroup.service1-admin = [ "adminchanged" ];
176 };
177 };
178
179 systems.oauth2.service2 = {
180 displayName = "Service Two (changed)";
181 originUrl = "https://changed-two.example.com/";
182 originLanding = "https://changed-landing2.example.com/";
183 # Test not setting secret
184 # basicSecretFile =
185 allowInsecureClientDisablePkce = false;
186 preferShortUsername = false;
187 };
188 };
189 };
190
191 specialisation.removeAttributes.configuration =
192 { ... }:
193 {
194 services.kanidm.provision = lib.mkForce {
195 enable = true;
196 idmAdminPasswordFile = pkgs.writeText "idm-admin-pw" provisionIdmAdminPassword;
197
198 groups.supergroup1 = { };
199
200 persons.testuser1 = {
201 displayName = "Test User (changed)";
202 };
203
204 persons.testuser2 = {
205 displayName = "Powerful Test User (changed)";
206 groups = [ "service1-admin" ];
207 };
208
209 groups.service1-access = { };
210 groups.service1-admin = { };
211 systems.oauth2.service1 = {
212 displayName = "Service One (changed)";
213 originUrl = "https://changed-one.example.com/";
214 originLanding = "https://changed-one.example.com/landing-changed";
215 basicSecretFile = pkgs.writeText "bs-service1" "changed-very-strong-secret-for-service1";
216 # Removing maps requires setting them to the empty list
217 scopeMaps.service1-access = [ ];
218 supplementaryScopeMaps.service1-admin = [ ];
219 };
220
221 systems.oauth2.service2 = {
222 displayName = "Service Two (changed)";
223 originUrl = "https://changed-two.example.com/";
224 originLanding = "https://changed-landing2.example.com/";
225 };
226 };
227 };
228
229 specialisation.removeEntities.configuration =
230 { ... }:
231 {
232 services.kanidm.provision = lib.mkForce {
233 enable = true;
234 idmAdminPasswordFile = pkgs.writeText "idm-admin-pw" provisionIdmAdminPassword;
235 };
236 };
237
238 security.pki.certificateFiles = [ certs.ca.cert ];
239
240 networking.hosts."::1" = [ serverDomain ];
241 networking.firewall.allowedTCPPorts = [ 443 ];
242
243 users.users.kanidm.shell = pkgs.bashInteractive;
244
245 environment.systemPackages = with pkgs; [
246 kanidm
247 openldap
248 ripgrep
249 jq
250 ];
251 };
252
253 testScript =
254 { nodes, ... }:
255 let
256 # We need access to the config file in the test script.
257 filteredConfig = pkgs.lib.converge (pkgs.lib.filterAttrsRecursive (
258 _: v: v != null
259 )) nodes.provision.services.kanidm.serverSettings;
260 serverConfigFile = (pkgs.formats.toml { }).generate "server.toml" filteredConfig;
261
262 specialisations = "${nodes.provision.system.build.toplevel}/specialisation";
263 in
264 ''
265 import re
266
267 def assert_contains(haystack, needle):
268 if needle not in haystack:
269 print("The haystack that will cause the following exception is:")
270 print("---")
271 print(haystack)
272 print("---")
273 raise Exception(f"Expected string '{needle}' was not found")
274
275 def assert_matches(haystack, expr):
276 if not re.search(expr, haystack):
277 print("The haystack that will cause the following exception is:")
278 print("---")
279 print(haystack)
280 print("---")
281 raise Exception(f"Expected regex '{expr}' did not match")
282
283 def assert_lacks(haystack, needle):
284 if needle in haystack:
285 print("The haystack that will cause the following exception is:")
286 print("---")
287 print(haystack, end="")
288 print("---")
289 raise Exception(f"Unexpected string '{needle}' was found")
290
291 provision.start()
292
293 def provision_login(pw):
294 provision.wait_for_unit("kanidm.service")
295 provision.wait_until_succeeds("curl -Lsf https://${serverDomain} | grep Kanidm")
296 if pw is None:
297 pw = provision.succeed("su - kanidm -c 'kanidmd recover-account -c ${serverConfigFile} idm_admin 2>&1 | rg -o \'[A-Za-z0-9]{48}\' '").strip().removeprefix("'").removesuffix("'")
298 out = provision.succeed(f"KANIDM_PASSWORD={pw} kanidm login -D idm_admin")
299 assert_contains(out, "Login Success for idm_admin")
300
301 with subtest("Test Provisioning - setup"):
302 provision_login(None)
303 provision.succeed("kanidm logout -D idm_admin")
304
305 with subtest("Test Provisioning - credentialProvision"):
306 provision.succeed('${specialisations}/credentialProvision/bin/switch-to-configuration test')
307 provision_login("${provisionIdmAdminPassword}")
308
309 # Make sure neither password is logged
310 provision.fail("journalctl --since -10m --unit kanidm.service --grep '${provisionAdminPassword}'")
311 provision.fail("journalctl --since -10m --unit kanidm.service --grep '${provisionIdmAdminPassword}'")
312
313 # Test provisioned admin pw
314 out = provision.succeed("KANIDM_PASSWORD=${provisionAdminPassword} kanidm login -D admin")
315 assert_contains(out, "Login Success for admin")
316 provision.succeed("kanidm logout -D admin")
317 provision.succeed("kanidm logout -D idm_admin")
318
319 with subtest("Test Provisioning - changedCredential"):
320 provision.succeed('${specialisations}/changedCredential/bin/switch-to-configuration test')
321 provision_login("${provisionIdmAdminPassword2}")
322 provision.succeed("kanidm logout -D idm_admin")
323
324 with subtest("Test Provisioning - addEntities"):
325 provision.succeed('${specialisations}/addEntities/bin/switch-to-configuration test')
326 # Unspecified idm admin password
327 provision_login(None)
328
329 out = provision.succeed("kanidm group get testgroup1")
330 assert_contains(out, "name: testgroup1")
331
332 out = provision.succeed("kanidm group get supergroup1")
333 assert_contains(out, "name: supergroup1")
334 assert_contains(out, "member: testgroup1")
335
336 out = provision.succeed("kanidm person get testuser1")
337 assert_contains(out, "name: testuser1")
338 assert_contains(out, "displayname: Test User")
339 assert_contains(out, "legalname: Jane Doe")
340 assert_contains(out, "mail: jane.doe@example.com")
341 assert_contains(out, "memberof: testgroup1")
342 assert_contains(out, "memberof: service1-access")
343
344 out = provision.succeed("kanidm person get testuser2")
345 assert_contains(out, "name: testuser2")
346 assert_contains(out, "displayname: Powerful Test User")
347 assert_contains(out, "legalname: Ryouiki Tenkai")
348 assert_contains(out, "memberof: service1-admin")
349 assert_lacks(out, "mail:")
350
351 out = provision.succeed("kanidm group get service1-access")
352 assert_contains(out, "name: service1-access")
353
354 out = provision.succeed("kanidm group get service1-admin")
355 assert_contains(out, "name: service1-admin")
356
357 out = provision.succeed("kanidm system oauth2 get service1")
358 assert_contains(out, "name: service1")
359 assert_contains(out, "displayname: Service One")
360 assert_contains(out, "oauth2_rs_origin: https://one.example.com/")
361 assert_contains(out, "oauth2_rs_origin_landing: https://one.example.com/landing")
362 assert_matches(out, 'oauth2_rs_scope_map: service1-access.*{"email", "openid", "profile"}')
363 assert_matches(out, 'oauth2_rs_sup_scope_map: service1-admin.*{"admin"}')
364 assert_matches(out, 'oauth2_rs_claim_map: groups:.*"admin"')
365
366 out = provision.succeed("kanidm system oauth2 show-basic-secret service1")
367 assert_contains(out, "very-strong-secret-for-service1")
368
369 out = provision.succeed("kanidm system oauth2 get service2")
370 assert_contains(out, "name: service2")
371 assert_contains(out, "displayname: Service Two")
372 assert_contains(out, "oauth2_rs_origin: https://two.example.com/")
373 assert_contains(out, "oauth2_rs_origin_landing: https://landing2.example.com/")
374 assert_contains(out, "oauth2_allow_insecure_client_disable_pkce: true")
375 assert_contains(out, "oauth2_prefer_short_username: true")
376
377 provision.succeed("kanidm logout -D idm_admin")
378
379 with subtest("Test Provisioning - changeAttributes"):
380 provision.succeed('${specialisations}/changeAttributes/bin/switch-to-configuration test')
381 provision_login("${provisionIdmAdminPassword}")
382
383 out = provision.succeed("kanidm group get testgroup1")
384 assert_contains(out, "name: testgroup1")
385
386 out = provision.succeed("kanidm group get supergroup1")
387 assert_contains(out, "name: supergroup1")
388 assert_lacks(out, "member: testgroup1")
389
390 out = provision.succeed("kanidm person get testuser1")
391 assert_contains(out, "name: testuser1")
392 assert_contains(out, "displayname: Test User (changed)")
393 assert_contains(out, "legalname: Jane Doe (changed)")
394 assert_contains(out, "mail: jane.doe@example.com")
395 assert_contains(out, "mail: second.doe@example.com")
396 assert_lacks(out, "memberof: testgroup1")
397 assert_contains(out, "memberof: service1-access")
398
399 out = provision.succeed("kanidm person get testuser2")
400 assert_contains(out, "name: testuser2")
401 assert_contains(out, "displayname: Powerful Test User (changed)")
402 assert_contains(out, "legalname: Ryouiki Tenkai (changed)")
403 assert_contains(out, "memberof: service1-admin")
404 assert_lacks(out, "mail:")
405
406 out = provision.succeed("kanidm group get service1-access")
407 assert_contains(out, "name: service1-access")
408
409 out = provision.succeed("kanidm group get service1-admin")
410 assert_contains(out, "name: service1-admin")
411
412 out = provision.succeed("kanidm system oauth2 get service1")
413 assert_contains(out, "name: service1")
414 assert_contains(out, "displayname: Service One (changed)")
415 assert_contains(out, "oauth2_rs_origin: https://changed-one.example.com/")
416 assert_contains(out, "oauth2_rs_origin: https://changed-one.example.org/")
417 assert_contains(out, "oauth2_rs_origin_landing: https://changed-one.example.com/landing")
418 assert_matches(out, 'oauth2_rs_scope_map: service1-access.*{"email", "openid"}')
419 assert_matches(out, 'oauth2_rs_sup_scope_map: service1-admin.*{"adminchanged"}')
420 assert_matches(out, 'oauth2_rs_claim_map: groups:.*"adminchanged"')
421
422 out = provision.succeed("kanidm system oauth2 show-basic-secret service1")
423 assert_contains(out, "changed-very-strong-secret-for-service1")
424
425 out = provision.succeed("kanidm system oauth2 get service2")
426 assert_contains(out, "name: service2")
427 assert_contains(out, "displayname: Service Two (changed)")
428 assert_contains(out, "oauth2_rs_origin: https://changed-two.example.com/")
429 assert_contains(out, "oauth2_rs_origin_landing: https://changed-landing2.example.com/")
430 assert_lacks(out, "oauth2_allow_insecure_client_disable_pkce: true")
431 assert_lacks(out, "oauth2_prefer_short_username: true")
432
433 provision.succeed("kanidm logout -D idm_admin")
434
435 with subtest("Test Provisioning - removeAttributes"):
436 provision.succeed('${specialisations}/removeAttributes/bin/switch-to-configuration test')
437 provision_login("${provisionIdmAdminPassword}")
438
439 out = provision.succeed("kanidm group get testgroup1")
440 assert_lacks(out, "name: testgroup1")
441
442 out = provision.succeed("kanidm group get supergroup1")
443 assert_contains(out, "name: supergroup1")
444 assert_lacks(out, "member: testgroup1")
445
446 out = provision.succeed("kanidm person get testuser1")
447 assert_contains(out, "name: testuser1")
448 assert_contains(out, "displayname: Test User (changed)")
449 assert_lacks(out, "legalname: Jane Doe (changed)")
450 assert_lacks(out, "mail: jane.doe@example.com")
451 assert_lacks(out, "mail: second.doe@example.com")
452 assert_lacks(out, "memberof: testgroup1")
453 assert_lacks(out, "memberof: service1-access")
454
455 out = provision.succeed("kanidm person get testuser2")
456 assert_contains(out, "name: testuser2")
457 assert_contains(out, "displayname: Powerful Test User (changed)")
458 assert_lacks(out, "legalname: Ryouiki Tenkai (changed)")
459 assert_contains(out, "memberof: service1-admin")
460 assert_lacks(out, "mail:")
461
462 out = provision.succeed("kanidm group get service1-access")
463 assert_contains(out, "name: service1-access")
464
465 out = provision.succeed("kanidm group get service1-admin")
466 assert_contains(out, "name: service1-admin")
467
468 out = provision.succeed("kanidm system oauth2 get service1")
469 assert_contains(out, "name: service1")
470 assert_contains(out, "displayname: Service One (changed)")
471 assert_contains(out, "oauth2_rs_origin: https://changed-one.example.com/")
472 assert_lacks(out, "oauth2_rs_origin: https://changed-one.example.org/")
473 assert_contains(out, "oauth2_rs_origin_landing: https://changed-one.example.com/landing")
474 assert_lacks(out, "oauth2_rs_scope_map")
475 assert_lacks(out, "oauth2_rs_sup_scope_map")
476 assert_lacks(out, "oauth2_rs_claim_map")
477
478 out = provision.succeed("kanidm system oauth2 show-basic-secret service1")
479 assert_contains(out, "changed-very-strong-secret-for-service1")
480
481 out = provision.succeed("kanidm system oauth2 get service2")
482 assert_contains(out, "name: service2")
483 assert_contains(out, "displayname: Service Two (changed)")
484 assert_contains(out, "oauth2_rs_origin: https://changed-two.example.com/")
485 assert_contains(out, "oauth2_rs_origin_landing: https://changed-landing2.example.com/")
486 assert_lacks(out, "oauth2_allow_insecure_client_disable_pkce: true")
487 assert_lacks(out, "oauth2_prefer_short_username: true")
488
489 provision.succeed("kanidm logout -D idm_admin")
490
491 with subtest("Test Provisioning - removeEntities"):
492 provision.succeed('${specialisations}/removeEntities/bin/switch-to-configuration test')
493 provision_login("${provisionIdmAdminPassword}")
494
495 out = provision.succeed("kanidm group get testgroup1")
496 assert_lacks(out, "name: testgroup1")
497
498 out = provision.succeed("kanidm group get supergroup1")
499 assert_lacks(out, "name: supergroup1")
500
501 out = provision.succeed("kanidm person get testuser1")
502 assert_lacks(out, "name: testuser1")
503
504 out = provision.succeed("kanidm person get testuser2")
505 assert_lacks(out, "name: testuser2")
506
507 out = provision.succeed("kanidm group get service1-access")
508 assert_lacks(out, "name: service1-access")
509
510 out = provision.succeed("kanidm group get service1-admin")
511 assert_lacks(out, "name: service1-admin")
512
513 out = provision.succeed("kanidm system oauth2 get service1")
514 assert_lacks(out, "name: service1")
515
516 out = provision.succeed("kanidm system oauth2 get service2")
517 assert_lacks(out, "name: service2")
518
519 provision.succeed("kanidm logout -D idm_admin")
520 '';
521 }
522)