1
0
Fork 1
mirror of https://github.com/NixOS/nixpkgs.git synced 2024-11-17 19:21:04 +00:00

nixos/kanidm: inherit lib, nixfmt

This commit is contained in:
TheRealGramdalf 2024-08-26 18:01:58 +00:00
parent e87eca7eb7
commit 8f18393d38

View file

@ -6,10 +6,42 @@
... ...
}: }:
let let
inherit (lib)
any
attrNames
attrValues
concatLines
concatLists
converge
filter
filterAttrs
filterAttrsRecursive
flip
foldl'
getExe
hasInfix
hasPrefix
isStorePath
last
mapAttrsToList
mkEnableOption
mkForce
mkIf
mkMerge
mkOption
mkPackageOption
optional
optionalString
splitString
subtractLists
types
unique
;
cfg = config.services.kanidm; cfg = config.services.kanidm;
settingsFormat = pkgs.formats.toml { }; settingsFormat = pkgs.formats.toml { };
# Remove null values, so we can document optional values that don't end up in the generated TOML file. # Remove null values, so we can document optional values that don't end up in the generated TOML file.
filterConfig = lib.converge (lib.filterAttrsRecursive (_: v: v != null)); filterConfig = converge (filterAttrsRecursive (_: v: v != null));
serverConfigFile = settingsFormat.generate "server.toml" (filterConfig cfg.serverSettings); serverConfigFile = settingsFormat.generate "server.toml" (filterConfig cfg.serverSettings);
clientConfigFile = settingsFormat.generate "kanidm-config.toml" (filterConfig cfg.clientSettings); clientConfigFile = settingsFormat.generate "kanidm-config.toml" (filterConfig cfg.clientSettings);
unixConfigFile = settingsFormat.generate "kanidm-unixd.toml" (filterConfig cfg.unixSettings); unixConfigFile = settingsFormat.generate "kanidm-unixd.toml" (filterConfig cfg.unixSettings);
@ -22,17 +54,14 @@ let
# This makes sure that if e.g. the tls_chain is in the nix store and /nix/store is already in the mount # 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. # paths, no new bind mount is added. Adding subpaths caused problems on ofborg.
hasPrefixInList = hasPrefixInList =
list: newPath: list: newPath: any (path: hasPrefix (builtins.toString path) (builtins.toString newPath)) list;
lib.any (path: lib.hasPrefix (builtins.toString path) (builtins.toString newPath)) list; mergePaths = foldl' (
mergePaths = lib.foldl' (
merged: newPath: merged: newPath:
let let
# If the new path is a prefix to some existing path, we need to filter it out # If the new path is a prefix to some existing path, we need to filter it out
filteredPaths = lib.filter ( filteredPaths = filter (p: !hasPrefix (builtins.toString newPath) (builtins.toString p)) merged;
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 # If a prefix of the new path is already in the list, do not add it
filteredNew = lib.optional (!hasPrefixInList filteredPaths newPath) newPath; filteredNew = optional (!hasPrefixInList filteredPaths newPath) newPath;
in in
filteredPaths ++ filteredNew filteredPaths ++ filteredNew
) [ ]; ) [ ];
@ -84,13 +113,13 @@ let
mkPresentOption = mkPresentOption =
what: what:
lib.mkOption { mkOption {
description = "Whether to ensure that this ${what} is present or absent."; description = "Whether to ensure that this ${what} is present or absent.";
type = lib.types.bool; type = types.bool;
default = true; default = true;
}; };
filterPresent = lib.filterAttrs (_: v: v.present); filterPresent = filterAttrs (_: v: v.present);
provisionStateJson = pkgs.writeText "provision-state.json" ( provisionStateJson = pkgs.writeText "provision-state.json" (
builtins.toJSON { inherit (cfg.provision) groups persons systems; } builtins.toJSON { inherit (cfg.provision) groups persons systems; }
@ -98,7 +127,7 @@ let
# Only recover the admin account if a password should explicitly be provisioned # Only recover the admin account if a password should explicitly be provisioned
# for the account. Otherwise it is not needed for provisioning. # for the account. Otherwise it is not needed for provisioning.
maybeRecoverAdmin = lib.optionalString (cfg.provision.adminPasswordFile != null) '' maybeRecoverAdmin = optionalString (cfg.provision.adminPasswordFile != null) ''
KANIDM_ADMIN_PASSWORD=$(< ${cfg.provision.adminPasswordFile}) KANIDM_ADMIN_PASSWORD=$(< ${cfg.provision.adminPasswordFile})
# We always reset the admin account password if a desired password was specified. # 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 if ! KANIDM_RECOVER_ACCOUNT_PASSWORD=$KANIDM_ADMIN_PASSWORD ${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} admin --from-environment >/dev/null; then
@ -128,7 +157,7 @@ let
echo "kanidm provision: Failed to recover admin account" >&2 echo "kanidm provision: Failed to recover admin account" >&2
exit 1 exit 1
fi fi
if ! KANIDM_IDM_ADMIN_PASSWORD=$(grep '{"password' <<< "$recover_out" | ${lib.getExe pkgs.jq} -r .password); then if ! KANIDM_IDM_ADMIN_PASSWORD=$(grep '{"password' <<< "$recover_out" | ${getExe pkgs.jq} -r .password); then
echo "$recover_out" >&2 echo "$recover_out" >&2
echo "kanidm provision: Failed to parse password for idm_admin account" >&2 echo "kanidm provision: Failed to parse password for idm_admin account" >&2
exit 1 exit 1
@ -140,8 +169,8 @@ let
# Wait for the kanidm server to come online # Wait for the kanidm server to come online
count=0 count=0
while ! ${lib.getExe pkgs.curl} -L --silent --max-time 1 --connect-timeout 1 --fail \ while ! ${getExe pkgs.curl} -L --silent --max-time 1 --connect-timeout 1 --fail \
${lib.optionalString cfg.provision.acceptInvalidCerts "--insecure"} \ ${optionalString cfg.provision.acceptInvalidCerts "--insecure"} \
${cfg.provision.instanceUrl} >/dev/null ${cfg.provision.instanceUrl} >/dev/null
do do
sleep 1 sleep 1
@ -156,58 +185,58 @@ let
${maybeRecoverAdmin} ${maybeRecoverAdmin}
KANIDM_PROVISION_IDM_ADMIN_TOKEN=$KANIDM_IDM_ADMIN_PASSWORD \ KANIDM_PROVISION_IDM_ADMIN_TOKEN=$KANIDM_IDM_ADMIN_PASSWORD \
${lib.getExe pkgs.kanidm-provision} \ ${getExe pkgs.kanidm-provision} \
${lib.optionalString (!cfg.provision.autoRemove) "--no-auto-remove"} \ ${optionalString (!cfg.provision.autoRemove) "--no-auto-remove"} \
${lib.optionalString cfg.provision.acceptInvalidCerts "--accept-invalid-certs"} \ ${optionalString cfg.provision.acceptInvalidCerts "--accept-invalid-certs"} \
--url "${cfg.provision.instanceUrl}" \ --url "${cfg.provision.instanceUrl}" \
--state ${provisionStateJson} --state ${provisionStateJson}
''; '';
serverPort = serverPort =
# ipv6: # ipv6:
if lib.hasInfix "]:" cfg.serverSettings.bindaddress then if hasInfix "]:" cfg.serverSettings.bindaddress then
lib.last (lib.splitString "]:" cfg.serverSettings.bindaddress) last (splitString "]:" cfg.serverSettings.bindaddress)
else else
# ipv4: # ipv4:
if lib.hasInfix "." cfg.serverSettings.bindaddress then if hasInfix "." cfg.serverSettings.bindaddress then
lib.last (lib.splitString ":" cfg.serverSettings.bindaddress) last (splitString ":" cfg.serverSettings.bindaddress)
# default is 8443 # default is 8443
else else
"8443"; "8443";
in in
{ {
options.services.kanidm = { options.services.kanidm = {
enableClient = lib.mkEnableOption "the Kanidm client"; enableClient = mkEnableOption "the Kanidm client";
enableServer = lib.mkEnableOption "the Kanidm server"; enableServer = mkEnableOption "the Kanidm server";
enablePam = lib.mkEnableOption "the Kanidm PAM and NSS integration"; enablePam = mkEnableOption "the Kanidm PAM and NSS integration";
package = lib.mkPackageOption pkgs "kanidm" { }; package = mkPackageOption pkgs "kanidm" { };
serverSettings = lib.mkOption { serverSettings = mkOption {
type = lib.types.submodule { type = types.submodule {
freeformType = settingsFormat.type; freeformType = settingsFormat.type;
options = { options = {
bindaddress = lib.mkOption { bindaddress = mkOption {
description = "Address/port combination the webserver binds to."; description = "Address/port combination the webserver binds to.";
example = "[::1]:8443"; example = "[::1]:8443";
type = lib.types.str; type = types.str;
}; };
# Should be optional but toml does not accept null # Should be optional but toml does not accept null
ldapbindaddress = lib.mkOption { ldapbindaddress = mkOption {
description = '' description = ''
Address and port the LDAP server is bound to. Setting this to `null` disables the LDAP interface. Address and port the LDAP server is bound to. Setting this to `null` disables the LDAP interface.
''; '';
example = "[::1]:636"; example = "[::1]:636";
default = null; default = null;
type = lib.types.nullOr lib.types.str; type = types.nullOr types.str;
}; };
origin = lib.mkOption { origin = mkOption {
description = "The origin of your Kanidm instance. Must have https as protocol."; description = "The origin of your Kanidm instance. Must have https as protocol.";
example = "https://idm.example.org"; example = "https://idm.example.org";
type = lib.types.strMatching "^https://.*"; type = types.strMatching "^https://.*";
}; };
domain = lib.mkOption { domain = mkOption {
description = '' description = ''
The `domain` that Kanidm manages. Must be below or equal to the domain The `domain` that Kanidm manages. Must be below or equal to the domain
specified in `serverSettings.origin`. specified in `serverSettings.origin`.
@ -218,58 +247,58 @@ in
''; '';
example = "example.org"; example = "example.org";
default = null; default = null;
type = lib.types.nullOr lib.types.str; type = types.nullOr types.str;
}; };
db_path = lib.mkOption { db_path = mkOption {
description = "Path to Kanidm database."; description = "Path to Kanidm database.";
default = "/var/lib/kanidm/kanidm.db"; default = "/var/lib/kanidm/kanidm.db";
readOnly = true; readOnly = true;
type = lib.types.path; type = types.path;
}; };
tls_chain = lib.mkOption { tls_chain = mkOption {
description = "TLS chain in pem format."; description = "TLS chain in pem format.";
type = lib.types.path; type = types.path;
}; };
tls_key = lib.mkOption { tls_key = mkOption {
description = "TLS key in pem format."; description = "TLS key in pem format.";
type = lib.types.path; type = types.path;
}; };
log_level = lib.mkOption { log_level = mkOption {
description = "Log level of the server."; description = "Log level of the server.";
default = "info"; default = "info";
type = lib.types.enum [ type = types.enum [
"info" "info"
"debug" "debug"
"trace" "trace"
]; ];
}; };
role = lib.mkOption { role = mkOption {
description = "The role of this server. This affects the replication relationship and thereby available features."; description = "The role of this server. This affects the replication relationship and thereby available features.";
default = "WriteReplica"; default = "WriteReplica";
type = lib.types.enum [ type = types.enum [
"WriteReplica" "WriteReplica"
"WriteReplicaNoUI" "WriteReplicaNoUI"
"ReadOnlyReplica" "ReadOnlyReplica"
]; ];
}; };
online_backup = { online_backup = {
path = lib.mkOption { path = mkOption {
description = "Path to the output directory for backups."; description = "Path to the output directory for backups.";
type = lib.types.path; type = types.path;
default = "/var/lib/kanidm/backups"; default = "/var/lib/kanidm/backups";
}; };
schedule = lib.mkOption { schedule = mkOption {
description = "The schedule for backups in cron format."; description = "The schedule for backups in cron format.";
type = lib.types.str; type = types.str;
default = "00 22 * * *"; default = "00 22 * * *";
}; };
versions = lib.mkOption { versions = mkOption {
description = '' description = ''
Number of backups to keep. Number of backups to keep.
The default is set to `0`, in order to disable backups by default. The default is set to `0`, in order to disable backups by default.
''; '';
type = lib.types.ints.unsigned; type = types.ints.unsigned;
default = 0; default = 0;
example = 7; example = 7;
}; };
@ -285,14 +314,14 @@ in
''; '';
}; };
clientSettings = lib.mkOption { clientSettings = mkOption {
type = lib.types.submodule { type = types.submodule {
freeformType = settingsFormat.type; freeformType = settingsFormat.type;
options.uri = lib.mkOption { options.uri = mkOption {
description = "Address of the Kanidm server."; description = "Address of the Kanidm server.";
example = "http://127.0.0.1:8080"; example = "http://127.0.0.1:8080";
type = lib.types.str; type = types.str;
}; };
}; };
description = '' description = ''
@ -303,20 +332,20 @@ in
''; '';
}; };
unixSettings = lib.mkOption { unixSettings = mkOption {
type = lib.types.submodule { type = types.submodule {
freeformType = settingsFormat.type; freeformType = settingsFormat.type;
options = { options = {
pam_allowed_login_groups = lib.mkOption { pam_allowed_login_groups = mkOption {
description = "Kanidm groups that are allowed to login using PAM."; description = "Kanidm groups that are allowed to login using PAM.";
example = "my_pam_group"; example = "my_pam_group";
type = lib.types.listOf lib.types.str; type = types.listOf types.str;
}; };
hsm_pin_path = lib.mkOption { hsm_pin_path = mkOption {
description = "Path to a HSM pin."; description = "Path to a HSM pin.";
default = "/var/cache/kanidm-unixd/hsm-pin"; default = "/var/cache/kanidm-unixd/hsm-pin";
type = lib.types.path; type = types.path;
}; };
}; };
}; };
@ -329,73 +358,73 @@ in
}; };
provision = { provision = {
enable = lib.mkEnableOption "provisioning of groups, users and oauth2 resource servers"; enable = mkEnableOption "provisioning of groups, users and oauth2 resource servers";
instanceUrl = lib.mkOption { instanceUrl = mkOption {
description = "The instance url to which the provisioning tool should connect."; description = "The instance url to which the provisioning tool should connect.";
default = "https://localhost:${serverPort}"; default = "https://localhost:${serverPort}";
defaultText = ''"https://localhost:<port from serverSettings.bindaddress>"''; defaultText = ''"https://localhost:<port from serverSettings.bindaddress>"'';
type = lib.types.str; type = types.str;
}; };
acceptInvalidCerts = lib.mkOption { acceptInvalidCerts = mkOption {
description = '' description = ''
Whether to allow invalid certificates when provisioning the target instance. Whether to allow invalid certificates when provisioning the target instance.
By default this is only allowed when the instanceUrl is localhost. This is By default this is only allowed when the instanceUrl is localhost. This is
dangerous when used with an external URL. dangerous when used with an external URL.
''; '';
type = lib.types.bool; type = types.bool;
default = lib.hasPrefix "https://localhost:" cfg.provision.instanceUrl; default = hasPrefix "https://localhost:" cfg.provision.instanceUrl;
defaultText = ''lib.hasPrefix "https://localhost:" cfg.provision.instanceUrl''; defaultText = ''hasPrefix "https://localhost:" cfg.provision.instanceUrl'';
}; };
adminPasswordFile = lib.mkOption { adminPasswordFile = mkOption {
description = "Path to a file containing the admin password for kanidm. Do NOT use a file from the nix store here!"; 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"; example = "/run/secrets/kanidm-admin-password";
default = null; default = null;
type = lib.types.nullOr lib.types.path; type = types.nullOr types.path;
}; };
idmAdminPasswordFile = lib.mkOption { idmAdminPasswordFile = mkOption {
description = '' description = ''
Path to a file containing the idm admin password for kanidm. Do NOT use a file from the nix store here! 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. 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"; example = "/run/secrets/kanidm-idm-admin-password";
default = null; default = null;
type = lib.types.nullOr lib.types.path; type = types.nullOr types.path;
}; };
autoRemove = lib.mkOption { autoRemove = mkOption {
description = '' description = ''
Determines whether deleting an entity in this provisioning config should automatically 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 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 all entities it has ever created. If this is set to false, you need to explicitly specify
`present = false` to delete an entity. `present = false` to delete an entity.
''; '';
type = lib.types.bool; type = types.bool;
default = true; default = true;
}; };
groups = lib.mkOption { groups = mkOption {
description = "Provisioning of kanidm groups"; description = "Provisioning of kanidm groups";
default = { }; default = { };
type = lib.types.attrsOf ( type = types.attrsOf (
lib.types.submodule (groupSubmod: { types.submodule (groupSubmod: {
options = { options = {
present = mkPresentOption "group"; present = mkPresentOption "group";
members = lib.mkOption { members = mkOption {
description = "List of kanidm entities (persons, groups, ...) which are part of this group."; description = "List of kanidm entities (persons, groups, ...) which are part of this group.";
type = lib.types.listOf lib.types.str; type = types.listOf types.str;
apply = lib.unique; apply = unique;
default = [ ]; default = [ ];
}; };
}; };
config.members = lib.concatLists ( config.members = concatLists (
lib.flip lib.mapAttrsToList cfg.provision.persons ( flip mapAttrsToList cfg.provision.persons (
person: personCfg: person: personCfg:
lib.optional ( optional (
personCfg.present && builtins.elem groupSubmod.config._module.args.name personCfg.groups personCfg.present && builtins.elem groupSubmod.config._module.args.name personCfg.groups
) person ) person
) )
@ -404,38 +433,38 @@ in
); );
}; };
persons = lib.mkOption { persons = mkOption {
description = "Provisioning of kanidm persons"; description = "Provisioning of kanidm persons";
default = { }; default = { };
type = lib.types.attrsOf ( type = types.attrsOf (
lib.types.submodule { types.submodule {
options = { options = {
present = mkPresentOption "person"; present = mkPresentOption "person";
displayName = lib.mkOption { displayName = mkOption {
description = "Display name"; description = "Display name";
type = lib.types.str; type = types.str;
example = "My User"; example = "My User";
}; };
legalName = lib.mkOption { legalName = mkOption {
description = "Full legal name"; description = "Full legal name";
type = lib.types.nullOr lib.types.str; type = types.nullOr types.str;
example = "Jane Doe"; example = "Jane Doe";
default = null; default = null;
}; };
mailAddresses = lib.mkOption { mailAddresses = mkOption {
description = "Mail addresses. First given address is considered the primary address."; description = "Mail addresses. First given address is considered the primary address.";
type = lib.types.listOf lib.types.str; type = types.listOf types.str;
example = [ "jane.doe@example.com" ]; example = [ "jane.doe@example.com" ];
default = [ ]; default = [ ];
}; };
groups = lib.mkOption { groups = mkOption {
description = "List of groups this person should belong to."; description = "List of groups this person should belong to.";
type = lib.types.listOf lib.types.str; type = types.listOf types.str;
apply = lib.unique; apply = unique;
default = [ ]; default = [ ];
}; };
}; };
@ -443,119 +472,119 @@ in
); );
}; };
systems.oauth2 = lib.mkOption { systems.oauth2 = mkOption {
description = "Provisioning of oauth2 resource servers"; description = "Provisioning of oauth2 resource servers";
default = { }; default = { };
type = lib.types.attrsOf ( type = types.attrsOf (
lib.types.submodule { types.submodule {
options = { options = {
present = mkPresentOption "oauth2 resource server"; present = mkPresentOption "oauth2 resource server";
public = lib.mkOption { public = mkOption {
description = "Whether this is a public client (enforces PKCE, doesn't use a basic secret)"; description = "Whether this is a public client (enforces PKCE, doesn't use a basic secret)";
type = lib.types.bool; type = types.bool;
default = false; default = false;
}; };
displayName = lib.mkOption { displayName = mkOption {
description = "Display name"; description = "Display name";
type = lib.types.str; type = types.str;
example = "Some Service"; example = "Some Service";
}; };
originUrl = lib.mkOption { originUrl = mkOption {
description = "The origin URL of the service. OAuth2 redirects will only be allowed to sites under this origin. Must end with a slash."; description = "The origin URL of the service. OAuth2 redirects will only be allowed to sites under this origin. Must end with a slash.";
type = type =
let let
originStrType = lib.types.strMatching ".*://.*/$"; originStrType = types.strMatching ".*://.*/$";
in in
lib.types.either originStrType (lib.types.nonEmptyListOf originStrType); types.either originStrType (types.nonEmptyListOf originStrType);
example = "https://someservice.example.com/"; example = "https://someservice.example.com/";
}; };
originLanding = lib.mkOption { originLanding = 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."; 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; type = types.str;
example = "https://someservice.example.com/home"; example = "https://someservice.example.com/home";
}; };
basicSecretFile = lib.mkOption { basicSecretFile = mkOption {
description = '' description = ''
The basic secret to use for this service. If null, the random secret generated 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! by kanidm will not be touched. Do NOT use a path from the nix store here!
''; '';
type = lib.types.nullOr lib.types.path; type = types.nullOr types.path;
example = "/run/secrets/some-oauth2-basic-secret"; example = "/run/secrets/some-oauth2-basic-secret";
default = null; default = null;
}; };
enableLocalhostRedirects = lib.mkOption { enableLocalhostRedirects = mkOption {
description = "Allow localhost redirects. Only for public clients."; description = "Allow localhost redirects. Only for public clients.";
type = lib.types.bool; type = types.bool;
default = false; default = false;
}; };
enableLegacyCrypto = lib.mkOption { enableLegacyCrypto = mkOption {
description = "Enable legacy crypto on this client. Allows JWT signing algorthms like RS256."; description = "Enable legacy crypto on this client. Allows JWT signing algorthms like RS256.";
type = lib.types.bool; type = types.bool;
default = false; default = false;
}; };
allowInsecureClientDisablePkce = lib.mkOption { allowInsecureClientDisablePkce = mkOption {
description = '' description = ''
Disable PKCE on this oauth2 resource server to work around insecure clients 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! that may not support it. You should request the client to enable PKCE!
Only for non-public clients. Only for non-public clients.
''; '';
type = lib.types.bool; type = types.bool;
default = false; default = false;
}; };
preferShortUsername = lib.mkOption { preferShortUsername = mkOption {
description = "Use 'name' instead of 'spn' in the preferred_username claim"; description = "Use 'name' instead of 'spn' in the preferred_username claim";
type = lib.types.bool; type = types.bool;
default = false; default = false;
}; };
scopeMaps = lib.mkOption { scopeMaps = mkOption {
description = '' description = ''
Maps kanidm groups to returned oauth scopes. Maps kanidm groups to returned oauth scopes.
See [Scope Relations](https://kanidm.github.io/kanidm/stable/integrations/oauth2.html#scope-relationships) for more information. 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); type = types.attrsOf (types.listOf types.str);
default = { }; default = { };
}; };
supplementaryScopeMaps = lib.mkOption { supplementaryScopeMaps = mkOption {
description = '' description = ''
Maps kanidm groups to additionally returned oauth scopes. 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. 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); type = types.attrsOf (types.listOf types.str);
default = { }; default = { };
}; };
removeOrphanedClaimMaps = lib.mkOption { removeOrphanedClaimMaps = mkOption {
description = "Whether claim maps not specified here but present in kanidm should be removed from kanidm."; description = "Whether claim maps not specified here but present in kanidm should be removed from kanidm.";
type = lib.types.bool; type = types.bool;
default = true; default = true;
}; };
claimMaps = lib.mkOption { claimMaps = mkOption {
description = '' description = ''
Adds additional claims (and values) based on which kanidm groups an authenticating party belongs to. 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. See [Claim Maps](https://kanidm.github.io/kanidm/master/integrations/oauth2.html#custom-claim-maps) for more information.
''; '';
default = { }; default = { };
type = lib.types.attrsOf ( type = types.attrsOf (
lib.types.submodule { types.submodule {
options = { options = {
joinType = lib.mkOption { joinType = mkOption {
description = '' description = ''
Determines how multiple values are joined to create the claim value. 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. See [Claim Maps](https://kanidm.github.io/kanidm/master/integrations/oauth2.html#custom-claim-maps) for more information.
''; '';
type = lib.types.enum [ type = types.enum [
"array" "array"
"csv" "csv"
"ssv" "ssv"
@ -563,10 +592,10 @@ in
default = "array"; default = "array";
}; };
valuesByGroup = lib.mkOption { valuesByGroup = mkOption {
description = "Maps kanidm groups to values for the claim."; description = "Maps kanidm groups to values for the claim.";
default = { }; default = { };
type = lib.types.attrsOf (lib.types.listOf lib.types.str); type = types.attrsOf (types.listOf types.str);
}; };
}; };
} }
@ -579,26 +608,26 @@ in
}; };
}; };
config = lib.mkIf (cfg.enableClient || cfg.enableServer || cfg.enablePam) { config = mkIf (cfg.enableClient || cfg.enableServer || cfg.enablePam) {
assertions = assertions =
let let
entityList = entityList =
type: attrs: lib.flip lib.mapAttrsToList (filterPresent attrs) (name: _: { inherit type name; }); type: attrs: flip mapAttrsToList (filterPresent attrs) (name: _: { inherit type name; });
entities = entities =
entityList "group" cfg.provision.groups entityList "group" cfg.provision.groups
++ entityList "person" cfg.provision.persons ++ entityList "person" cfg.provision.persons
++ entityList "oauth2" cfg.provision.systems.oauth2; ++ entityList "oauth2" cfg.provision.systems.oauth2;
# Accumulate entities by name. Track corresponding entity types for later duplicate check. # Accumulate entities by name. Track corresponding entity types for later duplicate check.
entitiesByName = lib.foldl' ( entitiesByName = foldl' (
acc: { type, name }: acc // { ${name} = (acc.${name} or [ ]) ++ [ type ]; } acc: { type, name }: acc // { ${name} = (acc.${name} or [ ]) ++ [ type ]; }
) { } entities; ) { } entities;
assertGroupsKnown = assertGroupsKnown =
opt: groups: opt: groups:
let let
knownGroups = lib.attrNames (filterPresent cfg.provision.groups); knownGroups = attrNames (filterPresent cfg.provision.groups);
unknownGroups = lib.subtractLists knownGroups groups; unknownGroups = subtractLists knownGroups groups;
in in
{ {
assertion = (cfg.enableServer && cfg.provision.enable) -> unknownGroups == [ ]; assertion = (cfg.enableServer && cfg.provision.enable) -> unknownGroups == [ ];
@ -608,7 +637,7 @@ in
assertEntitiesKnown = assertEntitiesKnown =
opt: entities: opt: entities:
let let
unknownEntities = lib.subtractLists (lib.attrNames entitiesByName) entities; unknownEntities = subtractLists (attrNames entitiesByName) entities;
in in
{ {
assertion = (cfg.enableServer && cfg.provision.enable) -> unknownEntities == [ ]; assertion = (cfg.enableServer && cfg.provision.enable) -> unknownEntities == [ ];
@ -620,7 +649,7 @@ in
assertion = assertion =
!cfg.enableServer !cfg.enableServer
|| ((cfg.serverSettings.tls_chain or null) == null) || ((cfg.serverSettings.tls_chain or null) == null)
|| (!lib.isStorePath cfg.serverSettings.tls_chain); || (!isStorePath cfg.serverSettings.tls_chain);
message = '' message = ''
<option>services.kanidm.serverSettings.tls_chain</option> points to <option>services.kanidm.serverSettings.tls_chain</option> points to
a file in the Nix store. You should use a quoted absolute path to a file in the Nix store. You should use a quoted absolute path to
@ -631,7 +660,7 @@ in
assertion = assertion =
!cfg.enableServer !cfg.enableServer
|| ((cfg.serverSettings.tls_key or null) == null) || ((cfg.serverSettings.tls_key or null) == null)
|| (!lib.isStorePath cfg.serverSettings.tls_key); || (!isStorePath cfg.serverSettings.tls_key);
message = '' message = ''
<option>services.kanidm.serverSettings.tls_key</option> points to <option>services.kanidm.serverSettings.tls_key</option> points to
a file in the Nix store. You should use a quoted absolute path to a file in the Nix store. You should use a quoted absolute path to
@ -677,9 +706,7 @@ in
&& ( && (
cfg.provision.adminPasswordFile != null cfg.provision.adminPasswordFile != null
|| cfg.provision.idmAdminPasswordFile != null || cfg.provision.idmAdminPasswordFile != null
|| lib.any (x: x.basicSecretFile != null) ( || any (x: x.basicSecretFile != null) (attrValues (filterPresent cfg.provision.systems.oauth2))
lib.attrValues (filterPresent cfg.provision.systems.oauth2)
)
) )
) )
-> cfg.package.enableSecretProvisioning; -> cfg.package.enableSecretProvisioning;
@ -692,48 +719,48 @@ in
( (
let let
# Filter all names that occurred in more than one entity type. # Filter all names that occurred in more than one entity type.
duplicateNames = lib.filterAttrs (_: v: builtins.length v > 1) entitiesByName; duplicateNames = filterAttrs (_: v: builtins.length v > 1) entitiesByName;
in in
{ {
assertion = cfg.provision.enable -> duplicateNames == { }; assertion = cfg.provision.enable -> duplicateNames == { };
message = '' message = ''
services.kanidm.provision requires all entity names (group, person, oauth2, ...) to be unique! services.kanidm.provision requires all entity names (group, person, oauth2, ...) to be unique!
${lib.concatLines ( ${concatLines (
lib.mapAttrsToList (name: xs: " - '${name}' used as: ${toString xs}") duplicateNames mapAttrsToList (name: xs: " - '${name}' used as: ${toString xs}") duplicateNames
)}''; )}'';
} }
) )
] ]
++ lib.flip lib.mapAttrsToList (filterPresent cfg.provision.persons) ( ++ flip mapAttrsToList (filterPresent cfg.provision.persons) (
person: personCfg: person: personCfg:
assertGroupsKnown "services.kanidm.provision.persons.${person}.groups" personCfg.groups assertGroupsKnown "services.kanidm.provision.persons.${person}.groups" personCfg.groups
) )
++ lib.flip lib.mapAttrsToList (filterPresent cfg.provision.groups) ( ++ flip mapAttrsToList (filterPresent cfg.provision.groups) (
group: groupCfg: group: groupCfg:
assertEntitiesKnown "services.kanidm.provision.groups.${group}.members" groupCfg.members assertEntitiesKnown "services.kanidm.provision.groups.${group}.members" groupCfg.members
) )
++ lib.concatLists ( ++ concatLists (
lib.flip lib.mapAttrsToList (filterPresent cfg.provision.systems.oauth2) ( flip mapAttrsToList (filterPresent cfg.provision.systems.oauth2) (
oauth2: oauth2Cfg: oauth2: oauth2Cfg:
[ [
(assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.scopeMaps" ( (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.scopeMaps" (
lib.attrNames oauth2Cfg.scopeMaps attrNames oauth2Cfg.scopeMaps
)) ))
(assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.supplementaryScopeMaps" ( (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.supplementaryScopeMaps" (
lib.attrNames oauth2Cfg.supplementaryScopeMaps attrNames oauth2Cfg.supplementaryScopeMaps
)) ))
] ]
++ lib.concatLists ( ++ concatLists (
lib.flip lib.mapAttrsToList oauth2Cfg.claimMaps ( flip mapAttrsToList oauth2Cfg.claimMaps (
claim: claimCfg: [ claim: claimCfg: [
(assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim}.valuesByGroup" ( (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim}.valuesByGroup" (
lib.attrNames claimCfg.valuesByGroup attrNames claimCfg.valuesByGroup
)) ))
# At least one group must map to a value in each claim map # At least one group must map to a value in each claim map
{ {
assertion = assertion =
(cfg.provision.enable && cfg.enableServer) (cfg.provision.enable && cfg.enableServer)
-> lib.any (xs: xs != [ ]) (lib.attrValues claimCfg.valuesByGroup); -> any (xs: xs != [ ]) (attrValues claimCfg.valuesByGroup);
message = "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim} does not specify any values for any group"; message = "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim} does not specify any values for any group";
} }
# Public clients cannot define a basic secret # Public clients cannot define a basic secret
@ -762,7 +789,7 @@ in
) )
); );
environment.systemPackages = lib.mkIf cfg.enableClient [ cfg.package ]; environment.systemPackages = mkIf cfg.enableClient [ cfg.package ];
systemd.tmpfiles.settings."10-kanidm" = { systemd.tmpfiles.settings."10-kanidm" = {
${cfg.serverSettings.online_backup.path}.d = { ${cfg.serverSettings.online_backup.path}.d = {
@ -772,11 +799,11 @@ in
}; };
}; };
systemd.services.kanidm = lib.mkIf cfg.enableServer { systemd.services.kanidm = mkIf cfg.enableServer {
description = "kanidm identity management daemon"; description = "kanidm identity management daemon";
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
after = [ "network.target" ]; after = [ "network.target" ];
serviceConfig = lib.mkMerge [ serviceConfig = mkMerge [
# Merge paths and ignore existing prefixes needs to sidestep mkMerge # Merge paths and ignore existing prefixes needs to sidestep mkMerge
( (
defaultServiceConfig defaultServiceConfig
@ -789,7 +816,7 @@ in
StateDirectoryMode = "0700"; StateDirectoryMode = "0700";
RuntimeDirectory = "kanidmd"; RuntimeDirectory = "kanidmd";
ExecStart = "${cfg.package}/bin/kanidmd server -c ${serverConfigFile}"; ExecStart = "${cfg.package}/bin/kanidmd server -c ${serverConfigFile}";
ExecStartPost = lib.mkIf cfg.provision.enable postStartScript; ExecStartPost = mkIf cfg.provision.enable postStartScript;
User = "kanidm"; User = "kanidm";
Group = "kanidm"; Group = "kanidm";
@ -803,9 +830,9 @@ in
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
# This would otherwise override the CAP_NET_BIND_SERVICE capability. # This would otherwise override the CAP_NET_BIND_SERVICE capability.
PrivateUsers = lib.mkForce false; PrivateUsers = mkForce false;
# Port needs to be exposed to the host network # Port needs to be exposed to the host network
PrivateNetwork = lib.mkForce false; PrivateNetwork = mkForce false;
RestrictAddressFamilies = [ RestrictAddressFamilies = [
"AF_INET" "AF_INET"
"AF_INET6" "AF_INET6"
@ -817,7 +844,7 @@ in
environment.RUST_LOG = "info"; environment.RUST_LOG = "info";
}; };
systemd.services.kanidm-unixd = lib.mkIf cfg.enablePam { systemd.services.kanidm-unixd = mkIf cfg.enablePam {
description = "Kanidm PAM daemon"; description = "Kanidm PAM daemon";
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
after = [ "network.target" ]; after = [ "network.target" ];
@ -825,7 +852,7 @@ in
unixConfigFile unixConfigFile
clientConfigFile clientConfigFile
]; ];
serviceConfig = lib.mkMerge [ serviceConfig = mkMerge [
defaultServiceConfig defaultServiceConfig
{ {
CacheDirectory = "kanidm-unixd"; CacheDirectory = "kanidm-unixd";
@ -848,7 +875,7 @@ in
"/run/kanidm-unixd:/var/run/kanidm-unixd" "/run/kanidm-unixd:/var/run/kanidm-unixd"
]; ];
# Needs to connect to kanidmd # Needs to connect to kanidmd
PrivateNetwork = lib.mkForce false; PrivateNetwork = mkForce false;
RestrictAddressFamilies = [ RestrictAddressFamilies = [
"AF_INET" "AF_INET"
"AF_INET6" "AF_INET6"
@ -860,7 +887,7 @@ in
environment.RUST_LOG = "info"; environment.RUST_LOG = "info";
}; };
systemd.services.kanidm-unixd-tasks = lib.mkIf cfg.enablePam { systemd.services.kanidm-unixd-tasks = mkIf cfg.enablePam {
description = "Kanidm PAM home management daemon"; description = "Kanidm PAM home management daemon";
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
after = [ after = [
@ -910,25 +937,25 @@ in
}; };
# These paths are hardcoded # These paths are hardcoded
environment.etc = lib.mkMerge [ environment.etc = mkMerge [
(lib.mkIf cfg.enableServer { "kanidm/server.toml".source = serverConfigFile; }) (mkIf cfg.enableServer { "kanidm/server.toml".source = serverConfigFile; })
(lib.mkIf options.services.kanidm.clientSettings.isDefined { (mkIf options.services.kanidm.clientSettings.isDefined {
"kanidm/config".source = clientConfigFile; "kanidm/config".source = clientConfigFile;
}) })
(lib.mkIf cfg.enablePam { "kanidm/unixd".source = unixConfigFile; }) (mkIf cfg.enablePam { "kanidm/unixd".source = unixConfigFile; })
]; ];
system.nssModules = lib.mkIf cfg.enablePam [ cfg.package ]; system.nssModules = mkIf cfg.enablePam [ cfg.package ];
system.nssDatabases.group = lib.optional cfg.enablePam "kanidm"; system.nssDatabases.group = optional cfg.enablePam "kanidm";
system.nssDatabases.passwd = lib.optional cfg.enablePam "kanidm"; system.nssDatabases.passwd = optional cfg.enablePam "kanidm";
users.groups = lib.mkMerge [ users.groups = mkMerge [
(lib.mkIf cfg.enableServer { kanidm = { }; }) (mkIf cfg.enableServer { kanidm = { }; })
(lib.mkIf cfg.enablePam { kanidm-unixd = { }; }) (mkIf cfg.enablePam { kanidm-unixd = { }; })
]; ];
users.users = lib.mkMerge [ users.users = mkMerge [
(lib.mkIf cfg.enableServer { (mkIf cfg.enableServer {
kanidm = { kanidm = {
description = "Kanidm server"; description = "Kanidm server";
isSystemUser = true; isSystemUser = true;
@ -936,7 +963,7 @@ in
packages = [ cfg.package ]; packages = [ cfg.package ];
}; };
}) })
(lib.mkIf cfg.enablePam { (mkIf cfg.enablePam {
kanidm-unixd = { kanidm-unixd = {
description = "Kanidm PAM daemon"; description = "Kanidm PAM daemon";
isSystemUser = true; isSystemUser = true;