1let
2 password1 = "foobar";
3 password2 = "helloworld";
4 hashed_bcrypt = "$2b$05$8xIEflrk2RxQtcVXbGIxs.Vl0x7dF1/JSv3cyX6JJt0npzkTCWvxK"; # fnord
5 hashed_yeshash = "$y$j9T$d8Z4EAf8P1SvM/aDFbxMS0$VnTXMp/Hnc7QdCBEaLTq5ZFOAFo2/PM0/xEAFuOE88."; # fnord
6 hashed_sha512crypt = "$6$ymzs8WINZ5wGwQcV$VC2S0cQiX8NVukOLymysTPn4v1zJoJp3NGyhnqyv/dAf4NWZsBWYveQcj6gEJr4ZUjRBRjM0Pj1L8TCQ8hUUp0"; # meow
7in
8
9{ pkgs, ... }:
10{
11 name = "password-option-override-ordering";
12 meta = with pkgs.lib.maintainers; {
13 maintainers = [ fidgetingbits ];
14 };
15
16 nodes =
17 let
18 # The following users are expected to have the same behavior between immutable and mutable systems
19 # NOTE: Below given A -> B it implies B overrides A . Each entry below builds off the next
20 users = {
21 # mutable true/false: initialHashedPassword -> hashedPassword
22 fran = {
23 isNormalUser = true;
24 initialHashedPassword = hashed_yeshash;
25 hashedPassword = hashed_sha512crypt;
26 };
27
28 # mutable false: initialHashedPassword -> hashedPassword -> initialPassword
29 # mutable true: initialHashedPassword -> initialPassword -> hashedPassword
30 greg = {
31 isNormalUser = true;
32 hashedPassword = hashed_sha512crypt;
33 initialPassword = password1;
34 };
35
36 # mutable false: initialHashedPassword -> hashedPassword -> initialPassword -> password
37 # mutable true: initialHashedPassword -> initialPassword -> hashedPassword -> password
38 egon = {
39 isNormalUser = true;
40 initialPassword = password2;
41 password = password1;
42 };
43
44 # mutable true/false: hashedPassword -> password
45 # NOTE: minor duplication of test above, but to verify no initialXXX use is consistent
46 alice = {
47 isNormalUser = true;
48 hashedPassword = hashed_sha512crypt;
49 password = password1;
50 };
51
52 # mutable false: initialHashedPassword -> hashedPassword -> initialPassword -> password -> hashedPasswordFile
53 # mutable true: initialHashedPassword -> initialPassword -> hashedPassword -> password -> hashedPasswordFile
54 bob = {
55 isNormalUser = true;
56 hashedPassword = hashed_sha512crypt;
57 password = password1;
58 hashedPasswordFile = (pkgs.writeText "hashed_bcrypt" hashed_bcrypt).outPath; # Expect override of everything above
59 };
60
61 # Show hashedPassword -> password -> hashedPasswordFile -> initialPassword is false
62 # to explicitly show the following lib.trace warning in users-groups.nix (which was
63 # the wording prior to PR 310484) is in fact wrong:
64 # ```
65 # The user 'root' has multiple of the options
66 # `hashedPassword`, `password`, `hashedPasswordFile`, `initialPassword`
67 # & `initialHashedPassword` set to a non-null value.
68 # The options silently discard others by the order of precedence
69 # given above which can lead to surprising results. To resolve this warning,
70 # set at most one of the options above to a non-`null` value.
71 # ```
72 cat = {
73 isNormalUser = true;
74 hashedPassword = hashed_sha512crypt;
75 password = password1;
76 hashedPasswordFile = (pkgs.writeText "hashed_bcrypt" hashed_bcrypt).outPath;
77 initialPassword = password2; # lib.trace message implies this overrides everything above
78 };
79
80 # Show hashedPassword -> password -> hashedPasswordFile -> initialHashedPassword is false
81 # to also explicitly show the lib.trace explained above (see cat user) is wrong
82 dan = {
83 isNormalUser = true;
84 hashedPassword = hashed_sha512crypt;
85 initialPassword = password2;
86 password = password1;
87 hashedPasswordFile = (pkgs.writeText "hashed_bcrypt" hashed_bcrypt).outPath;
88 initialHashedPassword = hashed_yeshash; # lib.trace message implies this overrides everything above
89 };
90 };
91
92 mkTestMachine = mutable: {
93 environment.systemPackages = [ pkgs.shadow ];
94 users = {
95 mutableUsers = mutable;
96 inherit users;
97 };
98 };
99 in
100 {
101 immutable = mkTestMachine false;
102 mutable = mkTestMachine true;
103 };
104
105 testScript = ''
106 import crypt
107
108 def assert_password_match(machine, username, password):
109 shadow_entry = machine.succeed(f"getent shadow {username}")
110 print(shadow_entry)
111 hash = shadow_entry.split(":")[1]
112 seed = "$".join(hash.split("$")[:-1])
113 assert crypt.crypt(password, seed) == hash, f"{username} user password does not match"
114
115 with subtest("alice user has correct password"):
116 for machine in machines:
117 assert_password_match(machine, "alice", "${password1}")
118 assert "${hashed_sha512crypt}" not in machine.succeed("getent shadow alice"), f"{machine}: alice user password is not correct"
119
120 with subtest("bob user has correct password"):
121 for machine in machines:
122 print(machine.succeed("getent shadow bob"))
123 assert "${hashed_bcrypt}" in machine.succeed("getent shadow bob"), f"{machine}: bob user password is not correct"
124
125 with subtest("cat user has correct password"):
126 for machine in machines:
127 print(machine.succeed("getent shadow cat"))
128 assert "${hashed_bcrypt}" in machine.succeed("getent shadow cat"), f"{machine}: cat user password is not correct"
129
130 with subtest("dan user has correct password"):
131 for machine in machines:
132 print(machine.succeed("getent shadow dan"))
133 assert "${hashed_bcrypt}" in machine.succeed("getent shadow dan"), f"{machine}: dan user password is not correct"
134
135 with subtest("greg user has correct password"):
136 print(mutable.succeed("getent shadow greg"))
137 assert "${hashed_sha512crypt}" in mutable.succeed("getent shadow greg"), "greg user password is not correct"
138
139 assert_password_match(immutable, "greg", "${password1}")
140 assert "${hashed_sha512crypt}" not in immutable.succeed("getent shadow greg"), "greg user password is not correct"
141
142 for machine in machines:
143 machine.wait_for_unit("multi-user.target")
144 machine.wait_until_succeeds("pgrep -f 'agetty.*tty1'")
145
146 def check_login(machine: Machine, tty_number: str, username: str, password: str):
147 machine.send_key(f"alt-f{tty_number}")
148 machine.wait_until_succeeds(f"[ $(fgconsole) = {tty_number} ]")
149 machine.wait_for_unit(f"getty@tty{tty_number}.service")
150 machine.wait_until_succeeds(f"pgrep -f 'agetty.*tty{tty_number}'")
151 machine.wait_until_tty_matches(tty_number, "login: ")
152 machine.send_chars(f"{username}\n")
153 machine.wait_until_tty_matches(tty_number, f"login: {username}")
154 machine.wait_until_succeeds("pgrep login")
155 machine.wait_until_tty_matches(tty_number, "Password: ")
156 machine.send_chars(f"{password}\n")
157 machine.send_chars(f"whoami > /tmp/{tty_number}\n")
158 machine.wait_for_file(f"/tmp/{tty_number}")
159 assert username in machine.succeed(f"cat /tmp/{tty_number}"), f"{machine}: {username} password is not correct"
160
161 with subtest("Test initialPassword override"):
162 for machine in machines:
163 check_login(machine, "2", "egon", "${password1}")
164
165 with subtest("Test initialHashedPassword override"):
166 for machine in machines:
167 check_login(machine, "3", "fran", "meow")
168 '';
169}