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 = with types; listOf (submodule subordinateUidRange);
135 default = [];
136 example = [
137 { startUid = 1000; count = 1; }
138 { startUid = 100001; count = 65534; }
139 ];
140 description = ''
141 Subordinate user ids that user is allowed to use.
142 They are set into <filename>/etc/subuid</filename> and are used
143 by <literal>newuidmap</literal> for user namespaces.
144 '';
145 };
146
147 subGidRanges = mkOption {
148 type = with types; listOf (submodule subordinateGidRange);
149 default = [];
150 example = [
151 { startGid = 100; count = 1; }
152 { startGid = 1001; count = 999; }
153 ];
154 description = ''
155 Subordinate group ids that user is allowed to use.
156 They are set into <filename>/etc/subgid</filename> and are used
157 by <literal>newgidmap</literal> for user namespaces.
158 '';
159 };
160
161 createHome = mkOption {
162 type = types.bool;
163 default = false;
164 description = ''
165 If true, the home directory will be created automatically. If this
166 option is true and the home directory already exists but is not
167 owned by the user, directory owner and group will be changed to
168 match the user.
169 '';
170 };
171
172 useDefaultShell = mkOption {
173 type = types.bool;
174 default = false;
175 description = ''
176 If true, the user's shell will be set to
177 <option>users.defaultUserShell</option>.
178 '';
179 };
180
181 hashedPassword = mkOption {
182 type = with types; uniq (nullOr str);
183 default = null;
184 description = ''
185 Specifies the hashed password for the user.
186 ${passwordDescription}
187 ${hashedPasswordDescription}
188 '';
189 };
190
191 password = mkOption {
192 type = with types; uniq (nullOr str);
193 default = null;
194 description = ''
195 Specifies the (clear text) password for the user.
196 Warning: do not set confidential information here
197 because it is world-readable in the Nix store. This option
198 should only be used for public accounts.
199 ${passwordDescription}
200 '';
201 };
202
203 passwordFile = mkOption {
204 type = with types; uniq (nullOr string);
205 default = null;
206 description = ''
207 The full path to a file that contains the user's password. The password
208 file is read on each system activation. The file should contain
209 exactly one line, which should be the password in an encrypted form
210 that is suitable for the <literal>chpasswd -e</literal> command.
211 ${passwordDescription}
212 '';
213 };
214
215 initialHashedPassword = mkOption {
216 type = with types; uniq (nullOr str);
217 default = null;
218 description = ''
219 Specifies the initial hashed password for the user, i.e. the
220 hashed password assigned if the user does not already
221 exist. If <option>users.mutableUsers</option> is true, the
222 password can be changed subsequently using the
223 <command>passwd</command> command. Otherwise, it's
224 equivalent to setting the <option>hashedPassword</option> option.
225
226 ${hashedPasswordDescription}
227 '';
228 };
229
230 initialPassword = mkOption {
231 type = with types; uniq (nullOr str);
232 default = null;
233 description = ''
234 Specifies the initial password for the user, i.e. the
235 password assigned if the user does not already exist. If
236 <option>users.mutableUsers</option> is true, the password
237 can be changed subsequently using the
238 <command>passwd</command> command. Otherwise, it's
239 equivalent to setting the <option>password</option>
240 option. The same caveat applies: the password specified here
241 is world-readable in the Nix store, so it should only be
242 used for guest accounts or passwords that will be changed
243 promptly.
244 '';
245 };
246
247 packages = mkOption {
248 type = types.listOf types.package;
249 default = [];
250 example = literalExample "[ pkgs.firefox pkgs.thunderbird ]";
251 description = ''
252 The set of packages that should be made availabe to the user.
253 This is in contrast to <option>environment.systemPackages</option>,
254 which adds packages to all users.
255 '';
256 };
257
258 };
259
260 config = mkMerge
261 [ { name = mkDefault name;
262 shell = mkIf config.useDefaultShell (mkDefault cfg.defaultUserShell);
263 }
264 (mkIf config.isNormalUser {
265 group = mkDefault "users";
266 createHome = mkDefault true;
267 home = mkDefault "/home/${name}";
268 useDefaultShell = mkDefault true;
269 isSystemUser = mkDefault false;
270 })
271 # If !mutableUsers, setting ‘initialPassword’ is equivalent to
272 # setting ‘password’ (and similarly for hashed passwords).
273 (mkIf (!cfg.mutableUsers && config.initialPassword != null) {
274 password = mkDefault config.initialPassword;
275 })
276 (mkIf (!cfg.mutableUsers && config.initialHashedPassword != null) {
277 hashedPassword = mkDefault config.initialHashedPassword;
278 })
279 ];
280
281 };
282
283 groupOpts = { name, config, ... }: {
284
285 options = {
286
287 name = mkOption {
288 type = types.str;
289 description = ''
290 The name of the group. If undefined, the name of the attribute set
291 will be used.
292 '';
293 };
294
295 gid = mkOption {
296 type = with types; nullOr int;
297 default = null;
298 description = ''
299 The group GID. If the GID is null, a free GID is picked on
300 activation.
301 '';
302 };
303
304 members = mkOption {
305 type = with types; listOf string;
306 default = [];
307 description = ''
308 The user names of the group members, added to the
309 <literal>/etc/group</literal> file.
310 '';
311 };
312
313 };
314
315 config = {
316 name = mkDefault name;
317 };
318
319 };
320
321 subordinateUidRange = {
322 options = {
323 startUid = mkOption {
324 type = types.int;
325 description = ''
326 Start of the range of subordinate user ids that user is
327 allowed to use.
328 '';
329 };
330 count = mkOption {
331 type = types.int;
332 default = 1;
333 description = ''Count of subordinate user ids'';
334 };
335 };
336 };
337
338 subordinateGidRange = {
339 options = {
340 startGid = mkOption {
341 type = types.int;
342 description = ''
343 Start of the range of subordinate group ids that user is
344 allowed to use.
345 '';
346 };
347 count = mkOption {
348 type = types.int;
349 default = 1;
350 description = ''Count of subordinate group ids'';
351 };
352 };
353 };
354
355 mkSubuidEntry = user: concatStrings (
356 map (range: "${user.name}:${toString range.startUid}:${toString range.count}\n")
357 user.subUidRanges);
358
359 subuidFile = concatStrings (map mkSubuidEntry (attrValues cfg.users));
360
361 mkSubgidEntry = user: concatStrings (
362 map (range: "${user.name}:${toString range.startGid}:${toString range.count}\n")
363 user.subGidRanges);
364
365 subgidFile = concatStrings (map mkSubgidEntry (attrValues cfg.users));
366
367 idsAreUnique = set: idAttr: !(fold (name: args@{ dup, acc }:
368 let
369 id = builtins.toString (builtins.getAttr idAttr (builtins.getAttr name set));
370 exists = builtins.hasAttr id acc;
371 newAcc = acc // (builtins.listToAttrs [ { name = id; value = true; } ]);
372 in if dup then args else if exists
373 then builtins.trace "Duplicate ${idAttr} ${id}" { dup = true; acc = null; }
374 else { dup = false; acc = newAcc; }
375 ) { dup = false; acc = {}; } (builtins.attrNames set)).dup;
376
377 uidsAreUnique = idsAreUnique (filterAttrs (n: u: u.uid != null) cfg.users) "uid";
378 gidsAreUnique = idsAreUnique (filterAttrs (n: g: g.gid != null) cfg.groups) "gid";
379
380 spec = pkgs.writeText "users-groups.json" (builtins.toJSON {
381 inherit (cfg) mutableUsers;
382 users = mapAttrsToList (_: u:
383 { inherit (u)
384 name uid group description home createHome isSystemUser
385 password passwordFile hashedPassword
386 initialPassword initialHashedPassword;
387 shell = utils.toShellPath u.shell;
388 }) cfg.users;
389 groups = mapAttrsToList (n: g:
390 { inherit (g) name gid;
391 members = g.members ++ (mapAttrsToList (n: u: u.name) (
392 filterAttrs (n: u: elem g.name u.extraGroups) cfg.users
393 ));
394 }) cfg.groups;
395 });
396
397 systemShells =
398 let
399 shells = mapAttrsToList (_: u: u.shell) cfg.users;
400 in
401 filter types.shellPackage.check shells;
402
403in {
404
405 ###### interface
406
407 options = {
408
409 users.mutableUsers = mkOption {
410 type = types.bool;
411 default = true;
412 description = ''
413 If set to <literal>true</literal>, you are free to add new users and groups to the system
414 with the ordinary <literal>useradd</literal> and
415 <literal>groupadd</literal> commands. On system activation, the
416 existing contents of the <literal>/etc/passwd</literal> and
417 <literal>/etc/group</literal> files will be merged with the
418 contents generated from the <literal>users.users</literal> and
419 <literal>users.groups</literal> options.
420 The initial password for a user will be set
421 according to <literal>users.users</literal>, but existing passwords
422 will not be changed.
423
424 <warning><para>
425 If set to <literal>false</literal>, the contents of the user and
426 group files will simply be replaced on system activation. This also
427 holds for the user passwords; all changed
428 passwords will be reset according to the
429 <literal>users.users</literal> configuration on activation.
430 </para></warning>
431 '';
432 };
433
434 users.enforceIdUniqueness = mkOption {
435 type = types.bool;
436 default = true;
437 description = ''
438 Whether to require that no two users/groups share the same uid/gid.
439 '';
440 };
441
442 users.users = mkOption {
443 default = {};
444 type = with types; loaOf (submodule userOpts);
445 example = {
446 alice = {
447 uid = 1234;
448 description = "Alice Q. User";
449 home = "/home/alice";
450 createHome = true;
451 group = "users";
452 extraGroups = ["wheel"];
453 shell = "/bin/sh";
454 };
455 };
456 description = ''
457 Additional user accounts to be created automatically by the system.
458 This can also be used to set options for root.
459 '';
460 };
461
462 users.groups = mkOption {
463 default = {};
464 example =
465 { students.gid = 1001;
466 hackers = { };
467 };
468 type = with types; loaOf (submodule groupOpts);
469 description = ''
470 Additional groups to be created automatically by the system.
471 '';
472 };
473
474 # FIXME: obsolete - will remove.
475 security.initialRootPassword = mkOption {
476 type = types.str;
477 default = "!";
478 example = "";
479 visible = false;
480 };
481
482 };
483
484
485 ###### implementation
486
487 config = {
488
489 users.users = {
490 root = {
491 uid = ids.uids.root;
492 description = "System administrator";
493 home = "/root";
494 shell = mkDefault cfg.defaultUserShell;
495 group = "root";
496 initialHashedPassword = mkDefault config.security.initialRootPassword;
497 };
498 nobody = {
499 uid = ids.uids.nobody;
500 description = "Unprivileged account (don't use!)";
501 group = "nogroup";
502 };
503 };
504
505 # Install all the user shells
506 environment.systemPackages = systemShells;
507
508 users.groups = {
509 root.gid = ids.gids.root;
510 wheel.gid = ids.gids.wheel;
511 disk.gid = ids.gids.disk;
512 kmem.gid = ids.gids.kmem;
513 tty.gid = ids.gids.tty;
514 floppy.gid = ids.gids.floppy;
515 uucp.gid = ids.gids.uucp;
516 lp.gid = ids.gids.lp;
517 cdrom.gid = ids.gids.cdrom;
518 tape.gid = ids.gids.tape;
519 audio.gid = ids.gids.audio;
520 video.gid = ids.gids.video;
521 dialout.gid = ids.gids.dialout;
522 nogroup.gid = ids.gids.nogroup;
523 users.gid = ids.gids.users;
524 nixbld.gid = ids.gids.nixbld;
525 utmp.gid = ids.gids.utmp;
526 adm.gid = ids.gids.adm;
527 input.gid = ids.gids.input;
528 };
529
530 system.activationScripts.users = stringAfter [ "stdio" ]
531 ''
532 ${pkgs.perl}/bin/perl -w \
533 -I${pkgs.perlPackages.FileSlurp}/lib/perl5/site_perl \
534 -I${pkgs.perlPackages.JSON}/lib/perl5/site_perl \
535 ${./update-users-groups.pl} ${spec}
536 '';
537
538 # for backwards compatibility
539 system.activationScripts.groups = stringAfter [ "users" ] "";
540
541 environment.etc."subuid" = {
542 text = subuidFile;
543 mode = "0644";
544 };
545 environment.etc."subgid" = {
546 text = subgidFile;
547 mode = "0644";
548 };
549
550 assertions = [
551 { assertion = !cfg.enforceIdUniqueness || (uidsAreUnique && gidsAreUnique);
552 message = "UIDs and GIDs must be unique!";
553 }
554 { # If mutableUsers is false, to prevent users creating a
555 # configuration that locks them out of the system, ensure that
556 # there is at least one "privileged" account that has a
557 # password or an SSH authorized key. Privileged accounts are
558 # root and users in the wheel group.
559 assertion = !cfg.mutableUsers ->
560 any id (mapAttrsToList (name: cfg:
561 (name == "root"
562 || cfg.group == "wheel"
563 || elem "wheel" cfg.extraGroups)
564 &&
565 ((cfg.hashedPassword != null && cfg.hashedPassword != "!")
566 || cfg.password != null
567 || cfg.passwordFile != null
568 || cfg.openssh.authorizedKeys.keys != []
569 || cfg.openssh.authorizedKeys.keyFiles != [])
570 ) cfg.users);
571 message = ''
572 Neither the root account nor any wheel user has a password or SSH authorized key.
573 You must set one to prevent being locked out of your system.'';
574 }
575 ];
576
577 };
578
579 imports =
580 [ (mkAliasOptionModule [ "users" "extraUsers" ] [ "users" "users" ])
581 (mkAliasOptionModule [ "users" "extraGroups" ] [ "users" "groups" ])
582 {
583 environment = {
584 etc = mapAttrs' (name: { packages, ... }: {
585 name = "per-user-pkgs/${name}";
586 value.source = pkgs.symlinkJoin {
587 name = "per-user-pkgs.${name}";
588 paths = packages;
589 };
590 }) (filterAttrs (_: { packages, ... }: packages != []) cfg.users);
591 profiles = ["/etc/per-user-pkgs/$LOGNAME"];
592 };
593 }
594 ];
595}