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