diff --git a/clan.nix b/clan.nix index f0ec880..1b02304 100644 --- a/clan.nix +++ b/clan.nix @@ -141,6 +141,16 @@ }; }; + persistence = { + module = { + name = "persistence"; + input = "self"; + }; + + # TODO :: Convert to use tags instead + roles.default.tags = ["operational:availability:always-on" "operational:storage:large"]; + }; + identity = { module = { name = "identity"; @@ -151,20 +161,139 @@ tags = ["operational:availability:always-on"]; settings = { + persistence_instance = "persistence"; + + organization = { + nix = { + user = { + chris = { + email = "chris@kruining.eu"; + firstName = "Chris"; + lastName = "Kruining"; + + roles = ["ORG_OWNER"]; + instanceRoles = ["IAM_OWNER"]; + }; + + kaas = { + email = "chris+kaas@kruining.eu"; + firstName = "Kaas"; + lastName = "Kruining"; + }; + }; + + project = { + ulmo = { + projectRoleCheck = true; + projectRoleAssertion = true; + hasProjectCheck = true; + + role = { + jellyfin = { + group = "jellyfin"; + }; + jellyfin_admin = { + group = "jellyfin"; + }; + }; + + assign = { + chris = ["jellyfin" "jellyfin_admin"]; + kaas = ["jellyfin"]; + }; + + application = { + jellyfin = { + redirectUris = ["https://jellyfin.kruining.eu/sso/OID/redirect/zitadel"]; + grantTypes = ["authorizationCode"]; + responseTypes = ["code"]; + }; + + forgejo = { + redirectUris = ["https://git.amarth.cloud/user/oauth2/zitadel/callback"]; + grantTypes = ["authorizationCode"]; + responseTypes = ["code"]; + }; + + vaultwarden = { + redirectUris = ["https://vault.kruining.eu/identity/connect/oidc-signin"]; + grantTypes = ["authorizationCode"]; + responseTypes = ["code"]; + exportMap = { + client_id = "SSO_CLIENT_ID"; + client_secret = "SSO_CLIENT_SECRET"; + }; + }; + + matrix = { + redirectUris = ["https://matrix.kruining.eu/_synapse/client/oidc/callback"]; + grantTypes = ["authorizationCode"]; + responseTypes = ["code"]; + }; + + mydia = { + redirectUris = ["http://localhost:2010/auth/oidc/callback"]; + grantTypes = ["authorizationCode"]; + responseTypes = ["code"]; + }; + + grafana = { + redirectUris = ["http://localhost:9001/login/generic_oauth"]; + grantTypes = ["authorizationCode"]; + responseTypes = ["code"]; + }; + }; + }; + + convex = { + projectRoleCheck = true; + projectRoleAssertion = true; + hasProjectCheck = true; + + application = { + scry = { + redirectUris = ["https://nautical-salamander-320.eu-west-1.convex.cloud/api/auth/callback/zitadel"]; + grantTypes = ["authorizationCode"]; + responseTypes = ["code"]; + }; + }; + }; + }; + + action = { + flattenRoles = { + script = '' + (ctx, api) => { + if (ctx.v1.user.grants == undefined || ctx.v1.user.grants.count == 0) { + return; + } + + const roles = ctx.v1.user.grants.grants.flatMap(({ roles, projectId }) => roles.map(role => projectId + ':' + role)); + + api.v1.claims.setClaim('nix:zitadel:custom', JSON.stringify({ roles })); + }; + ''; + }; + }; + + triggers = [ + { + flowType = "customiseToken"; + triggerType = "preUserinfoCreation"; + actions = ["flattenRoles"]; + } + { + flowType = "customiseToken"; + triggerType = "preAccessTokenCreation"; + actions = ["flattenRoles"]; + } + ]; + }; + }; }; }; }; - persistence = { - module = { - name = "persistence"; - input = "self"; - }; - - # TODO :: Convert to use tags instead - roles.default.tags = ["operational:availability:always-on" "operational:storage:large"]; - }; - servarr = { module = { name = "servarr"; diff --git a/clanServices/identity/default.nix b/clanServices/identity/default.nix index 1c07781..7def487 100644 --- a/clanServices/identity/default.nix +++ b/clanServices/identity/default.nix @@ -22,7 +22,7 @@ in { description = ''''; interface = {lib, ...}: let - inherit (lib) mkOption types; + inherit (lib) mkOption types toSentenceCase literalExpression; in { options = { driver = mkOption { @@ -30,16 +30,308 @@ in { default = "zitadel"; }; + persistence_instance = mkOption { + type = types.str; + }; + port = mkOption { type = types.port; default = 9092; }; + + organization = mkOption { + type = types.attrsOf (types.submodule ({ name, ... }: { + options = + let + org = name; + in + { + isDefault = mkOption { + type = types.bool; + default = false; + example = "true"; + description = '' + True sets the '${org}' org as default org for the instance. Only one org can be default org. + Nothing happens if you set it to false until you set another org as default org. + ''; + }; + + project = mkOption { + default = {}; + type = types.attrsOf (types.submodule { + options = { + hasProjectCheck = mkOption { + type = types.bool; + default = false; + example = "true"; + description = '' + ZITADEL checks if the org of the user has permission to this project. + ''; + }; + + privateLabelingSetting = mkOption { + type = types.nullOr (types.enum [ "unspecified" "enforceProjectResourceOwnerPolicy" "allowLoginUserResourceOwnerPolicy" ]); + default = null; + example = "enforceProjectResourceOwnerPolicy"; + description = '' + Defines from where the private labeling should be triggered, + + supported values: + - unspecified + - enforceProjectResourceOwnerPolicy + - allowLoginUserResourceOwnerPolicy + ''; + }; + + projectRoleAssertion = mkOption { + type = types.bool; + default = false; + example = "true"; + description = '' + Describes if roles of user should be added in token. + ''; + }; + + projectRoleCheck = mkOption { + type = types.bool; + default = false; + example = "true"; + description = '' + ZITADEL checks if the user has at least one on this project. + ''; + }; + + role = mkOption { + default = {}; + type = types.attrsOf (types.submodule ({ name, ... }: { + options = + let + roleName = name; + in + { + displayName = mkOption { + type = types.str; + default = toSentenceCase name; + example = "RoleName"; + description = '' + Name used for project role. + ''; + }; + + group = mkOption { + type = types.nullOr types.str; + default = null; + example = "some_group"; + description = '' + Group used for project role. + ''; + }; + }; + })); + }; + + assign = mkOption { + default = {}; + type = types.attrsOf (types.listOf types.str); + }; + + application = mkOption { + default = {}; + type = types.attrsOf (types.submodule { + options = { + redirectUris = mkOption { + type = types.nonEmptyListOf types.str; + example = '' + [ "https://example.com/redirect/url" ] + ''; + description = '' + . + ''; + }; + + grantTypes = mkOption { + type = types.nonEmptyListOf (types.enum [ "authorizationCode" "implicit" "refreshToken" "deviceCode" "tokenExchange" ]); + example = '' + [ "authorizationCode" ] + ''; + description = '' + . + ''; + }; + + responseTypes = mkOption { + type = types.nonEmptyListOf (types.enum [ "code" "idToken" "idTokenToken" ]); + example = '' + [ "code" ] + ''; + description = '' + . + ''; + }; + + exportMap = + let + strOpt = mkOption { type = types.nullOr types.str; default = null; }; + in + mkOption { + type = types.submodule { options = { client_id = strOpt; client_secret = strOpt; }; }; + default = {}; + example = literalExpression '' + { + client_id = "SSO_CLIENT_ID"; + client_secret = "SSO_CLIENT_SECRET"; + } + ''; + description = '' + Remap the outputted variables to another key. + ''; + }; + }; + }); + }; + }; + }); + }; + + user = mkOption { + default = {}; + type = types.attrsOf (types.submodule ({ name, ... }: { + options = + let + username = name; + in + { + email = mkOption { + type = types.str; + example = "someone@some.domain"; + description = '' + Username. + ''; + }; + + userName = mkOption { + type = types.nullOr types.str; + default = username; + example = "some_user_name"; + description = '' + Username. Default value is the key of the config object you created, you can overwrite that by setting this option + ''; + }; + + firstName = mkOption { + type = types.str; + example = "John"; + description = '' + First name of the user. + ''; + }; + + lastName = mkOption { + type = types.str; + example = "Doe"; + description = '' + Last name of the user. + ''; + }; + + roles = mkOption { + type = types.listOf types.str; + default = []; + example = "[ \"ORG_OWNER\" ]"; + description = '' + List of roles granted to organisation. + ''; + }; + + instanceRoles = mkOption { + type = types.listOf types.str; + default = []; + example = "[ \"IAM_OWNER\" ]"; + description = '' + List of roles granted to instance. + ''; + }; + }; + })); + }; + + action = mkOption { + default = {}; + type = types.attrsOf (types.submodule ({ name, ... }: { + options = { + script = mkOption { + type = types.str; + example = '' + (ctx, api) => { + api.v1.claims.setClaim('some_claim', 'some_value'); + }; + ''; + description = '' + The script to run. This must be a function that receives 2 parameters, and returns void. During the creation of the action's script this module simly does `const {{name}} = {{script}}`. + ''; + }; + + timeout = mkOption { + type = (types.ints.between 0 20); + default = 10; + example = "10"; + description = '' + After which time the action will be terminated if not finished. + ''; + }; + + allowedToFail = mkOption { + type = types.bool; + default = true; + example = "true"; + description = '' + Allowed to fail. + ''; + }; + }; + })); + }; + + triggers = mkOption { + default = []; + type = types.listOf (types.submodule { + options = { + flowType = mkOption { + type = types.enum [ "authentication" "customiseToken" "internalAuthentication" "samlResponse" ]; + example = "customiseToken"; + description = '' + Type of the flow to which the action triggers belong. + ''; + }; + + triggerType = mkOption { + type = types.enum [ "postAuthentication" "preCreation" "postCreation" "preUserinfoCreation" "preAccessTokenCreation" "preSamlResponse" ]; + example = "postAuthentication"; + description = '' + Trigger type on when the actions get triggered. + ''; + }; + + actions = mkOption { + type = types.nonEmptyListOf types.str; + example = ''[ "action_name" ]''; + description = '' + Names of actions to trigger + ''; + }; + }; + }); + }; + }; + })); + }; }; }; perInstance = { mkExports, settings, + machine, ... }: let database = @@ -117,6 +409,19 @@ in { settings = { Port = settings.port; + ExternalDomain = "auth.kruining.eu"; + ExternalPort = 443; + ExternalSecure = true; + + Metrics.Type = "otel"; + Tracing.Type = "otel"; + Telemetry.Enabled = true; + + SystemDefaults = { + PasswordHasher.Hasher.Algorithm = "argon2id"; + SecretHasher.Hasher.Algorithm = "argon2id"; + }; + Database.postgres = { Host = database.host; Port = database.port; @@ -129,6 +434,12 @@ in { }; }; }; + + steps = { + InstanceName = "eu"; + + MachineKeyPath = "/var/lib/zitadel/machine-key.json"; + } }; }) ];