From 9bbcc98e304962e4b9b6a911707b87d1cedf8b83 Mon Sep 17 00:00:00 2001 From: Nikolay Amiantov Date: Mon, 10 Jan 2022 00:46:10 +0300 Subject: [PATCH 1/6] keycloak: 15.1.0 -> 16.1.0 --- pkgs/servers/keycloak/default.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/servers/keycloak/default.nix b/pkgs/servers/keycloak/default.nix index ef168272fe76..9d8a2b31bf12 100644 --- a/pkgs/servers/keycloak/default.nix +++ b/pkgs/servers/keycloak/default.nix @@ -18,11 +18,11 @@ let in stdenv.mkDerivation rec { pname = "keycloak"; - version = "15.1.0"; + version = "16.1.0"; src = fetchzip { url = "https://github.com/keycloak/keycloak/releases/download/${version}/keycloak-${version}.zip"; - sha256 = "0s8nvp1ca30569k1a7glbn2zvvchz35s2r8d08fbs5zjngnz3276"; + sha256 = "sha256-QVFu3f+mwafoNUttLEVMdoZHMJjjH/TpZAGV7ZvIvh0="; }; nativeBuildInputs = [ makeWrapper ]; From 3c7e78cc6ab73ca9b0dbcb376122befa59098300 Mon Sep 17 00:00:00 2001 From: Nikolay Amiantov Date: Mon, 10 Jan 2022 00:43:00 +0300 Subject: [PATCH 2/6] keycloak service: ordering for CLI script Allow update commands in the script to be ordered using `mkOrder`. If we encounter ordered sub-objects we sort them by priority. To implement this we now explicitly pass current node in `recurse`, which also allows us to clean up edge case for top-level node. Also refactor `recurse` to avoid passing result text argument; we weren't tail recursive before anyway. --- nixos/modules/services/web-apps/keycloak.nix | 51 +++++++++++--------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/nixos/modules/services/web-apps/keycloak.nix b/nixos/modules/services/web-apps/keycloak.nix index e08f6dcabd2f..12111633919b 100644 --- a/nixos/modules/services/web-apps/keycloak.nix +++ b/nixos/modules/services/web-apps/keycloak.nix @@ -441,9 +441,9 @@ in # with `expression` to evaluate. prefixExpression = string: let - match = (builtins.match ''"\$\{.*}"'' string); + matchResult = builtins.match ''"\$\{.*}"'' string; in - if match != null then + if matchResult != null then "expression " + string else string; @@ -508,52 +508,57 @@ in "" else throw "Unsupported type '${type}' for attribute '${attribute}'!"; + in lib.concatStringsSep ", " (lib.mapAttrsToList makeArg set); - /* Recurses into the `attrs` attrset, beginning at the path - resolved from `state.path ++ node`; if `node` is `null`, - starts from `state.path`. Only subattrsets that are JBoss - paths, i.e. follows the `key=value` format, are recursed + /* Recurses into the `nodeValue` attrset. Only subattrsets that + are JBoss paths, i.e. follows the `key=value` format, are recursed into - the rest are considered JBoss attributes / maps. */ - recurse = state: node: + recurse = nodePath: nodeValue: let - path = state.path ++ (lib.optional (node != null) node); + nodeContent = + if builtins.isAttrs nodeValue && nodeValue._type or "" == "order" then + nodeValue.content + else + nodeValue; isPath = name: let - value = lib.getAttrFromPath (path ++ [ name ]) attrs; + value = nodeContent.${name}; in if (builtins.match ".*([=]).*" name) == [ "=" ] then if builtins.isAttrs value || value == null then true else - throw "Parsing path '${lib.concatStringsSep "." (path ++ [ name ])}' failed: JBoss attributes cannot contain '='!" + throw "Parsing path '${lib.concatStringsSep "." (nodePath ++ [ name ])}' failed: JBoss attributes cannot contain '='!" else false; - jbossPath = "/" + (lib.concatStringsSep "/" path); - nodeValue = lib.getAttrFromPath path attrs; - children = if !builtins.isAttrs nodeValue then {} else nodeValue; + jbossPath = "/" + lib.concatStringsSep "/" nodePath; + children = if !builtins.isAttrs nodeContent then {} else nodeContent; subPaths = builtins.filter isPath (builtins.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; - in - state // { - text = state.text + ( - if nodeValue != null then '' + text = + if nodeContent != null then + '' if (outcome != success) of ${jbossPath}:read-resource() ${jbossPath}:add(${makeArgList jbossAttrs}) end-if - '' + (writeAttributes jbossPath jbossAttrs) - else '' + '' + writeAttributes jbossPath jbossAttrs + else + '' if (outcome == success) of ${jbossPath}:read-resource() ${jbossPath}:remove() end-if - '') + (builtins.foldl' recurse { text = ""; inherit path; } subPaths).text; - }; + ''; + in text + lib.concatMapStringsSep "\n" (name: recurse (nodePath ++ [name]) children.${name}) orderedSubPaths; in - (recurse { text = ""; path = []; } null).text; - + recurse [] attrs; jbossCliScript = pkgs.writeText "jboss-cli-script" (mkJbossScript keycloakConfig'); From 827267a27f300a8fe503986da2570bc3b9252e69 Mon Sep 17 00:00:00 2001 From: Nikolay Amiantov Date: Mon, 10 Jan 2022 00:43:45 +0300 Subject: [PATCH 3/6] keycloak service: update HTTPS configuration Keycloak 16.1.0 uses different way to configure HTTPS. This requires us to order commands correctly, otherwise linked objects will fail. --- nixos/modules/services/web-apps/keycloak.nix | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/nixos/modules/services/web-apps/keycloak.nix b/nixos/modules/services/web-apps/keycloak.nix index 12111633919b..2dce4b242a30 100644 --- a/nixos/modules/services/web-apps/keycloak.nix +++ b/nixos/modules/services/web-apps/keycloak.nix @@ -348,11 +348,23 @@ in }) (lib.optionalAttrs (cfg.sslCertificate != null && cfg.sslCertificateKey != null) { "socket-binding-group=standard-sockets"."socket-binding=https".port = cfg.httpsPort; - "core-service=management"."security-realm=UndertowRealm"."server-identity=ssl" = { - keystore-path = "/run/keycloak/ssl/certificate_private_key_bundle.p12"; - keystore-password = "notsosecretpassword"; + "subsystem=elytron" = lib.mkOrder 900 { + "key-store=httpsKS" = lib.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-store = "httpsKS"; + credential-reference.clear-text = "notsosecretpassword"; + }; + "server-ssl-context=httpsSSC" = lib.mkOrder 902 { + key-manager = "httpsKM"; + }; + }; + "subsystem=undertow" = lib.mkOrder 901 { + "server=default-server"."https-listener=https".ssl-context = "httpsSSC"; }; - "subsystem=undertow"."server=default-server"."https-listener=https".security-realm = "UndertowRealm"; }) cfg.extraConfig ]; From a42abe27c0b58749f1c563fc77305d145c739746 Mon Sep 17 00:00:00 2001 From: Nikolay Amiantov Date: Mon, 10 Jan 2022 00:42:23 +0300 Subject: [PATCH 4/6] keycloak service: use 'attrsOf anything' for extraConfig --- nixos/modules/services/web-apps/keycloak.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixos/modules/services/web-apps/keycloak.nix b/nixos/modules/services/web-apps/keycloak.nix index 2dce4b242a30..d4177c77bce3 100644 --- a/nixos/modules/services/web-apps/keycloak.nix +++ b/nixos/modules/services/web-apps/keycloak.nix @@ -230,7 +230,7 @@ in }; extraConfig = lib.mkOption { - type = lib.types.attrs; + type = lib.types.attrsOf lib.types.anything; default = { }; example = lib.literalExpression '' { From 84f70eefd1c4f90e892164afa39931a9fc5ba8db Mon Sep 17 00:00:00 2001 From: Nikolay Amiantov Date: Sun, 9 Jan 2022 20:58:25 +0300 Subject: [PATCH 5/6] keycloak service: add themes support Custom themes can be packaged and then added using `themes` config attribute. --- nixos/modules/services/web-apps/keycloak.nix | 47 +++++++++++++++++++- nixos/modules/services/web-apps/keycloak.xml | 11 +++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/nixos/modules/services/web-apps/keycloak.nix b/nixos/modules/services/web-apps/keycloak.nix index d4177c77bce3..39e5ab970b94 100644 --- a/nixos/modules/services/web-apps/keycloak.nix +++ b/nixos/modules/services/web-apps/keycloak.nix @@ -229,6 +229,20 @@ in ''; }; + 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. + + 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 = lib.mkOption { type = lib.types.attrsOf lib.types.anything; default = { }; @@ -289,16 +303,45 @@ in ${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" {} '' + linkTheme() { + theme="$1" + name="$2" + + mkdir "$out/$name" + for typeDir in "$theme"/*; do + if [ -d "$typeDir" ]; then + type="$(basename "$typeDir")" + mkdir "$out/$name/$type" + for file in "$typeDir"/*; do + ln -sn "$file" "$out/$name/$type/$(basename "$file")" + done + fi + done + } + + mkdir -p "$out" + for theme in ${cfg.package}/themes/*; do + if [ -d "$theme" ]; then + linkTheme "$theme" "$(basename "$theme")" + fi + done + + ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: theme: "linkTheme ${theme} ${lib.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" = { + "subsystem=keycloak-server" = { + "spi=hostname"."provider=default" = { enabled = true; properties = { inherit (cfg) frontendUrl forceBackendUrlToFrontendUrl; }; }; + "theme=defaults".dir = toString themesBundle; }; "subsystem=datasources"."data-source=KeycloakDS" = { max-pool-size = "20"; diff --git a/nixos/modules/services/web-apps/keycloak.xml b/nixos/modules/services/web-apps/keycloak.xml index 7ba656c20f16..8c3e35a051bc 100644 --- a/nixos/modules/services/web-apps/keycloak.xml +++ b/nixos/modules/services/web-apps/keycloak.xml @@ -131,6 +131,17 @@ +
+ Themes + + You can package custom themes and make them visible to Keycloak via + + option. See the + Themes section of the Keycloak Server Development Guide + and respective NixOS option description for more information. + +
+
Additional configuration From 97a0cf62f098d21a31c4dc03294e4919e88c225f Mon Sep 17 00:00:00 2001 From: Nikolay Amiantov Date: Sun, 9 Jan 2022 21:01:11 +0300 Subject: [PATCH 6/6] keycloak service: allow to set empty frontend URL This together with extraConfig: { "subsystem=undertow"."server=default-server"."http-listener=default"."proxy-address-forwarding" = true; "subsystem=undertow"."server=default-server"."https-listener=https"."proxy-address-forwarding" = true; } Allows to run Keycloak behind a reverse proxy that provides X-Forwarded-* headers. --- nixos/modules/services/web-apps/keycloak.nix | 6 +++++- nixos/modules/services/web-apps/keycloak.xml | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/nixos/modules/services/web-apps/keycloak.nix b/nixos/modules/services/web-apps/keycloak.nix index 39e5ab970b94..aff4ed8dd608 100644 --- a/nixos/modules/services/web-apps/keycloak.nix +++ b/nixos/modules/services/web-apps/keycloak.nix @@ -55,7 +55,11 @@ in frontendUrl = lib.mkOption { type = lib.types.str; - apply = x: if lib.hasSuffix "/" x then x else x + "/"; + 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 diff --git a/nixos/modules/services/web-apps/keycloak.xml b/nixos/modules/services/web-apps/keycloak.xml index 8c3e35a051bc..cb706932f48f 100644 --- a/nixos/modules/services/web-apps/keycloak.xml +++ b/nixos/modules/services/web-apps/keycloak.xml @@ -85,7 +85,12 @@ The frontend URL is used as base for all frontend requests and must be configured through . It should normally include a trailing /auth - (the default web context). + (the default web context). If you use a reverse proxy, you need + to set this option to "", so that frontend URL + is derived from HTTP headers. X-Forwarded-* headers + support also should be enabled, using + respective guidelines.