From aa6cbcbf092ddd7daa94f6b37aafdc9dcbc2cd13 Mon Sep 17 00:00:00 2001 From: oddlama Date: Fri, 23 Aug 2024 20:55:03 +0200 Subject: [PATCH] nixos/kanidm: run nixfmt-rfc-style --- nixos/modules/services/security/kanidm.nix | 722 ++++++++++++--------- nixos/tests/kanidm-provisioning.nix | 2 +- 2 files changed, 422 insertions(+), 302 deletions(-) diff --git a/nixos/modules/services/security/kanidm.nix b/nixos/modules/services/security/kanidm.nix index d17b0d22118b..dd86fab09a17 100644 --- a/nixos/modules/services/security/kanidm.nix +++ b/nixos/modules/services/security/kanidm.nix @@ -1,4 +1,10 @@ -{ config, lib, options, pkgs, ... }: +{ + config, + lib, + options, + pkgs, + ... +}: let cfg = config.services.kanidm; settingsFormat = pkgs.formats.toml { }; @@ -7,18 +13,29 @@ let serverConfigFile = settingsFormat.generate "server.toml" (filterConfig cfg.serverSettings); clientConfigFile = settingsFormat.generate "kanidm-config.toml" (filterConfig cfg.clientSettings); unixConfigFile = settingsFormat.generate "kanidm-unixd.toml" (filterConfig cfg.unixSettings); - certPaths = builtins.map builtins.dirOf [ cfg.serverSettings.tls_chain cfg.serverSettings.tls_key ]; + certPaths = builtins.map builtins.dirOf [ + cfg.serverSettings.tls_chain + cfg.serverSettings.tls_key + ]; # Merge bind mount paths and remove paths where a prefix is already mounted. # This makes sure that if e.g. the tls_chain is in the nix store and /nix/store is already in the mount # paths, no new bind mount is added. Adding subpaths caused problems on ofborg. - hasPrefixInList = list: newPath: lib.any (path: lib.hasPrefix (builtins.toString path) (builtins.toString newPath)) list; - mergePaths = lib.foldl' (merged: newPath: let + hasPrefixInList = + list: newPath: + lib.any (path: lib.hasPrefix (builtins.toString path) (builtins.toString newPath)) list; + mergePaths = lib.foldl' ( + merged: newPath: + let # If the new path is a prefix to some existing path, we need to filter it out - filteredPaths = lib.filter (p: !lib.hasPrefix (builtins.toString newPath) (builtins.toString p)) merged; + filteredPaths = lib.filter ( + p: !lib.hasPrefix (builtins.toString newPath) (builtins.toString p) + ) merged; # If a prefix of the new path is already in the list, do not add it filteredNew = lib.optional (!hasPrefixInList filteredPaths newPath) newPath; - in filteredPaths ++ filteredNew) []; + in + filteredPaths ++ filteredNew + ) [ ]; defaultServiceConfig = { BindReadOnlyPaths = [ @@ -28,7 +45,7 @@ let "-/etc/hosts" "-/etc/localtime" ]; - CapabilityBoundingSet = []; + CapabilityBoundingSet = [ ]; # ProtectClock= adds DeviceAllow=char-rtc r DeviceAllow = ""; # Implies ProtectSystem=strict, which re-mounts all paths @@ -57,12 +74,16 @@ let RestrictRealtime = true; RestrictSUIDSGID = true; SystemCallArchitectures = "native"; - SystemCallFilter = [ "@system-service" "~@privileged @resources @setuid @keyring" ]; + SystemCallFilter = [ + "@system-service" + "~@privileged @resources @setuid @keyring" + ]; # Does not work well with the temporary root #UMask = "0066"; }; - mkPresentOption = what: + mkPresentOption = + what: lib.mkOption { description = "Whether to ensure that this ${what} is present or absent."; type = lib.types.bool; @@ -71,9 +92,9 @@ let filterPresent = lib.filterAttrs (_: v: v.present); - provisionStateJson = pkgs.writeText "provision-state.json" (builtins.toJSON { - inherit (cfg.provision) groups persons systems; - }); + 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. @@ -89,28 +110,30 @@ let # 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 - ''; + 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 @@ -142,14 +165,15 @@ let serverPort = # ipv6: - if lib.hasInfix "]:" cfg.serverSettings.bindaddress - then lib.last (lib.splitString "]:" cfg.serverSettings.bindaddress) + 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"; + # ipv4: + if lib.hasInfix "." cfg.serverSettings.bindaddress then + lib.last (lib.splitString ":" cfg.serverSettings.bindaddress) + # default is 8443 + else + "8443"; in { options.services.kanidm = { @@ -157,7 +181,7 @@ in enableServer = lib.mkEnableOption "the Kanidm server"; enablePam = lib.mkEnableOption "the Kanidm PAM and NSS integration"; - package = lib.mkPackageOption pkgs "kanidm" {}; + package = lib.mkPackageOption pkgs "kanidm" { }; serverSettings = lib.mkOption { type = lib.types.submodule { @@ -213,12 +237,20 @@ in log_level = lib.mkOption { description = "Log level of the server."; default = "info"; - type = lib.types.enum [ "info" "debug" "trace" ]; + type = lib.types.enum [ + "info" + "debug" + "trace" + ]; }; role = lib.mkOption { description = "The role of this server. This affects the replication relationship and thereby available features."; default = "WriteReplica"; - type = lib.types.enum [ "WriteReplica" "WriteReplicaNoUI" "ReadOnlyReplica" ]; + type = lib.types.enum [ + "WriteReplica" + "WriteReplicaNoUI" + "ReadOnlyReplica" + ]; }; online_backup = { path = lib.mkOption { @@ -347,221 +379,248 @@ in groups = lib.mkOption { description = "Provisioning of kanidm groups"; - default = {}; - type = lib.types.attrsOf (lib.types.submodule (groupSubmod: { - options = { - present = mkPresentOption "group"; + 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 = []; + 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 - )); - })); + 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"; + 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"; - }; + 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; - }; + 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 = []; - }; + 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 = []; + 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"; + 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; + 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 = + let + originStrType = lib.types.strMatching ".*://.*/$"; + in + lib.types.either originStrType (lib.types.nonEmptyListOf originStrType); + 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); + }; + }; + } + ); + }; }; - - 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 = let - originStrType = lib.types.strMatching ".*://.*/$"; - in - lib.types.either originStrType (lib.types.nonEmptyListOf originStrType); - 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 = 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; + 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; + # 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}"; - }; + 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 + 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); + assertion = + !cfg.enableServer + || ((cfg.serverSettings.tls_chain or null) == null) + || (!lib.isStorePath cfg.serverSettings.tls_chain); message = '' points to a file in the Nix store. You should use a quoted absolute path to @@ -569,7 +628,10 @@ in ''; } { - assertion = !cfg.enableServer || ((cfg.serverSettings.tls_key or null) == null) || (!lib.isStorePath cfg.serverSettings.tls_key); + assertion = + !cfg.enableServer + || ((cfg.serverSettings.tls_key or null) == null) + || (!lib.isStorePath cfg.serverSettings.tls_key); message = '' points to a file in the Nix store. You should use a quoted absolute path to @@ -591,8 +653,12 @@ in ''; } { - assertion = !cfg.enableServer || (cfg.serverSettings.domain == null - -> cfg.serverSettings.role == "WriteReplica" || cfg.serverSettings.role == "WriteReplicaNoUI"); + assertion = + !cfg.enableServer + || ( + cfg.serverSettings.domain == null + -> cfg.serverSettings.role == "WriteReplica" || cfg.serverSettings.role == "WriteReplicaNoUI" + ); message = '' can only be set if this instance is not a ReadOnlyReplica. Otherwise the db would inherit it from @@ -605,63 +671,96 @@ in } # 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; + 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)}''; - }) + ( + 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: + ++ 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: + ++ 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: + ++ 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)) + (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"; - } - ])) - )); + ++ 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 ]; @@ -679,9 +778,12 @@ in after = [ "network.target" ]; serviceConfig = lib.mkMerge [ # Merge paths and ignore existing prefixes needs to sidestep mkMerge - (defaultServiceConfig // { - BindReadOnlyPaths = mergePaths (defaultServiceConfig.BindReadOnlyPaths ++ certPaths); - }) + ( + defaultServiceConfig + // { + BindReadOnlyPaths = mergePaths (defaultServiceConfig.BindReadOnlyPaths ++ certPaths); + } + ) { StateDirectory = "kanidm"; StateDirectoryMode = "0700"; @@ -704,7 +806,11 @@ in PrivateUsers = lib.mkForce false; # Port needs to be exposed to the host network PrivateNetwork = lib.mkForce false; - RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ]; TemporaryFileSystem = "/:ro"; } ]; @@ -715,7 +821,10 @@ in description = "Kanidm PAM daemon"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; - restartTriggers = [ unixConfigFile clientConfigFile ]; + restartTriggers = [ + unixConfigFile + clientConfigFile + ]; serviceConfig = lib.mkMerge [ defaultServiceConfig { @@ -740,7 +849,11 @@ in ]; # Needs to connect to kanidmd PrivateNetwork = lib.mkForce false; - RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ]; TemporaryFileSystem = "/:ro"; } ]; @@ -750,9 +863,15 @@ in systemd.services.kanidm-unixd-tasks = lib.mkIf cfg.enablePam { description = "Kanidm PAM home management daemon"; wantedBy = [ "multi-user.target" ]; - after = [ "network.target" "kanidm-unixd.service" ]; + after = [ + "network.target" + "kanidm-unixd.service" + ]; partOf = [ "kanidm-unixd.service" ]; - restartTriggers = [ unixConfigFile clientConfigFile ]; + restartTriggers = [ + unixConfigFile + clientConfigFile + ]; serviceConfig = { ExecStart = "${cfg.package}/bin/kanidm_unixd_tasks"; @@ -772,7 +891,12 @@ in "/run/kanidm-unixd:/var/run/kanidm-unixd" ]; # CAP_DAC_OVERRIDE is needed to ignore ownership of unixd socket - CapabilityBoundingSet = [ "CAP_CHOWN" "CAP_FOWNER" "CAP_DAC_OVERRIDE" "CAP_DAC_READ_SEARCH" ]; + CapabilityBoundingSet = [ + "CAP_CHOWN" + "CAP_FOWNER" + "CAP_DAC_OVERRIDE" + "CAP_DAC_READ_SEARCH" + ]; IPAddressDeny = "any"; # Need access to users PrivateUsers = false; @@ -787,15 +911,11 @@ in # These paths are hardcoded environment.etc = lib.mkMerge [ - (lib.mkIf cfg.enableServer { - "kanidm/server.toml".source = serverConfigFile; - }) + (lib.mkIf cfg.enableServer { "kanidm/server.toml".source = serverConfigFile; }) (lib.mkIf options.services.kanidm.clientSettings.isDefined { "kanidm/config".source = clientConfigFile; }) - (lib.mkIf cfg.enablePam { - "kanidm/unixd".source = unixConfigFile; - }) + (lib.mkIf cfg.enablePam { "kanidm/unixd".source = unixConfigFile; }) ]; system.nssModules = lib.mkIf cfg.enablePam [ cfg.package ]; @@ -804,12 +924,8 @@ in system.nssDatabases.passwd = lib.optional cfg.enablePam "kanidm"; users.groups = lib.mkMerge [ - (lib.mkIf cfg.enableServer { - kanidm = { }; - }) - (lib.mkIf cfg.enablePam { - kanidm-unixd = { }; - }) + (lib.mkIf cfg.enableServer { kanidm = { }; }) + (lib.mkIf cfg.enablePam { kanidm-unixd = { }; }) ]; users.users = lib.mkMerge [ (lib.mkIf cfg.enableServer { @@ -830,6 +946,10 @@ in ]; }; - meta.maintainers = with lib.maintainers; [ erictapen Flakebi oddlama ]; + meta.maintainers = with lib.maintainers; [ + erictapen + Flakebi + oddlama + ]; meta.buildDocsInSandbox = false; } diff --git a/nixos/tests/kanidm-provisioning.nix b/nixos/tests/kanidm-provisioning.nix index 96d8cce1412e..27176c2086fe 100644 --- a/nixos/tests/kanidm-provisioning.nix +++ b/nixos/tests/kanidm-provisioning.nix @@ -158,7 +158,7 @@ import ./make-test-python.nix ( groups.service1-admin = { }; systems.oauth2.service1 = { displayName = "Service One (changed)"; - # multiple origin urls + # multiple origin urls originUrl = [ "https://changed-one.example.com/" "https://changed-one.example.org/"