mirror of
https://github.com/oddlama/nix-config.git
synced 2025-10-10 23:00:39 +02:00
feat: update kanidm and provisioning
This commit is contained in:
parent
ee5556401c
commit
1b0934b565
6 changed files with 449 additions and 395 deletions
|
@ -37,28 +37,24 @@ in {
|
||||||
|
|
||||||
age.secrets.kanidm-oauth2-immich = {
|
age.secrets.kanidm-oauth2-immich = {
|
||||||
generator.script = "alnum";
|
generator.script = "alnum";
|
||||||
generator.tags = ["oauth2"];
|
|
||||||
mode = "440";
|
mode = "440";
|
||||||
group = "kanidm";
|
group = "kanidm";
|
||||||
};
|
};
|
||||||
|
|
||||||
age.secrets.kanidm-oauth2-grafana = {
|
age.secrets.kanidm-oauth2-grafana = {
|
||||||
generator.script = "alnum";
|
generator.script = "alnum";
|
||||||
generator.tags = ["oauth2"];
|
|
||||||
mode = "440";
|
mode = "440";
|
||||||
group = "kanidm";
|
group = "kanidm";
|
||||||
};
|
};
|
||||||
|
|
||||||
age.secrets.kanidm-oauth2-forgejo = {
|
age.secrets.kanidm-oauth2-forgejo = {
|
||||||
generator.script = "alnum";
|
generator.script = "alnum";
|
||||||
generator.tags = ["oauth2"];
|
|
||||||
mode = "440";
|
mode = "440";
|
||||||
group = "kanidm";
|
group = "kanidm";
|
||||||
};
|
};
|
||||||
|
|
||||||
age.secrets.kanidm-oauth2-web-sentinel = {
|
age.secrets.kanidm-oauth2-web-sentinel = {
|
||||||
generator.script = "alnum";
|
generator.script = "alnum";
|
||||||
generator.tags = ["oauth2"];
|
|
||||||
mode = "440";
|
mode = "440";
|
||||||
group = "kanidm";
|
group = "kanidm";
|
||||||
};
|
};
|
||||||
|
@ -122,24 +118,24 @@ in {
|
||||||
inherit (config.repo.secrets.global.kanidm) persons;
|
inherit (config.repo.secrets.global.kanidm) persons;
|
||||||
|
|
||||||
# Immich
|
# Immich
|
||||||
groups.immich = {};
|
groups."immich.access" = {};
|
||||||
systems.oauth2.immich = {
|
systems.oauth2.immich = {
|
||||||
displayName = "Immich";
|
displayName = "Immich";
|
||||||
originUrl = "https://${sentinelCfg.networking.providedDomains.immich}";
|
originUrl = "https://${sentinelCfg.networking.providedDomains.immich}/";
|
||||||
basicSecretFile = config.age.secrets.kanidm-oauth2-immich.path;
|
basicSecretFile = config.age.secrets.kanidm-oauth2-immich.path;
|
||||||
scopeMaps.immich = ["openid" "email" "profile"];
|
scopeMaps."immich.access" = ["openid" "email" "profile"];
|
||||||
};
|
};
|
||||||
|
|
||||||
# Grafana
|
# Grafana
|
||||||
groups.grafana = {};
|
groups."grafana.access" = {};
|
||||||
groups."grafana.admins" = {};
|
groups."grafana.admins" = {};
|
||||||
groups."grafana.editors" = {};
|
groups."grafana.editors" = {};
|
||||||
groups."grafana.server-admins" = {};
|
groups."grafana.server-admins" = {};
|
||||||
systems.oauth2.grafana = {
|
systems.oauth2.grafana = {
|
||||||
displayName = "Grafana";
|
displayName = "Grafana";
|
||||||
originUrl = "https://${sentinelCfg.networking.providedDomains.grafana}";
|
originUrl = "https://${sentinelCfg.networking.providedDomains.grafana}/";
|
||||||
basicSecretFile = config.age.secrets.kanidm-oauth2-grafana.path;
|
basicSecretFile = config.age.secrets.kanidm-oauth2-grafana.path;
|
||||||
scopeMaps.grafana = ["openid" "email" "profile"];
|
scopeMaps."grafana.access" = ["openid" "email" "profile"];
|
||||||
supplementaryScopeMaps = {
|
supplementaryScopeMaps = {
|
||||||
"grafana.admins" = ["admin"];
|
"grafana.admins" = ["admin"];
|
||||||
"grafana.editors" = ["editor"];
|
"grafana.editors" = ["editor"];
|
||||||
|
@ -148,27 +144,27 @@ in {
|
||||||
};
|
};
|
||||||
|
|
||||||
# Forgejo
|
# Forgejo
|
||||||
groups.forgejo = {};
|
groups."forgejo.access" = {};
|
||||||
groups."forgejo.admins" = {};
|
groups."forgejo.admins" = {};
|
||||||
systems.oauth2.forgejo = {
|
systems.oauth2.forgejo = {
|
||||||
displayName = "Forgejo";
|
displayName = "Forgejo";
|
||||||
originUrl = "https://${sentinelCfg.networking.providedDomains.forgejo}";
|
originUrl = "https://${sentinelCfg.networking.providedDomains.forgejo}/";
|
||||||
basicSecretFile = config.age.secrets.kanidm-oauth2-forgejo.path;
|
basicSecretFile = config.age.secrets.kanidm-oauth2-forgejo.path;
|
||||||
scopeMaps.forgejo = ["openid" "email" "profile"];
|
scopeMaps."forgejo.access" = ["openid" "email" "profile"];
|
||||||
supplementaryScopeMaps = {
|
supplementaryScopeMaps = {
|
||||||
"forgejo.admins" = ["admin"];
|
"forgejo.admins" = ["admin"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# Web Sentinel
|
# Web Sentinel
|
||||||
groups.web-sentinel = {};
|
groups."web-sentinel.access" = {};
|
||||||
groups."web-sentinel.adguardhome" = {};
|
groups."web-sentinel.adguardhome" = {};
|
||||||
groups."web-sentinel.influxdb" = {};
|
groups."web-sentinel.influxdb" = {};
|
||||||
systems.oauth2.web-sentinel = {
|
systems.oauth2.web-sentinel = {
|
||||||
displayName = "Web Sentinel";
|
displayName = "Web Sentinel";
|
||||||
originUrl = "https://oauth2.${personalDomain}";
|
originUrl = "https://oauth2.${personalDomain}/";
|
||||||
basicSecretFile = config.age.secrets.kanidm-oauth2-web-sentinel.path;
|
basicSecretFile = config.age.secrets.kanidm-oauth2-web-sentinel.path;
|
||||||
scopeMaps.web-sentinel = ["openid" "email"];
|
scopeMaps."web-sentinel.access" = ["openid" "email"];
|
||||||
supplementaryScopeMaps = {
|
supplementaryScopeMaps = {
|
||||||
"web-sentinel.adguardhome" = ["access_adguardhome"];
|
"web-sentinel.adguardhome" = ["access_adguardhome"];
|
||||||
"web-sentinel.influxdb" = ["access_influxdb"];
|
"web-sentinel.influxdb" = ["access_influxdb"];
|
||||||
|
|
|
@ -12,20 +12,15 @@
|
||||||
attrValues
|
attrValues
|
||||||
concatLines
|
concatLines
|
||||||
concatLists
|
concatLists
|
||||||
concatMap
|
|
||||||
concatMapStrings
|
|
||||||
converge
|
converge
|
||||||
escapeShellArg
|
|
||||||
escapeShellArgs
|
|
||||||
filter
|
filter
|
||||||
|
filterAttrs
|
||||||
filterAttrsRecursive
|
filterAttrsRecursive
|
||||||
flip
|
flip
|
||||||
foldl'
|
foldl'
|
||||||
getExe
|
getExe
|
||||||
hasInfix
|
|
||||||
hasPrefix
|
hasPrefix
|
||||||
isStorePath
|
isStorePath
|
||||||
mapAttrs
|
|
||||||
mapAttrsToList
|
mapAttrsToList
|
||||||
mdDoc
|
mdDoc
|
||||||
mkEnableOption
|
mkEnableOption
|
||||||
|
@ -35,9 +30,10 @@
|
||||||
mkOption
|
mkOption
|
||||||
mkPackageOption
|
mkPackageOption
|
||||||
optional
|
optional
|
||||||
optionals
|
optionalString
|
||||||
subtractLists
|
subtractLists
|
||||||
types
|
types
|
||||||
|
unique
|
||||||
;
|
;
|
||||||
|
|
||||||
cfg = config.services.kanidm;
|
cfg = config.services.kanidm;
|
||||||
|
@ -88,10 +84,10 @@
|
||||||
ProtectHostname = true;
|
ProtectHostname = true;
|
||||||
# Would re-mount paths ignored by temporary root
|
# Would re-mount paths ignored by temporary root
|
||||||
#ProtectSystem = "strict";
|
#ProtectSystem = "strict";
|
||||||
# ProtectControlGroups = true; # needed for restarter script
|
ProtectControlGroups = true;
|
||||||
ProtectKernelLogs = true;
|
ProtectKernelLogs = true;
|
||||||
ProtectKernelModules = true;
|
ProtectKernelModules = true;
|
||||||
# ProtectKernelTunables = true; # needed for restarter script
|
ProtectKernelTunables = true;
|
||||||
ProtectProc = "invisible";
|
ProtectProc = "invisible";
|
||||||
RestrictAddressFamilies = [];
|
RestrictAddressFamilies = [];
|
||||||
RestrictNamespaces = true;
|
RestrictNamespaces = true;
|
||||||
|
@ -110,38 +106,49 @@
|
||||||
default = true;
|
default = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
mkScript = script:
|
filterPresent = filterAttrs (_: v: v.present);
|
||||||
mkOption {
|
|
||||||
readOnly = true;
|
|
||||||
internal = true;
|
|
||||||
type = types.str;
|
|
||||||
default = script;
|
|
||||||
};
|
|
||||||
|
|
||||||
mappingsJson = pkgs.writeText "mappings.json" (builtins.toJSON {
|
provisionStateJson = pkgs.writeText "provision-state.json" (builtins.toJSON {
|
||||||
account_credentials.admin = cfg.provision.adminPasswordFile;
|
inherit (cfg.provision) groups persons systems;
|
||||||
account_credentials.idm_admin = cfg.provision.idmAdminPasswordFile;
|
|
||||||
oauth2_basic_secrets = mapAttrs (_: x: x.basicSecretFile) cfg.provision.systems.oauth2;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
preStartScript = pkgs.writeShellScript "pre-start-manipulate" ''
|
# Only recover the admin account if a password should explicitly be provisioned
|
||||||
if ! test -e ${escapeShellArg cfg.serverSettings.db_path}; then
|
# for the account. Otherwise it is not needed for provisioning.
|
||||||
touch "$STATE_DIRECTORY/.first_startup"
|
maybeRecoverAdmin = optionalString (cfg.provision.adminPasswordFile != null) ''
|
||||||
else
|
KANIDM_ADMIN_PASSWORD=$(< ${cfg.provision.adminPasswordFile})
|
||||||
${getExe pkgs.kanidm-secret-manipulator} ${escapeShellArg cfg.serverSettings.db_path} ${mappingsJson}
|
# We always reset the admin account password if a desired password was specified.
|
||||||
|
if ! KANIDM_RECOVER_ACCOUNT_PASSWORD=$KANIDM_ADMIN_PASSWORD ${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} admin --from-environment >/dev/null; then
|
||||||
|
echo "Failed to recover admin account" >&2
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
'';
|
'';
|
||||||
|
|
||||||
restarterScript = pkgs.writeShellScript "post-start-restarter" ''
|
# Recover the idm_admin account. If a password should explicitly be provisioned
|
||||||
set -euo pipefail
|
# for the account we set it, otherwise we generate a new one because it is required
|
||||||
if test -e "$STATE_DIRECTORY/.needs_restart"; then
|
# for provisioning.
|
||||||
rm -f "$STATE_DIRECTORY/.needs_restart"
|
recoverIdmAdmin =
|
||||||
echo "Restarting kanidm.service..."
|
if cfg.provision.idmAdminPasswordFile != null
|
||||||
#kill -TERM $MAINPID
|
then ''
|
||||||
#echo "Restarting kanidm.service via dbus..."
|
KANIDM_IDM_ADMIN_PASSWORD=$(< ${cfg.provision.idmAdminPasswordFile})
|
||||||
${pkgs.dbus}/bin/dbus-send --system --type=method_call --print-reply --dest=org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager.RestartUnit string:"kanidm.service" string:"replace"
|
# We always reset the idm_admin account password if a desired password was specified.
|
||||||
fi
|
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" | ${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" ''
|
postStartScript = pkgs.writeShellScript "post-start" ''
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
@ -150,7 +157,7 @@
|
||||||
count=0
|
count=0
|
||||||
while ! test -e /run/kanidmd/sock; do
|
while ! test -e /run/kanidmd/sock; do
|
||||||
sleep 0.1
|
sleep 0.1
|
||||||
if [ "$count" -eq 600 ]; then
|
if [[ "$count" -eq 600 ]]; then
|
||||||
echo "Tried for 60 seconds, giving up..."
|
echo "Tried for 60 seconds, giving up..."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
@ -161,113 +168,11 @@
|
||||||
count=$((count++))
|
count=$((count++))
|
||||||
done
|
done
|
||||||
|
|
||||||
# If this is the first start, we login this time by recovering the admin account
|
${recoverIdmAdmin}
|
||||||
# and force a restart afterwards to rewrite the password.
|
${maybeRecoverAdmin}
|
||||||
if test -e "$STATE_DIRECTORY/.first_startup"; then
|
|
||||||
# Recover admin account
|
|
||||||
if ! recover_out=$(${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} admin); then
|
|
||||||
echo "$recover_out" >&2
|
|
||||||
echo "kanidm provision: Failed to recover admin account" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if ! KANIDM_PASSWORD_ADMIN=$(grep -o '[A-Za-z0-9]\{48\}' <<< "$recover_out"); then
|
|
||||||
echo "$recover_out" >&2
|
|
||||||
echo "kanidm provision: Failed to parse password for admin account" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Recover idm_admin account
|
KANIDM_PROVISION_IDM_ADMIN_TOKEN=$KANIDM_IDM_ADMIN_PASSWORD \
|
||||||
if ! recover_out=$(${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} idm_admin); then
|
${getExe pkgs.kanidm-provision} --url "${cfg.provision.instanceUrl}" --state ${provisionStateJson} ${optionalString cfg.provision.acceptInvalidCerts "--accept-invalid-certs"}
|
||||||
echo "$recover_out" >&2
|
|
||||||
echo "kanidm provision: Failed to recover admin account" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if ! KANIDM_PASSWORD_IDM=$(grep -o '[A-Za-z0-9]\{48\}' <<< "$recover_out"); then
|
|
||||||
echo "$recover_out" >&2
|
|
||||||
echo "kanidm provision: Failed to parse password for idm_admin account" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
needs_rewrite=1
|
|
||||||
rm -f "$STATE_DIRECTORY/.first_startup"
|
|
||||||
else
|
|
||||||
# Login using the admin password
|
|
||||||
KANIDM_PASSWORD_ADMIN="$(< ${escapeShellArg cfg.provision.adminPasswordFile})"
|
|
||||||
KANIDM_PASSWORD_IDM="$(< ${escapeShellArg cfg.provision.idmAdminPasswordFile})"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Set $HOME so kanidm can save the token temporarily
|
|
||||||
export TMPDIR=$(mktemp -d)
|
|
||||||
mkdir -p "$TMPDIR"/{.config,.cache}
|
|
||||||
touch "$TMPDIR/.config/kanidm"
|
|
||||||
trap 'rm -rf $TMPDIR' EXIT
|
|
||||||
export HOME=$TMPDIR
|
|
||||||
|
|
||||||
# Login to admin and idm_admin
|
|
||||||
KANIDM_PASSWORD=$KANIDM_PASSWORD_ADMIN ${cfg.package}/bin/kanidm login --name admin \
|
|
||||||
|| { echo "kanidm provision: Failed to login as admin, see kanidm logs." >&2; exit 1; }
|
|
||||||
KANIDM_PASSWORD=$KANIDM_PASSWORD_IDM ${cfg.package}/bin/kanidm login --name idm_admin \
|
|
||||||
|| { echo "kanidm provision: Failed to login as idm_admin, see kanidm logs." >&2; exit 1; }
|
|
||||||
|
|
||||||
# Wrapper function that detects kanidm errors by detecting any output to stderr
|
|
||||||
# (stderr and stdout are swapped when calling this)
|
|
||||||
function kanidm-detect-err() {
|
|
||||||
if ! err=$(${cfg.package}/bin/kanidm "$@" 3>&2 2>&1 1>&3-); then
|
|
||||||
echo "$err"
|
|
||||||
echo "kanidm ''${*@Q}: failed with status $?, see error above"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ -n "$err" ]]; then
|
|
||||||
echo "$err"
|
|
||||||
echo "kanidm ''${*@Q}: failed, see error above"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Wrapper function to easily execute commands as admin or idm_admin
|
|
||||||
function kanidm-as-user() {
|
|
||||||
name=$1
|
|
||||||
shift
|
|
||||||
kanidm-detect-err "$@" --name "$name" 3>&2 2>&1 1>&3-
|
|
||||||
}
|
|
||||||
|
|
||||||
function kanidm-admin() { kanidm-as-user admin "$@"; }
|
|
||||||
function kanidm-idm() { kanidm-as-user idm_admin "$@"; }
|
|
||||||
|
|
||||||
known_groups=$(kanidm-admin group list --output=json)
|
|
||||||
function group_exists() {
|
|
||||||
if ! x=$(${getExe pkgs.jq} <<< "$known_groups" ".[] | select(.name[0] == \"$1\")"); then
|
|
||||||
echo "kanidm provision: Failed to parse groups list." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
[[ -n "$x" ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
known_persons=$(kanidm-admin person list --output=json)
|
|
||||||
function person_exists() {
|
|
||||||
if ! x=$(${getExe pkgs.jq} <<< "$known_persons" ".[] | select(.name[0] == \"$1\")"); then
|
|
||||||
echo "kanidm provision: Failed to parse persons list." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
[[ -n "$x" ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
known_oauth2_systems=$(kanidm-admin system oauth2 list --output=json)
|
|
||||||
function oauth2_system_exists() {
|
|
||||||
if ! x=$(${getExe pkgs.jq} <<< "$known_oauth2_systems" ".[] | select(.oauth2_rs_name[0] == \"$1\")"); then
|
|
||||||
echo "kanidm provision: Failed to parse oauth2 systems list." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
[[ -n "$x" ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
${concatMapStrings (x: x._script) (attrValues cfg.provision.groups)}
|
|
||||||
${concatMapStrings (x: x._script) (attrValues cfg.provision.persons)}
|
|
||||||
${concatMapStrings (x: x._script) (attrValues cfg.provision.systems.oauth2)}
|
|
||||||
|
|
||||||
if [[ "''${needs_rewrite-0}" == 1 ]]; then
|
|
||||||
echo "Queueing service restart to rewrite secrets"
|
|
||||||
touch "$STATE_DIRECTORY/.needs_restart"
|
|
||||||
fi
|
|
||||||
'';
|
'';
|
||||||
in {
|
in {
|
||||||
options.services.kanidm = {
|
options.services.kanidm = {
|
||||||
|
@ -277,182 +182,6 @@ in {
|
||||||
|
|
||||||
package = mkPackageOption pkgs "kanidm" {};
|
package = mkPackageOption pkgs "kanidm" {};
|
||||||
|
|
||||||
provision = {
|
|
||||||
enable = mkEnableOption "provisioning of systems (oauth2), groups and users";
|
|
||||||
|
|
||||||
adminPasswordFile = mkOption {
|
|
||||||
description = mdDoc "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";
|
|
||||||
type = types.path;
|
|
||||||
};
|
|
||||||
|
|
||||||
idmAdminPasswordFile = mkOption {
|
|
||||||
description = mdDoc "Path to a file containing the idm admin password for kanidm. Do NOT use a file from the nix store here!";
|
|
||||||
example = "/run/secrets/kanidm-idm-admin-password";
|
|
||||||
type = types.path;
|
|
||||||
};
|
|
||||||
|
|
||||||
persons = mkOption {
|
|
||||||
description = mdDoc "Provisioning of kanidm persons";
|
|
||||||
default = {};
|
|
||||||
type = types.attrsOf (types.submodule (personSubmod: let
|
|
||||||
inherit (personSubmod.config._module.args) name;
|
|
||||||
updateArgs =
|
|
||||||
["--displayname" personSubmod.config.displayName]
|
|
||||||
++ optionals (personSubmod.config.legalName != null)
|
|
||||||
["--legalname" personSubmod.config.legalName]
|
|
||||||
# mail addresses
|
|
||||||
++ concatMap (addr: ["--mail" addr]) personSubmod.config.mailAddresses;
|
|
||||||
in {
|
|
||||||
options = {
|
|
||||||
_script = mkScript (
|
|
||||||
if personSubmod.config.present
|
|
||||||
then
|
|
||||||
''
|
|
||||||
if ! person_exists ${escapeShellArg name}; then
|
|
||||||
kanidm-idm person create ${escapeShellArg name} \
|
|
||||||
${escapeShellArg personSubmod.config.displayName}
|
|
||||||
fi
|
|
||||||
kanidm-idm person update ${escapeShellArg name} ${escapeShellArgs updateArgs}
|
|
||||||
''
|
|
||||||
+ flip concatMapStrings personSubmod.config.groups (group: ''
|
|
||||||
kanidm-idm group add-members ${escapeShellArg group} ${escapeShellArg name}
|
|
||||||
'')
|
|
||||||
else ''
|
|
||||||
if person_exists ${escapeShellArg name}; then
|
|
||||||
kanidm-idm person delete ${escapeShellArg name}
|
|
||||||
fi
|
|
||||||
''
|
|
||||||
);
|
|
||||||
|
|
||||||
present = mkPresentOption "person";
|
|
||||||
|
|
||||||
displayName = mkOption {
|
|
||||||
description = mdDoc "Display name";
|
|
||||||
type = types.str;
|
|
||||||
example = "My User";
|
|
||||||
};
|
|
||||||
|
|
||||||
legalName = mkOption {
|
|
||||||
description = mdDoc "Full legal name";
|
|
||||||
type = types.nullOr types.str;
|
|
||||||
example = "Jane Doe";
|
|
||||||
default = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
mailAddresses = mkOption {
|
|
||||||
description = mdDoc "Mail addresses. First given address is considered the primary address.";
|
|
||||||
type = types.listOf types.str;
|
|
||||||
example = ["jane.doe@example.com"];
|
|
||||||
default = [];
|
|
||||||
};
|
|
||||||
|
|
||||||
groups = mkOption {
|
|
||||||
description = mdDoc "List of kanidm groups to which this user belongs.";
|
|
||||||
type = types.listOf types.str;
|
|
||||||
default = [];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
groups = mkOption {
|
|
||||||
description = mdDoc "Provisioning of kanidm groups";
|
|
||||||
default = {};
|
|
||||||
type = types.attrsOf (types.submodule (groupSubmod: let
|
|
||||||
inherit (groupSubmod.config._module.args) name;
|
|
||||||
in {
|
|
||||||
options = {
|
|
||||||
_script = mkScript (
|
|
||||||
if groupSubmod.config.present
|
|
||||||
then ''
|
|
||||||
if ! group_exists ${escapeShellArg name}; then
|
|
||||||
kanidm-admin group create ${escapeShellArg name}
|
|
||||||
fi
|
|
||||||
''
|
|
||||||
else ''
|
|
||||||
if group_exists ${escapeShellArg name}; then
|
|
||||||
kanidm-admin group delete ${escapeShellArg name}
|
|
||||||
fi
|
|
||||||
''
|
|
||||||
);
|
|
||||||
|
|
||||||
present = mkPresentOption "group";
|
|
||||||
};
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
systems.oauth2 = mkOption {
|
|
||||||
description = mdDoc "Provisioning of oauth2 systems";
|
|
||||||
default = {};
|
|
||||||
type = types.attrsOf (types.submodule (oauth2Submod: let
|
|
||||||
inherit (oauth2Submod.config._module.args) name;
|
|
||||||
in {
|
|
||||||
options = {
|
|
||||||
_script = mkScript (
|
|
||||||
if oauth2Submod.config.present
|
|
||||||
then
|
|
||||||
''
|
|
||||||
if ! oauth2_system_exists ${escapeShellArg name}; then
|
|
||||||
kanidm-admin system oauth2 create \
|
|
||||||
${escapeShellArg name} \
|
|
||||||
${escapeShellArg oauth2Submod.config.displayName} \
|
|
||||||
${escapeShellArg oauth2Submod.config.originUrl}
|
|
||||||
needs_rewrite=1
|
|
||||||
fi
|
|
||||||
''
|
|
||||||
+ concatLines (flip mapAttrsToList oauth2Submod.config.scopeMaps (group: scopes: ''
|
|
||||||
kanidm-admin system oauth2 update-scope-map ${escapeShellArg name} \
|
|
||||||
${escapeShellArg group} ${escapeShellArgs scopes}
|
|
||||||
''))
|
|
||||||
+ concatLines (flip mapAttrsToList oauth2Submod.config.supplementaryScopeMaps (group: scopes: ''
|
|
||||||
kanidm-admin system oauth2 update-sup-scope-map ${escapeShellArg name} \
|
|
||||||
${escapeShellArg group} ${escapeShellArgs scopes}
|
|
||||||
''))
|
|
||||||
else ''
|
|
||||||
if oauth2_system_exists ${escapeShellArg name}; then
|
|
||||||
kanidm-admin system oauth2 delete ${escapeShellArg name}
|
|
||||||
fi
|
|
||||||
''
|
|
||||||
);
|
|
||||||
|
|
||||||
present = mkPresentOption "oauth2 system";
|
|
||||||
|
|
||||||
displayName = mkOption {
|
|
||||||
description = mdDoc "Display name";
|
|
||||||
type = types.str;
|
|
||||||
example = "Some Service";
|
|
||||||
};
|
|
||||||
|
|
||||||
originUrl = mkOption {
|
|
||||||
description = mdDoc "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 = types.str;
|
|
||||||
example = "https://someservice.example.com/";
|
|
||||||
};
|
|
||||||
|
|
||||||
basicSecretFile = mkOption {
|
|
||||||
description = mdDoc "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 = types.nullOr types.path;
|
|
||||||
example = "/run/secrets/some-oauth2-basic-secret";
|
|
||||||
default = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
scopeMaps = mkOption {
|
|
||||||
description = mdDoc "Maps kanidm groups to provided scopes. See [Scope Relations](https://kanidm.github.io/kanidm/stable/integrations/oauth2.html#scope-relationships) for more information.";
|
|
||||||
type = types.attrsOf (types.listOf types.str);
|
|
||||||
default = {};
|
|
||||||
};
|
|
||||||
|
|
||||||
supplementaryScopeMaps = mkOption {
|
|
||||||
description = mdDoc "Maps kanidm groups to provided supplementary scopes. See [Scope Relations](https://kanidm.github.io/kanidm/stable/integrations/oauth2.html#scope-relationships) for more information.";
|
|
||||||
type = types.attrsOf (types.listOf types.str);
|
|
||||||
default = {};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
serverSettings = mkOption {
|
serverSettings = mkOption {
|
||||||
type = types.submodule {
|
type = types.submodule {
|
||||||
freeformType = settingsFormat.type;
|
freeformType = settingsFormat.type;
|
||||||
|
@ -514,12 +243,34 @@ in {
|
||||||
default = "WriteReplica";
|
default = "WriteReplica";
|
||||||
type = types.enum ["WriteReplica" "WriteReplicaNoUI" "ReadOnlyReplica"];
|
type = types.enum ["WriteReplica" "WriteReplicaNoUI" "ReadOnlyReplica"];
|
||||||
};
|
};
|
||||||
|
online_backup = {
|
||||||
|
path = mkOption {
|
||||||
|
description = mdDoc "Path to the output directory for backups.";
|
||||||
|
type = types.path;
|
||||||
|
default = "/var/lib/kanidm/backups";
|
||||||
|
};
|
||||||
|
schedule = mkOption {
|
||||||
|
description = mdDoc "The schedule for backups in cron format.";
|
||||||
|
type = types.str;
|
||||||
|
default = "00 22 * * *";
|
||||||
|
};
|
||||||
|
versions = mkOption {
|
||||||
|
description = mdDoc ''
|
||||||
|
Number of backups to keep.
|
||||||
|
|
||||||
|
The default is set to `0`, in order to disable backups by default.
|
||||||
|
'';
|
||||||
|
type = types.ints.unsigned;
|
||||||
|
default = 0;
|
||||||
|
example = 7;
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
default = {};
|
default = {};
|
||||||
description = mdDoc ''
|
description = mdDoc ''
|
||||||
Settings for Kanidm, see
|
Settings for Kanidm, see
|
||||||
[the documentation](https://github.com/kanidm/kanidm/blob/master/kanidm_book/src/server_configuration.md)
|
[the documentation](https://kanidm.github.io/kanidm/stable/server_configuration.html)
|
||||||
and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/server.toml)
|
and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/server.toml)
|
||||||
for possible values.
|
for possible values.
|
||||||
'';
|
'';
|
||||||
|
@ -537,7 +288,7 @@ in {
|
||||||
};
|
};
|
||||||
description = mdDoc ''
|
description = mdDoc ''
|
||||||
Configure Kanidm clients, needed for the PAM daemon. See
|
Configure Kanidm clients, needed for the PAM daemon. See
|
||||||
[the documentation](https://github.com/kanidm/kanidm/blob/master/kanidm_book/src/client_tools.md#kanidm-configuration)
|
[the documentation](https://kanidm.github.io/kanidm/stable/client_tools.html#kanidm-configuration)
|
||||||
and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/config)
|
and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/config)
|
||||||
for possible values.
|
for possible values.
|
||||||
'';
|
'';
|
||||||
|
@ -547,23 +298,276 @@ in {
|
||||||
type = types.submodule {
|
type = types.submodule {
|
||||||
freeformType = settingsFormat.type;
|
freeformType = settingsFormat.type;
|
||||||
|
|
||||||
options.pam_allowed_login_groups = mkOption {
|
options = {
|
||||||
description = mdDoc "Kanidm groups that are allowed to login using PAM.";
|
pam_allowed_login_groups = mkOption {
|
||||||
example = "my_pam_group";
|
description = mdDoc "Kanidm groups that are allowed to login using PAM.";
|
||||||
type = types.listOf types.str;
|
example = "my_pam_group";
|
||||||
|
type = types.listOf types.str;
|
||||||
|
};
|
||||||
|
hsm_pin_path = mkOption {
|
||||||
|
description = mdDoc "Path to a HSM pin.";
|
||||||
|
default = "/var/cache/kanidm-unixd/hsm-pin";
|
||||||
|
type = types.path;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
description = mdDoc ''
|
description = mdDoc ''
|
||||||
Configure Kanidm unix daemon.
|
Configure Kanidm unix daemon.
|
||||||
See [the documentation](https://github.com/kanidm/kanidm/blob/master/kanidm_book/src/pam_and_nsswitch.md#the-unix-daemon)
|
See [the documentation](https://kanidm.github.io/kanidm/stable/integrations/pam_and_nsswitch.html#the-unix-daemon)
|
||||||
and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/unixd)
|
and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/unixd)
|
||||||
for possible values.
|
for possible values.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
provision = {
|
||||||
|
enable = mkEnableOption "provisioning of groups, users and oauth2 resource servers";
|
||||||
|
|
||||||
|
instanceUrl = mkOption {
|
||||||
|
description = "The instance url to which the provisioning tool should connect.";
|
||||||
|
default = "https://localhost";
|
||||||
|
type = types.str;
|
||||||
|
};
|
||||||
|
|
||||||
|
acceptInvalidCerts = mkOption {
|
||||||
|
description = ''
|
||||||
|
Whether to allow invalid certificates when provisioning the target instance.
|
||||||
|
By default this is only allowed when the instanceUrl is localhost. This is
|
||||||
|
dangerous when used with an external URL.
|
||||||
|
'';
|
||||||
|
type = types.bool;
|
||||||
|
default = cfg.provision.instanceUrl == "https://localhost";
|
||||||
|
defaultText = ''services.kanidm.provision.instanceUrl == "https://localhost"'';
|
||||||
|
};
|
||||||
|
|
||||||
|
adminPasswordFile = mkOption {
|
||||||
|
description = "Path to a file containing the admin password for kanidm. Do NOT use a file from the nix store here!";
|
||||||
|
example = "/run/secrets/kanidm-admin-password";
|
||||||
|
default = null;
|
||||||
|
type = types.nullOr types.path;
|
||||||
|
};
|
||||||
|
|
||||||
|
idmAdminPasswordFile = mkOption {
|
||||||
|
description = ''
|
||||||
|
Path to a file containing the idm admin password for kanidm. Do NOT use a file from the nix store here!
|
||||||
|
If this is not given but provisioning is enabled, the idm_admin password will be reset on each restart.
|
||||||
|
'';
|
||||||
|
example = "/run/secrets/kanidm-idm-admin-password";
|
||||||
|
default = null;
|
||||||
|
type = types.nullOr types.path;
|
||||||
|
};
|
||||||
|
|
||||||
|
autoRemove = mkOption {
|
||||||
|
description = ''
|
||||||
|
Determines whether deleting an entity in this provisioning config should automatically
|
||||||
|
cause them to be removed from kanidm, too. This works because the provisioning tool tracks
|
||||||
|
all entities it has ever created. If this is set to false, you need to explicitly specify
|
||||||
|
`present = false` to delete an entity.
|
||||||
|
'';
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
groups = mkOption {
|
||||||
|
description = "Provisioning of kanidm groups";
|
||||||
|
default = {};
|
||||||
|
type = types.attrsOf (types.submodule (groupSubmod: {
|
||||||
|
options = {
|
||||||
|
present = mkPresentOption "group";
|
||||||
|
|
||||||
|
members = mkOption {
|
||||||
|
description = "List of kanidm entities (persons, groups, ...) which are part of this group.";
|
||||||
|
type = types.listOf types.str;
|
||||||
|
apply = unique;
|
||||||
|
default = [];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
config.members = concatLists (flip mapAttrsToList cfg.provision.persons (
|
||||||
|
person: personCfg:
|
||||||
|
optional (personCfg.present && builtins.elem groupSubmod.config._module.args.name personCfg.groups) person
|
||||||
|
));
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
persons = mkOption {
|
||||||
|
description = "Provisioning of kanidm persons";
|
||||||
|
default = {};
|
||||||
|
type = types.attrsOf (types.submodule {
|
||||||
|
options = {
|
||||||
|
present = mkPresentOption "person";
|
||||||
|
|
||||||
|
displayName = mkOption {
|
||||||
|
description = "Display name";
|
||||||
|
type = types.str;
|
||||||
|
example = "My User";
|
||||||
|
};
|
||||||
|
|
||||||
|
legalName = mkOption {
|
||||||
|
description = "Full legal name";
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
example = "Jane Doe";
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
mailAddresses = mkOption {
|
||||||
|
description = "Mail addresses. First given address is considered the primary address.";
|
||||||
|
type = types.listOf types.str;
|
||||||
|
example = ["jane.doe@example.com"];
|
||||||
|
default = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
groups = mkOption {
|
||||||
|
description = "List of groups this person should belong to.";
|
||||||
|
type = types.listOf types.str;
|
||||||
|
apply = unique;
|
||||||
|
default = [];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
systems.oauth2 = mkOption {
|
||||||
|
description = "Provisioning of oauth2 resource servers";
|
||||||
|
default = {};
|
||||||
|
type = types.attrsOf (types.submodule {
|
||||||
|
options = {
|
||||||
|
present = mkPresentOption "oauth2 resource server";
|
||||||
|
|
||||||
|
displayName = mkOption {
|
||||||
|
description = "Display name";
|
||||||
|
type = types.str;
|
||||||
|
example = "Some Service";
|
||||||
|
};
|
||||||
|
|
||||||
|
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.";
|
||||||
|
type = types.strMatching ".*://.*/$";
|
||||||
|
example = "https://someservice.example.com/";
|
||||||
|
};
|
||||||
|
|
||||||
|
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.";
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
example = "https://someservice.example.com/home";
|
||||||
|
};
|
||||||
|
|
||||||
|
basicSecretFile = 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 = types.nullOr types.path;
|
||||||
|
example = "/run/secrets/some-oauth2-basic-secret";
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
allowInsecureClientDisablePkce = 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!
|
||||||
|
'';
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
preferShortUsername = mkOption {
|
||||||
|
description = "Use 'name' instead of 'spn' in the preferred_username claim";
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
scopeMaps = 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 = types.attrsOf (types.listOf types.str);
|
||||||
|
default = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
supplementaryScopeMaps = 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 = types.attrsOf (types.listOf types.str);
|
||||||
|
default = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
removeOrphanedClaimMaps = mkOption {
|
||||||
|
description = "Whether claim maps not specified here but present in kanidm should be removed from kanidm.";
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
claimMaps = 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 = types.attrsOf (types.submodule {
|
||||||
|
options = {
|
||||||
|
joinType = 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 = types.enum ["array" "csv" "ssv"];
|
||||||
|
default = "array";
|
||||||
|
};
|
||||||
|
|
||||||
|
valuesByGroup = mkOption {
|
||||||
|
description = "Maps kanidm groups to values for the claim.";
|
||||||
|
default = {};
|
||||||
|
type = types.attrsOf (types.listOf types.str);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = mkIf (cfg.enableClient || cfg.enableServer || cfg.enablePam) {
|
config = mkIf (cfg.enableClient || cfg.enableServer || cfg.enablePam) {
|
||||||
assertions =
|
assertions = let
|
||||||
|
entityList = type: attrs: flip 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 =
|
||||||
|
foldl' (
|
||||||
|
acc: {
|
||||||
|
type,
|
||||||
|
name,
|
||||||
|
}:
|
||||||
|
acc
|
||||||
|
// {
|
||||||
|
${name} = (acc.${name} or []) ++ [type];
|
||||||
|
}
|
||||||
|
) {}
|
||||||
|
entities;
|
||||||
|
|
||||||
|
assertGroupsKnown = opt: groups: let
|
||||||
|
knownGroups = attrNames (filterPresent cfg.provision.groups);
|
||||||
|
unknownGroups = subtractLists knownGroups groups;
|
||||||
|
in {
|
||||||
|
assertion = (cfg.enableServer && cfg.provision.enable) -> unknownGroups == [];
|
||||||
|
message = "${opt} refers to unknown groups: ${toString unknownGroups}";
|
||||||
|
};
|
||||||
|
|
||||||
|
assertEntitiesKnown = opt: entities: let
|
||||||
|
unknownEntities = subtractLists (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) || (!isStorePath cfg.serverSettings.tls_chain);
|
assertion = !cfg.enableServer || ((cfg.serverSettings.tls_chain or null) == null) || (!isStorePath cfg.serverSettings.tls_chain);
|
||||||
|
@ -611,38 +615,67 @@ in {
|
||||||
assertion = cfg.provision.enable -> cfg.enableServer;
|
assertion = cfg.provision.enable -> cfg.enableServer;
|
||||||
message = "<option>services.kanidm.provision</option> requires <option>services.kanidm.enableServer</option> to be true";
|
message = "<option>services.kanidm.provision</option> requires <option>services.kanidm.enableServer</option> to be true";
|
||||||
}
|
}
|
||||||
|
# If any secret is provisioned, the kanidm package must have some required patches applied to it
|
||||||
{
|
{
|
||||||
assertion = cfg.provision.enable -> cfg.enableClient;
|
assertion =
|
||||||
message = "<option>services.kanidm.provision</option> requires <option>services.kanidm.enableClient</option> to be able to use the kanidm client locally for provisioning.";
|
(cfg.provision.enable
|
||||||
|
&& (
|
||||||
|
cfg.provision.adminPasswordFile
|
||||||
|
!= null
|
||||||
|
|| cfg.provision.idmAdminPasswordFile != null
|
||||||
|
|| any (x: x.basicSecretFile != null) (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 = 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!
|
||||||
|
${concatLines (mapAttrsToList (name: xs: " - '${name}' used as: ${toString xs}") duplicateNames)}'';
|
||||||
|
})
|
||||||
]
|
]
|
||||||
++ flip mapAttrsToList cfg.provision.persons (person: personCfg: let
|
++ flip mapAttrsToList (filterPresent cfg.provision.persons) (
|
||||||
unknownGroups = subtractLists (attrNames cfg.provision.groups) personCfg.groups;
|
person: personCfg:
|
||||||
in {
|
assertGroupsKnown "services.kanidm.provision.persons.${person}.groups" personCfg.groups
|
||||||
assertion = (cfg.enableServer && cfg.provision.enable) -> unknownGroups == [];
|
)
|
||||||
message = "kanidm: provision.persons.${person}.groups: Refers to unknown groups: ${toString unknownGroups}";
|
++ flip mapAttrsToList (filterPresent cfg.provision.groups) (
|
||||||
})
|
group: groupCfg:
|
||||||
++ concatLists (flip mapAttrsToList cfg.provision.systems.oauth2 (oauth2: oauth2Cfg: [
|
assertEntitiesKnown "services.kanidm.provision.groups.${group}.members" groupCfg.members
|
||||||
{
|
)
|
||||||
assertion = (cfg.enableServer && cfg.provision.enable) -> hasInfix "://" oauth2Cfg.originUrl;
|
++ concatLists (flip mapAttrsToList (filterPresent cfg.provision.systems.oauth2) (
|
||||||
message = "kanidm: provision.systems.oauth2.${oauth2}.originUrl: Missing a schema like 'https://': ${oauth2Cfg.originUrl}";
|
oauth2: oauth2Cfg:
|
||||||
}
|
[
|
||||||
(let
|
(assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.scopeMaps" (attrNames oauth2Cfg.scopeMaps))
|
||||||
unknownGroups = subtractLists (attrNames cfg.provision.groups) (attrNames oauth2Cfg.scopeMaps);
|
(assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.supplementaryScopeMaps" (attrNames oauth2Cfg.supplementaryScopeMaps))
|
||||||
in {
|
]
|
||||||
assertion = (cfg.enableServer && cfg.provision.enable) -> unknownGroups == [];
|
++ concatLists (flip mapAttrsToList oauth2Cfg.claimMaps (claim: claimCfg: [
|
||||||
message = "kanidm: provision.systems.oauth2.${oauth2}.scopeMaps: Refers to unknown groups: ${toString unknownGroups}";
|
(assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim}.valuesByGroup" (attrNames claimCfg.valuesByGroup))
|
||||||
})
|
# At least one group must map to a value in each claim map
|
||||||
(let
|
{
|
||||||
unknownGroups = subtractLists (attrNames cfg.provision.groups) (attrNames oauth2Cfg.supplementaryScopeMaps);
|
assertion = (cfg.provision.enable && cfg.enableServer) -> any (xs: xs != []) (attrValues claimCfg.valuesByGroup);
|
||||||
in {
|
message = "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim} does not specify any values for any group";
|
||||||
assertion = (cfg.enableServer && cfg.provision.enable) -> unknownGroups == [];
|
}
|
||||||
message = "kanidm: provision.systems.oauth2.${oauth2}.supplementaryScopeMaps: Refers to unknown groups: ${toString unknownGroups}";
|
]))
|
||||||
})
|
));
|
||||||
]));
|
|
||||||
|
|
||||||
environment.systemPackages = mkIf cfg.enableClient [cfg.package];
|
environment.systemPackages = mkIf cfg.enableClient [cfg.package];
|
||||||
|
|
||||||
|
systemd.tmpfiles.settings."10-kanidm" = {
|
||||||
|
${cfg.serverSettings.online_backup.path}.d = {
|
||||||
|
mode = "0700";
|
||||||
|
user = "kanidm";
|
||||||
|
group = "kanidm";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
systemd.services.kanidm = 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"];
|
||||||
|
@ -651,39 +684,22 @@ in {
|
||||||
# Merge paths and ignore existing prefixes needs to sidestep mkMerge
|
# Merge paths and ignore existing prefixes needs to sidestep mkMerge
|
||||||
(defaultServiceConfig
|
(defaultServiceConfig
|
||||||
// {
|
// {
|
||||||
BindReadOnlyPaths = mergePaths (
|
BindReadOnlyPaths = mergePaths (defaultServiceConfig.BindReadOnlyPaths ++ certPaths);
|
||||||
defaultServiceConfig.BindReadOnlyPaths
|
|
||||||
++ certPaths
|
|
||||||
# If provisioning is enabled, we need access to the client config to use the kanidm cli,
|
|
||||||
# and to the installed system certificates.
|
|
||||||
++ optionals cfg.provision.enable [
|
|
||||||
"-/etc/ssl/certs"
|
|
||||||
"-/etc/static/ssl/certs"
|
|
||||||
"-/etc/kanidm"
|
|
||||||
"-/etc/static/kanidm"
|
|
||||||
]
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
StateDirectory = "kanidm";
|
StateDirectory = "kanidm";
|
||||||
StateDirectoryMode = "0700";
|
StateDirectoryMode = "0700";
|
||||||
RuntimeDirectory = "kanidmd";
|
RuntimeDirectory = "kanidmd";
|
||||||
ExecStartPre = mkIf cfg.provision.enable [preStartScript];
|
|
||||||
ExecStart = "${cfg.package}/bin/kanidmd server -c ${serverConfigFile}";
|
ExecStart = "${cfg.package}/bin/kanidmd server -c ${serverConfigFile}";
|
||||||
ExecStartPost =
|
ExecStartPost = mkIf cfg.provision.enable postStartScript;
|
||||||
mkIf cfg.provision.enable
|
|
||||||
[
|
|
||||||
postStartScript
|
|
||||||
# Only the restarter runs with elevated privileges
|
|
||||||
"+${restarterScript}"
|
|
||||||
];
|
|
||||||
User = "kanidm";
|
User = "kanidm";
|
||||||
Group = "kanidm";
|
Group = "kanidm";
|
||||||
|
|
||||||
BindPaths = [
|
BindPaths = [
|
||||||
# To create the socket
|
# To create the socket
|
||||||
"/run/kanidmd:/run/kanidmd"
|
"/run/kanidmd:/run/kanidmd"
|
||||||
"/run/dbus/system_bus_socket"
|
# To store backups
|
||||||
|
cfg.serverSettings.online_backup.path
|
||||||
];
|
];
|
||||||
|
|
||||||
AmbientCapabilities = ["CAP_NET_BIND_SERVICE"];
|
AmbientCapabilities = ["CAP_NET_BIND_SERVICE"];
|
||||||
|
|
|
@ -5,14 +5,31 @@
|
||||||
(_final: prev: {
|
(_final: prev: {
|
||||||
deploy = prev.callPackage ./deploy.nix {};
|
deploy = prev.callPackage ./deploy.nix {};
|
||||||
git-fuzzy = prev.callPackage ./git-fuzzy {};
|
git-fuzzy = prev.callPackage ./git-fuzzy {};
|
||||||
|
kanidm = prev.kanidm.overrideAttrs (old: let
|
||||||
|
provisionSrc = prev.fetchFromGitHub {
|
||||||
|
owner = "oddlama";
|
||||||
|
repo = "kanidm-provision";
|
||||||
|
rev = "aa7a1c8ec04622745b385bd3b0462e1878f56b51";
|
||||||
|
hash = "sha256-NRolS3l2kARjkhWP7FYUG//KCEiueh48ZrADdCDb9Zg=";
|
||||||
|
};
|
||||||
|
in {
|
||||||
|
patches =
|
||||||
|
old.patches
|
||||||
|
++ [
|
||||||
|
"${provisionSrc}/patches/${old.version}-oauth2-basic-secret-modify.patch"
|
||||||
|
"${provisionSrc}/patches/${old.version}-recover-account.patch"
|
||||||
|
];
|
||||||
|
passthru.enableSecretProvisioning = true;
|
||||||
|
});
|
||||||
|
kanidm-provision = prev.callPackage ./kanidm-provision.nix {};
|
||||||
kanidm-secret-manipulator = prev.callPackage ./kanidm-secret-manipulator.nix {};
|
kanidm-secret-manipulator = prev.callPackage ./kanidm-secret-manipulator.nix {};
|
||||||
segoe-ui-ttf = prev.callPackage ./segoe-ui-ttf.nix {};
|
segoe-ui-ttf = prev.callPackage ./segoe-ui-ttf.nix {};
|
||||||
zsh-histdb-skim = prev.callPackage ./zsh-skim-histdb.nix {};
|
zsh-histdb-skim = prev.callPackage ./zsh-skim-histdb.nix {};
|
||||||
awakened-poe-trade = prev.callPackage ./awakened-poe-trade.nix {};
|
awakened-poe-trade = prev.callPackage ./awakened-poe-trade.nix {};
|
||||||
neovim-clean = prev.neovim-unwrapped.overrideAttrs (_neovimFinal: neovimPrev: {
|
neovim-clean = prev.neovim-unwrapped.overrideAttrs (old: {
|
||||||
nativeBuildInputs = (neovimPrev.nativeBuildInputs or []) ++ [prev.makeWrapper];
|
nativeBuildInputs = (old.nativeBuildInputs or []) ++ [prev.makeWrapper];
|
||||||
postInstall =
|
postInstall =
|
||||||
(neovimPrev.postInstall or "")
|
(old.postInstall or "")
|
||||||
+ ''
|
+ ''
|
||||||
wrapProgram $out/bin/nvim --add-flags "--clean"
|
wrapProgram $out/bin/nvim --add-flags "--clean"
|
||||||
'';
|
'';
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
{
|
{
|
||||||
bc,
|
bc,
|
||||||
lib,
|
|
||||||
nvd,
|
|
||||||
writeShellApplication,
|
writeShellApplication,
|
||||||
}: let
|
}: let
|
||||||
deploy = writeShellApplication {
|
deploy = writeShellApplication {
|
||||||
|
@ -109,7 +107,8 @@
|
||||||
ssh "$host" -- "$store_path"/bin/switch-to-configuration "$ACTION" \
|
ssh "$host" -- "$store_path"/bin/switch-to-configuration "$ACTION" \
|
||||||
|| echo "Error while activating new system" >&2
|
|| echo "Error while activating new system" >&2
|
||||||
if [[ -n "$prev_system" ]]; then
|
if [[ -n "$prev_system" ]]; then
|
||||||
ssh "$host" -- ${lib.getExe nvd} --color always diff "$prev_system" "$store_path" || true
|
# nvd must be installed on the target system for this to work
|
||||||
|
ssh "$host" -- nvd --color always diff "$prev_system" "$store_path" || true
|
||||||
fi
|
fi
|
||||||
time_next
|
time_next
|
||||||
echo "[1;32m Applied [m✅ [34m$host[m [90min ''${T_LAST}s[m"
|
echo "[1;32m Applied [m✅ [34m$host[m [90min ''${T_LAST}s[m"
|
||||||
|
|
26
pkgs/kanidm-provision.nix
Normal file
26
pkgs/kanidm-provision.nix
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
rustPlatform,
|
||||||
|
fetchFromGitHub,
|
||||||
|
}:
|
||||||
|
rustPlatform.buildRustPackage rec {
|
||||||
|
pname = "kanidm-provision";
|
||||||
|
version = "1.0.0";
|
||||||
|
|
||||||
|
src = fetchFromGitHub {
|
||||||
|
owner = "oddlama";
|
||||||
|
repo = "kanidm-provision";
|
||||||
|
rev = "v${version}";
|
||||||
|
hash = "sha256-T6kiBUdOMHCWRUF/vepoPrvaULDQrUGYsd/3I11HCLY=";
|
||||||
|
};
|
||||||
|
|
||||||
|
cargoHash = "sha256-nHp3C6szJxOogH/kETIqcQQNhFqBCO0P66j7n3UHuwo=";
|
||||||
|
|
||||||
|
meta = with lib; {
|
||||||
|
description = "A small utility to help with kanidm provisioning";
|
||||||
|
homepage = "https://github.com/oddlama/kanidm-provision";
|
||||||
|
license = with licenses; [asl20 mit];
|
||||||
|
maintainers = with maintainers; [oddlama];
|
||||||
|
mainProgram = "kanidm-provision";
|
||||||
|
};
|
||||||
|
}
|
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue