nixos/kanidm: add provisioning of groups, persons and oauth2 systems

This commit is contained in:
oddlama 2024-03-10 21:32:24 +01:00
parent 59a463f593
commit 558fa6abc6
No known key found for this signature in database
GPG Key ID: 14EFE510775FE39A
6 changed files with 928 additions and 9 deletions

View File

@ -332,6 +332,8 @@
- `nixosTests` now provide a working IPv6 setup for VLAN 1 by default.
- Kanidm can now be provisioned using the new [`services.kanidm.provision`] option, but requires using a patched version available via `pkgs.kanidm.withSecretProvisioning`.
- To facilitate dependency injection, the `imgui` package now builds a static archive using vcpkg' CMake rules.
The derivation now installs "impl" headers selectively instead of by a wildcard.
Use `imgui.src` if you just want to access the unpacked sources.

View File

@ -62,6 +62,94 @@ let
#UMask = "0066";
};
mkPresentOption = what:
lib.mkOption {
description = "Whether to ensure that this ${what} is present or absent.";
type = lib.types.bool;
default = true;
};
filterPresent = lib.filterAttrs (_: v: v.present);
provisionStateJson = pkgs.writeText "provision-state.json" (builtins.toJSON {
inherit (cfg.provision) groups persons systems;
});
# Only recover the admin account if a password should explicitly be provisioned
# for the account. Otherwise it is not needed for provisioning.
maybeRecoverAdmin = lib.optionalString (cfg.provision.adminPasswordFile != null) ''
KANIDM_ADMIN_PASSWORD=$(< ${cfg.provision.adminPasswordFile})
# We always reset the admin account password if a desired password was specified.
if ! KANIDM_RECOVER_ACCOUNT_PASSWORD=$KANIDM_ADMIN_PASSWORD ${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} admin --from-environment >/dev/null; then
echo "Failed to recover admin account" >&2
exit 1
fi
'';
# Recover the idm_admin account. If a password should explicitly be provisioned
# for the account we set it, otherwise we generate a new one because it is required
# for provisioning.
recoverIdmAdmin = if cfg.provision.idmAdminPasswordFile != null
then ''
KANIDM_IDM_ADMIN_PASSWORD=$(< ${cfg.provision.idmAdminPasswordFile})
# We always reset the idm_admin account password if a desired password was specified.
if ! KANIDM_RECOVER_ACCOUNT_PASSWORD=$KANIDM_IDM_ADMIN_PASSWORD ${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} idm_admin --from-environment >/dev/null; then
echo "Failed to recover idm_admin account" >&2
exit 1
fi
''
else ''
# Recover idm_admin account
if ! recover_out=$(${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} idm_admin -o json); then
echo "$recover_out" >&2
echo "kanidm provision: Failed to recover admin account" >&2
exit 1
fi
if ! KANIDM_IDM_ADMIN_PASSWORD=$(grep '{"password' <<< "$recover_out" | ${lib.getExe pkgs.jq} -r .password); then
echo "$recover_out" >&2
echo "kanidm provision: Failed to parse password for idm_admin account" >&2
exit 1
fi
'';
postStartScript = pkgs.writeShellScript "post-start" ''
set -euo pipefail
# Wait for the kanidm server to come online
count=0
while ! ${lib.getExe pkgs.curl} -L --silent --max-time 1 --connect-timeout 1 --fail \
${lib.optionalString cfg.provision.acceptInvalidCerts "--insecure"} \
${cfg.provision.instanceUrl} >/dev/null
do
sleep 1
if [[ "$count" -eq 30 ]]; then
echo "Tried for at least 30 seconds, giving up..."
exit 1
fi
count=$((count++))
done
${recoverIdmAdmin}
${maybeRecoverAdmin}
KANIDM_PROVISION_IDM_ADMIN_TOKEN=$KANIDM_IDM_ADMIN_PASSWORD \
${lib.getExe pkgs.kanidm-provision} \
${lib.optionalString (!cfg.provision.autoRemove) "--no-auto-remove"} \
${lib.optionalString cfg.provision.acceptInvalidCerts "--accept-invalid-certs"} \
--url "${cfg.provision.instanceUrl}" \
--state ${provisionStateJson}
'';
serverPort =
# ipv6:
if lib.hasInfix "]:" cfg.serverSettings.bindaddress
then lib.last (lib.splitString "]:" cfg.serverSettings.bindaddress)
else
# ipv4:
if lib.hasInfix "." cfg.serverSettings.bindaddress
then lib.last (lib.splitString ":" cfg.serverSettings.bindaddress)
# default is 8443
else "8443";
in
{
options.services.kanidm = {
@ -207,10 +295,267 @@ in
for possible values.
'';
};
provision = {
enable = lib.mkEnableOption "provisioning of groups, users and oauth2 resource servers";
instanceUrl = lib.mkOption {
description = "The instance url to which the provisioning tool should connect.";
default = "https://localhost:${serverPort}";
defaultText = ''"https://localhost:<port from serverSettings.bindaddress>"'';
type = lib.types.str;
};
acceptInvalidCerts = lib.mkOption {
description = ''
Whether to allow invalid certificates when provisioning the target instance.
By default this is only allowed when the instanceUrl is localhost. This is
dangerous when used with an external URL.
'';
type = lib.types.bool;
default = lib.hasPrefix "https://localhost:" cfg.provision.instanceUrl;
defaultText = ''lib.hasPrefix "https://localhost:" cfg.provision.instanceUrl'';
};
adminPasswordFile = lib.mkOption {
description = "Path to a file containing the admin password for kanidm. Do NOT use a file from the nix store here!";
example = "/run/secrets/kanidm-admin-password";
default = null;
type = lib.types.nullOr lib.types.path;
};
idmAdminPasswordFile = lib.mkOption {
description = ''
Path to a file containing the idm admin password for kanidm. Do NOT use a file from the nix store here!
If this is not given but provisioning is enabled, the idm_admin password will be reset on each restart.
'';
example = "/run/secrets/kanidm-idm-admin-password";
default = null;
type = lib.types.nullOr lib.types.path;
};
autoRemove = lib.mkOption {
description = ''
Determines whether deleting an entity in this provisioning config should automatically
cause them to be removed from kanidm, too. This works because the provisioning tool tracks
all entities it has ever created. If this is set to false, you need to explicitly specify
`present = false` to delete an entity.
'';
type = lib.types.bool;
default = true;
};
groups = lib.mkOption {
description = "Provisioning of kanidm groups";
default = {};
type = lib.types.attrsOf (lib.types.submodule (groupSubmod: {
options = {
present = mkPresentOption "group";
members = lib.mkOption {
description = "List of kanidm entities (persons, groups, ...) which are part of this group.";
type = lib.types.listOf lib.types.str;
apply = lib.unique;
default = [];
};
};
config.members = lib.concatLists (lib.flip lib.mapAttrsToList cfg.provision.persons (person: personCfg:
lib.optional (personCfg.present && builtins.elem groupSubmod.config._module.args.name personCfg.groups) person
));
}));
};
persons = lib.mkOption {
description = "Provisioning of kanidm persons";
default = {};
type = lib.types.attrsOf (lib.types.submodule {
options = {
present = mkPresentOption "person";
displayName = lib.mkOption {
description = "Display name";
type = lib.types.str;
example = "My User";
};
legalName = lib.mkOption {
description = "Full legal name";
type = lib.types.nullOr lib.types.str;
example = "Jane Doe";
default = null;
};
mailAddresses = lib.mkOption {
description = "Mail addresses. First given address is considered the primary address.";
type = lib.types.listOf lib.types.str;
example = ["jane.doe@example.com"];
default = [];
};
groups = lib.mkOption {
description = "List of groups this person should belong to.";
type = lib.types.listOf lib.types.str;
apply = lib.unique;
default = [];
};
};
});
};
systems.oauth2 = lib.mkOption {
description = "Provisioning of oauth2 resource servers";
default = {};
type = lib.types.attrsOf (lib.types.submodule {
options = {
present = mkPresentOption "oauth2 resource server";
public = lib.mkOption {
description = "Whether this is a public client (enforces PKCE, doesn't use a basic secret)";
type = lib.types.bool;
default = false;
};
displayName = lib.mkOption {
description = "Display name";
type = lib.types.str;
example = "Some Service";
};
originUrl = lib.mkOption {
description = "The origin URL of the service. OAuth2 redirects will only be allowed to sites under this origin. Must end with a slash.";
type = lib.types.strMatching ".*://.*/$";
example = "https://someservice.example.com/";
};
originLanding = lib.mkOption {
description = "When redirecting from the Kanidm Apps Listing page, some linked applications may need to land on a specific page to trigger oauth2/oidc interactions.";
type = lib.types.str;
example = "https://someservice.example.com/home";
};
basicSecretFile = lib.mkOption {
description = ''
The basic secret to use for this service. If null, the random secret generated
by kanidm will not be touched. Do NOT use a path from the nix store here!
'';
type = lib.types.nullOr lib.types.path;
example = "/run/secrets/some-oauth2-basic-secret";
default = null;
};
enableLocalhostRedirects = lib.mkOption {
description = "Allow localhost redirects. Only for public clients.";
type = lib.types.bool;
default = false;
};
enableLegacyCrypto = lib.mkOption {
description = "Enable legacy crypto on this client. Allows JWT signing algorthms like RS256.";
type = lib.types.bool;
default = false;
};
allowInsecureClientDisablePkce = lib.mkOption {
description = ''
Disable PKCE on this oauth2 resource server to work around insecure clients
that may not support it. You should request the client to enable PKCE!
Only for non-public clients.
'';
type = lib.types.bool;
default = false;
};
preferShortUsername = lib.mkOption {
description = "Use 'name' instead of 'spn' in the preferred_username claim";
type = lib.types.bool;
default = false;
};
scopeMaps = lib.mkOption {
description = ''
Maps kanidm groups to returned oauth scopes.
See [Scope Relations](https://kanidm.github.io/kanidm/stable/integrations/oauth2.html#scope-relationships) for more information.
'';
type = lib.types.attrsOf (lib.types.listOf lib.types.str);
default = {};
};
supplementaryScopeMaps = lib.mkOption {
description = ''
Maps kanidm groups to additionally returned oauth scopes.
See [Scope Relations](https://kanidm.github.io/kanidm/stable/integrations/oauth2.html#scope-relationships) for more information.
'';
type = lib.types.attrsOf (lib.types.listOf lib.types.str);
default = {};
};
removeOrphanedClaimMaps = lib.mkOption {
description = "Whether claim maps not specified here but present in kanidm should be removed from kanidm.";
type = lib.types.bool;
default = true;
};
claimMaps = lib.mkOption {
description = ''
Adds additional claims (and values) based on which kanidm groups an authenticating party belongs to.
See [Claim Maps](https://kanidm.github.io/kanidm/master/integrations/oauth2.html#custom-claim-maps) for more information.
'';
default = {};
type = lib.types.attrsOf (lib.types.submodule {
options = {
joinType = lib.mkOption {
description = ''
Determines how multiple values are joined to create the claim value.
See [Claim Maps](https://kanidm.github.io/kanidm/master/integrations/oauth2.html#custom-claim-maps) for more information.
'';
type = lib.types.enum ["array" "csv" "ssv"];
default = "array";
};
valuesByGroup = lib.mkOption {
description = "Maps kanidm groups to values for the claim.";
default = {};
type = lib.types.attrsOf (lib.types.listOf lib.types.str);
};
};
});
};
};
});
};
};
};
config = lib.mkIf (cfg.enableClient || cfg.enableServer || cfg.enablePam) {
assertions =
assertions = let
entityList = type: attrs: lib.flip lib.mapAttrsToList (filterPresent attrs) (name: _: { inherit type name; });
entities =
entityList "group" cfg.provision.groups
++ entityList "person" cfg.provision.persons
++ entityList "oauth2" cfg.provision.systems.oauth2;
# Accumulate entities by name. Track corresponding entity types for later duplicate check.
entitiesByName = lib.foldl' (acc: { type, name }:
acc // {
${name} = (acc.${name} or []) ++ [type];
}
) {} entities;
assertGroupsKnown = opt: groups: let
knownGroups = lib.attrNames (filterPresent cfg.provision.groups);
unknownGroups = lib.subtractLists knownGroups groups;
in {
assertion = (cfg.enableServer && cfg.provision.enable) -> unknownGroups == [];
message = "${opt} refers to unknown groups: ${toString unknownGroups}";
};
assertEntitiesKnown = opt: entities: let
unknownEntities = lib.subtractLists (lib.attrNames entitiesByName) entities;
in {
assertion = (cfg.enableServer && cfg.provision.enable) -> unknownEntities == [];
message = "${opt} refers to unknown entities: ${toString unknownEntities}";
};
in
[
{
assertion = !cfg.enableServer || ((cfg.serverSettings.tls_chain or null) == null) || (!lib.isStorePath cfg.serverSettings.tls_chain);
@ -251,7 +596,69 @@ in
the instance it follows.
'';
}
];
{
assertion = cfg.provision.enable -> cfg.enableServer;
message = "<option>services.kanidm.provision</option> requires <option>services.kanidm.enableServer</option> to be true";
}
# If any secret is provisioned, the kanidm package must have some required patches applied to it
{
assertion = (cfg.provision.enable &&
(cfg.provision.adminPasswordFile != null
|| cfg.provision.idmAdminPasswordFile != null
|| lib.any (x: x.basicSecretFile != null) (lib.attrValues (filterPresent cfg.provision.systems.oauth2))
)) -> cfg.package.enableSecretProvisioning;
message = ''
Specifying an admin account password or oauth2 basicSecretFile requires kanidm to be built with the secret provisioning patches.
You may want to set `services.kanidm.package = pkgs.kanidm.withSecretProvisioning;`.
'';
}
# Entity names must be globally unique:
(let
# Filter all names that occurred in more than one entity type.
duplicateNames = lib.filterAttrs (_: v: builtins.length v > 1) entitiesByName;
in {
assertion = cfg.provision.enable -> duplicateNames == {};
message = ''
services.kanidm.provision requires all entity names (group, person, oauth2, ...) to be unique!
${lib.concatLines (lib.mapAttrsToList (name: xs: " - '${name}' used as: ${toString xs}") duplicateNames)}'';
})
]
++ lib.flip lib.mapAttrsToList (filterPresent cfg.provision.persons) (person: personCfg:
assertGroupsKnown "services.kanidm.provision.persons.${person}.groups" personCfg.groups
)
++ lib.flip lib.mapAttrsToList (filterPresent cfg.provision.groups) (group: groupCfg:
assertEntitiesKnown "services.kanidm.provision.groups.${group}.members" groupCfg.members
)
++ lib.concatLists (lib.flip lib.mapAttrsToList (filterPresent cfg.provision.systems.oauth2) (
oauth2: oauth2Cfg:
[
(assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.scopeMaps" (lib.attrNames oauth2Cfg.scopeMaps))
(assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.supplementaryScopeMaps" (lib.attrNames oauth2Cfg.supplementaryScopeMaps))
]
++ lib.concatLists (lib.flip lib.mapAttrsToList oauth2Cfg.claimMaps (claim: claimCfg: [
(assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim}.valuesByGroup" (lib.attrNames claimCfg.valuesByGroup))
# At least one group must map to a value in each claim map
{
assertion = (cfg.provision.enable && cfg.enableServer) -> lib.any (xs: xs != []) (lib.attrValues claimCfg.valuesByGroup);
message = "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim} does not specify any values for any group";
}
# Public clients cannot define a basic secret
{
assertion = (cfg.provision.enable && cfg.enableServer && oauth2Cfg.public) -> oauth2Cfg.basicSecretFile == null;
message = "services.kanidm.provision.systems.oauth2.${oauth2} is a public client and thus cannot specify a basic secret";
}
# Public clients cannot disable PKCE
{
assertion = (cfg.provision.enable && cfg.enableServer && oauth2Cfg.public) -> !oauth2Cfg.allowInsecureClientDisablePkce;
message = "services.kanidm.provision.systems.oauth2.${oauth2} is a public client and thus cannot disable PKCE";
}
# Non-public clients cannot enable localhost redirects
{
assertion = (cfg.provision.enable && cfg.enableServer && !oauth2Cfg.public) -> !oauth2Cfg.enableLocalhostRedirects;
message = "services.kanidm.provision.systems.oauth2.${oauth2} is a non-public client and thus cannot enable localhost redirects";
}
]))
));
environment.systemPackages = lib.mkIf cfg.enableClient [ cfg.package ];
@ -277,6 +684,7 @@ in
StateDirectoryMode = "0700";
RuntimeDirectory = "kanidmd";
ExecStart = "${cfg.package}/bin/kanidmd server -c ${serverConfigFile}";
ExecStartPost = lib.mkIf cfg.provision.enable postStartScript;
User = "kanidm";
Group = "kanidm";
@ -419,6 +827,6 @@ in
];
};
meta.maintainers = with lib.maintainers; [ erictapen Flakebi ];
meta.maintainers = with lib.maintainers; [ erictapen Flakebi oddlama ];
meta.buildDocsInSandbox = false;
}

View File

@ -484,6 +484,7 @@ in {
k3s = handleTest ./k3s {};
kafka = handleTest ./kafka.nix {};
kanidm = handleTest ./kanidm.nix {};
kanidm-provisioning = handleTest ./kanidm-provisioning.nix {};
karma = handleTest ./karma.nix {};
kavita = handleTest ./kavita.nix {};
kbd-setfont-decompress = handleTest ./kbd-setfont-decompress.nix {};

View File

@ -0,0 +1,505 @@
import ./make-test-python.nix (
{ pkgs, ... }:
let
certs = import ./common/acme/server/snakeoil-certs.nix;
serverDomain = certs.domain;
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 = certs."${serverDomain}".cert;
tls_key = certs."${serverDomain}".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)";
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";
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_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_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")
'';
}
)

View File

@ -9,9 +9,9 @@ import ./make-test-python.nix ({ pkgs, ... }:
in
{
name = "kanidm";
meta.maintainers = with pkgs.lib.maintainers; [ erictapen Flakebi ];
meta.maintainers = with pkgs.lib.maintainers; [ erictapen Flakebi oddlama ];
nodes.server = { config, pkgs, lib, ... }: {
nodes.server = { pkgs, ... }: {
services.kanidm = {
enableServer = true;
serverSettings = {
@ -34,7 +34,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
environment.systemPackages = with pkgs; [ kanidm openldap ripgrep ];
};
nodes.client = { pkgs, nodes, ... }: {
nodes.client = { nodes, ... }: {
services.kanidm = {
enableClient = true;
clientSettings = {
@ -62,10 +62,10 @@ import ./make-test-python.nix ({ pkgs, ... }:
(pkgs.lib.filterAttrsRecursive (_: v: v != null))
nodes.server.services.kanidm.serverSettings;
serverConfigFile = (pkgs.formats.toml { }).generate "server.toml" filteredConfig;
in
''
start_all()
server.start()
client.start()
server.wait_for_unit("kanidm.service")
client.systemctl("start network-online.target")
client.wait_for_unit("network-online.target")
@ -122,5 +122,8 @@ import ./make-test-python.nix ({ pkgs, ... }:
client.wait_until_succeeds("systemctl is-active user@$(id -u testuser).service")
client.send_chars("touch done\n")
client.wait_for_file("/home/testuser@${serverDomain}/done")
server.shutdown()
client.shutdown()
'';
})

View File

@ -107,7 +107,7 @@ rustPlatform.buildRustPackage rec {
passthru = {
tests = {
inherit (nixosTests) kanidm;
inherit (nixosTests) kanidm kanidm-provisioning;
};
updateScript = nix-update-script { };