python312Packages.millheater: 0.11.8 -> 0.12.0
[NixPkgs.git] / nixos / tests / kanidm-provisioning.nix
blob27176c2086fec0ece8fd8f726570dc2d62cf19d7
1 import ./make-test-python.nix (
2   { pkgs, ... }:
3   let
4     certs = import ./common/acme/server/snakeoil-certs.nix;
5     serverDomain = certs.domain;
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     '';
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 ];
22     nodes.provision =
23       { pkgs, lib, ... }:
24       {
25         services.kanidm = {
26           package = pkgs.kanidm.withSecretProvisioning;
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         };
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           };
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           };
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 =
72               groups.supergroup1 = {
73                 members = [ "testgroup1" ];
74               };
76               groups.testgroup1 = { };
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               };
88               persons.testuser2 = {
89                 displayName = "Powerful Test User";
90                 legalName = "Ryouiki Tenkai";
91                 groups = [ "service1-admin" ];
92               };
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               };
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           };
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;
132               groups.supergroup1 = {
133                 #members = ["testgroup1"];
134               };
136               groups.testgroup1 = { };
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               };
151               persons.testuser2 = {
152                 displayName = "Powerful Test User (changed)";
153                 legalName = "Ryouiki Tenkai (changed)";
154                 groups = [ "service1-admin" ];
155               };
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               };
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           };
191         specialisation.removeAttributes.configuration =
192           { ... }:
193           {
194             services.kanidm.provision = lib.mkForce {
195               enable = true;
196               idmAdminPasswordFile = pkgs.writeText "idm-admin-pw" provisionIdmAdminPassword;
198               groups.supergroup1 = { };
200               persons.testuser1 = {
201                 displayName = "Test User (changed)";
202               };
204               persons.testuser2 = {
205                 displayName = "Powerful Test User (changed)";
206                 groups = [ "service1-admin" ];
207               };
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               };
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           };
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           };
238         security.pki.certificateFiles = [ certs.ca.cert ];
240         networking.hosts."::1" = [ serverDomain ];
241         networking.firewall.allowedTCPPorts = [ 443 ];
243         users.users.kanidm.shell = pkgs.bashInteractive;
245         environment.systemPackages = with pkgs; [
246           kanidm
247           openldap
248           ripgrep
249           jq
250         ];
251       };
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;
262         specialisations = "${nodes.provision.system.build.toplevel}/specialisation";
263       in
264       ''
265         import re
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")
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")
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")
291         provision.start()
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")
301         with subtest("Test Provisioning - setup"):
302             provision_login(None)
303             provision.succeed("kanidm logout -D idm_admin")
305         with subtest("Test Provisioning - credentialProvision"):
306             provision.succeed('${specialisations}/credentialProvision/bin/switch-to-configuration test')
307             provision_login("${provisionIdmAdminPassword}")
309             # Test provisioned admin pw
310             out = provision.succeed("KANIDM_PASSWORD=${provisionAdminPassword} kanidm login -D admin")
311             assert_contains(out, "Login Success for admin")
312             provision.succeed("kanidm logout -D admin")
313             provision.succeed("kanidm logout -D idm_admin")
315         with subtest("Test Provisioning - changedCredential"):
316             provision.succeed('${specialisations}/changedCredential/bin/switch-to-configuration test')
317             provision_login("${provisionIdmAdminPassword2}")
318             provision.succeed("kanidm logout -D idm_admin")
320         with subtest("Test Provisioning - addEntities"):
321             provision.succeed('${specialisations}/addEntities/bin/switch-to-configuration test')
322             # Unspecified idm admin password
323             provision_login(None)
325             out = provision.succeed("kanidm group get testgroup1")
326             assert_contains(out, "name: testgroup1")
328             out = provision.succeed("kanidm group get supergroup1")
329             assert_contains(out, "name: supergroup1")
330             assert_contains(out, "member: testgroup1")
332             out = provision.succeed("kanidm person get testuser1")
333             assert_contains(out, "name: testuser1")
334             assert_contains(out, "displayname: Test User")
335             assert_contains(out, "legalname: Jane Doe")
336             assert_contains(out, "mail: jane.doe@example.com")
337             assert_contains(out, "memberof: testgroup1")
338             assert_contains(out, "memberof: service1-access")
340             out = provision.succeed("kanidm person get testuser2")
341             assert_contains(out, "name: testuser2")
342             assert_contains(out, "displayname: Powerful Test User")
343             assert_contains(out, "legalname: Ryouiki Tenkai")
344             assert_contains(out, "memberof: service1-admin")
345             assert_lacks(out, "mail:")
347             out = provision.succeed("kanidm group get service1-access")
348             assert_contains(out, "name: service1-access")
350             out = provision.succeed("kanidm group get service1-admin")
351             assert_contains(out, "name: service1-admin")
353             out = provision.succeed("kanidm system oauth2 get service1")
354             assert_contains(out, "name: service1")
355             assert_contains(out, "displayname: Service One")
356             assert_contains(out, "oauth2_rs_origin: https://one.example.com/")
357             assert_contains(out, "oauth2_rs_origin_landing: https://one.example.com/landing")
358             assert_matches(out, 'oauth2_rs_scope_map: service1-access.*{"email", "openid", "profile"}')
359             assert_matches(out, 'oauth2_rs_sup_scope_map: service1-admin.*{"admin"}')
360             assert_matches(out, 'oauth2_rs_claim_map: groups:.*"admin"')
362             out = provision.succeed("kanidm system oauth2 show-basic-secret service1")
363             assert_contains(out, "very-strong-secret-for-service1")
365             out = provision.succeed("kanidm system oauth2 get service2")
366             assert_contains(out, "name: service2")
367             assert_contains(out, "displayname: Service Two")
368             assert_contains(out, "oauth2_rs_origin: https://two.example.com/")
369             assert_contains(out, "oauth2_rs_origin_landing: https://landing2.example.com/")
370             assert_contains(out, "oauth2_allow_insecure_client_disable_pkce: true")
371             assert_contains(out, "oauth2_prefer_short_username: true")
373             provision.succeed("kanidm logout -D idm_admin")
375         with subtest("Test Provisioning - changeAttributes"):
376             provision.succeed('${specialisations}/changeAttributes/bin/switch-to-configuration test')
377             provision_login("${provisionIdmAdminPassword}")
379             out = provision.succeed("kanidm group get testgroup1")
380             assert_contains(out, "name: testgroup1")
382             out = provision.succeed("kanidm group get supergroup1")
383             assert_contains(out, "name: supergroup1")
384             assert_lacks(out, "member: testgroup1")
386             out = provision.succeed("kanidm person get testuser1")
387             assert_contains(out, "name: testuser1")
388             assert_contains(out, "displayname: Test User (changed)")
389             assert_contains(out, "legalname: Jane Doe (changed)")
390             assert_contains(out, "mail: jane.doe@example.com")
391             assert_contains(out, "mail: second.doe@example.com")
392             assert_lacks(out, "memberof: testgroup1")
393             assert_contains(out, "memberof: service1-access")
395             out = provision.succeed("kanidm person get testuser2")
396             assert_contains(out, "name: testuser2")
397             assert_contains(out, "displayname: Powerful Test User (changed)")
398             assert_contains(out, "legalname: Ryouiki Tenkai (changed)")
399             assert_contains(out, "memberof: service1-admin")
400             assert_lacks(out, "mail:")
402             out = provision.succeed("kanidm group get service1-access")
403             assert_contains(out, "name: service1-access")
405             out = provision.succeed("kanidm group get service1-admin")
406             assert_contains(out, "name: service1-admin")
408             out = provision.succeed("kanidm system oauth2 get service1")
409             assert_contains(out, "name: service1")
410             assert_contains(out, "displayname: Service One (changed)")
411             assert_contains(out, "oauth2_rs_origin: https://changed-one.example.com/")
412             assert_contains(out, "oauth2_rs_origin: https://changed-one.example.org/")
413             assert_contains(out, "oauth2_rs_origin_landing: https://changed-one.example.com/landing")
414             assert_matches(out, 'oauth2_rs_scope_map: service1-access.*{"email", "openid"}')
415             assert_matches(out, 'oauth2_rs_sup_scope_map: service1-admin.*{"adminchanged"}')
416             assert_matches(out, 'oauth2_rs_claim_map: groups:.*"adminchanged"')
418             out = provision.succeed("kanidm system oauth2 show-basic-secret service1")
419             assert_contains(out, "changed-very-strong-secret-for-service1")
421             out = provision.succeed("kanidm system oauth2 get service2")
422             assert_contains(out, "name: service2")
423             assert_contains(out, "displayname: Service Two (changed)")
424             assert_contains(out, "oauth2_rs_origin: https://changed-two.example.com/")
425             assert_contains(out, "oauth2_rs_origin_landing: https://changed-landing2.example.com/")
426             assert_lacks(out, "oauth2_allow_insecure_client_disable_pkce: true")
427             assert_lacks(out, "oauth2_prefer_short_username: true")
429             provision.succeed("kanidm logout -D idm_admin")
431         with subtest("Test Provisioning - removeAttributes"):
432             provision.succeed('${specialisations}/removeAttributes/bin/switch-to-configuration test')
433             provision_login("${provisionIdmAdminPassword}")
435             out = provision.succeed("kanidm group get testgroup1")
436             assert_lacks(out, "name: testgroup1")
438             out = provision.succeed("kanidm group get supergroup1")
439             assert_contains(out, "name: supergroup1")
440             assert_lacks(out, "member: testgroup1")
442             out = provision.succeed("kanidm person get testuser1")
443             assert_contains(out, "name: testuser1")
444             assert_contains(out, "displayname: Test User (changed)")
445             assert_lacks(out, "legalname: Jane Doe (changed)")
446             assert_lacks(out, "mail: jane.doe@example.com")
447             assert_lacks(out, "mail: second.doe@example.com")
448             assert_lacks(out, "memberof: testgroup1")
449             assert_lacks(out, "memberof: service1-access")
451             out = provision.succeed("kanidm person get testuser2")
452             assert_contains(out, "name: testuser2")
453             assert_contains(out, "displayname: Powerful Test User (changed)")
454             assert_lacks(out, "legalname: Ryouiki Tenkai (changed)")
455             assert_contains(out, "memberof: service1-admin")
456             assert_lacks(out, "mail:")
458             out = provision.succeed("kanidm group get service1-access")
459             assert_contains(out, "name: service1-access")
461             out = provision.succeed("kanidm group get service1-admin")
462             assert_contains(out, "name: service1-admin")
464             out = provision.succeed("kanidm system oauth2 get service1")
465             assert_contains(out, "name: service1")
466             assert_contains(out, "displayname: Service One (changed)")
467             assert_contains(out, "oauth2_rs_origin: https://changed-one.example.com/")
468             assert_lacks(out, "oauth2_rs_origin: https://changed-one.example.org/")
469             assert_contains(out, "oauth2_rs_origin_landing: https://changed-one.example.com/landing")
470             assert_lacks(out, "oauth2_rs_scope_map")
471             assert_lacks(out, "oauth2_rs_sup_scope_map")
472             assert_lacks(out, "oauth2_rs_claim_map")
474             out = provision.succeed("kanidm system oauth2 show-basic-secret service1")
475             assert_contains(out, "changed-very-strong-secret-for-service1")
477             out = provision.succeed("kanidm system oauth2 get service2")
478             assert_contains(out, "name: service2")
479             assert_contains(out, "displayname: Service Two (changed)")
480             assert_contains(out, "oauth2_rs_origin: https://changed-two.example.com/")
481             assert_contains(out, "oauth2_rs_origin_landing: https://changed-landing2.example.com/")
482             assert_lacks(out, "oauth2_allow_insecure_client_disable_pkce: true")
483             assert_lacks(out, "oauth2_prefer_short_username: true")
485             provision.succeed("kanidm logout -D idm_admin")
487         with subtest("Test Provisioning - removeEntities"):
488             provision.succeed('${specialisations}/removeEntities/bin/switch-to-configuration test')
489             provision_login("${provisionIdmAdminPassword}")
491             out = provision.succeed("kanidm group get testgroup1")
492             assert_lacks(out, "name: testgroup1")
494             out = provision.succeed("kanidm group get supergroup1")
495             assert_lacks(out, "name: supergroup1")
497             out = provision.succeed("kanidm person get testuser1")
498             assert_lacks(out, "name: testuser1")
500             out = provision.succeed("kanidm person get testuser2")
501             assert_lacks(out, "name: testuser2")
503             out = provision.succeed("kanidm group get service1-access")
504             assert_lacks(out, "name: service1-access")
506             out = provision.succeed("kanidm group get service1-admin")
507             assert_lacks(out, "name: service1-admin")
509             out = provision.succeed("kanidm system oauth2 get service1")
510             assert_lacks(out, "name: service1")
512             out = provision.succeed("kanidm system oauth2 get service2")
513             assert_lacks(out, "name: service2")
515             provision.succeed("kanidm logout -D idm_admin")
516       '';
517   }