WIP: feat/automated-account-deletion #174

Draft
b12f wants to merge 23 commits from feat/automated-account-deletion into main
11 changed files with 410 additions and 301 deletions
Showing only changes of commit 3bc699fccf - Show all commits

View file

@ -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

@ -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,11 +4,17 @@
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 { in
{
options.pub-solar-os.backups = { options.pub-solar-os.backups = {
stores = stores =
with lib; with lib;

View file

@ -59,9 +59,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,4 +1,10 @@
{ config, options, pkgs, lib, ... }: {
config,
options,
pkgs,
lib,
...
}:
let let
cfg = config.services.keycloak; cfg = config.services.keycloak;
@ -40,34 +46,78 @@ let
prefixUnlessEmpty = prefix: string: optionalString (string != "") "${prefix}${string}"; prefixUnlessEmpty = prefix: string: optionalString (string != "") "${prefix}${string}";
in in
{ {
imports = imports = [
(mkRenamedOptionModule
[ [
"services"
"keycloak"
"bindAddress"
]
[
"services"
"keycloak"
"settings"
"http-host"
]
)
(mkRenamedOptionModule (mkRenamedOptionModule
[ "services" "keycloak" "bindAddress" ] [
[ "services" "keycloak" "settings" "http-host" ]) "services"
(mkRenamedOptionModule "keycloak"
[ "services" "keycloak" "forceBackendUrlToFrontendUrl"] "forceBackendUrlToFrontendUrl"
[ "services" "keycloak" "settings" "hostname-strict-backchannel"]) ]
[
"services"
"keycloak"
"settings"
"hostname-strict-backchannel"
]
)
(mkChangedOptionModule (mkChangedOptionModule
[ "services" "keycloak" "httpPort" ] [
[ "services" "keycloak" "settings" "http-port" ] "services"
(config: "keycloak"
builtins.fromJSON config.services.keycloak.httpPort)) "httpPort"
]
[
"services"
"keycloak"
"settings"
"http-port"
]
(config: builtins.fromJSON config.services.keycloak.httpPort)
)
(mkChangedOptionModule (mkChangedOptionModule
[ "services" "keycloak" "httpsPort" ] [
[ "services" "keycloak" "settings" "https-port" ] "services"
(config: "keycloak"
builtins.fromJSON config.services.keycloak.httpsPort)) "httpsPort"
]
[
"services"
"keycloak"
"settings"
"https-port"
]
(config: builtins.fromJSON config.services.keycloak.httpsPort)
)
(mkRemovedOptionModule (mkRemovedOptionModule
[ "services" "keycloak" "frontendUrl" ] [
"services"
"keycloak"
"frontendUrl"
]
'' ''
Set `services.keycloak.settings.hostname' and `services.keycloak.settings.http-relative-path' instead. 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. NOTE: You likely want to set 'http-relative-path' to '/auth' to keep compatibility with your clients.
See its description for more information. See its description for more information.
'') ''
(mkRemovedOptionModule )
[ "services" "keycloak" "extraConfig" ] (mkRemovedOptionModule [
"Use `services.keycloak.settings' instead.") "services"
"keycloak"
"extraConfig"
] "Use `services.keycloak.settings' instead.")
]; ];
options.services.keycloak = options.services.keycloak =
@ -82,9 +132,11 @@ in
path path
enum enum
package package
port; port
;
assertStringPath = optionName: value: assertStringPath =
optionName: value:
if isPath value then if isPath value then
throw '' throw ''
services.keycloak.${optionName}: services.keycloak.${optionName}:
@ -92,7 +144,8 @@ in
is a Nix path, but should be a string, since Nix is a Nix path, but should be a string, since Nix
paths are copied into the world-readable Nix store. paths are copied into the world-readable Nix store.
'' ''
else value; else
value;
in in
{ {
enable = mkOption { enable = mkOption {
@ -139,7 +192,11 @@ in
database = { database = {
type = mkOption { type = mkOption {
type = enum [ "mysql" "mariadb" "postgresql" ]; type = enum [
"mysql"
"mariadb"
"postgresql"
];
default = "postgresql"; default = "postgresql";
example = "mariadb"; example = "mariadb";
description = '' description = ''
@ -286,7 +343,14 @@ in
settings = mkOption { settings = mkOption {
type = lib.types.submodule { type = lib.types.submodule {
freeformType = attrsOf (nullOr (oneOf [ str int bool (attrsOf path) ])); freeformType = attrsOf (
nullOr (oneOf [
str
int
bool
(attrsOf path)
])
);
options = { options = {
http-host = mkOption { http-host = mkOption {
@ -365,7 +429,12 @@ in
}; };
proxy = mkOption { proxy = mkOption {
type = enum [ "edge" "reencrypt" "passthrough" "none" ]; type = enum [
"edge"
"reencrypt"
"passthrough"
"none"
];
default = "none"; default = "none";
example = "edge"; example = "edge";
description = '' description = ''
@ -422,7 +491,12 @@ in
# connect to it. # connect to it.
databaseActuallyCreateLocally = cfg.database.createLocally && cfg.database.host == "localhost"; databaseActuallyCreateLocally = cfg.database.createLocally && cfg.database.host == "localhost";
createLocalPostgreSQL = databaseActuallyCreateLocally && cfg.database.type == "postgresql"; createLocalPostgreSQL = databaseActuallyCreateLocally && cfg.database.type == "postgresql";
createLocalMySQL = databaseActuallyCreateLocally && elem cfg.database.type [ "mysql" "mariadb" ]; createLocalMySQL =
databaseActuallyCreateLocally
&& elem cfg.database.type [
"mysql"
"mariadb"
];
mySqlCaKeystore = pkgs.runCommand "mysql-ca-keystore" { } '' mySqlCaKeystore = pkgs.runCommand "mysql-ca-keystore" { } ''
${pkgs.jre}/bin/keytool -importcert -trustcacerts -alias MySQLCACert -file ${cfg.database.caCert} -keystore $out -storepass notsosecretpassword -noprompt ${pkgs.jre}/bin/keytool -importcert -trustcacerts -alias MySQLCACert -file ${cfg.database.caCert} -keystore $out -storepass notsosecretpassword -noprompt
@ -454,39 +528,60 @@ in
fi fi
done done
${concatStringsSep "\n" (mapAttrsToList (name: theme: "linkTheme ${theme} ${escapeShellArg name}") cfg.themes)} ${concatStringsSep "\n" (
mapAttrsToList (name: theme: "linkTheme ${theme} ${escapeShellArg name}") cfg.themes
)}
''; '';
keycloakConfig = lib.generators.toKeyValue { keycloakConfig = lib.generators.toKeyValue {
mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" { mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" {
mkValueString = v: mkValueString =
if isInt v then toString v v:
else if isString v then v if isInt v then
else if true == v then "true" toString v
else if false == v then "false" else if isString v then
else if isSecret v then hashString "sha256" v._secret v
else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) 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; isSecret = v: isAttrs v && v ? _secret && isString v._secret;
filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! elem v [{ } null])) cfg.settings; filteredConfig = lib.converge (lib.filterAttrsRecursive (
_: v:
!elem v [
{ }
null
]
)) cfg.settings;
confFile = pkgs.writeText "keycloak.conf" (keycloakConfig filteredConfig); confFile = pkgs.writeText "keycloak.conf" (keycloakConfig filteredConfig);
keycloakBuild = cfg.package.override { keycloakBuild = cfg.package.override {
inherit confFile; inherit confFile;
plugins = cfg.package.enabledPlugins ++ cfg.plugins ++ plugins =
(with cfg.package.plugins; [quarkus-systemd-notify quarkus-systemd-notify-deployment]); cfg.package.enabledPlugins
++ cfg.plugins
++ (with cfg.package.plugins; [
quarkus-systemd-notify
quarkus-systemd-notify-deployment
]);
}; };
in in
mkIf cfg.enable mkIf cfg.enable {
{
assertions = [ assertions = [
{ {
assertion = (cfg.database.useSSL && cfg.database.type == "postgresql") -> (cfg.database.caCert != null); 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"; 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; 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"; message = "Setting up a local PostgreSQL db for Keycloak requires `standard_conforming_strings` turned on to work reliably";
} }
{ {
@ -516,23 +611,24 @@ in
services.keycloak.settings = services.keycloak.settings =
let let
postgresParams = concatStringsSep "&" ( postgresParams = concatStringsSep "&" (
optionals cfg.database.useSSL [ optionals cfg.database.useSSL [ "ssl=true" ]
"ssl=true" ++ optionals (cfg.database.caCert != null) [
] ++ optionals (cfg.database.caCert != null) [
"sslrootcert=${cfg.database.caCert}" "sslrootcert=${cfg.database.caCert}"
"sslmode=verify-ca" "sslmode=verify-ca"
] ]
); );
mariadbParams = concatStringsSep "&" ([ mariadbParams = concatStringsSep "&" (
"characterEncoding=UTF-8" [ "characterEncoding=UTF-8" ]
] ++ optionals cfg.database.useSSL [ ++ optionals cfg.database.useSSL [
"useSSL=true" "useSSL=true"
"requireSSL=true" "requireSSL=true"
"verifyServerCertificate=true" "verifyServerCertificate=true"
] ++ optionals (cfg.database.caCert != null) [ ]
++ optionals (cfg.database.caCert != null) [
"trustCertificateKeyStoreUrl=file:${mySqlCaKeystore}" "trustCertificateKeyStoreUrl=file:${mySqlCaKeystore}"
"trustCertificateKeyStorePassword=notsosecretpassword" "trustCertificateKeyStorePassword=notsosecretpassword"
]); ]
);
dbProps = if cfg.database.type == "postgresql" then postgresParams else mariadbParams; dbProps = if cfg.database.type == "postgresql" then postgresParams else mariadbParams;
in in
mkMerge [ mkMerge [
@ -618,15 +714,18 @@ in
systemd.services.keycloak = systemd.services.keycloak =
let let
databaseServices = databaseServices =
if createLocalPostgreSQL then [ if createLocalPostgreSQL then
[
"keycloakPostgreSQLInit.service" "keycloakPostgreSQLInit.service"
"postgresql.service" "postgresql.service"
] ]
else if createLocalMySQL then [ else if createLocalMySQL then
[
"keycloakMySQLInit.service" "keycloakMySQLInit.service"
"mysql.service" "mysql.service"
] ]
else [ ]; else
[ ];
secretPaths = catAttrs "_secret" (collect isSecret cfg.settings); secretPaths = catAttrs "_secret" (collect isSecret cfg.settings);
mkSecretReplacement = file: '' mkSecretReplacement = file: ''
replace-secret ${hashString "sha256" file} $CREDENTIALS_DIRECTORY/${baseNameOf file} /run/keycloak/conf/keycloak.conf replace-secret ${hashString "sha256" file} $CREDENTIALS_DIRECTORY/${baseNameOf file} /run/keycloak/conf/keycloak.conf
@ -663,7 +762,8 @@ in
Type = "notify"; # Requires quarkus-systemd-notify plugin Type = "notify"; # Requires quarkus-systemd-notify plugin
NotifyAccess = "all"; NotifyAccess = "all";
}; };
script = '' script =
''
set -o errexit -o pipefail -o nounset -o errtrace set -o errexit -o pipefail -o nounset -o errtrace
shopt -s inherit_errexit shopt -s inherit_errexit
@ -681,10 +781,12 @@ in
# sequences. # sequences.
sed -i '/db-/ s|\\|\\\\|g' /run/keycloak/conf/keycloak.conf sed -i '/db-/ s|\\|\\\\|g' /run/keycloak/conf/keycloak.conf
'' + optionalString (cfg.sslCertificate != null && cfg.sslCertificateKey != null) '' ''
+ optionalString (cfg.sslCertificate != null && cfg.sslCertificateKey != null) ''
mkdir -p /run/keycloak/ssl mkdir -p /run/keycloak/ssl
cp $CREDENTIALS_DIRECTORY/ssl_{cert,key} /run/keycloak/ssl/ cp $CREDENTIALS_DIRECTORY/ssl_{cert,key} /run/keycloak/ssl/
'' + '' ''
+ ''
export KEYCLOAK_ADMIN=admin export KEYCLOAK_ADMIN=admin
export KEYCLOAK_ADMIN_PASSWORD=${escapeShellArg cfg.initialAdminPassword} export KEYCLOAK_ADMIN_PASSWORD=${escapeShellArg cfg.initialAdminPassword}
kc.sh --verbose start --optimized ${extraStartupFlags} kc.sh --verbose start --optimized ${extraStartupFlags}

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

@ -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";