diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ce6ca2c..0eadd1e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,4 +10,9 @@ jobs: - uses: cachix/install-nix-action@v22 with: github_access_token: ${{ secrets.GITHUB_TOKEN }} - - run: nix flake check + extra_nix_config: "system-features = nixos-test benchmark big-parallel kvm" + - uses: cachix/cachix-action@v12 + with: + name: selfhostblocks + authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' + - run: nix flake check -L diff --git a/README.md b/README.md index 6fdf630..d874cca 100644 --- a/README.md +++ b/README.md @@ -692,12 +692,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 @@ -798,6 +801,11 @@ $ 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 +- https://nixos.org/manual/nixos/stable/index.html#sec-nixos-tests + 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 2d0e5f6..8bb849a 100644 --- a/flake.nix +++ b/flake.nix @@ -8,7 +8,7 @@ flake-utils.url = "github:numtide/flake-utils"; }; - outputs = inputs@{ self, nixpkgs, sops-nix, nix-flake-tests, flake-utils, ... }: flake-utils.lib.eachDefaultSystem (system: + outputs = { nixpkgs, nix-flake-tests, flake-utils, ... }: flake-utils.lib.eachDefaultSystem (system: let pkgs = nixpkgs.legacyPackages.${system}; in @@ -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,7 +63,9 @@ ./test/modules/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 new file mode 100644 index 0000000..c8ef789 --- /dev/null +++ b/test/vm/postgresql.nix @@ -0,0 +1,167 @@ +{ pkgs, lib, ... }: +{ + peerWithoutUser = pkgs.nixosTest { + name = "postgresql-peerWithoutUser"; + + nodes.machine = { config, pkgs, ... }: { + imports = [ + ../../modules/blocks/postgresql.nix + ]; + + 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) + + 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) + ''; + }; + + peerAuth = pkgs.nixosTest { + name = "postgresql-peerAuth"; + + 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-tcpIpWithoutPasswordAuth"; + + 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-tcpIPPasswordAuth"; + + 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) + ''; + }; +}