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.

+2
nixos/tests/all-tests.nix
···
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 {};
···
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 {};
+171
nixos/tests/password-option-override-ordering.nix
···
+
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")
+
'';
+
}
+
)
+77
nixos/tests/systemd-sysusers-password-option-override-ordering.nix
···
+
{
+
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"
+
'';
+
}