1{ config, lib, utils, pkgs, ... }:
2
3with lib;
4
5let
6 ids = config.ids;
7 cfg = config.users;
8
9 passwordDescription = ''
10 The options <option>hashedPassword</option>,
11 <option>password</option> and <option>passwordFile</option>
12 controls what password is set for the user.
13 <option>hashedPassword</option> overrides both
14 <option>password</option> and <option>passwordFile</option>.
15 <option>password</option> overrides <option>passwordFile</option>.
16 If none of these three options are set, no password is assigned to
17 the user, and the user will not be able to do password logins.
18 If the option <option>users.mutableUsers</option> is true, the
19 password defined in one of the three options will only be set when
20 the user is created for the first time. After that, you are free to
21 change the password with the ordinary user management commands. If
22 <option>users.mutableUsers</option> is false, you cannot change
23 user passwords, they will always be set according to the password
24 options.
25 '';
26
27 hashedPasswordDescription = ''
28 To generate hashed password install <literal>mkpasswd</literal>
29 package and run <literal>mkpasswd -m sha-512</literal>.
30 '';
31
32 userOpts = { name, config, ... }: {
33
34 options = {
35
36 name = mkOption {
37 type = types.str;
38 description = ''
39 The name of the user account. If undefined, the name of the
40 attribute set will be used.
41 '';
42 };
43
44 description = mkOption {
45 type = types.str;
46 default = "";
47 example = "Alice Q. User";
48 description = ''
49 A short description of the user account, typically the
50 user's full name. This is actually the “GECOS” or “comment”
51 field in <filename>/etc/passwd</filename>.
52 '';
53 };
54
55 uid = mkOption {
56 type = with types; nullOr int;
57 default = null;
58 description = ''
59 The account UID. If the UID is null, a free UID is picked on
60 activation.
61 '';
62 };
63
64 isSystemUser = mkOption {
65 type = types.bool;
66 default = false;
67 description = ''
68 Indicates if the user is a system user or not. This option
69 only has an effect if <option>uid</option> is
70 <option>null</option>, in which case it determines whether
71 the user's UID is allocated in the range for system users
72 (below 500) or in the range for normal users (starting at
73 1000).
74 '';
75 };
76
77 isNormalUser = mkOption {
78 type = types.bool;
79 default = false;
80 description = ''
81 Indicates whether this is an account for a “real” user. This
82 automatically sets <option>group</option> to
83 <literal>users</literal>, <option>createHome</option> to
84 <literal>true</literal>, <option>home</option> to
85 <filename>/home/<replaceable>username</replaceable></filename>,
86 <option>useDefaultShell</option> to <literal>true</literal>,
87 and <option>isSystemUser</option> to
88 <literal>false</literal>.
89 '';
90 };
91
92 group = mkOption {
93 type = types.str;
94 default = "nogroup";
95 description = "The user's primary group.";
96 };
97
98 extraGroups = mkOption {
99 type = types.listOf types.str;
100 default = [];
101 description = "The user's auxiliary groups.";
102 };
103
104 home = mkOption {
105 type = types.path;
106 default = "/var/empty";
107 description = "The user's home directory.";
108 };
109
110 cryptHomeLuks = mkOption {
111 type = with types; nullOr str;
112 default = null;
113 description = ''
114 Path to encrypted luks device that contains
115 the user's home directory.
116 '';
117 };
118
119 shell = mkOption {
120 type = types.either types.shellPackage types.path;
121 default = pkgs.nologin;
122 defaultText = "pkgs.nologin";
123 example = literalExample "pkgs.bashInteractive";
124 description = ''
125 The path to the user's shell. Can use shell derivations,
126 like <literal>pkgs.bashInteractive</literal>. Don’t
127 forget to enable your shell in
128 <literal>programs</literal> if necessary,
129 like <code>programs.zsh.enable = true;</code>.
130 '';
131 };
132
133 subUidRanges = mkOption {
134 type = types.listOf types.optionSet;
135 default = [];
136 example = [
137 { startUid = 1000; count = 1; }
138 { startUid = 100001; count = 65534; }
139 ];
140 options = [ subordinateUidRange ];
141 description = ''
142 Subordinate user ids that user is allowed to use.
143 They are set into <filename>/etc/subuid</filename> and are used
144 by <literal>newuidmap</literal> for user namespaces.
145 '';
146 };
147
148 subGidRanges = mkOption {
149 type = types.listOf types.optionSet;
150 default = [];
151 example = [
152 { startGid = 100; count = 1; }
153 { startGid = 1001; count = 999; }
154 ];
155 options = [ subordinateGidRange ];
156 description = ''
157 Subordinate group ids that user is allowed to use.
158 They are set into <filename>/etc/subgid</filename> and are used
159 by <literal>newgidmap</literal> for user namespaces.
160 '';
161 };
162
163 createHome = mkOption {
164 type = types.bool;
165 default = false;
166 description = ''
167 If true, the home directory will be created automatically. If this
168 option is true and the home directory already exists but is not
169 owned by the user, directory owner and group will be changed to
170 match the user.
171 '';
172 };
173
174 useDefaultShell = mkOption {
175 type = types.bool;
176 default = false;
177 description = ''
178 If true, the user's shell will be set to
179 <option>users.defaultUserShell</option>.
180 '';
181 };
182
183 hashedPassword = mkOption {
184 type = with types; uniq (nullOr str);
185 default = null;
186 description = ''
187 Specifies the hashed password for the user.
188 ${passwordDescription}
189 ${hashedPasswordDescription}
190 '';
191 };
192
193 password = mkOption {
194 type = with types; uniq (nullOr str);
195 default = null;
196 description = ''
197 Specifies the (clear text) password for the user.
198 Warning: do not set confidential information here
199 because it is world-readable in the Nix store. This option
200 should only be used for public accounts.
201 ${passwordDescription}
202 '';
203 };
204
205 passwordFile = mkOption {
206 type = with types; uniq (nullOr string);
207 default = null;
208 description = ''
209 The full path to a file that contains the user's password. The password
210 file is read on each system activation. The file should contain
211 exactly one line, which should be the password in an encrypted form
212 that is suitable for the <literal>chpasswd -e</literal> command.
213 ${passwordDescription}
214 '';
215 };
216
217 initialHashedPassword = mkOption {
218 type = with types; uniq (nullOr str);
219 default = null;
220 description = ''
221 Specifies the initial hashed password for the user, i.e. the
222 hashed password assigned if the user does not already
223 exist. If <option>users.mutableUsers</option> is true, the
224 password can be changed subsequently using the
225 <command>passwd</command> command. Otherwise, it's
226 equivalent to setting the <option>hashedPassword</option> option.
227
228 ${hashedPasswordDescription}
229 '';
230 };
231
232 initialPassword = mkOption {
233 type = with types; uniq (nullOr str);
234 default = null;
235 description = ''
236 Specifies the initial password for the user, i.e. the
237 password assigned if the user does not already exist. If
238 <option>users.mutableUsers</option> is true, the password
239 can be changed subsequently using the
240 <command>passwd</command> command. Otherwise, it's
241 equivalent to setting the <option>password</option>
242 option. The same caveat applies: the password specified here
243 is world-readable in the Nix store, so it should only be
244 used for guest accounts or passwords that will be changed
245 promptly.
246 '';
247 };
248
249 };
250
251 config = mkMerge
252 [ { name = mkDefault name;
253 shell = mkIf config.useDefaultShell (mkDefault cfg.defaultUserShell);
254 }
255 (mkIf config.isNormalUser {
256 group = mkDefault "users";
257 createHome = mkDefault true;
258 home = mkDefault "/home/${name}";
259 useDefaultShell = mkDefault true;
260 isSystemUser = mkDefault false;
261 })
262 # If !mutableUsers, setting ‘initialPassword’ is equivalent to
263 # setting ‘password’ (and similarly for hashed passwords).
264 (mkIf (!cfg.mutableUsers && config.initialPassword != null) {
265 password = mkDefault config.initialPassword;
266 })
267 (mkIf (!cfg.mutableUsers && config.initialHashedPassword != null) {
268 hashedPassword = mkDefault config.initialHashedPassword;
269 })
270 ];
271
272 };
273
274 groupOpts = { name, config, ... }: {
275
276 options = {
277
278 name = mkOption {
279 type = types.str;
280 description = ''
281 The name of the group. If undefined, the name of the attribute set
282 will be used.
283 '';
284 };
285
286 gid = mkOption {
287 type = with types; nullOr int;
288 default = null;
289 description = ''
290 The group GID. If the GID is null, a free GID is picked on
291 activation.
292 '';
293 };
294
295 members = mkOption {
296 type = with types; listOf string;
297 default = [];
298 description = ''
299 The user names of the group members, added to the
300 <literal>/etc/group</literal> file.
301 '';
302 };
303
304 };
305
306 config = {
307 name = mkDefault name;
308 };
309
310 };
311
312 subordinateUidRange = {
313 startUid = mkOption {
314 type = types.int;
315 description = ''
316 Start of the range of subordinate user ids that user is
317 allowed to use.
318 '';
319 };
320 count = mkOption {
321 type = types.int;
322 default = 1;
323 description = ''Count of subordinate user ids'';
324 };
325 };
326
327 subordinateGidRange = {
328 startGid = mkOption {
329 type = types.int;
330 description = ''
331 Start of the range of subordinate group ids that user is
332 allowed to use.
333 '';
334 };
335 count = mkOption {
336 type = types.int;
337 default = 1;
338 description = ''Count of subordinate group ids'';
339 };
340 };
341
342 mkSubuidEntry = user: concatStrings (
343 map (range: "${user.name}:${toString range.startUid}:${toString range.count}\n")
344 user.subUidRanges);
345
346 subuidFile = concatStrings (map mkSubuidEntry (attrValues cfg.users));
347
348 mkSubgidEntry = user: concatStrings (
349 map (range: "${user.name}:${toString range.startGid}:${toString range.count}\n")
350 user.subGidRanges);
351
352 subgidFile = concatStrings (map mkSubgidEntry (attrValues cfg.users));
353
354 idsAreUnique = set: idAttr: !(fold (name: args@{ dup, acc }:
355 let
356 id = builtins.toString (builtins.getAttr idAttr (builtins.getAttr name set));
357 exists = builtins.hasAttr id acc;
358 newAcc = acc // (builtins.listToAttrs [ { name = id; value = true; } ]);
359 in if dup then args else if exists
360 then builtins.trace "Duplicate ${idAttr} ${id}" { dup = true; acc = null; }
361 else { dup = false; acc = newAcc; }
362 ) { dup = false; acc = {}; } (builtins.attrNames set)).dup;
363
364 uidsAreUnique = idsAreUnique (filterAttrs (n: u: u.uid != null) cfg.users) "uid";
365 gidsAreUnique = idsAreUnique (filterAttrs (n: g: g.gid != null) cfg.groups) "gid";
366
367 spec = pkgs.writeText "users-groups.json" (builtins.toJSON {
368 inherit (cfg) mutableUsers;
369 users = mapAttrsToList (_: u:
370 { inherit (u)
371 name uid group description home createHome isSystemUser
372 password passwordFile hashedPassword
373 initialPassword initialHashedPassword;
374 shell = utils.toShellPath u.shell;
375 }) cfg.users;
376 groups = mapAttrsToList (n: g:
377 { inherit (g) name gid;
378 members = g.members ++ (mapAttrsToList (n: u: u.name) (
379 filterAttrs (n: u: elem g.name u.extraGroups) cfg.users
380 ));
381 }) cfg.groups;
382 });
383
384 systemShells =
385 let
386 shells = mapAttrsToList (_: u: u.shell) cfg.users;
387 in
388 filter types.shellPackage.check shells;
389
390in {
391
392 ###### interface
393
394 options = {
395
396 users.mutableUsers = mkOption {
397 type = types.bool;
398 default = true;
399 description = ''
400 If set to <literal>true</literal>, you are free to add new users and groups to the system
401 with the ordinary <literal>useradd</literal> and
402 <literal>groupadd</literal> commands. On system activation, the
403 existing contents of the <literal>/etc/passwd</literal> and
404 <literal>/etc/group</literal> files will be merged with the
405 contents generated from the <literal>users.users</literal> and
406 <literal>users.groups</literal> options.
407 The initial password for a user will be set
408 according to <literal>users.users</literal>, but existing passwords
409 will not be changed.
410
411 <warning><para>
412 If set to <literal>false</literal>, the contents of the user and
413 group files will simply be replaced on system activation. This also
414 holds for the user passwords; all changed
415 passwords will be reset according to the
416 <literal>users.users</literal> configuration on activation.
417 </para></warning>
418 '';
419 };
420
421 users.enforceIdUniqueness = mkOption {
422 type = types.bool;
423 default = true;
424 description = ''
425 Whether to require that no two users/groups share the same uid/gid.
426 '';
427 };
428
429 users.users = mkOption {
430 default = {};
431 type = types.loaOf types.optionSet;
432 example = {
433 alice = {
434 uid = 1234;
435 description = "Alice Q. User";
436 home = "/home/alice";
437 createHome = true;
438 group = "users";
439 extraGroups = ["wheel"];
440 shell = "/bin/sh";
441 };
442 };
443 description = ''
444 Additional user accounts to be created automatically by the system.
445 This can also be used to set options for root.
446 '';
447 options = [ userOpts ];
448 };
449
450 users.groups = mkOption {
451 default = {};
452 example =
453 { students.gid = 1001;
454 hackers = { };
455 };
456 type = types.loaOf types.optionSet;
457 description = ''
458 Additional groups to be created automatically by the system.
459 '';
460 options = [ groupOpts ];
461 };
462
463 # FIXME: obsolete - will remove.
464 security.initialRootPassword = mkOption {
465 type = types.str;
466 default = "!";
467 example = "";
468 visible = false;
469 };
470
471 };
472
473
474 ###### implementation
475
476 config = {
477
478 users.users = {
479 root = {
480 uid = ids.uids.root;
481 description = "System administrator";
482 home = "/root";
483 shell = mkDefault cfg.defaultUserShell;
484 group = "root";
485 initialHashedPassword = mkDefault config.security.initialRootPassword;
486 };
487 nobody = {
488 uid = ids.uids.nobody;
489 description = "Unprivileged account (don't use!)";
490 group = "nogroup";
491 };
492 };
493
494 # Install all the user shells
495 environment.systemPackages = systemShells;
496
497 users.groups = {
498 root.gid = ids.gids.root;
499 wheel.gid = ids.gids.wheel;
500 disk.gid = ids.gids.disk;
501 kmem.gid = ids.gids.kmem;
502 tty.gid = ids.gids.tty;
503 floppy.gid = ids.gids.floppy;
504 uucp.gid = ids.gids.uucp;
505 lp.gid = ids.gids.lp;
506 cdrom.gid = ids.gids.cdrom;
507 tape.gid = ids.gids.tape;
508 audio.gid = ids.gids.audio;
509 video.gid = ids.gids.video;
510 dialout.gid = ids.gids.dialout;
511 nogroup.gid = ids.gids.nogroup;
512 users.gid = ids.gids.users;
513 nixbld.gid = ids.gids.nixbld;
514 utmp.gid = ids.gids.utmp;
515 adm.gid = ids.gids.adm;
516 input.gid = ids.gids.input;
517 };
518
519 system.activationScripts.users = stringAfter [ "etc" ]
520 ''
521 ${pkgs.perl}/bin/perl -w \
522 -I${pkgs.perlPackages.FileSlurp}/lib/perl5/site_perl \
523 -I${pkgs.perlPackages.JSON}/lib/perl5/site_perl \
524 ${./update-users-groups.pl} ${spec}
525 '';
526
527 # for backwards compatibility
528 system.activationScripts.groups = stringAfter [ "users" ] "";
529
530 environment.etc."subuid" = {
531 text = subuidFile;
532 mode = "0644";
533 };
534 environment.etc."subgid" = {
535 text = subgidFile;
536 mode = "0644";
537 };
538
539 assertions = [
540 { assertion = !cfg.enforceIdUniqueness || (uidsAreUnique && gidsAreUnique);
541 message = "UIDs and GIDs must be unique!";
542 }
543 { # If mutableUsers is false, to prevent users creating a
544 # configuration that locks them out of the system, ensure that
545 # there is at least one "privileged" account that has a
546 # password or an SSH authorized key. Privileged accounts are
547 # root and users in the wheel group.
548 assertion = !cfg.mutableUsers ->
549 any id (mapAttrsToList (name: cfg:
550 (name == "root"
551 || cfg.group == "wheel"
552 || elem "wheel" cfg.extraGroups)
553 &&
554 ((cfg.hashedPassword != null && cfg.hashedPassword != "!")
555 || cfg.password != null
556 || cfg.passwordFile != null
557 || cfg.openssh.authorizedKeys.keys != []
558 || cfg.openssh.authorizedKeys.keyFiles != [])
559 ) cfg.users);
560 message = ''
561 Neither the root account nor any wheel user has a password or SSH authorized key.
562 You must set one to prevent being locked out of your system.'';
563 }
564 ];
565
566 };
567
568 imports =
569 [ (mkAliasOptionModule [ "users" "extraUsers" ] [ "users" "users" ])
570 (mkAliasOptionModule [ "users" "extraGroups" ] [ "users" "groups" ])
571 ];
572}