diff --git a/nixos/modules/services/web-apps/keycloak.nix b/nixos/modules/services/web-apps/keycloak.nix index aff4ed8dd608..a01f0049b2c7 100644 --- a/nixos/modules/services/web-apps/keycloak.nix +++ b/nixos/modules/services/web-apps/keycloak.nix @@ -3,299 +3,312 @@ let cfg = config.services.keycloak; opt = options.services.keycloak; + + inherit (lib) types mkOption concatStringsSep mapAttrsToList + escapeShellArg recursiveUpdate optionalAttrs boolToString mkOrder + sort filterAttrs concatMapStringsSep concatStrings mkIf + optionalString optionals mkDefault literalExpression hasSuffix + foldl' isAttrs filter attrNames elem literalDocBook + maintainers; + + inherit (builtins) match typeOf; in { - options.services.keycloak = { - - enable = lib.mkOption { - type = lib.types.bool; - default = false; - example = true; - description = '' - Whether to enable the Keycloak identity and access management - server. - ''; - }; - - bindAddress = lib.mkOption { - type = lib.types.str; - default = "\${jboss.bind.address:0.0.0.0}"; - example = "127.0.0.1"; - description = '' - On which address Keycloak should accept new connections. - - A special syntax can be used to allow command line Java system - properties to override the value: ''${property.name:value} - ''; - }; - - httpPort = lib.mkOption { - type = lib.types.str; - default = "\${jboss.http.port:80}"; - example = "8080"; - description = '' - On which port Keycloak should listen for new HTTP connections. - - A special syntax can be used to allow command line Java system - properties to override the value: ''${property.name:value} - ''; - }; - - httpsPort = lib.mkOption { - type = lib.types.str; - default = "\${jboss.https.port:443}"; - example = "8443"; - description = '' - On which port Keycloak should listen for new HTTPS connections. - - A special syntax can be used to allow command line Java system - properties to override the value: ''${property.name:value} - ''; - }; - - frontendUrl = lib.mkOption { - type = lib.types.str; - apply = x: - if x == "" || lib.hasSuffix "/" x then - x - else - x + "/"; - example = "keycloak.example.com/auth"; - description = '' - The public URL used as base for all frontend requests. Should - normally include a trailing /auth. - - See the - Hostname section of the Keycloak server installation - manual for more information. - ''; - }; - - forceBackendUrlToFrontendUrl = lib.mkOption { - type = lib.types.bool; - default = false; - example = true; - description = '' - Whether Keycloak should force all requests to go through the - frontend URL configured in . By default, - Keycloak allows backend requests to instead use its local - hostname or IP address and may also advertise it to clients - through its OpenID Connect Discovery endpoint. - - See the - Hostname section of the Keycloak server installation - manual for more information. - ''; - }; - - sslCertificate = lib.mkOption { - type = lib.types.nullOr lib.types.path; - default = null; - example = "/run/keys/ssl_cert"; - description = '' - The path to a PEM formatted certificate to use for TLS/SSL - connections. - - This should be a string, not a Nix path, since Nix paths are - copied into the world-readable Nix store. - ''; - }; - - sslCertificateKey = lib.mkOption { - type = lib.types.nullOr lib.types.path; - default = null; - example = "/run/keys/ssl_key"; - description = '' - The path to a PEM formatted private key to use for TLS/SSL - connections. - - This should be a string, not a Nix path, since Nix paths are - copied into the world-readable Nix store. - ''; - }; - - database = { - type = lib.mkOption { - type = lib.types.enum [ "mysql" "postgresql" ]; - default = "postgresql"; - example = "mysql"; + options.services.keycloak = + let + inherit (types) bool str nullOr attrsOf path enum anything + package port; + in + { + enable = mkOption { + type = bool; + default = false; + example = true; description = '' - The type of database Keycloak should connect to. + Whether to enable the Keycloak identity and access management + server. ''; }; - host = lib.mkOption { - type = lib.types.str; - default = "localhost"; + bindAddress = mkOption { + type = str; + default = "\${jboss.bind.address:0.0.0.0}"; + example = "127.0.0.1"; description = '' - Hostname of the database to connect to. + On which address Keycloak should accept new connections. + + A special syntax can be used to allow command line Java system + properties to override the value: ''${property.name:value} ''; }; - port = - let - dbPorts = { - postgresql = 5432; - mysql = 3306; - }; - in - lib.mkOption { - type = lib.types.port; - default = dbPorts.${cfg.database.type}; - defaultText = lib.literalDocBook "default port of selected database"; - description = '' - Port of the database to connect to. - ''; - }; - - useSSL = lib.mkOption { - type = lib.types.bool; - default = cfg.database.host != "localhost"; - defaultText = lib.literalExpression ''config.${opt.database.host} != "localhost"''; + httpPort = mkOption { + type = str; + default = "\${jboss.http.port:80}"; + example = "8080"; description = '' - Whether the database connection should be secured by SSL / - TLS. + On which port Keycloak should listen for new HTTP connections. + + A special syntax can be used to allow command line Java system + properties to override the value: ''${property.name:value} ''; }; - caCert = lib.mkOption { - type = lib.types.nullOr lib.types.path; + httpsPort = mkOption { + type = str; + default = "\${jboss.https.port:443}"; + example = "8443"; + description = '' + On which port Keycloak should listen for new HTTPS connections. + + A special syntax can be used to allow command line Java system + properties to override the value: ''${property.name:value} + ''; + }; + + frontendUrl = mkOption { + type = str; + apply = x: + if x == "" || hasSuffix "/" x then + x + else + x + "/"; + example = "keycloak.example.com/auth"; + description = '' + The public URL used as base for all frontend requests. Should + normally include a trailing /auth. + + See the + Hostname section of the Keycloak server installation + manual for more information. + ''; + }; + + forceBackendUrlToFrontendUrl = mkOption { + type = bool; + default = false; + example = true; + description = '' + Whether Keycloak should force all requests to go through the + frontend URL configured in . By default, + Keycloak allows backend requests to instead use its local + hostname or IP address and may also advertise it to clients + through its OpenID Connect Discovery endpoint. + + See the + Hostname section of the Keycloak server installation + manual for more information. + ''; + }; + + sslCertificate = mkOption { + type = nullOr path; default = null; + example = "/run/keys/ssl_cert"; description = '' - The SSL / TLS CA certificate that verifies the identity of the - database server. - - Required when PostgreSQL is used and SSL is turned on. - - For MySQL, if left at null, the default - Java keystore is used, which should suffice if the server - certificate is issued by an official CA. - ''; - }; - - createLocally = lib.mkOption { - type = lib.types.bool; - default = true; - description = '' - Whether a database should be automatically created on the - local host. Set this to false if you plan on provisioning a - local database yourself. This has no effect if - services.keycloak.database.host is customized. - ''; - }; - - username = lib.mkOption { - type = lib.types.str; - default = "keycloak"; - description = '' - Username to use when connecting to an external or manually - provisioned database; has no effect when a local database is - automatically provisioned. - - To use this with a local database, set to - false and create the database and user - manually. The database should be called - keycloak. - ''; - }; - - passwordFile = lib.mkOption { - type = lib.types.path; - example = "/run/keys/db_password"; - description = '' - File containing the database password. + The path to a PEM formatted certificate to use for TLS/SSL + connections. This should be a string, not a Nix path, since Nix paths are copied into the world-readable Nix store. ''; }; - }; - package = lib.mkOption { - type = lib.types.package; - default = pkgs.keycloak; - defaultText = lib.literalExpression "pkgs.keycloak"; - description = '' - Keycloak package to use. - ''; - }; + sslCertificateKey = mkOption { + type = nullOr path; + default = null; + example = "/run/keys/ssl_key"; + description = '' + The path to a PEM formatted private key to use for TLS/SSL + connections. - initialAdminPassword = lib.mkOption { - type = lib.types.str; - default = "changeme"; - description = '' - Initial password set for the admin - user. The password is not stored safely and should be changed - immediately in the admin panel. - ''; - }; + This should be a string, not a Nix path, since Nix paths are + copied into the world-readable Nix store. + ''; + }; - themes = lib.mkOption { - type = lib.types.attrsOf lib.types.package; - default = {}; - description = '' - Additional theme packages for Keycloak. Each theme is linked into - subdirectory with a corresponding attribute name. + database = { + type = mkOption { + type = enum [ "mysql" "postgresql" ]; + default = "postgresql"; + example = "mysql"; + description = '' + The type of database Keycloak should connect to. + ''; + }; - Theme packages consist of several subdirectories which provide - different theme types: for example, account, - login etc. After adding a theme to this option you - can select it by its name in Keycloak administration console. - ''; - }; + host = mkOption { + type = str; + default = "localhost"; + description = '' + Hostname of the database to connect to. + ''; + }; - extraConfig = lib.mkOption { - type = lib.types.attrsOf lib.types.anything; - default = { }; - example = lib.literalExpression '' - { - "subsystem=keycloak-server" = { - "spi=hostname" = { - "provider=default" = null; - "provider=fixed" = { - enabled = true; - properties.hostname = "keycloak.example.com"; - }; - default-provider = "fixed"; + port = + let + dbPorts = { + postgresql = 5432; + mysql = 3306; }; + in + mkOption { + type = port; + default = dbPorts.${cfg.database.type}; + defaultText = literalDocBook "default port of selected database"; + description = '' + Port of the database to connect to. + ''; }; - } - ''; - description = '' - Additional Keycloak configuration options to set in - standalone.xml. - Options are expressed as a Nix attribute set which matches the - structure of the jboss-cli configuration. The configuration is - effectively overlayed on top of the default configuration - shipped with Keycloak. To remove existing nodes and undefine - attributes from the default configuration, set them to - null. + useSSL = mkOption { + type = bool; + default = cfg.database.host != "localhost"; + defaultText = literalExpression ''config.${opt.database.host} != "localhost"''; + description = '' + Whether the database connection should be secured by SSL / + TLS. + ''; + }; - The example configuration does the equivalent of the following - script, which removes the hostname provider - default, adds the deprecated hostname - provider fixed and defines it the default: + caCert = mkOption { + type = nullOr path; + default = null; + description = '' + The SSL / TLS CA certificate that verifies the identity of the + database server. - - /subsystem=keycloak-server/spi=hostname/provider=default:remove() - /subsystem=keycloak-server/spi=hostname/provider=fixed:add(enabled = true, properties = { hostname = "keycloak.example.com" }) - /subsystem=keycloak-server/spi=hostname:write-attribute(name=default-provider, value="fixed") - + Required when PostgreSQL is used and SSL is turned on. + + For MySQL, if left at null, the default + Java keystore is used, which should suffice if the server + certificate is issued by an official CA. + ''; + }; + + createLocally = mkOption { + type = bool; + default = true; + description = '' + Whether a database should be automatically created on the + local host. Set this to false if you plan on provisioning a + local database yourself. This has no effect if + services.keycloak.database.host is customized. + ''; + }; + + username = mkOption { + type = str; + default = "keycloak"; + description = '' + Username to use when connecting to an external or manually + provisioned database; has no effect when a local database is + automatically provisioned. + + To use this with a local database, set to + false and create the database and user + manually. The database should be called + keycloak. + ''; + }; + + passwordFile = mkOption { + type = path; + example = "/run/keys/db_password"; + description = '' + File containing the database password. + + This should be a string, not a Nix path, since Nix paths are + copied into the world-readable Nix store. + ''; + }; + }; + + package = mkOption { + type = package; + default = pkgs.keycloak; + defaultText = literalExpression "pkgs.keycloak"; + description = '' + Keycloak package to use. + ''; + }; + + initialAdminPassword = mkOption { + type = str; + default = "changeme"; + description = '' + Initial password set for the admin + user. The password is not stored safely and should be changed + immediately in the admin panel. + ''; + }; + + themes = mkOption { + type = attrsOf package; + default = { }; + description = '' + Additional theme packages for Keycloak. Each theme is linked into + subdirectory with a corresponding attribute name. + + Theme packages consist of several subdirectories which provide + different theme types: for example, account, + login etc. After adding a theme to this option you + can select it by its name in Keycloak administration console. + ''; + }; + + extraConfig = mkOption { + type = attrsOf anything; + default = { }; + example = literalExpression '' + { + "subsystem=keycloak-server" = { + "spi=hostname" = { + "provider=default" = null; + "provider=fixed" = { + enabled = true; + properties.hostname = "keycloak.example.com"; + }; + default-provider = "fixed"; + }; + }; + } + ''; + description = '' + Additional Keycloak configuration options to set in + standalone.xml. + + Options are expressed as a Nix attribute set which matches the + structure of the jboss-cli configuration. The configuration is + effectively overlayed on top of the default configuration + shipped with Keycloak. To remove existing nodes and undefine + attributes from the default configuration, set them to + null. + + The example configuration does the equivalent of the following + script, which removes the hostname provider + default, adds the deprecated hostname + provider fixed and defines it the default: + + + /subsystem=keycloak-server/spi=hostname/provider=default:remove() + /subsystem=keycloak-server/spi=hostname/provider=fixed:add(enabled = true, properties = { hostname = "keycloak.example.com" }) + /subsystem=keycloak-server/spi=hostname:write-attribute(name=default-provider, value="fixed") + + + You can discover available options by using the jboss-cli.sh + program and by referring to the Keycloak + Server Installation and Configuration Guide. + ''; + }; - You can discover available options by using the jboss-cli.sh - program and by referring to the Keycloak - Server Installation and Configuration Guide. - ''; }; - }; - config = let # We only want to create a database if we're actually going to connect to it. @@ -303,12 +316,12 @@ in createLocalPostgreSQL = databaseActuallyCreateLocally && cfg.database.type == "postgresql"; createLocalMySQL = databaseActuallyCreateLocally && cfg.database.type == "mysql"; - mySqlCaKeystore = pkgs.runCommand "mysql-ca-keystore" {} '' + mySqlCaKeystore = pkgs.runCommand "mysql-ca-keystore" { } '' ${pkgs.jre}/bin/keytool -importcert -trustcacerts -alias MySQLCACert -file ${cfg.database.caCert} -keystore $out -storepass notsosecretpassword -noprompt ''; # Both theme and theme type directories need to be actual directories in one hierarchy to pass Keycloak checks. - themesBundle = pkgs.runCommand "keycloak-themes" {} '' + themesBundle = pkgs.runCommand "keycloak-themes" { } '' linkTheme() { theme="$1" name="$2" @@ -332,28 +345,29 @@ in fi done - ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: theme: "linkTheme ${theme} ${lib.escapeShellArg name}") cfg.themes)} + ${concatStringsSep "\n" (mapAttrsToList (name: theme: "linkTheme ${theme} ${escapeShellArg name}") cfg.themes)} ''; - keycloakConfig' = builtins.foldl' lib.recursiveUpdate { - "interface=public".inet-address = cfg.bindAddress; - "socket-binding-group=standard-sockets"."socket-binding=http".port = cfg.httpPort; - "subsystem=keycloak-server" = { - "spi=hostname"."provider=default" = { - enabled = true; - properties = { - inherit (cfg) frontendUrl forceBackendUrlToFrontendUrl; + keycloakConfig' = foldl' recursiveUpdate + { + "interface=public".inet-address = cfg.bindAddress; + "socket-binding-group=standard-sockets"."socket-binding=http".port = cfg.httpPort; + "subsystem=keycloak-server" = { + "spi=hostname"."provider=default" = { + enabled = true; + properties = { + inherit (cfg) frontendUrl forceBackendUrlToFrontendUrl; + }; }; + "theme=defaults".dir = toString themesBundle; }; - "theme=defaults".dir = toString themesBundle; - }; - "subsystem=datasources"."data-source=KeycloakDS" = { - max-pool-size = "20"; - user-name = if databaseActuallyCreateLocally then "keycloak" else cfg.database.username; - password = "@db-password@"; - }; - } [ - (lib.optionalAttrs (cfg.database.type == "postgresql") { + "subsystem=datasources"."data-source=KeycloakDS" = { + max-pool-size = "20"; + user-name = if databaseActuallyCreateLocally then "keycloak" else cfg.database.username; + password = "@db-password@"; + }; + } [ + (optionalAttrs (cfg.database.type == "postgresql") { "subsystem=datasources" = { "jdbc-driver=postgresql" = { driver-module-name = "org.postgresql"; @@ -361,16 +375,16 @@ in driver-xa-datasource-class-name = "org.postgresql.xa.PGXADataSource"; }; "data-source=KeycloakDS" = { - connection-url = "jdbc:postgresql://${cfg.database.host}:${builtins.toString cfg.database.port}/keycloak"; + connection-url = "jdbc:postgresql://${cfg.database.host}:${toString cfg.database.port}/keycloak"; driver-name = "postgresql"; - "connection-properties=ssl".value = lib.boolToString cfg.database.useSSL; - } // (lib.optionalAttrs (cfg.database.caCert != null) { + "connection-properties=ssl".value = boolToString cfg.database.useSSL; + } // (optionalAttrs (cfg.database.caCert != null) { "connection-properties=sslrootcert".value = cfg.database.caCert; "connection-properties=sslmode".value = "verify-ca"; }); }; }) - (lib.optionalAttrs (cfg.database.type == "mysql") { + (optionalAttrs (cfg.database.type == "mysql") { "subsystem=datasources" = { "jdbc-driver=mysql" = { driver-module-name = "com.mysql"; @@ -378,38 +392,38 @@ in driver-class-name = "com.mysql.jdbc.Driver"; }; "data-source=KeycloakDS" = { - connection-url = "jdbc:mysql://${cfg.database.host}:${builtins.toString cfg.database.port}/keycloak"; + connection-url = "jdbc:mysql://${cfg.database.host}:${toString cfg.database.port}/keycloak"; driver-name = "mysql"; - "connection-properties=useSSL".value = lib.boolToString cfg.database.useSSL; - "connection-properties=requireSSL".value = lib.boolToString cfg.database.useSSL; - "connection-properties=verifyServerCertificate".value = lib.boolToString cfg.database.useSSL; + "connection-properties=useSSL".value = boolToString cfg.database.useSSL; + "connection-properties=requireSSL".value = boolToString cfg.database.useSSL; + "connection-properties=verifyServerCertificate".value = boolToString cfg.database.useSSL; "connection-properties=characterEncoding".value = "UTF-8"; valid-connection-checker-class-name = "org.jboss.jca.adapters.jdbc.extensions.mysql.MySQLValidConnectionChecker"; validate-on-match = true; exception-sorter-class-name = "org.jboss.jca.adapters.jdbc.extensions.mysql.MySQLExceptionSorter"; - } // (lib.optionalAttrs (cfg.database.caCert != null) { + } // (optionalAttrs (cfg.database.caCert != null) { "connection-properties=trustCertificateKeyStoreUrl".value = "file:${mySqlCaKeystore}"; "connection-properties=trustCertificateKeyStorePassword".value = "notsosecretpassword"; }); }; }) - (lib.optionalAttrs (cfg.sslCertificate != null && cfg.sslCertificateKey != null) { + (optionalAttrs (cfg.sslCertificate != null && cfg.sslCertificateKey != null) { "socket-binding-group=standard-sockets"."socket-binding=https".port = cfg.httpsPort; - "subsystem=elytron" = lib.mkOrder 900 { - "key-store=httpsKS" = lib.mkOrder 900 { + "subsystem=elytron" = mkOrder 900 { + "key-store=httpsKS" = mkOrder 900 { path = "/run/keycloak/ssl/certificate_private_key_bundle.p12"; credential-reference.clear-text = "notsosecretpassword"; type = "JKS"; }; - "key-manager=httpsKM" = lib.mkOrder 901 { + "key-manager=httpsKM" = mkOrder 901 { key-store = "httpsKS"; credential-reference.clear-text = "notsosecretpassword"; }; - "server-ssl-context=httpsSSC" = lib.mkOrder 902 { + "server-ssl-context=httpsSSC" = mkOrder 902 { key-manager = "httpsKM"; }; }; - "subsystem=undertow" = lib.mkOrder 901 { + "subsystem=undertow" = mkOrder 901 { "server=default-server"."https-listener=https".ssl-context = "httpsSSC"; }; }) @@ -500,41 +514,42 @@ in # with `expression` to evaluate. prefixExpression = string: let - matchResult = builtins.match ''"\$\{.*}"'' string; + matchResult = match ''"\$\{.*}"'' string; in - if matchResult != null then - "expression " + string - else - string; + if matchResult != null then + "expression " + string + else + string; writeAttribute = attribute: value: let - type = builtins.typeOf value; + type = typeOf value; in - if type == "set" then - let - names = builtins.attrNames value; - in - builtins.foldl' (text: name: text + (writeAttribute "${attribute}.${name}" value.${name})) "" names - else if value == null then '' - if (outcome == success) of ${path}:read-attribute(name="${attribute}") - ${path}:undefine-attribute(name="${attribute}") + if type == "set" then + let + names = attrNames value; + in + foldl' (text: name: text + (writeAttribute "${attribute}.${name}" value.${name})) "" names + else if value == null then '' + if (outcome == success) of ${path}:read-attribute(name="${attribute}") + ${path}:undefine-attribute(name="${attribute}") + end-if + '' + else if elem type [ "string" "path" "bool" ] then + let + value' = if type == "bool" then boolToString value else ''"${value}"''; + in + '' + if (result != ${prefixExpression value'}) of ${path}:read-attribute(name="${attribute}") + ${path}:write-attribute(name=${attribute}, value=${value'}) end-if '' - else if builtins.elem type [ "string" "path" "bool" ] then - let - value' = if type == "bool" then lib.boolToString value else ''"${value}"''; - in '' - if (result != ${prefixExpression value'}) of ${path}:read-attribute(name="${attribute}") - ${path}:write-attribute(name=${attribute}, value=${value'}) - end-if - '' - else throw "Unsupported type '${type}' for path '${path}'!"; + else throw "Unsupported type '${type}' for path '${path}'!"; in - lib.concatStrings - (lib.mapAttrsToList - (attribute: value: (writeAttribute attribute value)) - set); + concatStrings + (mapAttrsToList + (attribute: value: (writeAttribute attribute value)) + set); /* Produces an argument list for the JBoss `add()` function, @@ -557,19 +572,19 @@ in let makeArg = attribute: value: let - type = builtins.typeOf value; + type = typeOf value; in - if type == "set" then - "${attribute} = { " + (makeArgList value) + " }" - else if builtins.elem type [ "string" "path" "bool" ] then - "${attribute} = ${if type == "bool" then lib.boolToString value else ''"${value}"''}" - else if value == null then - "" - else - throw "Unsupported type '${type}' for attribute '${attribute}'!"; + if type == "set" then + "${attribute} = { " + (makeArgList value) + " }" + else if elem type [ "string" "path" "bool" ] then + "${attribute} = ${if type == "bool" then boolToString value else ''"${value}"''}" + else if value == null then + "" + else + throw "Unsupported type '${type}' for attribute '${attribute}'!"; in - lib.concatStringsSep ", " (lib.mapAttrsToList makeArg set); + concatStringsSep ", " (mapAttrsToList makeArg set); /* Recurses into the `nodeValue` attrset. Only subattrsets that @@ -579,7 +594,7 @@ in recurse = nodePath: nodeValue: let nodeContent = - if builtins.isAttrs nodeValue && nodeValue._type or "" == "order" then + if isAttrs nodeValue && nodeValue._type or "" == "order" then nodeValue.content else nodeValue; @@ -587,21 +602,23 @@ in let value = nodeContent.${name}; in - if (builtins.match ".*([=]).*" name) == [ "=" ] then - if builtins.isAttrs value || value == null then - true - else - throw "Parsing path '${lib.concatStringsSep "." (nodePath ++ [ name ])}' failed: JBoss attributes cannot contain '='!" + if (match ".*([=]).*" name) == [ "=" ] then + if isAttrs value || value == null then + true else - false; - jbossPath = "/" + lib.concatStringsSep "/" nodePath; - children = if !builtins.isAttrs nodeContent then {} else nodeContent; - subPaths = builtins.filter isPath (builtins.attrNames children); + throw "Parsing path '${concatStringsSep "." (nodePath ++ [ name ])}' failed: JBoss attributes cannot contain '='!" + else + false; + jbossPath = "/" + concatStringsSep "/" nodePath; + children = if !isAttrs nodeContent then { } else nodeContent; + subPaths = filter isPath (attrNames children); getPriority = name: - let value = children.${name}; - in if value._type or "" == "order" then value.priority else 1000; - orderedSubPaths = lib.sort (a: b: getPriority a < getPriority b) subPaths; - jbossAttrs = lib.filterAttrs (name: _: !(isPath name)) children; + let + value = children.${name}; + in + if value._type or "" == "order" then value.priority else 1000; + orderedSubPaths = sort (a: b: getPriority a < getPriority b) subPaths; + jbossAttrs = filterAttrs (name: _: !(isPath name)) children; text = if nodeContent != null then '' @@ -615,45 +632,48 @@ in ${jbossPath}:remove() end-if ''; - in text + lib.concatMapStringsSep "\n" (name: recurse (nodePath ++ [name]) children.${name}) orderedSubPaths; + in + text + concatMapStringsSep "\n" (name: recurse (nodePath ++ [ name ]) children.${name}) orderedSubPaths; in - recurse [] attrs; + recurse [ ] attrs; jbossCliScript = pkgs.writeText "jboss-cli-script" (mkJbossScript keycloakConfig'); - keycloakConfig = pkgs.runCommand "keycloak-config" { - nativeBuildInputs = [ cfg.package ]; - } '' - export JBOSS_BASE_DIR="$(pwd -P)"; - export JBOSS_MODULEPATH="${cfg.package}/modules"; - export JBOSS_LOG_DIR="$JBOSS_BASE_DIR/log"; + keycloakConfig = pkgs.runCommand "keycloak-config" + { + nativeBuildInputs = [ cfg.package ]; + } + '' + export JBOSS_BASE_DIR="$(pwd -P)"; + export JBOSS_MODULEPATH="${cfg.package}/modules"; + export JBOSS_LOG_DIR="$JBOSS_BASE_DIR/log"; - cp -r ${cfg.package}/standalone/configuration . - chmod -R u+rwX ./configuration + cp -r ${cfg.package}/standalone/configuration . + chmod -R u+rwX ./configuration - mkdir -p {deployments,ssl} + mkdir -p {deployments,ssl} - standalone.sh& + standalone.sh& - attempt=1 - max_attempts=30 - while ! jboss-cli.sh --connect ':read-attribute(name=server-state)'; do - if [[ "$attempt" == "$max_attempts" ]]; then - echo "ERROR: Could not connect to Keycloak after $attempt attempts! Failing.." >&2 - exit 1 - fi - echo "Keycloak not fully started yet, retrying.. ($attempt/$max_attempts)" - sleep 1 - (( attempt++ )) - done + attempt=1 + max_attempts=30 + while ! jboss-cli.sh --connect ':read-attribute(name=server-state)'; do + if [[ "$attempt" == "$max_attempts" ]]; then + echo "ERROR: Could not connect to Keycloak after $attempt attempts! Failing.." >&2 + exit 1 + fi + echo "Keycloak not fully started yet, retrying.. ($attempt/$max_attempts)" + sleep 1 + (( attempt++ )) + done - jboss-cli.sh --connect --file=${jbossCliScript} --echo-command + jboss-cli.sh --connect --file=${jbossCliScript} --echo-command - cp configuration/standalone.xml $out - ''; + cp configuration/standalone.xml $out + ''; in - lib.mkIf cfg.enable { - + mkIf cfg.enable + { assertions = [ { assertion = (cfg.database.useSSL && cfg.database.type == "postgresql") -> (cfg.database.caCert != null); @@ -663,7 +683,7 @@ in environment.systemPackages = [ cfg.package ]; - systemd.services.keycloakPostgreSQLInit = lib.mkIf createLocalPostgreSQL { + systemd.services.keycloakPostgreSQLInit = mkIf createLocalPostgreSQL { after = [ "postgresql.service" ]; before = [ "keycloak.service" ]; bindsTo = [ "postgresql.service" ]; @@ -687,7 +707,7 @@ in ''; }; - systemd.services.keycloakMySQLInit = lib.mkIf createLocalMySQL { + systemd.services.keycloakMySQLInit = mkIf createLocalMySQL { after = [ "mysql.service" ]; before = [ "keycloak.service" ]; bindsTo = [ "mysql.service" ]; @@ -714,13 +734,16 @@ in let databaseServices = if createLocalPostgreSQL then [ - "keycloakPostgreSQLInit.service" "postgresql.service" + "keycloakPostgreSQLInit.service" + "postgresql.service" ] else if createLocalMySQL then [ - "keycloakMySQLInit.service" "mysql.service" + "keycloakMySQLInit.service" + "mysql.service" ] else [ ]; - in { + in + { after = databaseServices; bindsTo = databaseServices; wantedBy = [ "multi-user.target" ]; @@ -735,52 +758,16 @@ in JBOSS_MODULEPATH = "${cfg.package}/modules"; }; serviceConfig = { - ExecStartPre = let - startPreFullPrivileges = '' - set -o errexit -o pipefail -o nounset -o errtrace - shopt -s inherit_errexit - - umask u=rwx,g=,o= - - install -T -m 0400 -o keycloak -g keycloak '${cfg.database.passwordFile}' /run/keycloak/secrets/db_password - '' + lib.optionalString (cfg.sslCertificate != null && cfg.sslCertificateKey != null) '' - install -T -m 0400 -o keycloak -g keycloak '${cfg.sslCertificate}' /run/keycloak/secrets/ssl_cert - install -T -m 0400 -o keycloak -g keycloak '${cfg.sslCertificateKey}' /run/keycloak/secrets/ssl_key - ''; - startPre = '' - set -o errexit -o pipefail -o nounset -o errtrace - shopt -s inherit_errexit - - umask u=rwx,g=,o= - - install -m 0600 ${cfg.package}/standalone/configuration/*.properties /run/keycloak/configuration - install -T -m 0600 ${keycloakConfig} /run/keycloak/configuration/standalone.xml - - replace-secret '@db-password@' '/run/keycloak/secrets/db_password' /run/keycloak/configuration/standalone.xml - - export JAVA_OPTS=-Djboss.server.config.user.dir=/run/keycloak/configuration - add-user-keycloak.sh -u admin -p '${cfg.initialAdminPassword}' - '' + lib.optionalString (cfg.sslCertificate != null && cfg.sslCertificateKey != null) '' - pushd /run/keycloak/ssl/ - cat /run/keycloak/secrets/ssl_cert <(echo) \ - /run/keycloak/secrets/ssl_key <(echo) \ - /etc/ssl/certs/ca-certificates.crt \ - > allcerts.pem - openssl pkcs12 -export -in /run/keycloak/secrets/ssl_cert -inkey /run/keycloak/secrets/ssl_key -chain \ - -name "${cfg.frontendUrl}" -out certificate_private_key_bundle.p12 \ - -CAfile allcerts.pem -passout pass:notsosecretpassword - popd - ''; - in [ - "+${pkgs.writeShellScript "keycloak-start-pre-full-privileges" startPreFullPrivileges}" - "${pkgs.writeShellScript "keycloak-start-pre" startPre}" + LoadCredential = [ + "db_password:${cfg.database.passwordFile}" + ] ++ optionals (cfg.sslCertificate != null && cfg.sslCertificateKey != null) [ + "ssl_cert:${cfg.sslCertificate}" + "ssl_key:${cfg.sslCertificateKey}" ]; - ExecStart = "${cfg.package}/bin/standalone.sh"; User = "keycloak"; Group = "keycloak"; DynamicUser = true; RuntimeDirectory = map (p: "keycloak/" + p) [ - "secrets" "configuration" "deployments" "data" @@ -792,13 +779,39 @@ in LogsDirectory = "keycloak"; AmbientCapabilities = "CAP_NET_BIND_SERVICE"; }; + script = '' + set -o errexit -o pipefail -o nounset -o errtrace + shopt -s inherit_errexit + + umask u=rwx,g=,o= + + install -m 0600 ${cfg.package}/standalone/configuration/*.properties /run/keycloak/configuration + install -T -m 0600 ${keycloakConfig} /run/keycloak/configuration/standalone.xml + + replace-secret '@db-password@' "$CREDENTIALS_DIRECTORY/db_password" /run/keycloak/configuration/standalone.xml + + export JAVA_OPTS=-Djboss.server.config.user.dir=/run/keycloak/configuration + add-user-keycloak.sh -u admin -p '${cfg.initialAdminPassword}' + '' + optionalString (cfg.sslCertificate != null && cfg.sslCertificateKey != null) '' + pushd /run/keycloak/ssl/ + cat "$CREDENTIALS_DIRECTORY/ssl_cert" <(echo) \ + "$CREDENTIALS_DIRECTORY/ssl_key" <(echo) \ + /etc/ssl/certs/ca-certificates.crt \ + > allcerts.pem + openssl pkcs12 -export -in "$CREDENTIALS_DIRECTORY/ssl_cert" -inkey "$CREDENTIALS_DIRECTORY/ssl_key" -chain \ + -name "${cfg.frontendUrl}" -out certificate_private_key_bundle.p12 \ + -CAfile allcerts.pem -passout pass:notsosecretpassword + popd + '' + '' + ${cfg.package}/bin/standalone.sh + ''; }; - services.postgresql.enable = lib.mkDefault createLocalPostgreSQL; - services.mysql.enable = lib.mkDefault createLocalMySQL; - services.mysql.package = lib.mkIf createLocalMySQL pkgs.mariadb; + services.postgresql.enable = mkDefault createLocalPostgreSQL; + services.mysql.enable = mkDefault createLocalMySQL; + services.mysql.package = mkIf createLocalMySQL pkgs.mariadb; }; meta.doc = ./keycloak.xml; - meta.maintainers = [ lib.maintainers.talyz ]; + meta.maintainers = [ maintainers.talyz ]; }