sneeuwvlok/clanServices/identity/lib.nix
2026-04-13 16:31:43 +02:00

372 lines
11 KiB
Nix

{
lib,
ardaLib,
self,
pkgs,
settings,
...
}: let
createTerranixModule = {
users,
email_password,
key_file,
...
}: terra: let
inherit (lib) toUpper toSentenceCase nameValuePair mapAttrs mapAttrs' concatMapAttrs concatMapStringsSep filterAttrsRecursive listToAttrs imap0 head drop length literalExpression attrNames;
inherit (ardaLib) toSnakeCase;
inherit (terra.lib) tfRef;
_refTypeMap = {
org = {type = "org";};
project = {type = "project";};
user = {
type = "user";
tfType = "human_user";
};
};
mapRef' = {
type,
tfType ? type,
}: name: {"${type}Id" = "\${ resource.zitadel_${tfType}.${toSnakeCase name}.id }";};
mapRef = type: name: mapRef' (_refTypeMap.${type}) name;
mapEnum = prefix: value: "${prefix}_${value |> toSnakeCase |> toUpper}";
mapValue = type: value: ({
appType = mapEnum "OIDC_APP_TYPE" value;
grantTypes = map (t: mapEnum "OIDC_GRANT_TYPE" t) value;
responseTypes = map (t: mapEnum "OIDC_RESPONSE_TYPE" t) value;
authMethodType = mapEnum "OIDC_AUTH_METHOD_TYPE" value;
flowType = mapEnum "FLOW_TYPE" value;
triggerType = mapEnum "TRIGGER_TYPE" value;
accessTokenType = mapEnum "OIDC_TOKEN_TYPE" value;
}."${type}" or value);
toResource = name: value:
nameValuePair
(toSnakeCase name)
(lib.mapAttrs' (k: v: nameValuePair (toSnakeCase k) (mapValue k v)) value);
withRef = type: name: attrs: attrs // (mapRef type name);
select = keys: callback: set:
if (length keys) == 0
then mapAttrs' callback set
else let
key = head keys;
in
concatMapAttrs (k: v: select (drop 1 keys) (callback k) (v.${key} or {})) set;
append = attrList: set: set // (listToAttrs attrList);
forEach = src: key: set: let
_key = concatMapStringsSep "_" (k: "\${item.${k}}") key;
in
{
forEach = tfRef '' {
for item in ${src} :
"''${item.org}_''${item.name}" => item
}'';
}
// set;
in {
terraform.required_providers.zitadel = {
source = "zitadel/zitadel";
version = "2.2.0";
};
provider.zitadel = {
domain = "auth.kruining.eu";
insecure = false;
system_api = {
user = "infra";
inherit key_file;
};
};
locals = {
extra_users = tfRef "
flatten([ for org, users in jsondecode(file(\"${users}\")): [
for name, details in users: {
org = org
name = name
email = details.email
firstName = details.firstName
lastName = details.lastName
}
] ])
";
orgs = settings.organization |> mapAttrs (org: _: tfRef "resource.zitadel_org.${org}.id");
};
resource = {
# Organizations
zitadel_org =
settings.organization
|> select [] (
name: {isDefault, ...}:
{inherit name isDefault;}
|> toResource name
);
# Projects per organization
zitadel_project =
settings.organization
|> select ["project"] (
org: name: {
hasProjectCheck,
privateLabelingSetting,
projectRoleAssertion,
projectRoleCheck,
...
}:
{
inherit name hasProjectCheck privateLabelingSetting projectRoleAssertion projectRoleCheck;
}
|> withRef "org" org
|> toResource "${org}_${name}"
);
# Each OIDC app per project
zitadel_application_oidc =
settings.organization
|> select ["project" "application"] (
org: project: name: {
redirectUris,
grantTypes,
responseTypes,
...
}:
{
inherit name redirectUris grantTypes responseTypes;
accessTokenRoleAssertion = true;
idTokenRoleAssertion = true;
accessTokenType = "JWT";
}
|> withRef "org" org
|> withRef "project" "${org}_${project}"
|> toResource "${org}_${project}_${name}"
);
# Each project role
zitadel_project_role =
settings.organization
|> select ["project" "role"] (
org: project: name: value:
{
inherit (value) displayName group;
roleKey = name;
}
|> withRef "org" org
|> withRef "project" "${org}_${project}"
|> toResource "${org}_${project}_${name}"
);
# Each project role assignment
zitadel_user_grant =
settings.organization
|> select ["project" "assign"] (
org: project: user: roles:
{roleKeys = roles;}
|> withRef "org" org
|> withRef "project" "${org}_${project}"
|> withRef "user" "${org}_${user}"
|> toResource "${org}_${project}_${user}"
);
# Users
zitadel_human_user =
settings.organization
|> select ["user"] (
org: name: {
email,
userName,
firstName,
lastName,
...
}:
{
inherit email userName firstName lastName;
isEmailVerified = true;
lifecycle = {
ignore_changes = ["first_name" "last_name" "user_name"];
};
}
|> withRef "org" org
|> toResource "${org}_${name}"
)
|> append [
(forEach "local.extra_users" ["org" "name"] {
orgId = tfRef "local.orgs[each.value.org]";
userName = tfRef "each.value.name";
email = tfRef "each.value.email";
firstName = tfRef "each.value.firstName";
lastName = tfRef "each.value.lastName";
isEmailVerified = true;
}
|> toResource "extraUsers")
];
# Global user roles
zitadel_instance_member =
settings.organization
|> filterAttrsRecursive (n: v: !(v ? "instanceRoles" && (length v.instanceRoles) == 0))
|> select ["user"] (
org: name: {instanceRoles, ...}:
{roles = instanceRoles;}
|> withRef "user" "${org}_${name}"
|> toResource "${org}_${name}"
);
# Organazation specific roles
zitadel_org_member =
settings.organization
|> filterAttrsRecursive (n: v: !(v ? "roles" && (length v.roles) == 0))
|> select ["user"] (
org: name: {roles, ...}:
{inherit roles;}
|> withRef "org" org
|> withRef "user" "${org}_${name}"
|> toResource "${org}_${name}"
);
# Organazation's actions
zitadel_action =
settings.organization
|> select ["action"] (
org: name: {
timeout,
allowedToFail,
script,
...
}:
{
inherit allowedToFail name;
timeout = "${toString timeout}s";
script = "const ${name} = ${script}";
}
|> withRef "org" org
|> toResource "${org}_${name}"
);
# Organazation's action assignments
zitadel_trigger_actions =
settings.organization
|> concatMapAttrs (
org: {triggers, ...}:
triggers
|> imap0 (i: {
flowType,
triggerType,
actions,
...
}: (
let
name = "trigger_${toString i}";
in
{
inherit flowType triggerType;
actionIds =
actions
|> map (action: (tfRef "zitadel_action.${org}_${toSnakeCase action}.id"));
}
|> withRef "org" org
|> toResource "${org}_${name}"
))
|> listToAttrs
);
# SMTP config
zitadel_smtp_config.default = {
sender_address = "chris@kruining.eu";
sender_name = "no-reply (Zitadel)";
tls = true;
host = "black-mail.nl:587";
user = "chris@kruining.eu";
password = tfRef "file(\"${email_password}\")";
set_active = true;
};
# Client credentials per app
local_sensitive_file =
settings.organization
|> select ["project" "application"] (
org: project: name: {exportMap, ...}:
nameValuePair "${org}_${project}_${name}" {
content = ''
${
if exportMap.client_id != null
then exportMap.client_id
else "CLIENT_ID"
}=${tfRef "resource.zitadel_application_oidc.${org}_${project}_${name}.client_id"}
${
if exportMap.client_secret != null
then exportMap.client_secret
else "CLIENT_SECRET"
}=${tfRef "resource.zitadel_application_oidc.${org}_${project}_${name}.client_secret"}
'';
filename = "/var/lib/zitadel/clients/${org}_${project}_${name}";
}
);
};
};
in {
createInfra = args @ {...}: let
tofu = "${lib.getExe pkgs.opentofu} -input=false";
terraformConfiguration = self.inputs.terranix.lib.terranixConfiguration {
system = pkgs.stdenv.hostPlatform.system;
modules = [
(createTerranixModule args)
];
};
in {
systemd.services."infra-zitadel" = {
description = "Infra for Zitadel";
wantedBy = ["multi-user.target"];
wants = ["zitadel.service"];
after = ["zitadel.service"];
preStart = ''
install -d -m 0770 -o zitadel -g media /var/lib/infra-zitadel
'';
script = ''
# Sleep for a bit to give the service a chance to start up
sleep 5s
if [ "$(systemctl is-active zitadel)" != "active" ]; then
echo "zitadel is not running"
exit 1
fi
# Print the path to the source for easier debugging
echo "config location: ${terraformConfiguration}"
# Copy infra code into workspace
cp -f ${terraformConfiguration} config.tf.json
# Initialize OpenTofu
${tofu} init
# Run the infrastructure code
${tofu} plan -out=tfplan
${tofu} apply -json -auto-approve tfplan
'';
serviceConfig = {
Type = "oneshot";
User = "zitadel";
Group = "zitadel";
StateDirectory = "/var/lib/infra-zitadel";
};
};
};
}