-
-
Notifications
You must be signed in to change notification settings - Fork 14.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
1 parent
52ce5ca
commit b84fb1e
Showing
3 changed files
with
250 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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") | ||
''; | ||
} | ||
) |
77 changes: 77 additions & 0 deletions
77
nixos/tests/systemd-sysusers-password-option-override-ordering.nix
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
''; | ||
} |