Compare commits

...

11 commits

Author SHA1 Message Date
b12f dec2d76d2a
tests: move back to old keycloak module
Some checks failed
Flake checks / Check (pull_request) Failing after 1m42s
2024-08-27 13:37:28 +02:00
b12f 6efc884353
hosts: remove nachtigall-test 2024-08-27 13:32:00 +02:00
b12f 38a34f4345
Merge branch 'main' into feat/automated-account-deletion 2024-08-27 13:31:52 +02:00
b12f 3bc699fccf
chore: run nix fmt 2024-08-27 13:17:30 +02:00
teutat3s fb8ee1278a
Merge pull request 'feat/tests' (#224) from feat/tests into main
Reviewed-on: #224
Reviewed-by: teutat3s <teutat3s@noreply.git.pub.solar>
2024-08-27 10:45:56 +00:00
teutat3s 66ed87e666
ci: avoid garbage collection of checks
All checks were successful
Flake checks / Check (pull_request) Successful in 17m36s
2024-08-27 12:37:37 +02:00
teutat3s 88b76beb5c
keycloak: use backups module
All checks were successful
Flake checks / Check (pull_request) Successful in 19m4s
Co-authored-by: b12f <b12f@noreply.git.pub.solar>
Co-authored-by: Hendrik Sokolowski <hensoko@noreply.git.pub.solar>
2024-08-27 10:09:07 +02:00
teutat3s e857c6198b
modules/backup: init
Co-authored-by: b12f <b12f@noreply.git.pub.solar>
Co-authored-by: Hendrik Sokolowski <hensoko@noreply.git.pub.solar>
2024-08-27 10:04:10 +02:00
teutat3s 998cf4c63d
website: force HTTPS
Co-authored-by: b12f <b12f@noreply.git.pub.solar>
Co-authored-by: Hendrik Sokolowski <hensoko@noreply.git.pub.solar>
2024-08-27 10:03:43 +02:00
teutat3s a0b52d51e5
nachtigall: make postgres wait for zfs mount
Co-authored-by: b12f <b12f@noreply.git.pub.solar>
Co-authored-by: Hendrik Sokolowski <hensoko@noreply.git.pub.solar>
2024-08-27 10:00:42 +02:00
teutat3s 701c62dd69
tests: create keycloak test, add working test for website
Co-authored-by: b12f <b12f@noreply.git.pub.solar>
Co-authored-by: Hendrik Sokolowski <hensoko@noreply.git.pub.solar>
2024-08-27 09:55:25 +02:00
14 changed files with 322 additions and 1031 deletions

View file

@ -18,14 +18,20 @@ jobs:
# Prevent cache garbage collection by creating GC roots # Prevent cache garbage collection by creating GC roots
mkdir -p /var/lib/gitea-runner/tankstelle/.local/state/nix/results mkdir -p /var/lib/gitea-runner/tankstelle/.local/state/nix/results
for target in $(nix flake show --json --all-systems | jq ' for target in $(nix flake show --json --all-systems | jq --raw-output '
.["nixosConfigurations"] | .["nixosConfigurations"] |
to_entries[] | to_entries[] |
.key .key'
' | tr -d '"'
); do ); do
nix --print-build-logs --verbose --accept-flake-config --access-tokens '' \ nix --print-build-logs --verbose --accept-flake-config --access-tokens '' \
build --out-link /var/lib/gitea-runner/tankstelle/.local/state/nix/results/"$target" ".#nixosConfigurations.${target}.config.system.build.toplevel" build --out-link /var/lib/gitea-runner/tankstelle/.local/state/nix/results/"$target" ".#nixosConfigurations.${target}.config.system.build.toplevel"
done done
nix --print-build-logs --verbose --accept-flake-config --access-tokens '' flake check for check in $(nix flake show --json --all-systems | jq --raw-output '
.checks."x86_64-linux" |
to_entries[] |
.key'
); do
nix --print-build-logs --verbose --accept-flake-config --access-tokens '' \
build --out-link /var/lib/gitea-runner/tankstelle/.local/state/nix/results/"$check" ".#checks.x86_64-linux.${check}"
done

View file

@ -82,7 +82,7 @@
master = import inputs.master { inherit system; }; master = import inputs.master { inherit system; };
}; };
packages = import ./tests ({ inherit inputs self; } // args); checks = import ./tests ({ inherit inputs self; } // args);
devShells.default = pkgs.mkShell { devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [ buildInputs = with pkgs; [
@ -111,7 +111,8 @@
devShells.ci = pkgs.mkShell { buildInputs = with pkgs; [ nodejs ]; }; devShells.ci = pkgs.mkShell { buildInputs = with pkgs; [ nodejs ]; };
}; };
flake = let flake =
let
username = "barkeeper"; username = "barkeeper";
in in
{ {

View file

@ -43,17 +43,6 @@
]; ];
}; };
nachtigall-test = {
imports = [
self.inputs.agenix.nixosModules.default
self.nixosModules.home-manager
./nachtigall/test-vm.nix
self.nixosModules.overlays
self.nixosModules.core
self.nixosModules.docker
];
};
flora-6 = self.nixos-flake.lib.mkLinuxSystem { flora-6 = self.nixos-flake.lib.mkLinuxSystem {
imports = [ imports = [
self.inputs.agenix.nixosModules.default self.inputs.agenix.nixosModules.default

View file

@ -59,11 +59,16 @@
database-password-file = config.age.secrets.keycloak-database-password.path; database-password-file = config.age.secrets.keycloak-database-password.path;
}; };
pub-solar-os.backups.stores.storagebox = { pub-solar-os.backups.repos.storagebox = {
passwordFile = config.age.secrets."restic-repo-storagebox".path; passwordFile = config.age.secrets."restic-repo-storagebox".path;
repository = "sftp:u377325@u377325.your-storagebox.de:/backups"; repository = "sftp:u377325@u377325.your-storagebox.de:/backups";
}; };
systemd.services.postgresql = {
after = [ "var-lib-postgresql.mount" ];
requisite = [ "var-lib-postgresql.mount" ];
};
# This value determines the NixOS release with which your system is to be # This value determines the NixOS release with which your system is to be
# compatible, in order to avoid breaking some software such as database # compatible, in order to avoid breaking some software such as database
# servers. You should change this only after NixOS release notes say you # servers. You should change this only after NixOS release notes say you

View file

@ -1,8 +1,7 @@
{ flake, lib, ... }: { flake, lib, ... }:
{ {
imports = imports = [
[
./backups.nix ./backups.nix
./apps/nginx.nix ./apps/nginx.nix

View file

@ -7,7 +7,8 @@
{ {
# Configuration common to all Linux systems # Configuration common to all Linux systems
flake = { flake = {
lib = let lib =
let
callLibs = file: import file { inherit lib; }; callLibs = file: import file { inherit lib; };
in in
rec { rec {

View file

@ -4,17 +4,27 @@
lib, lib,
pkgs, pkgs,
... ...
}: let }:
utils = import "${flake.inputs.nixpkgs}/nixos/lib/utils.nix" { inherit lib; inherit config; inherit pkgs; }; let
utils = import "${flake.inputs.nixpkgs}/nixos/lib/utils.nix" {
inherit lib;
inherit config;
inherit pkgs;
};
# Type for a valid systemd unit option. Needed for correctly passing "timerConfig" to "systemd.timers" # Type for a valid systemd unit option. Needed for correctly passing "timerConfig" to "systemd.timers"
inherit (utils.systemdUtils.unitOptions) unitOption; inherit (utils.systemdUtils.unitOptions) unitOption;
in { inherit (lib)
literalExpression
mkOption
mkPackageOption
types
;
in
{
options.pub-solar-os.backups = { options.pub-solar-os.backups = {
stores = repos = mkOption {
with lib;
mkOption {
description = '' description = ''
Periodic backups to create with Restic. Configuration of Restic repositories.
''; '';
type = types.attrsOf ( type = types.attrsOf (
types.submodule ( types.submodule (
@ -51,9 +61,7 @@ in {
}; };
}; };
backups = backups = mkOption {
with lib;
mkOption {
description = '' description = ''
Periodic backups to create with Restic. Periodic backups to create with Restic.
''; '';
@ -247,17 +255,17 @@ in {
config = { config = {
services.restic.backups = services.restic.backups =
let let
stores = config.pub-solar-os.backups.stores; repos = config.pub-solar-os.backups.repos;
backups = config.pub-solar-os.backups.backups; backups = config.pub-solar-os.backups.backups;
storeNames = builtins.attrNames stores; storeNames = builtins.attrNames repos;
backupNames = builtins.attrNames backups; backupNames = builtins.attrNames backups;
createBackups = createBackups =
backupName: backupName:
map (storeName: { map (storeName: {
name = "${backupName}-${storeName}"; name = "${backupName}-${storeName}";
value = stores."${storeName}" // backups."${backupName}"; value = repos."${storeName}" // backups."${backupName}";
}) storeNames; }) storeNames;
in in

View file

@ -6,9 +6,6 @@
... ...
}: }:
{ {
disabledModules = [ "services/web-apps/keycloak.nix" ];
imports = [ ./keycloak.nix ];
options.pub-solar-os.auth = with lib; { options.pub-solar-os.auth = with lib; {
enable = mkEnableOption "Enable keycloak to run on the node"; enable = mkEnableOption "Enable keycloak to run on the node";
@ -59,9 +56,7 @@
"pub.solar" = "pub.solar" =
flake.inputs.keycloak-theme-pub-solar.legacyPackages.${pkgs.system}.keycloak-theme-pub-solar; flake.inputs.keycloak-theme-pub-solar.legacyPackages.${pkgs.system}.keycloak-theme-pub-solar;
}; };
plugins = [ plugins = [ flake.inputs.keycloak-event-listener.packages.${pkgs.system}.keycloak-event-listener ];
flake.inputs.keycloak-event-listener.packages.${pkgs.system}.keycloak-event-listener
];
}; };
pub-solar-os.backups.backups.keycloak = { pub-solar-os.backups.backups.keycloak = {

View file

@ -1,705 +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 <https://www.keycloak.org/server/configuration>.
--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 (&lt;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 <https://www.keycloak.org/migration/migrating-to-quarkus>
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 <https://www.keycloak.org/server/hostname>
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 <https://www.keycloak.org/server/hostname>
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 <https://www.keycloak.org/server/reverseproxy> 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 <https://www.keycloak.org/server/all-config>.
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 ];
}

View file

@ -5,15 +5,22 @@ args@{
pkgs, pkgs,
inputs, inputs,
... ...
}: let }:
let
nixos-lib = import (inputs.nixpkgs + "/nixos/lib") { }; nixos-lib = import (inputs.nixpkgs + "/nixos/lib") { };
loadTestFiles = with lib; dir: mapAttrs' (name: _: let loadTestFiles =
with lib;
dir:
mapAttrs' (
name: _:
let
test = ((import (dir + "/${name}")) args); test = ((import (dir + "/${name}")) args);
in { in
{
name = "test-" + (lib.strings.removeSuffix ".nix" name); name = "test-" + (lib.strings.removeSuffix ".nix" name);
value = nixos-lib.runTest test; value = nixos-lib.runTest test;
}) }
(filterAttrs (name: _: (hasSuffix ".nix" name) && name != "default.nix") ) (filterAttrs (name: _: (hasSuffix ".nix" name) && name != "default.nix") (builtins.readDir dir));
(builtins.readDir dir)); in
in loadTestFiles ./. loadTestFiles ./.

View file

@ -72,17 +72,7 @@ in
}; };
testScript = 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
''
def puppeteer_run(cmd): def puppeteer_run(cmd):
client.succeed(f'puppeteer-run \'{cmd}\' ') client.succeed(f'puppeteer-run \'{cmd}\' ')

View file

@ -3,10 +3,12 @@
lib, lib,
config, config,
... ...
}: let }:
let
puppeteer-socket = (pkgs.callPackage (import ./puppeteer-socket/puppeteer-socket.nix) { }); puppeteer-socket = (pkgs.callPackage (import ./puppeteer-socket/puppeteer-socket.nix) { });
puppeteer-run = (pkgs.callPackage (import ./puppeteer-socket/puppeteer-run.nix) { }); puppeteer-run = (pkgs.callPackage (import ./puppeteer-socket/puppeteer-run.nix) { });
in { in
{
imports = [ ./global.nix ]; imports = [ ./global.nix ];
security.polkit.enable = true; security.polkit.enable = true;
@ -18,9 +20,7 @@ in {
services.getty.autologinUser = config.pub-solar-os.authentication.username; services.getty.autologinUser = config.pub-solar-os.authentication.username;
virtualisation.qemu.options = [ virtualisation.qemu.options = [ "-vga std" ];
"-vga std"
];
home-manager.users.${config.pub-solar-os.authentication.username} = { home-manager.users.${config.pub-solar-os.authentication.username} = {
programs.bash.profileExtra = '' programs.bash.profileExtra = ''

View file

@ -1,7 +1,5 @@
{ { writeShellScriptBin, curl }:
writeShellScriptBin, writeShellScriptBin "puppeteer-run" ''
curl
}: writeShellScriptBin "puppeteer-run" ''
set -e set -e
exec ${curl}/bin/curl --fail-with-body -X POST -d "$@" --unix-socket "/tmp/puppeteer.sock" http://puppeteer-socket exec ${curl}/bin/curl --fail-with-body -X POST -d "$@" --unix-socket "/tmp/puppeteer.sock" http://puppeteer-socket

View file

@ -1,7 +1,4 @@
{ { buildNpmPackage, nodejs }:
buildNpmPackage,
nodejs,
}:
buildNpmPackage rec { buildNpmPackage rec {
src = ./.; src = ./.;
name = "puppeteer-socket"; name = "puppeteer-socket";