From a3f812875642e880e824c0aecdfd05db1afb5d6e Mon Sep 17 00:00:00 2001 From: ibizaman Date: Thu, 23 Nov 2023 00:13:33 -0800 Subject: [PATCH] add nixos tests for postgresql and refactor options The tests actually showed a flaw in the implementation, we needed "password" and not "trust" in the auth file. Also, having the port defined at the same time as enabling listening for TCP/IP connection made no sense. --- README.md | 9 +- flake.nix | 12 ++- modules/blocks/authelia.nix | 2 +- modules/blocks/monitoring.nix | 2 +- modules/blocks/postgresql.nix | 35 +++---- modules/services/vaultwarden.nix | 4 +- test/modules/postgresql.nix | 17 ++- test/vm/postgresql.nix | 172 ++++++++++++++++++++++++++++--- 8 files changed, 205 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index cd5ee8b..f3b61e9 100644 --- a/README.md +++ b/README.md @@ -690,12 +690,15 @@ Run all tests: ```bash $ nix build .#checks.${system}.all +# or +$ nix flake check ``` Run one group of tests: ```bash -$ nix build .#checks.${system}.module +$ nix build .#checks.${system}.modules +$ nix build .#checks.${system}.vm_postgresql_peerAuth ``` ### Deploy using colmena @@ -796,6 +799,10 @@ $ nix run nixpkgs#openssl -- rand -hex 64 ## Links that helped +While creating NixOS tests: + +- https://www.haskellforall.com/2020/11/how-to-use-nixos-for-lightweight.html + While creating an XML config generator for Radarr: - https://stackoverflow.com/questions/4906977/how-can-i-access-environment-variables-in-python diff --git a/flake.nix b/flake.nix index 1ae95c2..8bb849a 100644 --- a/flake.nix +++ b/flake.nix @@ -45,7 +45,11 @@ }) files; mergeTests = pkgs.lib.lists.foldl pkgs.lib.trivial.mergeAttrs {}; - in rec { + + flattenAttrs = root: attrset: pkgs.lib.attrsets.foldlAttrs (acc: name: value: acc // { + "${root}_${name}" = value; + }) {} attrset; + in (rec { all = mergeTests [ modules ]; @@ -59,9 +63,9 @@ ./test/modules/postgresql.nix ]); }; - - vm_postgresql = pkgs.callPackage ./test/vm/postgresql.nix {}; - }; + } + // (flattenAttrs "vm_postgresql" (import ./test/vm/postgresql.nix {inherit pkgs; inherit (pkgs) lib;})) + ); } ); } diff --git a/modules/blocks/authelia.nix b/modules/blocks/authelia.nix index 0980dc6..36b005e 100644 --- a/modules/blocks/authelia.nix +++ b/modules/blocks/authelia.nix @@ -279,7 +279,7 @@ in user = autheliaCfg.user; }; - shb.postgresql.passwords = [ + shb.postgresql.ensures = [ { username = autheliaCfg.user; database = autheliaCfg.user; diff --git a/modules/blocks/monitoring.nix b/modules/blocks/monitoring.nix index ec48f8b..fb6ee0b 100644 --- a/modules/blocks/monitoring.nix +++ b/modules/blocks/monitoring.nix @@ -36,7 +36,7 @@ in }; config = lib.mkIf cfg.enable { - shb.postgresql.passwords = [ + shb.postgresql.ensures = [ { username = "grafana"; database = "grafana"; diff --git a/modules/blocks/postgresql.nix b/modules/blocks/postgresql.nix index 117a52c..3b85b84 100644 --- a/modules/blocks/postgresql.nix +++ b/modules/blocks/postgresql.nix @@ -14,13 +14,13 @@ in See https://www.postgresql.org/docs/current/pgstatstatements.html''; default = false; }; - tcpIPPort = lib.mkOption { - type = lib.types.nullOr lib.types.port; + enableTCPIP = lib.mkOption { + type = lib.types.bool; description = "Enable TCP/IP connection on given port."; - default = null; + default = false; }; - passwords = lib.mkOption { + ensures = lib.mkOption { type = lib.types.listOf (lib.types.submodule { options = { username = lib.mkOption { @@ -35,7 +35,7 @@ in passwordFile = lib.mkOption { type = lib.types.nullOr lib.types.str; - description = "Optional password file for the postgres user."; + description = "Optional password file for the postgres user. If not given, only peer auth is accepted for this user, otherwise password auth is allowed."; default = null; example = "/run/secrets/postgresql/password"; }; @@ -47,22 +47,21 @@ in config = let - tcpConfig = port: { + tcpConfig = { services.postgresql.enableTCPIP = true; - services.postgresql.port = port; services.postgresql.authentication = lib.mkOverride 10 '' #type database DBuser origin-address auth-method local all all peer # ipv4 - host all all 127.0.0.1/32 trust + host all all 127.0.0.1/32 password # ipv6 - host all all ::1/128 trust + host all all ::1/128 password ''; }; - dbConfig = passwordCfgs: { - services.postgresql.enable = lib.mkDefault ((builtins.length passwordCfgs) > 0); - services.postgresql.ensureDatabases = map ({ database, ... }: database) passwordCfgs; + dbConfig = ensureCfgs: { + services.postgresql.enable = lib.mkDefault ((builtins.length ensureCfgs) > 0); + services.postgresql.ensureDatabases = map ({ database, ... }: database) ensureCfgs; services.postgresql.ensureUsers = map ({ username, database, ... }: { name = username; ensurePermissions = { @@ -71,10 +70,10 @@ in ensureClauses = { "login" = true; }; - }) passwordCfgs; + }) ensureCfgs; }; - pwdConfig = passwordCfgs: { + pwdConfig = ensureCfgs: { systemd.services.postgresql.postStart = let prefix = '' @@ -91,7 +90,7 @@ in password := trim(both from replace(pg_read_file('${passwordFile}'), E'\n', ''')); EXECUTE format('ALTER ROLE ${username} WITH PASSWORD '''%s''';', password); ''; - cfgsWithPasswords = builtins.filter (cfg: cfg.passwordFile != null) passwordCfgs; + cfgsWithPasswords = builtins.filter (cfg: cfg.passwordFile != null) ensureCfgs; in if (builtins.length cfgsWithPasswords) == 0 then "" else prefix + (lib.concatStrings (map exec cfgsWithPasswords)) + suffix; @@ -103,9 +102,9 @@ in in lib.mkMerge ( [ - (dbConfig cfg.passwords) - (pwdConfig cfg.passwords) - (lib.mkIf (!(isNull cfg.tcpIPPort)) (tcpConfig cfg.tcpIPPort)) + (dbConfig cfg.ensures) + (pwdConfig cfg.ensures) + (lib.mkIf cfg.enableTCPIP tcpConfig) (debugConfig cfg.debug) ] ); diff --git a/modules/services/vaultwarden.nix b/modules/services/vaultwarden.nix index 0768d05..68c9def 100644 --- a/modules/services/vaultwarden.nix +++ b/modules/services/vaultwarden.nix @@ -183,8 +183,8 @@ in } ]; - shb.postgresql.tcpIPPort= 5432; - shb.postgresql.passwords = [ + shb.postgresql.enableTCPIP = true; + shb.postgresql.ensures = [ { username = "vaultwarden"; database = "vaultwarden"; diff --git a/test/modules/postgresql.nix b/test/modules/postgresql.nix index 132df68..6468058 100644 --- a/test/modules/postgresql.nix +++ b/test/modules/postgresql.nix @@ -68,7 +68,7 @@ in systemd.services.postgresql.postStart = ""; }; expr = testConfig { - shb.postgresql.passwords = [ + shb.postgresql.ensures = [ { username = "myuser"; database = "mydatabase"; @@ -104,7 +104,7 @@ in ''; }; expr = testConfig { - shb.postgresql.passwords = [ + shb.postgresql.ensures = [ { username = "myuser"; database = "mydatabase"; @@ -143,7 +143,7 @@ in systemd.services.postgresql.postStart = ""; }; expr = testConfig { - shb.postgresql.passwords = [ + shb.postgresql.ensures = [ { username = "user1"; database = "db1"; @@ -196,7 +196,7 @@ in ''; }; expr = testConfig { - shb.postgresql.passwords = [ + shb.postgresql.ensures = [ { username = "user1"; database = "db1"; @@ -249,7 +249,7 @@ in ''; }; expr = testConfig { - shb.postgresql.passwords = [ + shb.postgresql.ensures = [ { username = "user1"; database = "db1"; @@ -271,20 +271,19 @@ in ensureDatabases = []; enableTCPIP = true; - port = 1234; authentication = '' #type database DBuser origin-address auth-method local all all peer # ipv4 - host all all 127.0.0.1/32 trust + host all all 127.0.0.1/32 password # ipv6 - host all all ::1/128 trust + host all all ::1/128 password ''; }; systemd.services.postgresql.postStart = ""; }; expr = testConfig { - shb.postgresql.tcpIPPort = 1234; + shb.postgresql.enableTCPIP = true; }; }; } diff --git a/test/vm/postgresql.nix b/test/vm/postgresql.nix index f595bf8..8ead6fd 100644 --- a/test/vm/postgresql.nix +++ b/test/vm/postgresql.nix @@ -1,19 +1,167 @@ { pkgs, lib, ... }: -let +{ + peerWithoutUser = pkgs.nixosTest { + name = "postgresql"; -in pkgs.nixosTest { - name = "postgresql"; + nodes.machine = { config, pkgs, ... }: { + imports = [ + ../../modules/blocks/postgresql.nix + ]; - nodes.machine = { config, pkgs, ... }: { - imports = [ - ../../modules/blocks/postgresql.nix - ]; + shb.postgresql.ensures = [ + { + username = "me"; + database = "mine"; + } + ]; + }; - services.postgresql.enable = true; + testScript = { nodes, ... }: '' + start_all() + machine.wait_for_unit("postgresql.service") + + def peer_cmd(user, database): + return "sudo -u me psql -U {user} {db} --command \"\"".format(user=user, db=database) + + with subtest("cannot login because of missing user"): + machine.fail(peer_cmd("me", "mine"), timeout=10) + + with subtest("cannot login with unknown user"): + machine.fail(peer_cmd("notme", "mine"), timeout=10) + + with subtest("cannot login to unknown database"): + machine.fail(peer_cmd("me", "notmine"), timeout=10) + ''; }; - testScript = { nodes, ... }: '' - start_all() - machine.wait_for_unit("postgresql.service") - ''; + peerAuth = pkgs.nixosTest { + name = "postgresql"; + + nodes.machine = { config, pkgs, ... }: { + imports = [ + ../../modules/blocks/postgresql.nix + ]; + + users.users.me = { + isSystemUser = true; + group = "me"; + extraGroups = [ "sudoers" ]; + }; + users.groups.me = {}; + + shb.postgresql.ensures = [ + { + username = "me"; + database = "mine"; + } + ]; + }; + + testScript = { nodes, ... }: '' + start_all() + machine.wait_for_unit("postgresql.service") + + def peer_cmd(user, database): + return "sudo -u me psql -U {user} {db} --command \"\"".format(user=user, db=database) + + def tcpip_cmd(user, database, port): + return "psql -h 127.0.0.1 -p {port} -U {user} {db} --command \"\"".format(user=user, db=database, port=port) + + with subtest("can login with provisioned user and database"): + machine.succeed(peer_cmd("me", "mine"), timeout=10) + + with subtest("cannot login with unknown user"): + machine.fail(peer_cmd("notme", "mine"), timeout=10) + + with subtest("cannot login to unknown database"): + machine.fail(peer_cmd("me", "notmine"), timeout=10) + + with subtest("cannot login with tcpip"): + machine.fail(tcpip_cmd("me", "mine", "5432"), timeout=10) + ''; + }; + + tcpIPWithoutPasswordAuth = pkgs.nixosTest { + name = "postgresql"; + + nodes.machine = { config, pkgs, ... }: { + imports = [ + ../../modules/blocks/postgresql.nix + ]; + + shb.postgresql.enableTCPIP = true; + shb.postgresql.ensures = [ + { + username = "me"; + database = "mine"; + } + ]; + }; + + testScript = { nodes, ... }: '' + start_all() + machine.wait_for_unit("postgresql.service") + + def peer_cmd(user, database): + return "sudo -u me psql -U {user} {db} --command \"\"".format(user=user, db=database) + + def tcpip_cmd(user, database, port): + return "psql -h 127.0.0.1 -p {port} -U {user} {db} --command \"\"".format(user=user, db=database, port=port) + + with subtest("cannot login without existing user"): + machine.fail(peer_cmd("me", "mine"), timeout=10) + + with subtest("cannot login with user without password"): + machine.fail(tcpip_cmd("me", "mine", "5432"), timeout=10) + ''; + }; + + tcpIPPasswordAuth = pkgs.nixosTest { + name = "postgresql"; + + nodes.machine = { config, pkgs, ... }: { + imports = [ + ../../modules/blocks/postgresql.nix + ]; + + users.users.me = { + isSystemUser = true; + group = "me"; + extraGroups = [ "sudoers" ]; + }; + users.groups.me = {}; + + system.activationScripts.secret = '' + echo secretpw > /run/dbsecret + ''; + shb.postgresql.enableTCPIP = true; + shb.postgresql.ensures = [ + { + username = "me"; + database = "mine"; + passwordFile = "/run/dbsecret"; + } + ]; + }; + + testScript = { nodes, ... }: '' + start_all() + machine.wait_for_unit("postgresql.service") + + def peer_cmd(user, database): + return "sudo -u me psql -U {user} {db} --command \"\"".format(user=user, db=database) + + def tcpip_cmd(user, database, port, password): + return "PGPASSWORD={password} psql -h 127.0.0.1 -p {port} -U {user} {db} --command \"\"".format(user=user, db=database, port=port, password=password) + + with subtest("can peer login with provisioned user and database"): + machine.succeed(peer_cmd("me", "mine"), timeout=10) + + with subtest("can tcpip login with provisioned user and database"): + machine.succeed(tcpip_cmd("me", "mine", "5432", "secretpw"), timeout=10) + + with subtest("cannot tcpip login with wrong password"): + machine.fail(tcpip_cmd("me", "mine", "5432", "oops"), timeout=10) + ''; + }; }