diff --git a/modules/keycloak/default.nix b/modules/keycloak/default.nix index 5cd5f2d..b40234a 100644 --- a/modules/keycloak/default.nix +++ b/modules/keycloak/default.nix @@ -6,9 +6,6 @@ ... }: { - disabledModules = [ "services/web-apps/keycloak.nix" ]; - imports = [ ./keycloak.nix ]; - options.pub-solar-os.auth = with lib; { enable = mkEnableOption "Enable keycloak to run on the node"; diff --git a/modules/keycloak/keycloak.nix b/modules/keycloak/keycloak.nix deleted file mode 100644 index 283f98c..0000000 --- a/modules/keycloak/keycloak.nix +++ /dev/null @@ -1,807 +0,0 @@ -{ - config, - options, - pkgs, - lib, - ... -}: - -let - cfg = config.services.keycloak; - opt = options.services.keycloak; - - inherit (lib) - types - mkMerge - mkOption - mkChangedOptionModule - mkRenamedOptionModule - mkRemovedOptionModule - mkPackageOption - concatStringsSep - mapAttrsToList - escapeShellArg - mkIf - optionalString - optionals - mkDefault - literalExpression - isAttrs - literalMD - maintainers - catAttrs - collect - hasPrefix - ; - - inherit (builtins) - elem - typeOf - isInt - isString - hashString - isPath - ; - - prefixUnlessEmpty = prefix: string: optionalString (string != "") "${prefix}${string}"; -in -{ - imports = [ - (mkRenamedOptionModule - [ - "services" - "keycloak" - "bindAddress" - ] - [ - "services" - "keycloak" - "settings" - "http-host" - ] - ) - (mkRenamedOptionModule - [ - "services" - "keycloak" - "forceBackendUrlToFrontendUrl" - ] - [ - "services" - "keycloak" - "settings" - "hostname-strict-backchannel" - ] - ) - (mkChangedOptionModule - [ - "services" - "keycloak" - "httpPort" - ] - [ - "services" - "keycloak" - "settings" - "http-port" - ] - (config: builtins.fromJSON config.services.keycloak.httpPort) - ) - (mkChangedOptionModule - [ - "services" - "keycloak" - "httpsPort" - ] - [ - "services" - "keycloak" - "settings" - "https-port" - ] - (config: builtins.fromJSON config.services.keycloak.httpsPort) - ) - (mkRemovedOptionModule - [ - "services" - "keycloak" - "frontendUrl" - ] - '' - Set `services.keycloak.settings.hostname' and `services.keycloak.settings.http-relative-path' instead. - NOTE: You likely want to set 'http-relative-path' to '/auth' to keep compatibility with your clients. - See its description for more information. - '' - ) - (mkRemovedOptionModule [ - "services" - "keycloak" - "extraConfig" - ] "Use `services.keycloak.settings' instead.") - ]; - - options.services.keycloak = - let - inherit (types) - bool - str - int - nullOr - attrsOf - oneOf - path - enum - package - port - ; - - assertStringPath = - optionName: value: - if isPath value then - throw '' - services.keycloak.${optionName}: - ${toString value} - is a Nix path, but should be a string, since Nix - paths are copied into the world-readable Nix store. - '' - else - value; - in - { - enable = mkOption { - type = bool; - default = false; - example = true; - description = '' - Whether to enable the Keycloak identity and access management - server. - ''; - }; - - sslCertificate = mkOption { - type = nullOr path; - default = null; - example = "/run/keys/ssl_cert"; - apply = assertStringPath "sslCertificate"; - description = '' - The path to a PEM formatted certificate to use for TLS/SSL - connections. - ''; - }; - - sslCertificateKey = mkOption { - type = nullOr path; - default = null; - example = "/run/keys/ssl_key"; - apply = assertStringPath "sslCertificateKey"; - description = '' - The path to a PEM formatted private key to use for TLS/SSL - connections. - ''; - }; - - plugins = lib.mkOption { - type = lib.types.listOf lib.types.path; - default = [ ]; - description = '' - Keycloak plugin jar, ear files or derivations containing - them. Packaged plugins are available through - `pkgs.keycloak.plugins`. - ''; - }; - - database = { - type = mkOption { - type = enum [ - "mysql" - "mariadb" - "postgresql" - ]; - default = "postgresql"; - example = "mariadb"; - description = '' - The type of database Keycloak should connect to. - ''; - }; - - host = mkOption { - type = str; - default = "localhost"; - description = '' - Hostname of the database to connect to. - ''; - }; - - port = - let - dbPorts = { - postgresql = 5432; - mariadb = 3306; - mysql = 3306; - }; - in - mkOption { - type = port; - default = dbPorts.${cfg.database.type}; - defaultText = literalMD "default port of selected database"; - description = '' - Port of the database to connect to. - ''; - }; - - 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. - ''; - }; - - caCert = mkOption { - type = nullOr 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. - ''; - }; - - 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. - ''; - }; - - name = mkOption { - type = str; - default = "keycloak"; - description = '' - Database name 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 [](#opt-services.keycloak.database.createLocally) to - `false` and create the database and user - manually. - ''; - }; - - 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 [](#opt-services.keycloak.database.createLocally) to - `false` and create the database and user - manually. - ''; - }; - - passwordFile = mkOption { - type = path; - example = "/run/keys/db_password"; - apply = assertStringPath "passwordFile"; - description = '' - The path to a file containing the database password. - ''; - }; - }; - - package = mkPackageOption pkgs "keycloak" { }; - - 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. - ''; - }; - - extraStartupFlags = lib.mkOption { - type = lib.types.listOf str; - default = [ ]; - description = '' - Extra flags to be added to the startup command kc.sh. - This can be used to import a realm during startup or to - set configuration variables, see . - - --verbose and --optimized are always added. - ''; - }; - - settings = mkOption { - type = lib.types.submodule { - freeformType = attrsOf ( - nullOr (oneOf [ - str - int - bool - (attrsOf path) - ]) - ); - - options = { - http-host = mkOption { - type = str; - default = "0.0.0.0"; - example = "127.0.0.1"; - description = '' - On which address Keycloak should accept new connections. - ''; - }; - - http-port = mkOption { - type = port; - default = 80; - example = 8080; - description = '' - On which port Keycloak should listen for new HTTP connections. - ''; - }; - - https-port = mkOption { - type = port; - default = 443; - example = 8443; - description = '' - On which port Keycloak should listen for new HTTPS connections. - ''; - }; - - http-relative-path = mkOption { - type = str; - default = "/"; - example = "/auth"; - apply = x: if !(hasPrefix "/") x then "/" + x else x; - description = '' - The path relative to `/` for serving - resources. - - ::: {.note} - In versions of Keycloak using Wildfly (<17), - this defaulted to `/auth`. If - upgrading from the Wildfly version of Keycloak, - i.e. a NixOS version before 22.05, you'll likely - want to set this to `/auth` to - keep compatibility with your clients. - - See - for more information on migrating from Wildfly to Quarkus. - ::: - ''; - }; - - hostname = mkOption { - type = nullOr str; - example = "keycloak.example.com"; - description = '' - The hostname part of the public URL used as base for - all frontend requests. - - See - for more information about hostname configuration. - ''; - }; - - hostname-backchannel-dynamic = mkOption { - type = bool; - default = false; - example = true; - description = '' - Enables dynamic resolving of backchannel URLs, - including hostname, scheme, port and context path. - - See - for more information about hostname configuration. - ''; - }; - - proxy = mkOption { - type = enum [ - "edge" - "reencrypt" - "passthrough" - "none" - ]; - default = "none"; - example = "edge"; - description = '' - The proxy address forwarding mode if the server is - behind a reverse proxy. - - - `edge`: - Enables communication through HTTP between the - proxy and Keycloak. - - `reencrypt`: - Requires communication through HTTPS between the - proxy and Keycloak. - - `passthrough`: - Enables communication through HTTP or HTTPS between - the proxy and Keycloak. - - See for more information. - ''; - }; - }; - }; - - example = literalExpression '' - { - hostname = "keycloak.example.com"; - proxy = "reencrypt"; - https-key-store-file = "/path/to/file"; - https-key-store-password = { _secret = "/run/keys/store_password"; }; - } - ''; - - description = '' - Configuration options corresponding to parameters set in - {file}`conf/keycloak.conf`. - - Most available options are documented at . - - Options containing secret data should be set to an attribute - set containing the attribute `_secret` - a - string pointing to a file containing the value the option - should be set to. See the example to get a better picture of - this: in the resulting - {file}`conf/keycloak.conf` file, the - `https-key-store-password` key will be set - to the contents of the - {file}`/run/keys/store_password` file. - ''; - }; - }; - - config = - let - # We only want to create a database if we're actually going to - # connect to it. - databaseActuallyCreateLocally = cfg.database.createLocally && cfg.database.host == "localhost"; - createLocalPostgreSQL = databaseActuallyCreateLocally && cfg.database.type == "postgresql"; - createLocalMySQL = - databaseActuallyCreateLocally - && elem cfg.database.type [ - "mysql" - "mariadb" - ]; - - 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" { } '' - 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 ${keycloakBuild}/themes/*; do - if [ -d "$theme" ]; then - linkTheme "$theme" "$(basename "$theme")" - fi - done - - ${concatStringsSep "\n" ( - mapAttrsToList (name: theme: "linkTheme ${theme} ${escapeShellArg name}") cfg.themes - )} - ''; - - keycloakConfig = lib.generators.toKeyValue { - mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" { - mkValueString = - v: - if isInt v then - toString v - else if isString v then - v - else if true == v then - "true" - else if false == v then - "false" - else if isSecret v then - hashString "sha256" v._secret - else - throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty { }) v}"; - }; - }; - - isSecret = v: isAttrs v && v ? _secret && isString v._secret; - filteredConfig = lib.converge (lib.filterAttrsRecursive ( - _: v: - !elem v [ - { } - null - ] - )) cfg.settings; - confFile = pkgs.writeText "keycloak.conf" (keycloakConfig filteredConfig); - keycloakBuild = cfg.package.override { - inherit confFile; - plugins = - cfg.package.enabledPlugins - ++ cfg.plugins - ++ (with cfg.package.plugins; [ - quarkus-systemd-notify - quarkus-systemd-notify-deployment - ]); - }; - in - mkIf cfg.enable { - assertions = [ - { - assertion = - (cfg.database.useSSL && cfg.database.type == "postgresql") -> (cfg.database.caCert != null); - message = "A CA certificate must be specified (in 'services.keycloak.database.caCert') when PostgreSQL is used with SSL"; - } - { - assertion = - createLocalPostgreSQL -> config.services.postgresql.settings.standard_conforming_strings or true; - message = "Setting up a local PostgreSQL db for Keycloak requires `standard_conforming_strings` turned on to work reliably"; - } - { - assertion = cfg.settings.hostname != null || !cfg.settings.hostname-strict or true; - message = "Setting the Keycloak hostname is required, see `services.keycloak.settings.hostname`"; - } - { - assertion = cfg.settings.hostname-url or null == null; - message = '' - The option `services.keycloak.settings.hostname-url' has been removed. - Set `services.keycloak.settings.hostname' instead. - See [New Hostname options](https://www.keycloak.org/docs/25.0.0/upgrading/#new-hostname-options) for details. - ''; - } - { - assertion = cfg.settings.hostname-strict-backchannel or null == null; - message = '' - The option `services.keycloak.settings.hostname-strict-backchannel' has been removed. - Set `services.keycloak.settings.hostname-backchannel-dynamic' instead. - See [New Hostname options](https://www.keycloak.org/docs/25.0.0/upgrading/#new-hostname-options) for details. - ''; - } - ]; - - environment.systemPackages = [ keycloakBuild ]; - - services.keycloak.settings = - let - postgresParams = concatStringsSep "&" ( - optionals cfg.database.useSSL [ "ssl=true" ] - ++ optionals (cfg.database.caCert != null) [ - "sslrootcert=${cfg.database.caCert}" - "sslmode=verify-ca" - ] - ); - mariadbParams = concatStringsSep "&" ( - [ "characterEncoding=UTF-8" ] - ++ optionals cfg.database.useSSL [ - "useSSL=true" - "requireSSL=true" - "verifyServerCertificate=true" - ] - ++ optionals (cfg.database.caCert != null) [ - "trustCertificateKeyStoreUrl=file:${mySqlCaKeystore}" - "trustCertificateKeyStorePassword=notsosecretpassword" - ] - ); - dbProps = if cfg.database.type == "postgresql" then postgresParams else mariadbParams; - in - mkMerge [ - { - db = if cfg.database.type == "postgresql" then "postgres" else cfg.database.type; - db-username = if databaseActuallyCreateLocally then "keycloak" else cfg.database.username; - db-password._secret = cfg.database.passwordFile; - db-url-host = cfg.database.host; - db-url-port = toString cfg.database.port; - db-url-database = if databaseActuallyCreateLocally then "keycloak" else cfg.database.name; - db-url-properties = prefixUnlessEmpty "?" dbProps; - db-url = null; - } - (mkIf (cfg.sslCertificate != null && cfg.sslCertificateKey != null) { - https-certificate-file = "/run/keycloak/ssl/ssl_cert"; - https-certificate-key-file = "/run/keycloak/ssl/ssl_key"; - }) - ]; - - systemd.services.keycloakPostgreSQLInit = mkIf createLocalPostgreSQL { - after = [ "postgresql.service" ]; - before = [ "keycloak.service" ]; - bindsTo = [ "postgresql.service" ]; - path = [ config.services.postgresql.package ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - User = "postgres"; - Group = "postgres"; - LoadCredential = [ "db_password:${cfg.database.passwordFile}" ]; - }; - script = '' - set -o errexit -o pipefail -o nounset -o errtrace - shopt -s inherit_errexit - - create_role="$(mktemp)" - trap 'rm -f "$create_role"' EXIT - - # Read the password from the credentials directory and - # escape any single quotes by adding additional single - # quotes after them, following the rules laid out here: - # https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-CONSTANTS - db_password="$(<"$CREDENTIALS_DIRECTORY/db_password")" - db_password="''${db_password//\'/\'\'}" - - echo "CREATE ROLE keycloak WITH LOGIN PASSWORD '$db_password' CREATEDB" > "$create_role" - psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='keycloak'" | grep -q 1 || psql -tA --file="$create_role" - psql -tAc "SELECT 1 FROM pg_database WHERE datname = 'keycloak'" | grep -q 1 || psql -tAc 'CREATE DATABASE "keycloak" OWNER "keycloak"' - ''; - }; - - systemd.services.keycloakMySQLInit = mkIf createLocalMySQL { - after = [ "mysql.service" ]; - before = [ "keycloak.service" ]; - bindsTo = [ "mysql.service" ]; - path = [ config.services.mysql.package ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - User = config.services.mysql.user; - Group = config.services.mysql.group; - LoadCredential = [ "db_password:${cfg.database.passwordFile}" ]; - }; - script = '' - set -o errexit -o pipefail -o nounset -o errtrace - shopt -s inherit_errexit - - # Read the password from the credentials directory and - # escape any single quotes by adding additional single - # quotes after them, following the rules laid out here: - # https://dev.mysql.com/doc/refman/8.0/en/string-literals.html - db_password="$(<"$CREDENTIALS_DIRECTORY/db_password")" - db_password="''${db_password//\'/\'\'}" - - ( echo "SET sql_mode = 'NO_BACKSLASH_ESCAPES';" - echo "CREATE USER IF NOT EXISTS 'keycloak'@'localhost' IDENTIFIED BY '$db_password';" - echo "CREATE DATABASE IF NOT EXISTS keycloak CHARACTER SET utf8 COLLATE utf8_unicode_ci;" - echo "GRANT ALL PRIVILEGES ON keycloak.* TO 'keycloak'@'localhost';" - ) | mysql -N - ''; - }; - - systemd.services.keycloak = - let - databaseServices = - if createLocalPostgreSQL then - [ - "keycloakPostgreSQLInit.service" - "postgresql.service" - ] - else if createLocalMySQL then - [ - "keycloakMySQLInit.service" - "mysql.service" - ] - else - [ ]; - secretPaths = catAttrs "_secret" (collect isSecret cfg.settings); - mkSecretReplacement = file: '' - replace-secret ${hashString "sha256" file} $CREDENTIALS_DIRECTORY/${baseNameOf file} /run/keycloak/conf/keycloak.conf - ''; - secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths; - extraStartupFlags = lib.concatStringsSep " " cfg.extraStartupFlags; - in - { - after = databaseServices; - bindsTo = databaseServices; - wantedBy = [ "multi-user.target" ]; - path = with pkgs; [ - keycloakBuild - openssl - replace-secret - ]; - environment = { - KC_HOME_DIR = "/run/keycloak"; - KC_CONF_DIR = "/run/keycloak/conf"; - }; - serviceConfig = { - LoadCredential = - map (p: "${baseNameOf p}:${p}") secretPaths - ++ optionals (cfg.sslCertificate != null && cfg.sslCertificateKey != null) [ - "ssl_cert:${cfg.sslCertificate}" - "ssl_key:${cfg.sslCertificateKey}" - ]; - User = "keycloak"; - Group = "keycloak"; - DynamicUser = true; - RuntimeDirectory = "keycloak"; - RuntimeDirectoryMode = "0700"; - AmbientCapabilities = "CAP_NET_BIND_SERVICE"; - Type = "notify"; # Requires quarkus-systemd-notify plugin - NotifyAccess = "all"; - }; - script = - '' - set -o errexit -o pipefail -o nounset -o errtrace - shopt -s inherit_errexit - - umask u=rwx,g=,o= - - ln -s ${themesBundle} /run/keycloak/themes - ln -s ${keycloakBuild}/providers /run/keycloak/ - - install -D -m 0600 ${confFile} /run/keycloak/conf/keycloak.conf - - ${secretReplacements} - - # Escape any backslashes in the db parameters, since - # they're otherwise unexpectedly read as escape - # sequences. - sed -i '/db-/ s|\\|\\\\|g' /run/keycloak/conf/keycloak.conf - - '' - + optionalString (cfg.sslCertificate != null && cfg.sslCertificateKey != null) '' - mkdir -p /run/keycloak/ssl - cp $CREDENTIALS_DIRECTORY/ssl_{cert,key} /run/keycloak/ssl/ - '' - + '' - export KEYCLOAK_ADMIN=admin - export KEYCLOAK_ADMIN_PASSWORD=${escapeShellArg cfg.initialAdminPassword} - kc.sh --verbose start --optimized ${extraStartupFlags} - ''; - }; - - services.postgresql.enable = mkDefault createLocalPostgreSQL; - services.mysql.enable = mkDefault createLocalMySQL; - services.mysql.package = - let - dbPkg = if cfg.database.type == "mariadb" then pkgs.mariadb else pkgs.mysql80; - in - mkIf createLocalMySQL (mkDefault dbPkg); - }; - - meta.doc = ./keycloak.md; - meta.maintainers = [ maintainers.talyz ]; -} diff --git a/tests/keycloak.nix b/tests/keycloak.nix index 251707a..eaa7676 100644 --- a/tests/keycloak.nix +++ b/tests/keycloak.nix @@ -72,23 +72,10 @@ in }; testScript = - { nodes, ... }: - let - user = nodes.client.users.users.${nodes.client.pub-solar-os.authentication.username}; - #uid = toString user.uid; - bus = "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u ${user.name})/bus"; - gdbus = "${bus} gdbus"; - su = command: "su - ${user.name} -c '${command}'"; - gseval = "call --session -d org.gnome.Shell -o /org/gnome/Shell -m org.gnome.Shell.Eval"; - wmClass = su "${gdbus} ${gseval} global.display.focus_window.wm_class"; - in - '' -<<<<<<< HEAD + { ... }: '' def puppeteer_run(cmd): client.succeed(f'puppeteer-run \'{cmd}\' ') -======= ->>>>>>> main start_all() nachtigall.wait_for_unit("system.slice") @@ -99,7 +86,6 @@ in nachtigall.wait_until_succeeds("curl https://auth.test.pub.solar/") client.wait_for_unit("system.slice") -<<<<<<< HEAD client.wait_for_file("/tmp/puppeteer.sock") puppeteer_run('page.goto("https://auth.test.pub.solar")') puppeteer_run('page.waitForNetworkIdle()') @@ -118,10 +104,5 @@ in puppeteer_run('page.locator("button::-p-text(Register)").click()') puppeteer_run('page.waitForNetworkIdle()') client.screenshot("after-register") -======= - client.sleep(30) - # client.wait_until_succeeds("${wmClass} | grep -q 'firefox'") - client.screenshot("screen") ->>>>>>> main ''; }