nixos/kanidm: run nixfmt-rfc-style

This commit is contained in:
oddlama 2024-08-23 20:55:03 +02:00
parent 391d05ce95
commit aa6cbcbf09
No known key found for this signature in database
GPG Key ID: 14EFE510775FE39A
2 changed files with 422 additions and 302 deletions

View File

@ -1,4 +1,10 @@
{ config, lib, options, pkgs, ... }: {
config,
lib,
options,
pkgs,
...
}:
let let
cfg = config.services.kanidm; cfg = config.services.kanidm;
settingsFormat = pkgs.formats.toml { }; settingsFormat = pkgs.formats.toml { };
@ -7,18 +13,29 @@ let
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);
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. # 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 # 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 = list: newPath: lib.any (path: lib.hasPrefix (builtins.toString path) (builtins.toString newPath)) list; hasPrefixInList =
mergePaths = lib.foldl' (merged: newPath: let 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 # 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 # If a prefix of the new path is already in the list, do not add it
filteredNew = lib.optional (!hasPrefixInList filteredPaths newPath) newPath; filteredNew = lib.optional (!hasPrefixInList filteredPaths newPath) newPath;
in filteredPaths ++ filteredNew) []; in
filteredPaths ++ filteredNew
) [ ];
defaultServiceConfig = { defaultServiceConfig = {
BindReadOnlyPaths = [ BindReadOnlyPaths = [
@ -28,7 +45,7 @@ let
"-/etc/hosts" "-/etc/hosts"
"-/etc/localtime" "-/etc/localtime"
]; ];
CapabilityBoundingSet = []; CapabilityBoundingSet = [ ];
# ProtectClock= adds DeviceAllow=char-rtc r # ProtectClock= adds DeviceAllow=char-rtc r
DeviceAllow = ""; DeviceAllow = "";
# Implies ProtectSystem=strict, which re-mounts all paths # Implies ProtectSystem=strict, which re-mounts all paths
@ -57,12 +74,16 @@ let
RestrictRealtime = true; RestrictRealtime = true;
RestrictSUIDSGID = true; RestrictSUIDSGID = true;
SystemCallArchitectures = "native"; SystemCallArchitectures = "native";
SystemCallFilter = [ "@system-service" "~@privileged @resources @setuid @keyring" ]; SystemCallFilter = [
"@system-service"
"~@privileged @resources @setuid @keyring"
];
# Does not work well with the temporary root # Does not work well with the temporary root
#UMask = "0066"; #UMask = "0066";
}; };
mkPresentOption = what: mkPresentOption =
what:
lib.mkOption { lib.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 = lib.types.bool;
@ -71,9 +92,9 @@ let
filterPresent = lib.filterAttrs (_: v: v.present); filterPresent = lib.filterAttrs (_: v: v.present);
provisionStateJson = pkgs.writeText "provision-state.json" (builtins.toJSON { provisionStateJson = pkgs.writeText "provision-state.json" (
inherit (cfg.provision) groups persons systems; builtins.toJSON { inherit (cfg.provision) groups persons systems; }
}); );
# 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.
@ -89,28 +110,30 @@ let
# Recover the idm_admin account. If a password should explicitly be provisioned # 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 the account we set it, otherwise we generate a new one because it is required
# for provisioning. # for provisioning.
recoverIdmAdmin = if cfg.provision.idmAdminPasswordFile != null recoverIdmAdmin =
then '' 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. KANIDM_IDM_ADMIN_PASSWORD=$(< ${cfg.provision.idmAdminPasswordFile})
if ! KANIDM_RECOVER_ACCOUNT_PASSWORD=$KANIDM_IDM_ADMIN_PASSWORD ${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} idm_admin --from-environment >/dev/null; then # We always reset the idm_admin account password if a desired password was specified.
echo "Failed to recover idm_admin account" >&2 if ! KANIDM_RECOVER_ACCOUNT_PASSWORD=$KANIDM_IDM_ADMIN_PASSWORD ${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} idm_admin --from-environment >/dev/null; then
exit 1 echo "Failed to recover idm_admin account" >&2
fi exit 1
'' fi
else '' ''
# Recover idm_admin account else
if ! recover_out=$(${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} idm_admin -o json); then ''
echo "$recover_out" >&2 # Recover idm_admin account
echo "kanidm provision: Failed to recover admin account" >&2 if ! recover_out=$(${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} idm_admin -o json); then
exit 1 echo "$recover_out" >&2
fi echo "kanidm provision: Failed to recover admin account" >&2
if ! KANIDM_IDM_ADMIN_PASSWORD=$(grep '{"password' <<< "$recover_out" | ${lib.getExe pkgs.jq} -r .password); then exit 1
echo "$recover_out" >&2 fi
echo "kanidm provision: Failed to parse password for idm_admin account" >&2 if ! KANIDM_IDM_ADMIN_PASSWORD=$(grep '{"password' <<< "$recover_out" | ${lib.getExe pkgs.jq} -r .password); then
exit 1 echo "$recover_out" >&2
fi echo "kanidm provision: Failed to parse password for idm_admin account" >&2
''; exit 1
fi
'';
postStartScript = pkgs.writeShellScript "post-start" '' postStartScript = pkgs.writeShellScript "post-start" ''
set -euo pipefail set -euo pipefail
@ -142,14 +165,15 @@ let
serverPort = serverPort =
# ipv6: # ipv6:
if lib.hasInfix "]:" cfg.serverSettings.bindaddress if lib.hasInfix "]:" cfg.serverSettings.bindaddress then
then lib.last (lib.splitString "]:" cfg.serverSettings.bindaddress) lib.last (lib.splitString "]:" cfg.serverSettings.bindaddress)
else else
# ipv4: # ipv4:
if lib.hasInfix "." cfg.serverSettings.bindaddress if lib.hasInfix "." cfg.serverSettings.bindaddress then
then lib.last (lib.splitString ":" cfg.serverSettings.bindaddress) lib.last (lib.splitString ":" cfg.serverSettings.bindaddress)
# default is 8443 # default is 8443
else "8443"; else
"8443";
in in
{ {
options.services.kanidm = { options.services.kanidm = {
@ -157,7 +181,7 @@ in
enableServer = lib.mkEnableOption "the Kanidm server"; enableServer = lib.mkEnableOption "the Kanidm server";
enablePam = lib.mkEnableOption "the Kanidm PAM and NSS integration"; enablePam = lib.mkEnableOption "the Kanidm PAM and NSS integration";
package = lib.mkPackageOption pkgs "kanidm" {}; package = lib.mkPackageOption pkgs "kanidm" { };
serverSettings = lib.mkOption { serverSettings = lib.mkOption {
type = lib.types.submodule { type = lib.types.submodule {
@ -213,12 +237,20 @@ in
log_level = lib.mkOption { log_level = lib.mkOption {
description = "Log level of the server."; description = "Log level of the server.";
default = "info"; default = "info";
type = lib.types.enum [ "info" "debug" "trace" ]; type = lib.types.enum [
"info"
"debug"
"trace"
];
}; };
role = lib.mkOption { role = lib.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 [ "WriteReplica" "WriteReplicaNoUI" "ReadOnlyReplica" ]; type = lib.types.enum [
"WriteReplica"
"WriteReplicaNoUI"
"ReadOnlyReplica"
];
}; };
online_backup = { online_backup = {
path = lib.mkOption { path = lib.mkOption {
@ -347,221 +379,248 @@ in
groups = lib.mkOption { groups = lib.mkOption {
description = "Provisioning of kanidm groups"; description = "Provisioning of kanidm groups";
default = {}; default = { };
type = lib.types.attrsOf (lib.types.submodule (groupSubmod: { type = lib.types.attrsOf (
options = { lib.types.submodule (groupSubmod: {
present = mkPresentOption "group"; options = {
present = mkPresentOption "group";
members = lib.mkOption { members = lib.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 = lib.types.listOf lib.types.str;
apply = lib.unique; apply = lib.unique;
default = []; default = [ ];
};
}; };
}; config.members = lib.concatLists (
config.members = lib.concatLists (lib.flip lib.mapAttrsToList cfg.provision.persons (person: personCfg: lib.flip lib.mapAttrsToList cfg.provision.persons (
lib.optional (personCfg.present && builtins.elem groupSubmod.config._module.args.name personCfg.groups) person person: personCfg:
)); lib.optional (
})); personCfg.present && builtins.elem groupSubmod.config._module.args.name personCfg.groups
) person
)
);
})
);
}; };
persons = lib.mkOption { persons = lib.mkOption {
description = "Provisioning of kanidm persons"; description = "Provisioning of kanidm persons";
default = {}; default = { };
type = lib.types.attrsOf (lib.types.submodule { type = lib.types.attrsOf (
options = { lib.types.submodule {
present = mkPresentOption "person"; options = {
present = mkPresentOption "person";
displayName = lib.mkOption { displayName = lib.mkOption {
description = "Display name"; description = "Display name";
type = lib.types.str; type = lib.types.str;
example = "My User"; example = "My User";
}; };
legalName = lib.mkOption { legalName = lib.mkOption {
description = "Full legal name"; description = "Full legal name";
type = lib.types.nullOr lib.types.str; type = lib.types.nullOr lib.types.str;
example = "Jane Doe"; example = "Jane Doe";
default = null; default = null;
}; };
mailAddresses = lib.mkOption { mailAddresses = lib.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 = lib.types.listOf lib.types.str;
example = ["jane.doe@example.com"]; example = [ "jane.doe@example.com" ];
default = []; default = [ ];
}; };
groups = lib.mkOption { groups = lib.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 = lib.types.listOf lib.types.str;
apply = lib.unique; apply = lib.unique;
default = []; default = [ ];
};
}; };
}; }
}); );
}; };
systems.oauth2 = lib.mkOption { systems.oauth2 = lib.mkOption {
description = "Provisioning of oauth2 resource servers"; description = "Provisioning of oauth2 resource servers";
default = {}; default = { };
type = lib.types.attrsOf (lib.types.submodule { type = lib.types.attrsOf (
options = { lib.types.submodule {
present = mkPresentOption "oauth2 resource server"; options = {
present = mkPresentOption "oauth2 resource server";
public = lib.mkOption { public = lib.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 = lib.types.bool;
default = false; 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) { config = lib.mkIf (cfg.enableClient || cfg.enableServer || cfg.enablePam) {
assertions = let assertions =
entityList = type: attrs: lib.flip lib.mapAttrsToList (filterPresent attrs) (name: _: { inherit type name; }); let
entities = entityList =
entityList "group" cfg.provision.groups type: attrs: lib.flip lib.mapAttrsToList (filterPresent attrs) (name: _: { inherit type name; });
++ entityList "person" cfg.provision.persons entities =
++ entityList "oauth2" cfg.provision.systems.oauth2; 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. # Accumulate entities by name. Track corresponding entity types for later duplicate check.
entitiesByName = lib.foldl' (acc: { type, name }: entitiesByName = lib.foldl' (
acc // { acc: { type, name }: acc // { ${name} = (acc.${name} or [ ]) ++ [ type ]; }
${name} = (acc.${name} or []) ++ [type]; ) { } entities;
}
) {} entities;
assertGroupsKnown = opt: groups: let assertGroupsKnown =
knownGroups = lib.attrNames (filterPresent cfg.provision.groups); opt: groups:
unknownGroups = lib.subtractLists knownGroups groups; let
in { knownGroups = lib.attrNames (filterPresent cfg.provision.groups);
assertion = (cfg.enableServer && cfg.provision.enable) -> unknownGroups == []; unknownGroups = lib.subtractLists knownGroups groups;
message = "${opt} refers to unknown groups: ${toString unknownGroups}"; in
}; {
assertion = (cfg.enableServer && cfg.provision.enable) -> unknownGroups == [ ];
message = "${opt} refers to unknown groups: ${toString unknownGroups}";
};
assertEntitiesKnown = opt: entities: let assertEntitiesKnown =
unknownEntities = lib.subtractLists (lib.attrNames entitiesByName) entities; opt: entities:
in { let
assertion = (cfg.enableServer && cfg.provision.enable) -> unknownEntities == []; unknownEntities = lib.subtractLists (lib.attrNames entitiesByName) entities;
message = "${opt} refers to unknown entities: ${toString unknownEntities}"; in
}; {
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 = '' 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
@ -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 = '' 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
@ -591,8 +653,12 @@ in
''; '';
} }
{ {
assertion = !cfg.enableServer || (cfg.serverSettings.domain == null assertion =
-> cfg.serverSettings.role == "WriteReplica" || cfg.serverSettings.role == "WriteReplicaNoUI"); !cfg.enableServer
|| (
cfg.serverSettings.domain == null
-> cfg.serverSettings.role == "WriteReplica" || cfg.serverSettings.role == "WriteReplicaNoUI"
);
message = '' message = ''
<option>services.kanidm.serverSettings.domain</option> can only be set if this instance <option>services.kanidm.serverSettings.domain</option> can only be set if this instance
is not a ReadOnlyReplica. Otherwise the db would inherit it from 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 # If any secret is provisioned, the kanidm package must have some required patches applied to it
{ {
assertion = (cfg.provision.enable && assertion =
(cfg.provision.adminPasswordFile != null (
|| cfg.provision.idmAdminPasswordFile != null cfg.provision.enable
|| lib.any (x: x.basicSecretFile != null) (lib.attrValues (filterPresent cfg.provision.systems.oauth2)) && (
)) -> cfg.package.enableSecretProvisioning; 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 = '' message = ''
Specifying an admin account password or oauth2 basicSecretFile requires kanidm to be built with the secret provisioning patches. 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;`. You may want to set `services.kanidm.package = pkgs.kanidm.withSecretProvisioning;`.
''; '';
} }
# Entity names must be globally unique: # Entity names must be globally unique:
(let (
# Filter all names that occurred in more than one entity type. let
duplicateNames = lib.filterAttrs (_: v: builtins.length v > 1) entitiesByName; # Filter all names that occurred in more than one entity type.
in { duplicateNames = lib.filterAttrs (_: v: builtins.length v > 1) entitiesByName;
assertion = cfg.provision.enable -> duplicateNames == {}; in
message = '' {
services.kanidm.provision requires all entity names (group, person, oauth2, ...) to be unique! assertion = cfg.provision.enable -> duplicateNames == { };
${lib.concatLines (lib.mapAttrsToList (name: xs: " - '${name}' used as: ${toString xs}") 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 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 assertEntitiesKnown "services.kanidm.provision.groups.${group}.members" groupCfg.members
) )
++ lib.concatLists (lib.flip lib.mapAttrsToList (filterPresent cfg.provision.systems.oauth2) ( ++ lib.concatLists (
oauth2: oauth2Cfg: 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}.scopeMaps" (
(assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.supplementaryScopeMaps" (lib.attrNames oauth2Cfg.supplementaryScopeMaps)) 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: [ ++ lib.concatLists (
(assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim}.valuesByGroup" (lib.attrNames claimCfg.valuesByGroup)) lib.flip lib.mapAttrsToList oauth2Cfg.claimMaps (
# At least one group must map to a value in each claim map claim: claimCfg: [
{ (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim}.valuesByGroup" (
assertion = (cfg.provision.enable && cfg.enableServer) -> lib.any (xs: xs != []) (lib.attrValues claimCfg.valuesByGroup); lib.attrNames claimCfg.valuesByGroup
message = "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim} does not specify any values for any group"; ))
} # At least one group must map to a value in each claim map
# Public clients cannot define a basic secret {
{ assertion =
assertion = (cfg.provision.enable && cfg.enableServer && oauth2Cfg.public) -> oauth2Cfg.basicSecretFile == null; (cfg.provision.enable && cfg.enableServer)
message = "services.kanidm.provision.systems.oauth2.${oauth2} is a public client and thus cannot specify a basic secret"; -> 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 disable PKCE }
{ # Public clients cannot define a basic secret
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"; assertion =
} (cfg.provision.enable && cfg.enableServer && oauth2Cfg.public) -> oauth2Cfg.basicSecretFile == null;
# Non-public clients cannot enable localhost redirects message = "services.kanidm.provision.systems.oauth2.${oauth2} is a public client and thus cannot specify a basic secret";
{ }
assertion = (cfg.provision.enable && cfg.enableServer && !oauth2Cfg.public) -> !oauth2Cfg.enableLocalhostRedirects; # Public clients cannot disable PKCE
message = "services.kanidm.provision.systems.oauth2.${oauth2} is a non-public client and thus cannot enable localhost redirects"; {
} 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 ]; environment.systemPackages = lib.mkIf cfg.enableClient [ cfg.package ];
@ -679,9 +778,12 @@ in
after = [ "network.target" ]; after = [ "network.target" ];
serviceConfig = lib.mkMerge [ serviceConfig = lib.mkMerge [
# Merge paths and ignore existing prefixes needs to sidestep 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"; StateDirectory = "kanidm";
StateDirectoryMode = "0700"; StateDirectoryMode = "0700";
@ -704,7 +806,11 @@ in
PrivateUsers = lib.mkForce false; PrivateUsers = lib.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 = lib.mkForce false;
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
TemporaryFileSystem = "/:ro"; TemporaryFileSystem = "/:ro";
} }
]; ];
@ -715,7 +821,10 @@ in
description = "Kanidm PAM daemon"; description = "Kanidm PAM daemon";
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
after = [ "network.target" ]; after = [ "network.target" ];
restartTriggers = [ unixConfigFile clientConfigFile ]; restartTriggers = [
unixConfigFile
clientConfigFile
];
serviceConfig = lib.mkMerge [ serviceConfig = lib.mkMerge [
defaultServiceConfig defaultServiceConfig
{ {
@ -740,7 +849,11 @@ in
]; ];
# Needs to connect to kanidmd # Needs to connect to kanidmd
PrivateNetwork = lib.mkForce false; PrivateNetwork = lib.mkForce false;
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
TemporaryFileSystem = "/:ro"; TemporaryFileSystem = "/:ro";
} }
]; ];
@ -750,9 +863,15 @@ in
systemd.services.kanidm-unixd-tasks = lib.mkIf cfg.enablePam { systemd.services.kanidm-unixd-tasks = lib.mkIf cfg.enablePam {
description = "Kanidm PAM home management daemon"; description = "Kanidm PAM home management daemon";
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
after = [ "network.target" "kanidm-unixd.service" ]; after = [
"network.target"
"kanidm-unixd.service"
];
partOf = [ "kanidm-unixd.service" ]; partOf = [ "kanidm-unixd.service" ];
restartTriggers = [ unixConfigFile clientConfigFile ]; restartTriggers = [
unixConfigFile
clientConfigFile
];
serviceConfig = { serviceConfig = {
ExecStart = "${cfg.package}/bin/kanidm_unixd_tasks"; ExecStart = "${cfg.package}/bin/kanidm_unixd_tasks";
@ -772,7 +891,12 @@ in
"/run/kanidm-unixd:/var/run/kanidm-unixd" "/run/kanidm-unixd:/var/run/kanidm-unixd"
]; ];
# CAP_DAC_OVERRIDE is needed to ignore ownership of unixd socket # 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"; IPAddressDeny = "any";
# Need access to users # Need access to users
PrivateUsers = false; PrivateUsers = false;
@ -787,15 +911,11 @@ in
# These paths are hardcoded # These paths are hardcoded
environment.etc = lib.mkMerge [ environment.etc = lib.mkMerge [
(lib.mkIf cfg.enableServer { (lib.mkIf cfg.enableServer { "kanidm/server.toml".source = serverConfigFile; })
"kanidm/server.toml".source = serverConfigFile;
})
(lib.mkIf options.services.kanidm.clientSettings.isDefined { (lib.mkIf options.services.kanidm.clientSettings.isDefined {
"kanidm/config".source = clientConfigFile; "kanidm/config".source = clientConfigFile;
}) })
(lib.mkIf cfg.enablePam { (lib.mkIf cfg.enablePam { "kanidm/unixd".source = unixConfigFile; })
"kanidm/unixd".source = unixConfigFile;
})
]; ];
system.nssModules = lib.mkIf cfg.enablePam [ cfg.package ]; system.nssModules = lib.mkIf cfg.enablePam [ cfg.package ];
@ -804,12 +924,8 @@ in
system.nssDatabases.passwd = lib.optional cfg.enablePam "kanidm"; system.nssDatabases.passwd = lib.optional cfg.enablePam "kanidm";
users.groups = lib.mkMerge [ users.groups = lib.mkMerge [
(lib.mkIf cfg.enableServer { (lib.mkIf cfg.enableServer { kanidm = { }; })
kanidm = { }; (lib.mkIf cfg.enablePam { kanidm-unixd = { }; })
})
(lib.mkIf cfg.enablePam {
kanidm-unixd = { };
})
]; ];
users.users = lib.mkMerge [ users.users = lib.mkMerge [
(lib.mkIf cfg.enableServer { (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; meta.buildDocsInSandbox = false;
} }

View File

@ -158,7 +158,7 @@ import ./make-test-python.nix (
groups.service1-admin = { }; groups.service1-admin = { };
systems.oauth2.service1 = { systems.oauth2.service1 = {
displayName = "Service One (changed)"; displayName = "Service One (changed)";
# multiple origin urls # multiple origin urls
originUrl = [ originUrl = [
"https://changed-one.example.com/" "https://changed-one.example.com/"
"https://changed-one.example.org/" "https://changed-one.example.org/"