From b84fb1e5cdc27d9f2103d260398567116daa5ce4 Mon Sep 17 00:00:00 2001 From: fidgetingbits Date: Sun, 15 Dec 2024 12:22:02 +0800 Subject: [PATCH] nixos/tests: Add two new tests for password option override ordering This commit adds two new tests to show that the ordering of password overrides documentation in nixos/modules/config/user-groups.nix is correct. The override behavior differs depending on whether a system has systemd-sysusers enabled, so there are two tests. --- nixos/tests/all-tests.nix | 2 + .../password-option-override-ordering.nix | 171 ++++++++++++++++++ ...sers-password-option-override-ordering.nix | 77 ++++++++ 3 files changed, 250 insertions(+) create mode 100644 nixos/tests/password-option-override-ordering.nix create mode 100644 nixos/tests/systemd-sysusers-password-option-override-ordering.nix diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 37e005f128a2e..6029033f0eb3e 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -770,6 +770,7 @@ in { pantheon = handleTest ./pantheon.nix {}; paperless = handleTest ./paperless.nix {}; parsedmarc = handleTest ./parsedmarc {}; + password-option-override-ordering = handleTest ./password-option-override-ordering.nix {}; pdns-recursor = handleTest ./pdns-recursor.nix {}; peerflix = handleTest ./peerflix.nix {}; peering-manager = handleTest ./web-apps/peering-manager.nix {}; @@ -1013,6 +1014,7 @@ in { systemd-sysupdate = runTest ./systemd-sysupdate.nix; systemd-sysusers-mutable = runTest ./systemd-sysusers-mutable.nix; systemd-sysusers-immutable = runTest ./systemd-sysusers-immutable.nix; + systemd-sysusers-password-option-override-ordering = runTest ./systemd-sysusers-password-option-override-ordering.nix; systemd-timesyncd = handleTest ./systemd-timesyncd.nix {}; systemd-timesyncd-nscd-dnssec = handleTest ./systemd-timesyncd-nscd-dnssec.nix {}; systemd-user-linger = handleTest ./systemd-user-linger.nix {}; diff --git a/nixos/tests/password-option-override-ordering.nix b/nixos/tests/password-option-override-ordering.nix new file mode 100644 index 0000000000000..5b06ab0bdbff0 --- /dev/null +++ b/nixos/tests/password-option-override-ordering.nix @@ -0,0 +1,171 @@ +let + password1 = "foobar"; + password2 = "helloworld"; + hashed_bcrypt = "$2b$05$8xIEflrk2RxQtcVXbGIxs.Vl0x7dF1/JSv3cyX6JJt0npzkTCWvxK"; # fnord + hashed_yeshash = "$y$j9T$d8Z4EAf8P1SvM/aDFbxMS0$VnTXMp/Hnc7QdCBEaLTq5ZFOAFo2/PM0/xEAFuOE88."; # fnord + hashed_sha512crypt = "$6$ymzs8WINZ5wGwQcV$VC2S0cQiX8NVukOLymysTPn4v1zJoJp3NGyhnqyv/dAf4NWZsBWYveQcj6gEJr4ZUjRBRjM0Pj1L8TCQ8hUUp0"; # meow +in + +import ./make-test-python.nix ( + { pkgs, ... }: + { + name = "password-option-override-ordering"; + meta = with pkgs.lib.maintainers; { + maintainers = [ fidgetingbits ]; + }; + + nodes = + let + # The following users are expected to have the same behavior between immutable and mutable systems + # NOTE: Below given A -> B it implies B overrides A . Each entry below builds off the next + users = { + # mutable true/false: initialHashedPassword -> hashedPassword + fran = { + isNormalUser = true; + initialHashedPassword = hashed_yeshash; + hashedPassword = hashed_sha512crypt; + }; + + # mutable false: initialHashedPassword -> hashedPassword -> initialPassword + # mutable true: initialHashedPassword -> initialPassword -> hashedPassword + greg = { + isNormalUser = true; + hashedPassword = hashed_sha512crypt; + initialPassword = password1; + }; + + # mutable false: initialHashedPassword -> hashedPassword -> initialPassword -> password + # mutable true: initialHashedPassword -> initialPassword -> hashedPassword -> password + egon = { + isNormalUser = true; + initialPassword = password2; + password = password1; + }; + + # mutable true/false: hashedPassword -> password + # NOTE: minor duplication of test above, but to verify no initialXXX use is consistent + alice = { + isNormalUser = true; + hashedPassword = hashed_sha512crypt; + password = password1; + }; + + # mutable false: initialHashedPassword -> hashedPassword -> initialPassword -> password -> hashedPasswordFile + # mutable true: initialHashedPassword -> initialPassword -> hashedPassword -> password -> hashedPasswordFile + bob = { + isNormalUser = true; + hashedPassword = hashed_sha512crypt; + password = password1; + hashedPasswordFile = (pkgs.writeText "hashed_bcrypt" hashed_bcrypt).outPath; # Expect override of everything above + }; + + # Show hashedPassword -> password -> hashedPasswordFile -> initialPassword is false + # to explicitly show the following lib.trace warning in users-groups.nix (which was + # the wording prior to PR 310484) is in fact wrong: + # ``` + # The user 'root' has multiple of the options + # `hashedPassword`, `password`, `hashedPasswordFile`, `initialPassword` + # & `initialHashedPassword` set to a non-null value. + # The options silently discard others by the order of precedence + # given above which can lead to surprising results. To resolve this warning, + # set at most one of the options above to a non-`null` value. + # ``` + cat = { + isNormalUser = true; + hashedPassword = hashed_sha512crypt; + password = password1; + hashedPasswordFile = (pkgs.writeText "hashed_bcrypt" hashed_bcrypt).outPath; + initialPassword = password2; # lib.trace message implies this overrides everything above + }; + + # Show hashedPassword -> password -> hashedPasswordFile -> initialHashedPassword is false + # to also explicitly show the lib.trace explained above (see cat user) is wrong + dan = { + isNormalUser = true; + hashedPassword = hashed_sha512crypt; + initialPassword = password2; + password = password1; + hashedPasswordFile = (pkgs.writeText "hashed_bcrypt" hashed_bcrypt).outPath; + initialHashedPassword = hashed_yeshash; # lib.trace message implies this overrides everything above + }; + }; + + mkTestMachine = mutable: { + environment.systemPackages = [ pkgs.shadow ]; + users = { + mutableUsers = mutable; + inherit users; + }; + }; + in + { + immutable = mkTestMachine false; + mutable = mkTestMachine true; + }; + + testScript = '' + import crypt + + def assert_password_match(machine, username, password): + shadow_entry = machine.succeed(f"getent shadow {username}") + print(shadow_entry) + hash = shadow_entry.split(":")[1] + seed = "$".join(hash.split("$")[:-1]) + assert crypt.crypt(password, seed) == hash, f"{username} user password does not match" + + with subtest("alice user has correct password"): + for machine in machines: + assert_password_match(machine, "alice", "${password1}") + assert "${hashed_sha512crypt}" not in machine.succeed("getent shadow alice"), f"{machine}: alice user password is not correct" + + with subtest("bob user has correct password"): + for machine in machines: + print(machine.succeed("getent shadow bob")) + assert "${hashed_bcrypt}" in machine.succeed("getent shadow bob"), f"{machine}: bob user password is not correct" + + with subtest("cat user has correct password"): + for machine in machines: + print(machine.succeed("getent shadow cat")) + assert "${hashed_bcrypt}" in machine.succeed("getent shadow cat"), f"{machine}: cat user password is not correct" + + with subtest("dan user has correct password"): + for machine in machines: + print(machine.succeed("getent shadow dan")) + assert "${hashed_bcrypt}" in machine.succeed("getent shadow dan"), f"{machine}: dan user password is not correct" + + with subtest("greg user has correct password"): + print(mutable.succeed("getent shadow greg")) + assert "${hashed_sha512crypt}" in mutable.succeed("getent shadow greg"), "greg user password is not correct" + + assert_password_match(immutable, "greg", "${password1}") + assert "${hashed_sha512crypt}" not in immutable.succeed("getent shadow greg"), "greg user password is not correct" + + for machine in machines: + machine.wait_for_unit("multi-user.target") + machine.wait_until_succeeds("pgrep -f 'agetty.*tty1'") + + def check_login(machine: Machine, tty_number: str, username: str, password: str): + machine.send_key(f"alt-f{tty_number}") + machine.wait_until_succeeds(f"[ $(fgconsole) = {tty_number} ]") + machine.wait_for_unit(f"getty@tty{tty_number}.service") + machine.wait_until_succeeds(f"pgrep -f 'agetty.*tty{tty_number}'") + machine.wait_until_tty_matches(tty_number, "login: ") + machine.send_chars(f"{username}\n") + machine.wait_until_tty_matches(tty_number, f"login: {username}") + machine.wait_until_succeeds("pgrep login") + machine.wait_until_tty_matches(tty_number, "Password: ") + machine.send_chars(f"{password}\n") + machine.send_chars(f"whoami > /tmp/{tty_number}\n") + machine.wait_for_file(f"/tmp/{tty_number}") + assert username in machine.succeed(f"cat /tmp/{tty_number}"), f"{machine}: {username} password is not correct" + + with subtest("Test initialPassword override"): + for machine in machines: + check_login(machine, "2", "egon", "${password1}") + + with subtest("Test initialHashedPassword override"): + for machine in machines: + check_login(machine, "3", "fran", "meow") + ''; + } +) diff --git a/nixos/tests/systemd-sysusers-password-option-override-ordering.nix b/nixos/tests/systemd-sysusers-password-option-override-ordering.nix new file mode 100644 index 0000000000000..4cb13512549c7 --- /dev/null +++ b/nixos/tests/systemd-sysusers-password-option-override-ordering.nix @@ -0,0 +1,77 @@ +{ + lib, + pkgs ? import ../.., + ... +}: +let + password = "test"; + password1 = "test1"; + hashedPassword = "$y$j9T$wLgKY231.8j.ciV2MfEXe1$P0k5j3bCwHgnwW0Ive3w4knrgpiA4TzhCYLAnHvDZ51"; # test + hashedPassword1 = "$y$j9T$s8TyQJtNImvobhGM5Nlez0$3E8/O8EVGuA4sr1OQmrzi8GrRcy/AEhj454JjAn72A2"; # test + hashed_sha512crypt = "$6$ymzs8WINZ5wGwQcV$VC2S0cQiX8NVukOLymysTPn4v1zJoJp3NGyhnqyv/dAf4NWZsBWYveQcj6gEJr4ZUjRBRjM0Pj1L8TCQ8hUUp0"; # meow + + hashedPasswordFile = pkgs.writeText "hashed-password" hashedPassword1; +in +{ + name = "systemd-sysusers-password-option-override-ordering"; + + meta.maintainers = with lib.maintainers; [ fidgetingbits ]; + + nodes.machine = { + systemd.sysusers.enable = true; + system.etc.overlay.enable = true; + boot.initrd.systemd.enable = true; + + users.mutableUsers = true; + + # NOTE: Below given A -> B it implies B overrides A . Each entry below builds off the next + + users.users.root = { + hashedPasswordFile = lib.mkForce null; + initialHashedPassword = password; + }; + + users.groups.test = { }; + + # initialPassword -> initialHashedPassword + users.users.alice = { + isSystemUser = true; + group = "test"; + initialPassword = password; + initialHashedPassword = hashedPassword; + }; + + # initialPassword -> initialHashedPassword -> hashedPasswordFile + users.users.bob = { + isSystemUser = true; + group = "test"; + initialPassword = password; + initialHashedPassword = hashedPassword; + hashedPasswordFile = hashedPasswordFile.outPath; + }; + }; + + testScript = '' + machine.wait_for_unit("systemd-sysusers.service") + + with subtest("systemd-sysusers.service contains the credentials"): + sysusers_service = machine.succeed("systemctl cat systemd-sysusers.service") + print(sysusers_service) + assert "SetCredential=passwd.plaintext-password.alice:${password}" in sysusers_service + + with subtest("Correct mode on the password files"): + assert machine.succeed("stat -c '%a' /etc/passwd") == "644\n" + assert machine.succeed("stat -c '%a' /etc/group") == "644\n" + assert machine.succeed("stat -c '%a' /etc/shadow") == "0\n" + assert machine.succeed("stat -c '%a' /etc/gshadow") == "0\n" + + with subtest("alice user has correct password"): + print(machine.succeed("getent shadow alice")) + assert "${hashedPassword}" in machine.succeed("getent shadow alice"), "alice user password is not correct" + + with subtest("bob user has new password after switching to new generation"): + print(machine.succeed("getent passwd bob")) + print(machine.succeed("getent shadow bob")) + assert "${hashedPassword1}" in machine.succeed("getent shadow bob"), "bob user password is not correct" + ''; +}