From 513599a6d783d2a76d5e0d9759d6b18ce4b9d71b Mon Sep 17 00:00:00 2001 From: talyz Date: Mon, 5 Oct 2020 15:58:44 +0200 Subject: [PATCH 1/6] nixos/keycloak: Init --- nixos/modules/module-list.nix | 1 + nixos/modules/services/web-apps/keycloak.nix | 465 +++++++++++++++++++ pkgs/servers/keycloak/default.nix | 40 +- 3 files changed, 499 insertions(+), 7 deletions(-) create mode 100644 nixos/modules/services/web-apps/keycloak.nix diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index ed6201237b3..c0c8429608b 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -863,6 +863,7 @@ ./services/web-apps/ihatemoney ./services/web-apps/jirafeau.nix ./services/web-apps/jitsi-meet.nix + ./services/web-apps/keycloak.nix ./services/web-apps/limesurvey.nix ./services/web-apps/mattermost.nix ./services/web-apps/mediawiki.nix diff --git a/nixos/modules/services/web-apps/keycloak.nix b/nixos/modules/services/web-apps/keycloak.nix new file mode 100644 index 00000000000..766df48d55f --- /dev/null +++ b/nixos/modules/services/web-apps/keycloak.nix @@ -0,0 +1,465 @@ +{ config, pkgs, lib, ... }: + +let + cfg = config.services.keycloak; +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; + 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. + ''; + }; + + certificatePrivateKeyBundle = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + example = "/run/keys/ssl_cert"; + description = '' + The path to a PEM formatted bundle of the private key and + certificate to use for TLS connections. + + This should be a string, not a Nix path, since Nix paths are + copied into the world-readable Nix store. + ''; + }; + + databaseHost = lib.mkOption { + type = lib.types.str; + default = "localhost"; + description = '' + Hostname of the PostgreSQL database to connect to. + ''; + }; + + databaseCreateLocally = 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.databaseHost is customized. + ''; + }; + + databaseUsername = 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. + ''; + }; + + databasePasswordFile = lib.mkOption { + type = lib.types.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 = lib.mkOption { + type = lib.types.package; + default = pkgs.keycloak; + description = '' + Keycloak package to use. + ''; + }; + + 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. + ''; + }; + + extraConfig = lib.mkOption { + type = lib.types.attrs; + default = { }; + example = lib.literalExample '' + { + "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. + ''; + }; + + }; + + config = + let + # We only want to create a database if we're actually going to connect to it. + databaseActuallyCreateLocally = cfg.databaseCreateLocally && cfg.databaseHost == "localhost"; + + 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; + }; + }; + }; + "subsystem=datasources"."jdbc-driver=postgresql" = { + driver-module-name = "org.postgresql"; + driver-name = "postgresql"; + driver-xa-datasource-class-name = "org.postgresql.xa.PGXADataSource"; + }; + "subsystem=datasources"."data-source=KeycloakDS" = { + connection-url = "jdbc:postgresql://${cfg.databaseHost}/keycloak"; + driver-name = "postgresql"; + max-pool-size = "20"; + user-name = if databaseActuallyCreateLocally then "keycloak" else cfg.databaseUsername; + password = "@db-password@"; + }; + } [ + (lib.optionalAttrs (cfg.certificatePrivateKeyBundle != 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=undertow"."server=default-server"."https-listener=https".security-realm = "UndertowRealm"; + }) + cfg.extraConfig + ]; + + mkJbossScript = attrs: + let + writeAttributes = path: set: + let + prefixExpression = string: + let + match = (builtins.match ''"\$\{.*}"'' string); + in + if match != null then + "expression " + string + else + string; + + writeAttribute = attribute: value: + let + type = builtins.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}") + 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}'!"; + in + lib.concatStrings + (lib.mapAttrsToList + (attribute: value: (writeAttribute attribute value)) + set); + + makeArgList = set: + let + makeArg = attribute: value: + let + type = builtins.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}'!"; + in + lib.concatStringsSep ", " (lib.mapAttrsToList makeArg set); + + recurse = state: node: + let + path = state.path ++ (lib.optional (node != null) node); + isPath = name: + let + value = lib.getAttrFromPath (path ++ [ name ]) attrs; + 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 '='!" + else + false; + jbossPath = "/" + (lib.concatStringsSep "/" path); + nodeValue = lib.getAttrFromPath path attrs; + children = if !builtins.isAttrs nodeValue then {} else nodeValue; + subPaths = builtins.filter isPath (builtins.attrNames children); + jbossAttrs = lib.filterAttrs (name: _: !(isPath name)) children; + in + state // { + text = state.text + ( + if nodeValue != null then '' + if (outcome != success) of ${jbossPath}:read-resource() + ${jbossPath}:add(${makeArgList jbossAttrs}) + end-if + '' + (writeAttributes jbossPath jbossAttrs) + else '' + if (outcome == success) of ${jbossPath}:read-resource() + ${jbossPath}:remove() + end-if + '') + (builtins.foldl' recurse { text = ""; inherit path; } subPaths).text; + }; + in + (recurse { text = ""; path = []; } null).text; + + + jbossCliScript = pkgs.writeText "jboss-cli-script" (mkJbossScript keycloakConfig'); + + keycloakConfig = pkgs.runCommand "keycloak-config" {} '' + 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 + + mkdir -p {deployments,ssl} + + "${cfg.package}/bin/standalone.sh"& + + attempt=1 + max_attempts=30 + while ! ${cfg.package}/bin/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 + + ${cfg.package}/bin/jboss-cli.sh --connect --file=${jbossCliScript} --echo-command + + cp configuration/standalone.xml $out + ''; + in + lib.mkIf cfg.enable { + + environment.systemPackages = [ cfg.package ]; + + systemd.services.keycloakDatabaseInit = lib.mkIf databaseActuallyCreateLocally { + after = [ "postgresql.service" ]; + before = [ "keycloak.service" ]; + bindsTo = [ "postgresql.service" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + User = "postgres"; + Group = "postgres"; + }; + script = '' + set -eu + + PSQL=${config.services.postgresql.package}/bin/psql + + db_password="$(<'${cfg.databasePasswordFile}')" + $PSQL -tAc "SELECT 1 FROM pg_roles WHERE rolname='keycloak'" | grep -q 1 || $PSQL -tAc "CREATE ROLE keycloak WITH LOGIN PASSWORD '$db_password' CREATEDB" + $PSQL -tAc "SELECT 1 FROM pg_database WHERE datname = 'keycloak'" | grep -q 1 || $PSQL -tAc 'CREATE DATABASE "keycloak" OWNER "keycloak"' + ''; + }; + + systemd.services.keycloak = { + after = lib.optionals databaseActuallyCreateLocally [ + "keycloakDatabaseInit.service" "postgresql.service" + ]; + bindsTo = lib.optionals databaseActuallyCreateLocally [ + "keycloakDatabaseInit.service" "postgresql.service" + ]; + wantedBy = [ "multi-user.target" ]; + environment = { + JBOSS_LOG_DIR = "/var/log/keycloak"; + JBOSS_BASE_DIR = "/run/keycloak"; + JBOSS_MODULEPATH = "${cfg.package}/modules"; + }; + serviceConfig = { + ExecStartPre = let + startPreFullPrivileges = '' + set -eu + + install -T -m 0400 -o keycloak -g keycloak '${cfg.databasePasswordFile}' /run/keycloak/secrets/db_password + '' + lib.optionalString (cfg.certificatePrivateKeyBundle != null) '' + install -T -m 0400 -o keycloak -g keycloak '${cfg.certificatePrivateKeyBundle}' /run/keycloak/secrets/ssl_cert_pk_bundle + ''; + startPre = '' + set -eu + + install -m 0600 ${cfg.package}/standalone/configuration/*.properties /run/keycloak/configuration + install -T -m 0600 ${keycloakConfig} /run/keycloak/configuration/standalone.xml + + db_password="$( allcerts.pem + ${pkgs.openssl}/bin/openssl pkcs12 -export -in /run/keycloak/secrets/ssl_cert_pk_bundle -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}" + ]; + ExecStart = "${cfg.package}/bin/standalone.sh"; + User = "keycloak"; + Group = "keycloak"; + DynamicUser = true; + RuntimeDirectory = map (p: "keycloak/" + p) [ + "secrets" + "configuration" + "deployments" + "data" + "ssl" + "log" + "tmp" + ]; + RuntimeDirectoryMode = 0700; + LogsDirectory = "keycloak"; + AmbientCapabilities = "CAP_NET_BIND_SERVICE"; + }; + }; + + services.postgresql.enable = lib.mkDefault databaseActuallyCreateLocally; + }; +} diff --git a/pkgs/servers/keycloak/default.nix b/pkgs/servers/keycloak/default.nix index 614eb2a4679..c694b6a419f 100644 --- a/pkgs/servers/keycloak/default.nix +++ b/pkgs/servers/keycloak/default.nix @@ -1,5 +1,21 @@ -{ stdenv, fetchzip, makeWrapper, jre }: +{ stdenv, fetchzip, makeWrapper, jre, writeText +, postgresql_jdbc ? null +}: +let + mkModuleXml = name: jarFile: writeText "module.xml" '' + + + + + + + + + + + ''; +in stdenv.mkDerivation rec { pname = "keycloak"; version = "11.0.2"; @@ -16,12 +32,22 @@ stdenv.mkDerivation rec { cp -r * $out rm -rf $out/bin/*.{ps1,bat} - rm -rf $out/bin/add-user-keycloak.sh - rm -rf $out/bin/jconsole.sh - chmod +x $out/bin/standalone.sh - wrapProgram $out/bin/standalone.sh \ - --prefix PATH ":" ${jre}/bin ; + module_path=$out/modules/system/layers/keycloak/org + if ! [[ -d $module_path ]]; then + echo "The module path $module_path not found!" + exit 1 + fi + + ${if postgresql_jdbc != null then '' + mkdir -p $module_path/postgresql/main + ln -s ${postgresql_jdbc}/share/java/postgresql-jdbc.jar $module_path/postgresql/main + ln -s ${mkModuleXml "postgresql" "postgresql-jdbc.jar"} $module_path/postgresql/main/module.xml + '' else ""} + + wrapProgram $out/bin/standalone.sh --set JAVA_HOME ${jre} + wrapProgram $out/bin/add-user-keycloak.sh --set JAVA_HOME ${jre} + wrapProgram $out/bin/jboss-cli.sh --set JAVA_HOME ${jre} ''; meta = with stdenv.lib; { @@ -29,7 +55,7 @@ stdenv.mkDerivation rec { description = "Identity and access management for modern applications and services"; license = licenses.asl20; platforms = jre.meta.platforms; - maintainers = [ maintainers.ngerstle ]; + maintainers = with maintainers; [ ngerstle talyz ]; }; } From 31fe90d6effeb480057b400a9d7bf976021626a0 Mon Sep 17 00:00:00 2001 From: talyz Date: Tue, 13 Oct 2020 11:44:02 +0200 Subject: [PATCH 2/6] nixos/keycloak: Add test --- nixos/tests/all-tests.nix | 1 + nixos/tests/keycloak.nix | 139 ++++++++++++++++++++++++++++++ pkgs/servers/keycloak/default.nix | 4 +- 3 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 nixos/tests/keycloak.nix diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 9ffeba27a7f..5a10f60fc9a 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -175,6 +175,7 @@ in kernel-latest = handleTest ./kernel-latest.nix {}; kernel-lts = handleTest ./kernel-lts.nix {}; kernel-testing = handleTest ./kernel-testing.nix {}; + keycloak = handleTest ./keycloak.nix {}; keymap = handleTest ./keymap.nix {}; knot = handleTest ./knot.nix {}; krb5 = discoverTests (import ./krb5 {}); diff --git a/nixos/tests/keycloak.nix b/nixos/tests/keycloak.nix new file mode 100644 index 00000000000..e5e31b038e9 --- /dev/null +++ b/nixos/tests/keycloak.nix @@ -0,0 +1,139 @@ +# This tests Keycloak: it starts the service, creates a realm with an +# OIDC client and a user, and simulates the user logging in to the +# client using their Keycloak login. + +import ./make-test-python.nix ( + { pkgs, ... }: + let + frontendUrl = "http://keycloak/auth"; + initialAdminPassword = "h4IhoJFnt2iQIR9"; + in + { + name = "keycloak"; + meta = with pkgs.stdenv.lib.maintainers; { + maintainers = [ talyz ]; + }; + + nodes = { + keycloak = { ... }: { + virtualisation.memorySize = 1024; + services.keycloak = { + enable = true; + inherit frontendUrl initialAdminPassword; + databasePasswordFile = pkgs.writeText "dbPassword" "wzf6vOCbPp6cqTH"; + }; + environment.systemPackages = with pkgs; [ + xmlstarlet + libtidy + jq + ]; + }; + }; + + testScript = + let + client = { + clientId = "test-client"; + name = "test-client"; + redirectUris = [ "urn:ietf:wg:oauth:2.0:oob" ]; + }; + + user = { + firstName = "Chuck"; + lastName = "Testa"; + username = "chuck.testa"; + email = "chuck.testa@example.com"; + }; + + password = "password1234"; + + realm = { + enabled = true; + realm = "test-realm"; + clients = [ client ]; + users = [( + user // { + enabled = true; + credentials = [{ + type = "password"; + temporary = false; + value = password; + }]; + } + )]; + }; + + realmDataJson = pkgs.writeText "realm-data.json" (builtins.toJSON realm); + + jqCheckUserinfo = pkgs.writeText "check-userinfo.jq" '' + if { + "firstName": .given_name, + "lastName": .family_name, + "username": .preferred_username, + "email": .email + } != ${builtins.toJSON user} then + error("Wrong user info!") + else + empty + end + ''; + in '' + keycloak.start() + keycloak.wait_for_unit("keycloak.service") + keycloak.wait_until_succeeds("curl -sSf ${frontendUrl}") + + + ### Realm Setup ### + + # Get an admin interface access token + keycloak.succeed( + "curl -sSf -d 'client_id=admin-cli' -d 'username=admin' -d 'password=${initialAdminPassword}' -d 'grant_type=password' '${frontendUrl}/realms/master/protocol/openid-connect/token' | jq -r '\"Authorization: bearer \" + .access_token' >admin_auth_header" + ) + + # Publish the realm, including a test OIDC client and user + keycloak.succeed( + "curl -sSf -H @admin_auth_header -X POST -H 'Content-Type: application/json' -d @${realmDataJson} '${frontendUrl}/admin/realms/'" + ) + + # Generate and save the client secret. To do this we need + # Keycloak's internal id for the client. + keycloak.succeed( + "curl -sSf -H @admin_auth_header '${frontendUrl}/admin/realms/${realm.realm}/clients?clientId=${client.name}' | jq -r '.[].id' >client_id", + "curl -sSf -H @admin_auth_header -X POST '${frontendUrl}/admin/realms/${realm.realm}/clients/'$(client_secret", + ) + + + ### Authentication Testing ### + + # Start the login process by sending an initial request to the + # OIDC authentication endpoint, saving the returned page. Tidy + # up the HTML (XmlStarlet is picky) and extract the login form + # post url. + keycloak.succeed( + "curl -sSf -c cookie '${frontendUrl}/realms/${realm.realm}/protocol/openid-connect/auth?client_id=${client.name}&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&scope=openid+email&response_type=code&response_mode=query&nonce=qw4o89g3qqm' >login_form", + "tidy -q -m login_form || true", + "xml sel -T -t -m \"_:html/_:body/_:div/_:div/_:div/_:div/_:div/_:div/_:form[@id='kc-form-login']\" -v @action login_form >form_post_url", + ) + + # Post the login form and save the response. Once again tidy up + # the HTML, then extract the authorization code. + keycloak.succeed( + "curl -sSf -L -b cookie -d 'username=${user.username}' -d 'password=${password}' -d 'credentialId=' \"$(auth_code_html", + "tidy -q -m auth_code_html || true", + "xml sel -T -t -m \"_:html/_:body/_:div/_:div/_:div/_:div/_:div/_:input[@id='code']\" -v @value auth_code_html >auth_code", + ) + + # Exchange the authorization code for an access token. + keycloak.succeed( + "curl -sSf -d grant_type=authorization_code -d code=$(auth_header" + ) + + # Use the access token on the OIDC userinfo endpoint and check + # that the returned user info matches what we initialized the + # realm with. + keycloak.succeed( + "curl -sSf -H @auth_header '${frontendUrl}/realms/${realm.realm}/protocol/openid-connect/userinfo' | jq -f ${jqCheckUserinfo}" + ) + ''; + } +) diff --git a/pkgs/servers/keycloak/default.nix b/pkgs/servers/keycloak/default.nix index c694b6a419f..95935ce8f8a 100644 --- a/pkgs/servers/keycloak/default.nix +++ b/pkgs/servers/keycloak/default.nix @@ -1,4 +1,4 @@ -{ stdenv, fetchzip, makeWrapper, jre, writeText +{ stdenv, fetchzip, makeWrapper, jre, writeText, nixosTests , postgresql_jdbc ? null }: @@ -50,6 +50,8 @@ stdenv.mkDerivation rec { wrapProgram $out/bin/jboss-cli.sh --set JAVA_HOME ${jre} ''; + passthru.tests = nixosTests.keycloak; + meta = with stdenv.lib; { homepage = "https://www.keycloak.org/"; description = "Identity and access management for modern applications and services"; From fe5a16aee67d20cea73bf2ec862fdde2a7524859 Mon Sep 17 00:00:00 2001 From: talyz Date: Thu, 15 Oct 2020 18:36:37 +0200 Subject: [PATCH 3/6] nixos/keycloak: Document internal functions --- nixos/modules/services/web-apps/keycloak.nix | 102 +++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/nixos/modules/services/web-apps/keycloak.nix b/nixos/modules/services/web-apps/keycloak.nix index 766df48d55f..c1020690299 100644 --- a/nixos/modules/services/web-apps/keycloak.nix +++ b/nixos/modules/services/web-apps/keycloak.nix @@ -244,10 +244,88 @@ in cfg.extraConfig ]; + + /* Produces a JBoss CLI script that creates paths and sets + attributes matching those described by `attrs`. When the + script is run, the existing settings are effectively overlayed + by those from `attrs`. Existing attributes can be unset by + defining them `null`. + + JBoss paths and attributes / maps are distinguished by their + name, where paths follow a `key=value` scheme. + + Example: + mkJbossScript { + "subsystem=keycloak-server"."spi=hostname" = { + "provider=fixed" = null; + "provider=default" = { + enabled = true; + properties = { + inherit frontendUrl; + forceBackendUrlToFrontendUrl = false; + }; + }; + }; + } + => '' + if (outcome != success) of /:read-resource() + /:add() + end-if + if (outcome != success) of /subsystem=keycloak-server:read-resource() + /subsystem=keycloak-server:add() + end-if + if (outcome != success) of /subsystem=keycloak-server/spi=hostname:read-resource() + /subsystem=keycloak-server/spi=hostname:add() + end-if + if (outcome != success) of /subsystem=keycloak-server/spi=hostname/provider=default:read-resource() + /subsystem=keycloak-server/spi=hostname/provider=default:add(enabled = true, properties = { forceBackendUrlToFrontendUrl = false, frontendUrl = "https://keycloak.example.com/auth" }) + end-if + if (result != true) of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="enabled") + /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=enabled, value=true) + end-if + if (result != false) of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="properties.forceBackendUrlToFrontendUrl") + /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.forceBackendUrlToFrontendUrl, value=false) + end-if + if (result != "https://keycloak.example.com/auth") of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="properties.frontendUrl") + /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.frontendUrl, value="https://keycloak.example.com/auth") + end-if + if (outcome != success) of /subsystem=keycloak-server/spi=hostname/provider=fixed:read-resource() + /subsystem=keycloak-server/spi=hostname/provider=fixed:remove() + end-if + '' + */ mkJbossScript = attrs: let + /* From a JBoss path and an attrset, produces a JBoss CLI + snippet that writes the corresponding attributes starting + at `path`. Recurses down into subattrsets as necessary, + producing the variable name from its full path in the + attrset. + + Example: + writeAttributes "/subsystem=keycloak-server/spi=hostname/provider=default" { + enabled = true; + properties = { + forceBackendUrlToFrontendUrl = false; + frontendUrl = "https://keycloak.example.com/auth"; + }; + } + => '' + if (result != true) of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="enabled") + /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=enabled, value=true) + end-if + if (result != false) of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="properties.forceBackendUrlToFrontendUrl") + /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.forceBackendUrlToFrontendUrl, value=false) + end-if + if (result != "https://keycloak.example.com/auth") of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="properties.frontendUrl") + /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.frontendUrl, value="https://keycloak.example.com/auth") + end-if + '' + */ writeAttributes = path: set: let + # JBoss expressions like `${var}` need to be prefixed + # with `expression` to evaluate. prefixExpression = string: let match = (builtins.match ''"\$\{.*}"'' string); @@ -286,6 +364,23 @@ in (attribute: value: (writeAttribute attribute value)) set); + + /* Produces an argument list for the JBoss `add()` function, + which adds a JBoss path and takes as its arguments the + required subpaths and attributes. + + Example: + makeArgList { + enabled = true; + properties = { + forceBackendUrlToFrontendUrl = false; + frontendUrl = "https://keycloak.example.com/auth"; + }; + } + => '' + enabled = true, properties = { forceBackendUrlToFrontendUrl = false, frontendUrl = "https://keycloak.example.com/auth" } + '' + */ makeArgList = set: let makeArg = attribute: value: @@ -303,6 +398,13 @@ in 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 + into - the rest are considered JBoss attributes / maps. + */ recurse = state: node: let path = state.path ++ (lib.optional (node != null) node); From c6e4388449a108ed5dcc64315111edb8d0f33cb5 Mon Sep 17 00:00:00 2001 From: talyz Date: Mon, 19 Oct 2020 11:53:55 +0200 Subject: [PATCH 4/6] nixos/keycloak: Add documentation --- nixos/modules/services/web-apps/keycloak.nix | 2 + nixos/modules/services/web-apps/keycloak.xml | 190 +++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 nixos/modules/services/web-apps/keycloak.xml diff --git a/nixos/modules/services/web-apps/keycloak.nix b/nixos/modules/services/web-apps/keycloak.nix index c1020690299..9c6a5ca305c 100644 --- a/nixos/modules/services/web-apps/keycloak.nix +++ b/nixos/modules/services/web-apps/keycloak.nix @@ -564,4 +564,6 @@ in services.postgresql.enable = lib.mkDefault databaseActuallyCreateLocally; }; + + meta.doc = ./keycloak.xml; } diff --git a/nixos/modules/services/web-apps/keycloak.xml b/nixos/modules/services/web-apps/keycloak.xml new file mode 100644 index 00000000000..6b97d48e0bd --- /dev/null +++ b/nixos/modules/services/web-apps/keycloak.xml @@ -0,0 +1,190 @@ + + Keycloak + + Keycloak is an + open source identity and access management server with support for + OpenID + Connect, OAUTH + 2.0 and SAML + 2.0. + +
+ Administration + + An administrative user with the username + admin is automatically created in the + master realm. Its initial password can be + configured by setting + and defaults to changeme. The password is + not stored safely and should be changed immediately in the + admin panel. + + + + Refer to the Admin + Console section of the Keycloak Server Administration Guide for + information on how to administer your + Keycloak instance. + +
+ +
+ Database access + + Keycloak depends on + PostgreSQL and will automatically + enable it and create a database and role unless configured not + to, either by changing + from its default of localhost or setting + + to false. + + + + + must be set to the path to a file containing the password used + to log in to the database. If + and + are kept at their defaults, the database role + keycloak with that password is provisioned + on the local database instance. + + + + + The path should be provided as a string, not a Nix path, since Nix + paths are copied into the world readable Nix store. + + +
+ +
+ Frontend URL + + 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). + + + + + determines whether Keycloak should force all requests to go + through the frontend URL. 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 and Configuration + Guide for more information. + +
+ +
+ Setting up TLS/SSL + + By default, Keycloak won't accept + unsecured HTTP connections originating from outside its local + network. + + + + For HTTPS support, a TLS certificate and private key is + required. They should be PEM + formatted and concatenated into a single file. The path + to this file should be configured in + . + + + + + The path should be provided as a string, not a Nix path, + since Nix paths are copied into the world readable Nix store. + + +
+ +
+ Additional configuration + + Additional Keycloak configuration options, for which no + explicit NixOS options are provided, + can be set in . + + + + 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. + + + For example, 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") + + + would be expressed as + + +services.keycloak.extraConfig = { + "subsystem=keycloak-server" = { + "spi=hostname" = { + "provider=default" = null; + "provider=fixed" = { + enabled = true; + properties.hostname = "keycloak.example.com"; + }; + default-provider = "fixed"; + }; + }; +}; + + + + You can discover available options by using the jboss-cli.sh + program and by referring to the Keycloak + Server Installation and Configuration Guide. + +
+ +
+ Example configuration + + A basic configuration with some custom settings could look like this: + +services.keycloak = { + enable = true; + initialAdminPassword = "e6Wcm0RrtegMEHl"; # change on first login + frontendUrl = "https://keycloak.example.com/auth"; + forceBackendUrlToFrontendUrl = true; + certificatePrivateKeyBundle = "/run/keys/ssl_cert"; + databasePasswordFile = "/run/keys/db_password"; +}; + + + +
+
From d1d3c86c70cad38944f50f7be544326133fff292 Mon Sep 17 00:00:00 2001 From: talyz Date: Fri, 23 Oct 2020 17:01:10 +0200 Subject: [PATCH 5/6] rl-2103: Note the addition of the Keycloak service --- nixos/doc/manual/release-notes/rl-2103.xml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/nixos/doc/manual/release-notes/rl-2103.xml b/nixos/doc/manual/release-notes/rl-2103.xml index c160ab5783d..76019e00dbe 100644 --- a/nixos/doc/manual/release-notes/rl-2103.xml +++ b/nixos/doc/manual/release-notes/rl-2103.xml @@ -39,7 +39,19 @@ - + + Keycloak, + an open source identity and access management server with + support for OpenID Connect, + OAUTH 2.0 and + SAML + 2.0. + + + See the Keycloak + section of the NixOS manual for more information. + From 89e83833af35bd0ec3fdc65c435358a676a41d89 Mon Sep 17 00:00:00 2001 From: talyz Date: Mon, 26 Oct 2020 15:33:57 +0100 Subject: [PATCH 6/6] nixos/keycloak: Add support for MySQL and external DBs with SSL - Add support for using MySQL as an option to PostgreSQL. - Enable connecting to external DBs with SSL - Add a database port config option --- nixos/modules/services/web-apps/keycloak.nix | 265 ++++++++++++++----- nixos/modules/services/web-apps/keycloak.xml | 27 +- nixos/tests/all-tests.nix | 2 +- nixos/tests/keycloak.nix | 21 +- pkgs/servers/keycloak/default.nix | 23 +- 5 files changed, 243 insertions(+), 95 deletions(-) diff --git a/nixos/modules/services/web-apps/keycloak.nix b/nixos/modules/services/web-apps/keycloak.nix index 9c6a5ca305c..bbb0c8d0483 100644 --- a/nixos/modules/services/web-apps/keycloak.nix +++ b/nixos/modules/services/web-apps/keycloak.nix @@ -97,11 +97,59 @@ in ''; }; + databaseType = lib.mkOption { + type = lib.types.enum [ "mysql" "postgresql" ]; + default = "postgresql"; + example = "mysql"; + description = '' + The type of database Keycloak should connect to. + ''; + }; + databaseHost = lib.mkOption { type = lib.types.str; default = "localhost"; description = '' - Hostname of the PostgreSQL database to connect to. + Hostname of the database to connect to. + ''; + }; + + databasePort = + let + dbPorts = { + postgresql = 5432; + mysql = 3306; + }; + in + lib.mkOption { + type = lib.types.port; + default = dbPorts.${cfg.databaseType}; + description = '' + Port of the database to connect to. + ''; + }; + + databaseUseSSL = lib.mkOption { + type = lib.types.bool; + default = cfg.databaseHost != "localhost"; + description = '' + Whether the database connection should be secured by SSL / + TLS. + ''; + }; + + databaseCaCert = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + 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. ''; }; @@ -208,6 +256,12 @@ in let # We only want to create a database if we're actually going to connect to it. databaseActuallyCreateLocally = cfg.databaseCreateLocally && cfg.databaseHost == "localhost"; + createLocalPostgreSQL = databaseActuallyCreateLocally && cfg.databaseType == "postgresql"; + createLocalMySQL = databaseActuallyCreateLocally && cfg.databaseType == "mysql"; + + mySqlCaKeystore = pkgs.runCommandNoCC "mysql-ca-keystore" {} '' + ${pkgs.jre}/bin/keytool -importcert -trustcacerts -alias MySQLCACert -file ${cfg.databaseCaCert} -keystore $out -storepass notsosecretpassword -noprompt + ''; keycloakConfig' = builtins.foldl' lib.recursiveUpdate { "interface=public".inet-address = cfg.bindAddress; @@ -220,19 +274,52 @@ in }; }; }; - "subsystem=datasources"."jdbc-driver=postgresql" = { - driver-module-name = "org.postgresql"; - driver-name = "postgresql"; - driver-xa-datasource-class-name = "org.postgresql.xa.PGXADataSource"; - }; "subsystem=datasources"."data-source=KeycloakDS" = { - connection-url = "jdbc:postgresql://${cfg.databaseHost}/keycloak"; - driver-name = "postgresql"; max-pool-size = "20"; user-name = if databaseActuallyCreateLocally then "keycloak" else cfg.databaseUsername; password = "@db-password@"; }; } [ + (lib.optionalAttrs (cfg.databaseType == "postgresql") { + "subsystem=datasources" = { + "jdbc-driver=postgresql" = { + driver-module-name = "org.postgresql"; + driver-name = "postgresql"; + driver-xa-datasource-class-name = "org.postgresql.xa.PGXADataSource"; + }; + "data-source=KeycloakDS" = { + connection-url = "jdbc:postgresql://${cfg.databaseHost}:${builtins.toString cfg.databasePort}/keycloak"; + driver-name = "postgresql"; + "connection-properties=ssl".value = lib.boolToString cfg.databaseUseSSL; + } // (lib.optionalAttrs (cfg.databaseCaCert != null) { + "connection-properties=sslrootcert".value = cfg.databaseCaCert; + "connection-properties=sslmode".value = "verify-ca"; + }); + }; + }) + (lib.optionalAttrs (cfg.databaseType == "mysql") { + "subsystem=datasources" = { + "jdbc-driver=mysql" = { + driver-module-name = "com.mysql"; + driver-name = "mysql"; + driver-class-name = "com.mysql.jdbc.Driver"; + }; + "data-source=KeycloakDS" = { + connection-url = "jdbc:mysql://${cfg.databaseHost}:${builtins.toString cfg.databasePort}/keycloak"; + driver-name = "mysql"; + "connection-properties=useSSL".value = lib.boolToString cfg.databaseUseSSL; + "connection-properties=requireSSL".value = lib.boolToString cfg.databaseUseSSL; + "connection-properties=verifyServerCertificate".value = lib.boolToString cfg.databaseUseSSL; + "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.databaseCaCert != null) { + "connection-properties=trustCertificateKeyStoreUrl".value = "file:${mySqlCaKeystore}"; + "connection-properties=trustCertificateKeyStorePassword".value = "notsosecretpassword"; + }); + }; + }) (lib.optionalAttrs (cfg.certificatePrivateKeyBundle != null) { "socket-binding-group=standard-sockets"."socket-binding=https".port = cfg.httpsPort; "core-service=management"."security-realm=UndertowRealm"."server-identity=ssl" = { @@ -444,7 +531,7 @@ in jbossCliScript = pkgs.writeText "jboss-cli-script" (mkJbossScript keycloakConfig'); - keycloakConfig = pkgs.runCommand "keycloak-config" {} '' + keycloakConfig = pkgs.runCommandNoCC "keycloak-config" {} '' export JBOSS_BASE_DIR="$(pwd -P)"; export JBOSS_MODULEPATH="${cfg.package}/modules"; export JBOSS_LOG_DIR="$JBOSS_BASE_DIR/log"; @@ -475,9 +562,16 @@ in in lib.mkIf cfg.enable { + assertions = [ + { + assertion = (cfg.databaseUseSSL && cfg.databaseType == "postgresql") -> (cfg.databaseCaCert != null); + message = ''A CA certificate must be specified (in 'services.keycloak.databaseCaCert') when PostgreSQL is used with SSL''; + } + ]; + environment.systemPackages = [ cfg.package ]; - systemd.services.keycloakDatabaseInit = lib.mkIf databaseActuallyCreateLocally { + systemd.services.keycloakPostgreSQLInit = lib.mkIf createLocalPostgreSQL { after = [ "postgresql.service" ]; before = [ "keycloak.service" ]; bindsTo = [ "postgresql.service" ]; @@ -498,71 +592,100 @@ in ''; }; - systemd.services.keycloak = { - after = lib.optionals databaseActuallyCreateLocally [ - "keycloakDatabaseInit.service" "postgresql.service" - ]; - bindsTo = lib.optionals databaseActuallyCreateLocally [ - "keycloakDatabaseInit.service" "postgresql.service" - ]; - wantedBy = [ "multi-user.target" ]; - environment = { - JBOSS_LOG_DIR = "/var/log/keycloak"; - JBOSS_BASE_DIR = "/run/keycloak"; - JBOSS_MODULEPATH = "${cfg.package}/modules"; - }; + systemd.services.keycloakMySQLInit = lib.mkIf createLocalMySQL { + after = [ "mysql.service" ]; + before = [ "keycloak.service" ]; + bindsTo = [ "mysql.service" ]; serviceConfig = { - ExecStartPre = let - startPreFullPrivileges = '' - set -eu - - install -T -m 0400 -o keycloak -g keycloak '${cfg.databasePasswordFile}' /run/keycloak/secrets/db_password - '' + lib.optionalString (cfg.certificatePrivateKeyBundle != null) '' - install -T -m 0400 -o keycloak -g keycloak '${cfg.certificatePrivateKeyBundle}' /run/keycloak/secrets/ssl_cert_pk_bundle - ''; - startPre = '' - set -eu - - install -m 0600 ${cfg.package}/standalone/configuration/*.properties /run/keycloak/configuration - install -T -m 0600 ${keycloakConfig} /run/keycloak/configuration/standalone.xml - - db_password="$( allcerts.pem - ${pkgs.openssl}/bin/openssl pkcs12 -export -in /run/keycloak/secrets/ssl_cert_pk_bundle -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}" - ]; - ExecStart = "${cfg.package}/bin/standalone.sh"; - User = "keycloak"; - Group = "keycloak"; - DynamicUser = true; - RuntimeDirectory = map (p: "keycloak/" + p) [ - "secrets" - "configuration" - "deployments" - "data" - "ssl" - "log" - "tmp" - ]; - RuntimeDirectoryMode = 0700; - LogsDirectory = "keycloak"; - AmbientCapabilities = "CAP_NET_BIND_SERVICE"; + Type = "oneshot"; + RemainAfterExit = true; + User = config.services.mysql.user; + Group = config.services.mysql.group; }; + script = '' + set -eu + + db_password="$(<'${cfg.databasePasswordFile}')" + ( echo "CREATE USER IF NOT EXISTS 'keycloak'@'localhost' IDENTIFIED BY '$db_password';" + echo "CREATE DATABASE keycloak CHARACTER SET utf8 COLLATE utf8_unicode_ci;" + echo "GRANT ALL PRIVILEGES ON keycloak.* TO 'keycloak'@'localhost';" + ) | ${config.services.mysql.package}/bin/mysql -N + ''; }; - services.postgresql.enable = lib.mkDefault databaseActuallyCreateLocally; + systemd.services.keycloak = + let + databaseServices = + if createLocalPostgreSQL then [ + "keycloakPostgreSQLInit.service" "postgresql.service" + ] + else if createLocalMySQL then [ + "keycloakMySQLInit.service" "mysql.service" + ] + else [ ]; + in { + after = databaseServices; + bindsTo = databaseServices; + wantedBy = [ "multi-user.target" ]; + environment = { + JBOSS_LOG_DIR = "/var/log/keycloak"; + JBOSS_BASE_DIR = "/run/keycloak"; + JBOSS_MODULEPATH = "${cfg.package}/modules"; + }; + serviceConfig = { + ExecStartPre = let + startPreFullPrivileges = '' + set -eu + + install -T -m 0400 -o keycloak -g keycloak '${cfg.databasePasswordFile}' /run/keycloak/secrets/db_password + '' + lib.optionalString (cfg.certificatePrivateKeyBundle != null) '' + install -T -m 0400 -o keycloak -g keycloak '${cfg.certificatePrivateKeyBundle}' /run/keycloak/secrets/ssl_cert_pk_bundle + ''; + startPre = '' + set -eu + + install -m 0600 ${cfg.package}/standalone/configuration/*.properties /run/keycloak/configuration + install -T -m 0600 ${keycloakConfig} /run/keycloak/configuration/standalone.xml + + db_password="$( allcerts.pem + ${pkgs.openssl}/bin/openssl pkcs12 -export -in /run/keycloak/secrets/ssl_cert_pk_bundle -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}" + ]; + ExecStart = "${cfg.package}/bin/standalone.sh"; + User = "keycloak"; + Group = "keycloak"; + DynamicUser = true; + RuntimeDirectory = map (p: "keycloak/" + p) [ + "secrets" + "configuration" + "deployments" + "data" + "ssl" + "log" + "tmp" + ]; + RuntimeDirectoryMode = 0700; + LogsDirectory = "keycloak"; + AmbientCapabilities = "CAP_NET_BIND_SERVICE"; + }; + }; + + services.postgresql.enable = lib.mkDefault createLocalPostgreSQL; + services.mysql.enable = lib.mkDefault createLocalMySQL; + services.mysql.package = lib.mkIf createLocalMySQL pkgs.mysql; }; meta.doc = ./keycloak.xml; diff --git a/nixos/modules/services/web-apps/keycloak.xml b/nixos/modules/services/web-apps/keycloak.xml index 6b97d48e0bd..ca5e223eee4 100644 --- a/nixos/modules/services/web-apps/keycloak.xml +++ b/nixos/modules/services/web-apps/keycloak.xml @@ -37,15 +37,30 @@
Database access - Keycloak depends on - PostgreSQL and will automatically - enable it and create a database and role unless configured not - to, either by changing - from its default of localhost or setting - + Keycloak can be used with either + PostgreSQL or + MySQL. Which one is used can be + configured in . The selected + database will automatically be enabled and a database and role + created unless is changed from + its default of localhost or is set to false. + + External database access can also be configured by setting + , , and as + appropriate. Note that you need to manually create a database + called keycloak and allow the configured + database user full access to it. + + must be set to the path to a file containing the password used diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 5a10f60fc9a..d49357cb463 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -175,7 +175,7 @@ in kernel-latest = handleTest ./kernel-latest.nix {}; kernel-lts = handleTest ./kernel-lts.nix {}; kernel-testing = handleTest ./kernel-testing.nix {}; - keycloak = handleTest ./keycloak.nix {}; + keycloak = discoverTests (import ./keycloak.nix); keymap = handleTest ./keymap.nix {}; knot = handleTest ./knot.nix {}; krb5 = discoverTests (import ./krb5 {}); diff --git a/nixos/tests/keycloak.nix b/nixos/tests/keycloak.nix index e5e31b038e9..f448a0f7095 100644 --- a/nixos/tests/keycloak.nix +++ b/nixos/tests/keycloak.nix @@ -2,12 +2,12 @@ # OIDC client and a user, and simulates the user logging in to the # client using their Keycloak login. -import ./make-test-python.nix ( - { pkgs, ... }: - let - frontendUrl = "http://keycloak/auth"; - initialAdminPassword = "h4IhoJFnt2iQIR9"; - in +let + frontendUrl = "http://keycloak/auth"; + initialAdminPassword = "h4IhoJFnt2iQIR9"; + + keycloakTest = import ./make-test-python.nix ( + { pkgs, databaseType, ... }: { name = "keycloak"; meta = with pkgs.stdenv.lib.maintainers; { @@ -19,7 +19,7 @@ import ./make-test-python.nix ( virtualisation.memorySize = 1024; services.keycloak = { enable = true; - inherit frontendUrl initialAdminPassword; + inherit frontendUrl databaseType initialAdminPassword; databasePasswordFile = pkgs.writeText "dbPassword" "wzf6vOCbPp6cqTH"; }; environment.systemPackages = with pkgs; [ @@ -136,4 +136,9 @@ import ./make-test-python.nix ( ) ''; } -) + ); +in +{ + postgres = keycloakTest { databaseType = "postgresql"; }; + mysql = keycloakTest { databaseType = "mysql"; }; +} diff --git a/pkgs/servers/keycloak/default.nix b/pkgs/servers/keycloak/default.nix index 95935ce8f8a..67d3d9bd45a 100644 --- a/pkgs/servers/keycloak/default.nix +++ b/pkgs/servers/keycloak/default.nix @@ -1,11 +1,11 @@ -{ stdenv, fetchzip, makeWrapper, jre, writeText, nixosTests -, postgresql_jdbc ? null +{ stdenv, lib, fetchzip, makeWrapper, jre, writeText, nixosTests +, postgresql_jdbc ? null, mysql_jdbc ? null }: let mkModuleXml = name: jarFile: writeText "module.xml" '' - + @@ -33,17 +33,22 @@ stdenv.mkDerivation rec { rm -rf $out/bin/*.{ps1,bat} - module_path=$out/modules/system/layers/keycloak/org + module_path=$out/modules/system/layers/keycloak if ! [[ -d $module_path ]]; then echo "The module path $module_path not found!" exit 1 fi - ${if postgresql_jdbc != null then '' - mkdir -p $module_path/postgresql/main - ln -s ${postgresql_jdbc}/share/java/postgresql-jdbc.jar $module_path/postgresql/main - ln -s ${mkModuleXml "postgresql" "postgresql-jdbc.jar"} $module_path/postgresql/main/module.xml - '' else ""} + ${lib.optionalString (postgresql_jdbc != null) '' + mkdir -p $module_path/org/postgresql/main + ln -s ${postgresql_jdbc}/share/java/postgresql-jdbc.jar $module_path/org/postgresql/main/ + ln -s ${mkModuleXml "org.postgresql" "postgresql-jdbc.jar"} $module_path/org/postgresql/main/module.xml + ''} + ${lib.optionalString (mysql_jdbc != null) '' + mkdir -p $module_path/com/mysql/main + ln -s ${mysql_jdbc}/share/java/mysql-connector-java.jar $module_path/com/mysql/main/ + ln -s ${mkModuleXml "com.mysql" "mysql-connector-java.jar"} $module_path/com/mysql/main/module.xml + ''} wrapProgram $out/bin/standalone.sh --set JAVA_HOME ${jre} wrapProgram $out/bin/add-user-keycloak.sh --set JAVA_HOME ${jre}