nixpkgs/nixos/tests/kanidm-provisioning.nix
2024-08-23 20:55:03 +02:00

519 lines
22 KiB
Nix

import ./make-test-python.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.kanidm.withSecretProvisioning;
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;
};
};
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}")
# 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")
'';
}
)