nixpkgs/nixos/tests/kanidm-provisioning.nix
Ilan Joselevich 3b6b50dfad
nixos/kanidm: merge recursively with extraJsonFile
Previously, if you set group memberships in both locations, they will
get replaced by the ones in extraJsonFile, which is unexpected as it
kicks users from the group. Now the state files get merged recursively,
including the arrays.
2025-05-27 23:44:44 +03:00

562 lines
23 KiB
Nix

{ pkgs, ... }:
let
certs = import ./common/acme/server/snakeoil-certs.nix;
serverDomain = certs.domain;
# copy certs to store to work around mount namespacing
certsPath = pkgs.runCommandNoCC "snakeoil-certs" { } ''
mkdir $out
cp ${certs."${serverDomain}".cert} $out/snakeoil.crt
cp ${certs."${serverDomain}".key} $out/snakeoil.key
'';
provisionAdminPassword = "very-strong-password-for-admin";
provisionIdmAdminPassword = "very-strong-password-for-idm-admin";
provisionIdmAdminPassword2 = "very-strong-alternative-password-for-idm-admin";
in
{
name = "kanidm-provisioning";
meta.maintainers = with pkgs.lib.maintainers; [ oddlama ];
nodes.provision =
{ pkgs, lib, ... }:
{
services.kanidm = {
package = pkgs.kanidmWithSecretProvisioning_1_6;
enableServer = true;
serverSettings = {
origin = "https://${serverDomain}";
domain = serverDomain;
bindaddress = "[::]:443";
ldapbindaddress = "[::1]:636";
tls_chain = "${certsPath}/snakeoil.crt";
tls_key = "${certsPath}/snakeoil.key";
};
# So we can check whether provisioning did what we wanted
enableClient = true;
clientSettings = {
uri = "https://${serverDomain}";
verify_ca = true;
verify_hostnames = true;
};
};
specialisation.credentialProvision.configuration =
{ ... }:
{
services.kanidm.provision = lib.mkForce {
enable = true;
adminPasswordFile = pkgs.writeText "admin-pw" provisionAdminPassword;
idmAdminPasswordFile = pkgs.writeText "idm-admin-pw" provisionIdmAdminPassword;
};
};
specialisation.changedCredential.configuration =
{ ... }:
{
services.kanidm.provision = lib.mkForce {
enable = true;
idmAdminPasswordFile = pkgs.writeText "idm-admin-pw" provisionIdmAdminPassword2;
};
};
specialisation.addEntities.configuration =
{ ... }:
{
services.kanidm.provision = lib.mkForce {
enable = true;
# Test whether credential recovery works without specific idmAdmin password
#idmAdminPasswordFile =
groups.supergroup1 = {
members = [ "testgroup1" ];
};
groups.testgroup1 = { };
persons.testuser1 = {
displayName = "Test User";
legalName = "Jane Doe";
mailAddresses = [ "jane.doe@example.com" ];
groups = [
"testgroup1"
"service1-access"
];
};
persons.testuser2 = {
displayName = "Powerful Test User";
legalName = "Ryouiki Tenkai";
groups = [ "service1-admin" ];
};
groups.service1-access = { };
groups.service1-admin = { };
systems.oauth2.service1 = {
displayName = "Service One";
originUrl = "https://one.example.com/";
originLanding = "https://one.example.com/landing";
basicSecretFile = pkgs.writeText "bs-service1" "very-strong-secret-for-service1";
scopeMaps.service1-access = [
"openid"
"email"
"profile"
];
supplementaryScopeMaps.service1-admin = [ "admin" ];
claimMaps.groups = {
valuesByGroup.service1-admin = [ "admin" ];
};
};
systems.oauth2.service2 = {
displayName = "Service Two";
originUrl = "https://two.example.com/";
originLanding = "https://landing2.example.com/";
# Test not setting secret
# basicSecretFile =
allowInsecureClientDisablePkce = true;
preferShortUsername = true;
};
};
};
specialisation.changeAttributes.configuration =
{ ... }:
{
services.kanidm.provision = lib.mkForce {
enable = true;
# Changing admin credentials at any time should not be a problem:
idmAdminPasswordFile = pkgs.writeText "idm-admin-pw" provisionIdmAdminPassword;
groups.supergroup1 = {
#members = ["testgroup1"];
};
groups.testgroup1 = { };
persons.testuser1 = {
displayName = "Test User (changed)";
legalName = "Jane Doe (changed)";
mailAddresses = [
"jane.doe@example.com"
"second.doe@example.com"
];
groups = [
#"testgroup1"
"service1-access"
];
};
persons.testuser2 = {
displayName = "Powerful Test User (changed)";
legalName = "Ryouiki Tenkai (changed)";
groups = [ "service1-admin" ];
};
groups.service1-access = { };
groups.service1-admin = { };
systems.oauth2.service1 = {
displayName = "Service One (changed)";
# multiple origin urls
originUrl = [
"https://changed-one.example.com/"
"https://changed-one.example.org/"
];
originLanding = "https://changed-one.example.com/landing-changed";
basicSecretFile = pkgs.writeText "bs-service1" "changed-very-strong-secret-for-service1";
scopeMaps.service1-access = [
"openid"
"email"
#"profile"
];
supplementaryScopeMaps.service1-admin = [ "adminchanged" ];
claimMaps.groups = {
valuesByGroup.service1-admin = [ "adminchanged" ];
};
};
systems.oauth2.service2 = {
displayName = "Service Two (changed)";
originUrl = "https://changed-two.example.com/";
originLanding = "https://changed-landing2.example.com/";
# Test not setting secret
# basicSecretFile =
allowInsecureClientDisablePkce = false;
preferShortUsername = false;
};
};
};
specialisation.removeAttributes.configuration =
{ ... }:
{
services.kanidm.provision = lib.mkForce {
enable = true;
idmAdminPasswordFile = pkgs.writeText "idm-admin-pw" provisionIdmAdminPassword;
groups.supergroup1 = { };
persons.testuser1 = {
displayName = "Test User (changed)";
};
persons.testuser2 = {
displayName = "Powerful Test User (changed)";
groups = [ "service1-admin" ];
};
groups.service1-access = { };
groups.service1-admin = { };
systems.oauth2.service1 = {
displayName = "Service One (changed)";
originUrl = "https://changed-one.example.com/";
originLanding = "https://changed-one.example.com/landing-changed";
basicSecretFile = pkgs.writeText "bs-service1" "changed-very-strong-secret-for-service1";
# Removing maps requires setting them to the empty list
scopeMaps.service1-access = [ ];
supplementaryScopeMaps.service1-admin = [ ];
};
systems.oauth2.service2 = {
displayName = "Service Two (changed)";
originUrl = "https://changed-two.example.com/";
originLanding = "https://changed-landing2.example.com/";
};
};
};
specialisation.removeEntities.configuration =
{ ... }:
{
services.kanidm.provision = lib.mkForce {
enable = true;
idmAdminPasswordFile = pkgs.writeText "idm-admin-pw" provisionIdmAdminPassword;
};
};
specialisation.extraJsonFile.configuration =
{ ... }:
{
services.kanidm.provision = lib.mkForce {
enable = true;
idmAdminPasswordFile = pkgs.writeText "idm-admin-pw" provisionIdmAdminPassword;
extraJsonFile = pkgs.writeText "extra-json.json" (
builtins.toJSON {
persons.testuser2.displayName = "Test User 2";
groups.testgroup1.members = [ "testuser2" ];
}
);
groups.testgroup1 = { };
persons.testuser1 = {
displayName = "Test User 1";
groups = [ "testgroup1" ];
};
};
};
security.pki.certificateFiles = [ certs.ca.cert ];
networking.hosts."::1" = [ serverDomain ];
networking.firewall.allowedTCPPorts = [ 443 ];
users.users.kanidm.shell = pkgs.bashInteractive;
environment.systemPackages = with pkgs; [
kanidm
openldap
ripgrep
jq
];
};
testScript =
{ nodes, ... }:
let
# We need access to the config file in the test script.
filteredConfig = pkgs.lib.converge (pkgs.lib.filterAttrsRecursive (
_: v: v != null
)) nodes.provision.services.kanidm.serverSettings;
serverConfigFile = (pkgs.formats.toml { }).generate "server.toml" filteredConfig;
specialisations = "${nodes.provision.system.build.toplevel}/specialisation";
in
''
import re
def assert_contains(haystack, needle):
if needle not in haystack:
print("The haystack that will cause the following exception is:")
print("---")
print(haystack)
print("---")
raise Exception(f"Expected string '{needle}' was not found")
def assert_matches(haystack, expr):
if not re.search(expr, haystack):
print("The haystack that will cause the following exception is:")
print("---")
print(haystack)
print("---")
raise Exception(f"Expected regex '{expr}' did not match")
def assert_lacks(haystack, needle):
if needle in haystack:
print("The haystack that will cause the following exception is:")
print("---")
print(haystack, end="")
print("---")
raise Exception(f"Unexpected string '{needle}' was found")
provision.start()
def provision_login(pw):
provision.wait_for_unit("kanidm.service")
provision.wait_until_succeeds("curl -Lsf https://${serverDomain} | grep Kanidm")
if pw is None:
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("'")
out = provision.succeed(f"KANIDM_PASSWORD={pw} kanidm login -D idm_admin")
assert_contains(out, "Login Success for idm_admin")
with subtest("Test Provisioning - setup"):
provision_login(None)
provision.succeed("kanidm logout -D idm_admin")
with subtest("Test Provisioning - credentialProvision"):
provision.succeed('${specialisations}/credentialProvision/bin/switch-to-configuration test')
provision_login("${provisionIdmAdminPassword}")
# Make sure neither password is logged
provision.fail("journalctl --since -10m --unit kanidm.service --grep '${provisionAdminPassword}'")
provision.fail("journalctl --since -10m --unit kanidm.service --grep '${provisionIdmAdminPassword}'")
# Test provisioned admin pw
out = provision.succeed("KANIDM_PASSWORD=${provisionAdminPassword} kanidm login -D admin")
assert_contains(out, "Login Success for admin")
provision.succeed("kanidm logout -D admin")
provision.succeed("kanidm logout -D idm_admin")
with subtest("Test Provisioning - changedCredential"):
provision.succeed('${specialisations}/changedCredential/bin/switch-to-configuration test')
provision_login("${provisionIdmAdminPassword2}")
provision.succeed("kanidm logout -D idm_admin")
with subtest("Test Provisioning - addEntities"):
provision.succeed('${specialisations}/addEntities/bin/switch-to-configuration test')
# Unspecified idm admin password
provision_login(None)
out = provision.succeed("kanidm group get testgroup1")
assert_contains(out, "name: testgroup1")
out = provision.succeed("kanidm group get supergroup1")
assert_contains(out, "name: supergroup1")
assert_contains(out, "member: testgroup1")
out = provision.succeed("kanidm person get testuser1")
assert_contains(out, "name: testuser1")
assert_contains(out, "displayname: Test User")
assert_contains(out, "legalname: Jane Doe")
assert_contains(out, "mail: jane.doe@example.com")
assert_contains(out, "memberof: testgroup1")
assert_contains(out, "memberof: service1-access")
out = provision.succeed("kanidm person get testuser2")
assert_contains(out, "name: testuser2")
assert_contains(out, "displayname: Powerful Test User")
assert_contains(out, "legalname: Ryouiki Tenkai")
assert_contains(out, "memberof: service1-admin")
assert_lacks(out, "mail:")
out = provision.succeed("kanidm group get service1-access")
assert_contains(out, "name: service1-access")
out = provision.succeed("kanidm group get service1-admin")
assert_contains(out, "name: service1-admin")
out = provision.succeed("kanidm system oauth2 get service1")
assert_contains(out, "name: service1")
assert_contains(out, "displayname: Service One")
assert_contains(out, "oauth2_rs_origin: https://one.example.com/")
assert_contains(out, "oauth2_rs_origin_landing: https://one.example.com/landing")
assert_matches(out, 'oauth2_rs_scope_map: service1-access.*{"email", "openid", "profile"}')
assert_matches(out, 'oauth2_rs_sup_scope_map: service1-admin.*{"admin"}')
assert_matches(out, 'oauth2_rs_claim_map: groups:.*"admin"')
out = provision.succeed("kanidm system oauth2 show-basic-secret service1")
assert_contains(out, "very-strong-secret-for-service1")
out = provision.succeed("kanidm system oauth2 get service2")
assert_contains(out, "name: service2")
assert_contains(out, "displayname: Service Two")
assert_contains(out, "oauth2_rs_origin: https://two.example.com/")
assert_contains(out, "oauth2_rs_origin_landing: https://landing2.example.com/")
assert_contains(out, "oauth2_allow_insecure_client_disable_pkce: true")
assert_contains(out, "oauth2_prefer_short_username: true")
provision.succeed("kanidm logout -D idm_admin")
with subtest("Test Provisioning - changeAttributes"):
provision.succeed('${specialisations}/changeAttributes/bin/switch-to-configuration test')
provision_login("${provisionIdmAdminPassword}")
out = provision.succeed("kanidm group get testgroup1")
assert_contains(out, "name: testgroup1")
out = provision.succeed("kanidm group get supergroup1")
assert_contains(out, "name: supergroup1")
assert_lacks(out, "member: testgroup1")
out = provision.succeed("kanidm person get testuser1")
assert_contains(out, "name: testuser1")
assert_contains(out, "displayname: Test User (changed)")
assert_contains(out, "legalname: Jane Doe (changed)")
assert_contains(out, "mail: jane.doe@example.com")
assert_contains(out, "mail: second.doe@example.com")
assert_lacks(out, "memberof: testgroup1")
assert_contains(out, "memberof: service1-access")
out = provision.succeed("kanidm person get testuser2")
assert_contains(out, "name: testuser2")
assert_contains(out, "displayname: Powerful Test User (changed)")
assert_contains(out, "legalname: Ryouiki Tenkai (changed)")
assert_contains(out, "memberof: service1-admin")
assert_lacks(out, "mail:")
out = provision.succeed("kanidm group get service1-access")
assert_contains(out, "name: service1-access")
out = provision.succeed("kanidm group get service1-admin")
assert_contains(out, "name: service1-admin")
out = provision.succeed("kanidm system oauth2 get service1")
assert_contains(out, "name: service1")
assert_contains(out, "displayname: Service One (changed)")
assert_contains(out, "oauth2_rs_origin: https://changed-one.example.com/")
assert_contains(out, "oauth2_rs_origin: https://changed-one.example.org/")
assert_contains(out, "oauth2_rs_origin_landing: https://changed-one.example.com/landing")
assert_matches(out, 'oauth2_rs_scope_map: service1-access.*{"email", "openid"}')
assert_matches(out, 'oauth2_rs_sup_scope_map: service1-admin.*{"adminchanged"}')
assert_matches(out, 'oauth2_rs_claim_map: groups:.*"adminchanged"')
out = provision.succeed("kanidm system oauth2 show-basic-secret service1")
assert_contains(out, "changed-very-strong-secret-for-service1")
out = provision.succeed("kanidm system oauth2 get service2")
assert_contains(out, "name: service2")
assert_contains(out, "displayname: Service Two (changed)")
assert_contains(out, "oauth2_rs_origin: https://changed-two.example.com/")
assert_contains(out, "oauth2_rs_origin_landing: https://changed-landing2.example.com/")
assert_lacks(out, "oauth2_allow_insecure_client_disable_pkce: true")
assert_lacks(out, "oauth2_prefer_short_username: true")
provision.succeed("kanidm logout -D idm_admin")
with subtest("Test Provisioning - removeAttributes"):
provision.succeed('${specialisations}/removeAttributes/bin/switch-to-configuration test')
provision_login("${provisionIdmAdminPassword}")
out = provision.succeed("kanidm group get testgroup1")
assert_lacks(out, "name: testgroup1")
out = provision.succeed("kanidm group get supergroup1")
assert_contains(out, "name: supergroup1")
assert_lacks(out, "member: testgroup1")
out = provision.succeed("kanidm person get testuser1")
assert_contains(out, "name: testuser1")
assert_contains(out, "displayname: Test User (changed)")
assert_lacks(out, "legalname: Jane Doe (changed)")
assert_lacks(out, "mail: jane.doe@example.com")
assert_lacks(out, "mail: second.doe@example.com")
assert_lacks(out, "memberof: testgroup1")
assert_lacks(out, "memberof: service1-access")
out = provision.succeed("kanidm person get testuser2")
assert_contains(out, "name: testuser2")
assert_contains(out, "displayname: Powerful Test User (changed)")
assert_lacks(out, "legalname: Ryouiki Tenkai (changed)")
assert_contains(out, "memberof: service1-admin")
assert_lacks(out, "mail:")
out = provision.succeed("kanidm group get service1-access")
assert_contains(out, "name: service1-access")
out = provision.succeed("kanidm group get service1-admin")
assert_contains(out, "name: service1-admin")
out = provision.succeed("kanidm system oauth2 get service1")
assert_contains(out, "name: service1")
assert_contains(out, "displayname: Service One (changed)")
assert_contains(out, "oauth2_rs_origin: https://changed-one.example.com/")
assert_lacks(out, "oauth2_rs_origin: https://changed-one.example.org/")
assert_contains(out, "oauth2_rs_origin_landing: https://changed-one.example.com/landing")
assert_lacks(out, "oauth2_rs_scope_map")
assert_lacks(out, "oauth2_rs_sup_scope_map")
assert_lacks(out, "oauth2_rs_claim_map")
out = provision.succeed("kanidm system oauth2 show-basic-secret service1")
assert_contains(out, "changed-very-strong-secret-for-service1")
out = provision.succeed("kanidm system oauth2 get service2")
assert_contains(out, "name: service2")
assert_contains(out, "displayname: Service Two (changed)")
assert_contains(out, "oauth2_rs_origin: https://changed-two.example.com/")
assert_contains(out, "oauth2_rs_origin_landing: https://changed-landing2.example.com/")
assert_lacks(out, "oauth2_allow_insecure_client_disable_pkce: true")
assert_lacks(out, "oauth2_prefer_short_username: true")
provision.succeed("kanidm logout -D idm_admin")
with subtest("Test Provisioning - removeEntities"):
provision.succeed('${specialisations}/removeEntities/bin/switch-to-configuration test')
provision_login("${provisionIdmAdminPassword}")
out = provision.succeed("kanidm group get testgroup1")
assert_lacks(out, "name: testgroup1")
out = provision.succeed("kanidm group get supergroup1")
assert_lacks(out, "name: supergroup1")
out = provision.succeed("kanidm person get testuser1")
assert_lacks(out, "name: testuser1")
out = provision.succeed("kanidm person get testuser2")
assert_lacks(out, "name: testuser2")
out = provision.succeed("kanidm group get service1-access")
assert_lacks(out, "name: service1-access")
out = provision.succeed("kanidm group get service1-admin")
assert_lacks(out, "name: service1-admin")
out = provision.succeed("kanidm system oauth2 get service1")
assert_lacks(out, "name: service1")
out = provision.succeed("kanidm system oauth2 get service2")
assert_lacks(out, "name: service2")
provision.succeed("kanidm logout -D idm_admin")
with subtest("Test Provisioning - extraJsonFile"):
provision.succeed('${specialisations}/extraJsonFile/bin/switch-to-configuration test')
provision_login("${provisionIdmAdminPassword}")
out = provision.succeed("kanidm group get testgroup1")
assert_contains(out, "name: testgroup1")
out = provision.succeed("kanidm person get testuser1")
assert_contains(out, "name: testuser1")
out = provision.succeed("kanidm person get testuser2")
assert_contains(out, "name: testuser2")
out = provision.succeed("kanidm group get testgroup1")
assert_contains(out, "member: testuser1")
assert_contains(out, "member: testuser2")
provision.succeed("kanidm logout -D idm_admin")
'';
}