about summary refs log tree commit diff
path: root/nixos/modules/services/web-apps
diff options
context:
space:
mode:
authorKim Lindberger <kim.lindberger@gmail.com>2022-01-16 11:27:29 +0100
committerGitHub <noreply@github.com>2022-01-16 11:27:29 +0100
commitcdd600c430530641ceb9806ea1ac4f50c0e4512d (patch)
treecb83f29b2837b2062a82da0e529a815d38b43385 /nixos/modules/services/web-apps
parent2dd54612139d3bc3585b820514d407b8f6da874f (diff)
parent97a0cf62f098d21a31c4dc03294e4919e88c225f (diff)
downloadnixlib-cdd600c430530641ceb9806ea1ac4f50c0e4512d.tar
nixlib-cdd600c430530641ceb9806ea1ac4f50c0e4512d.tar.gz
nixlib-cdd600c430530641ceb9806ea1ac4f50c0e4512d.tar.bz2
nixlib-cdd600c430530641ceb9806ea1ac4f50c0e4512d.tar.lz
nixlib-cdd600c430530641ceb9806ea1ac4f50c0e4512d.tar.xz
nixlib-cdd600c430530641ceb9806ea1ac4f50c0e4512d.tar.zst
nixlib-cdd600c430530641ceb9806ea1ac4f50c0e4512d.zip
Merge pull request #154193 from abbradar/keycloak-changes
keycloak: 15.1.0 -> 16.1.0 + module improvements
Diffstat (limited to 'nixos/modules/services/web-apps')
-rw-r--r--nixos/modules/services/web-apps/keycloak.nix126
-rw-r--r--nixos/modules/services/web-apps/keycloak.xml18
2 files changed, 112 insertions, 32 deletions
diff --git a/nixos/modules/services/web-apps/keycloak.nix b/nixos/modules/services/web-apps/keycloak.nix
index e08f6dcabd2f..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
@@ -229,8 +233,22 @@ 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, <literal>account</literal>,
+        <literal>login</literal> 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.attrs;
+      type = lib.types.attrsOf lib.types.anything;
       default = { };
       example = lib.literalExpression ''
         {
@@ -289,16 +307,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";
@@ -348,11 +395,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
       ];
@@ -441,9 +500,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 +567,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');
 
diff --git a/nixos/modules/services/web-apps/keycloak.xml b/nixos/modules/services/web-apps/keycloak.xml
index 7ba656c20f16..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 <xref linkend="opt-services.keycloak.frontendUrl" />.
        It should normally include a trailing <literal>/auth</literal>
-       (the default web context).
+       (the default web context). If you use a reverse proxy, you need
+       to set this option to <literal>""</literal>, so that frontend URL
+       is derived from HTTP headers. <literal>X-Forwarded-*</literal> headers
+       support also should be enabled, using <link
+       xlink:href="https://www.keycloak.org/docs/latest/server_installation/index.html#identifying-client-ip-addresses">
+       respective guidelines</link>.
      </para>
 
      <para>
@@ -131,6 +136,17 @@
      </warning>
    </section>
 
+   <section xml:id="module-services-keycloak-themes">
+     <title>Themes</title>
+     <para>
+        You can package custom themes and make them visible to Keycloak via
+        <xref linkend="opt-services.keycloak.themes" />
+        option. See the <link xlink:href="https://www.keycloak.org/docs/latest/server_development/#_themes">
+        Themes section of the Keycloak Server Development Guide</link>
+        and respective NixOS option description for more information.
+     </para>
+   </section>
+
    <section xml:id="module-services-keycloak-extra-config">
      <title>Additional configuration</title>
      <para>