diff --git a/flake.lock b/flake.lock index 2bc7385..935fbaf 100644 --- a/flake.lock +++ b/flake.lock @@ -265,6 +265,27 @@ "type": "github" } }, + "flake-parts_3": { + "inputs": { + "nixpkgs-lib": [ + "terranix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1736143030, + "narHash": "sha256-+hu54pAoLDEZT9pjHlqL9DNzWz0NbUn8NEAHP7PQPzU=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "b905f6fc23a9051a6e1b741e1438dbfc0634c6de", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, "flake-utils": { "inputs": { "systems": "systems" @@ -906,6 +927,7 @@ "snowfall-lib": "snowfall-lib", "sops-nix": "sops-nix", "stylix": "stylix", + "terranix": "terranix", "zen-browser": "zen-browser" } }, @@ -1109,6 +1131,43 @@ "type": "github" } }, + "systems_7": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "terranix": { + "inputs": { + "flake-parts": "flake-parts_3", + "nixpkgs": [ + "nixpkgs" + ], + "systems": "systems_7" + }, + "locked": { + "lastModified": 1757278723, + "narHash": "sha256-hTMi6oGU+6VRnW9SZZ+muFcbfMEf2ajjOp7Z2KM5MMY=", + "owner": "terranix", + "repo": "terranix", + "rev": "924573fa6587ac57b0d15037fbd2d3f0fcdf17fb", + "type": "github" + }, + "original": { + "owner": "terranix", + "repo": "terranix", + "type": "github" + } + }, "tinted-foot": { "flake": false, "locked": { diff --git a/flake.nix b/flake.nix index c659d4f..8ea1571 100644 --- a/flake.nix +++ b/flake.nix @@ -78,6 +78,11 @@ flake-compat.follows = ""; }; }; + + terranix = { + url = "github:terranix/terranix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; outputs = inputs: inputs.snowfall-lib.mkFlake { diff --git a/lib/strings/default.nix b/lib/strings/default.nix new file mode 100644 index 0000000..52b05e3 --- /dev/null +++ b/lib/strings/default.nix @@ -0,0 +1,17 @@ +{ lib, ...}: +let + inherit (builtins) isString typeOf; + inherit (lib) throwIfNot concatStringsSep splitStringBy toLower map; +in +{ + strings = { + toSnakeCase = + str: + throwIfNot (isString str) "toSnakeCase only accepts string values, but got ${typeOf str}" ( + str + |> splitStringBy (prev: curr: builtins.match "[a-z]" prev != null && builtins.match "[A-Z]" curr != null) true + |> map (p: toLower p) + |> concatStringsSep "_" + ); + }; +} \ No newline at end of file diff --git a/modules/nixos/services/authentication/zitadel/default.nix b/modules/nixos/services/authentication/zitadel/default.nix index e0e4a59..66f5fc0 100644 --- a/modules/nixos/services/authentication/zitadel/default.nix +++ b/modules/nixos/services/authentication/zitadel/default.nix @@ -1,6 +1,7 @@ -{ config, lib, pkgs, namespace, ... }: +{ config, lib, pkgs, namespace, system, inputs, ... }: let - inherit (lib) mkIf mkEnableOption; + inherit (lib) mkIf mkEnableOption mkOption types toUpper nameValuePair; + inherit (lib.${namespace}.strings) toSnakeCase; cfg = config.${namespace}.services.authentication.zitadel; @@ -9,15 +10,223 @@ in { options.${namespace}.services.authentication.zitadel = { enable = mkEnableOption "Zitadel"; + + organization = mkOption { + type = types.attrsOf (types.submodule { + options = { + isDefault = mkOption { + type = types.bool; + default = false; + example = "true"; + description = '' + True sets the 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. + ''; + }; + + 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 = '' + . + ''; + }; + }; + }); + }; + }; + }); + }; + }; + }); + }; }; - config = mkIf cfg.enable { + config = let + mapRef = type: name: { "${type}Id" = "\${ resource.zitadel_${type}.${toSnakeCase name}.id }"; }; + mapEnum = prefix: value: "${prefix}_${value |> toSnakeCase |> toUpper}"; + + mapValue = type: value: ({ + grantTypes = map (t: mapEnum "OIDC_GRANT_TYPE" t) value; + responseTypes = map (t: mapEnum "OIDC_RESPONSE_TYPE" t) value; + }."${type}" or value); + + toResource = name: value: nameValuePair + (toSnakeCase name) + (lib.mapAttrs' (k: v: nameValuePair (toSnakeCase k) (mapValue k v)) value); + + withName = name: attrs: attrs // { inherit name; }; + withRef = type: name: attrs: attrs // (mapRef type name); + + # this is a nix package, the generated json file to be exact + terraformConfiguration = inputs.terranix.lib.terranixConfiguration { + inherit system; + + modules = + let + inherit (lib) mapAttrs' concatMapAttrs nameValuePair getAttrs getAttr hasAttr typeOf head drop length; + + 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; + in + [ + ({ config, lib, ... }: { + config = { + terraform.required_providers.zitadel = { + source = "zitadel/zitadel"; + version = "2.2.0"; + }; + + provider.zitadel = { + domain = "auth.kruining.eu"; + insecure = "false"; + jwt_profile_file = "/var/lib/zitadel/machine-key.json"; + }; + + resource = { + zitadel_org = cfg.organization |> select [] (name: value: + value + |> getAttrs [ "isDefault" ] + |> withName name + |> toResource name + ); + + zitadel_project = cfg.organization |> select [ "project" ] (org: name: value: + value + |> getAttrs [ "hasProjectCheck" "privateLabelingSetting" "projectRoleAssertion" "projectRoleCheck" ] + |> withName name + |> withRef "org" org + |> toResource name + ); + + zitadel_application_oidc = cfg.organization |> select [ "project" "application" ] (org: project: name: value: + value + |> getAttrs [ "redirectUris" "grantTypes" "responseTypes" ] + |> withName name + |> withRef "org" org + |> withRef "project" project + |> toResource name + ); + }; + }; + }) + ]; + }; + in + mkIf cfg.enable { ${namespace}.services.persistance.postgresql.enable = true; environment.systemPackages = with pkgs; [ zitadel ]; + systemd.tmpfiles.rules = [ + "d /tmp/zitadelApplyTerraform 0755 zitadel zitadel -" + ]; + + systemd.services.zitadelApplyTerraform = { + description = "Zitadel terraform apply"; + + wantedBy = [ "multi-user.target" ]; + wants = [ "zitadel.service" ]; + + script = '' + #!/usr/bin/env bash + + # Copy infra code into workspace + cp -f ${terraformConfiguration} config.tf.json + + # Initialize OpenTofu + ${lib.getExe pkgs.opentofu} init + + # Run the infrastructure code + # ${lib.getExe pkgs.opentofu} plan + ${lib.getExe pkgs.opentofu} apply -auto-approve + ''; + + serviceConfig = { + Type = "oneshot"; + User = "zitadel"; + Group = "zitadel"; + + WorkingDirectory = "/tmp/zitadelApplyTerraform"; + }; + }; + services = { zitadel = { enable = true; @@ -41,31 +250,31 @@ in SecretHasher.Hasher.Algorithm = "argon2id"; }; - DefaultInstance = { - PasswordComplexityPolicy = { - MinLength = 20; - HasLowercase = false; - HasUppercase = false; - HasNumber = false; - HasSymbol = false; - }; - LoginPolicy = { - AllowRegister = false; - ForceMFA = true; - }; - LockoutPolicy = { - MaxPasswordAttempts = 5; - MaxOTPAttempts = 10; - }; - SMTPConfiguration = { - SMTP = { - Host = "black-mail.nl:587"; - User = "chris@kruining.eu"; - Password = "__TODO_USE_SOPS__"; - }; - FromName = "Amarth Zitadel"; - }; - }; + # DefaultInstance = { + # # PasswordComplexityPolicy = { + # # MinLength = 0; + # # HasLowercase = false; + # # HasUppercase = false; + # # HasNumber = false; + # # HasSymbol = false; + # # }; + # LoginPolicy = { + # AllowRegister = false; + # ForceMFA = true; + # }; + # LockoutPolicy = { + # MaxPasswordAttempts = 5; + # MaxOTPAttempts = 10; + # }; + # # SMTPConfiguration = { + # # SMTP = { + # # Host = "black-mail.nl:587"; + # # User = "chris@kruining.eu"; + # # Password = "__TODO_USE_SOPS__"; + # # }; + # # FromName = "Amarth Zitadel"; + # # }; + # }; Database.postgres = { Host = "localhost"; @@ -84,9 +293,16 @@ in }; steps = { FirstInstance = { - InstanceName = "auth.kruining.eu"; + # Not sure, this option seems to be mostly irrelevant + InstanceName = "eu"; + + MachineKeyPath = "/var/lib/zitadel/machine-key.json"; + # PatPath = "/var/lib/zitadel/machine-key.pat"; + # LoginClientPatPath = "/var/lib/zitadel/machine-key.json"; + Org = { - Name = "Amarth"; + Name = "kruining"; + Human = { UserName = "chris"; FirstName = "Chris"; @@ -97,6 +313,20 @@ in }; Password = "KaasIsAwesome1!"; }; + + Machine = { + Machine = { + Username = "terraform-service-user"; + Name = "Terraform"; + }; + MachineKey = { ExpirationDate = "2026-01-01T00:00:00Z"; Type = 1; }; + # Pat = { ExpirationDate = "2026-01-01T00:00:00Z"; }; + }; + + # LoginClient.Machine = { + # Username = "terraform-service-user"; + # Name = "Terraform"; + # }; }; }; }; diff --git a/systems/x86_64-linux/ulmo/default.nix b/systems/x86_64-linux/ulmo/default.nix index 0794585..4845e73 100644 --- a/systems/x86_64-linux/ulmo/default.nix +++ b/systems/x86_64-linux/ulmo/default.nix @@ -39,7 +39,33 @@ sneeuwvlok = { services = { # authentication.authelia.enable = true; - authentication.zitadel.enable = true; + authentication.zitadel = { + enable = true; + + organization = { + thisIsMyAwesomeOrg = {}; + + nix = { + project = { + ulmo = { + 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" ]; + }; + }; + }; + }; + }; + }; + }; communication.matrix.enable = true;