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 apply = x: assert (builtins.stringLength x < 32 || abort "Username '${x}' is longer than 31 characters which is not allowed!"); x;
39 description = ''
40 The name of the user account. If undefined, the name of the
41 attribute set will be used.
42 '';
43 };
44
45 description = mkOption {
46 type = types.str;
47 default = "";
48 example = "Alice Q. User";
49 description = ''
50 A short description of the user account, typically the
51 user's full name. This is actually the “GECOS” or “comment”
52 field in <filename>/etc/passwd</filename>.
53 '';
54 };
55
56 uid = mkOption {
57 type = with types; nullOr int;
58 default = null;
59 description = ''
60 The account UID. If the UID is null, a free UID is picked on
61 activation.
62 '';
63 };
64
65 isSystemUser = mkOption {
66 type = types.bool;
67 default = false;
68 description = ''
69 Indicates if the user is a system user or not. This option
70 only has an effect if <option>uid</option> is
71 <option>null</option>, in which case it determines whether
72 the user's UID is allocated in the range for system users
73 (below 500) or in the range for normal users (starting at
74 1000).
75 '';
76 };
77
78 isNormalUser = mkOption {
79 type = types.bool;
80 default = false;
81 description = ''
82 Indicates whether this is an account for a “real” user. This
83 automatically sets <option>group</option> to
84 <literal>users</literal>, <option>createHome</option> to
85 <literal>true</literal>, <option>home</option> to
86 <filename>/home/<replaceable>username</replaceable></filename>,
87 <option>useDefaultShell</option> to <literal>true</literal>,
88 and <option>isSystemUser</option> to
89 <literal>false</literal>.
90 '';
91 };
92
93 group = mkOption {
94 type = types.str;
95 apply = x: assert (builtins.stringLength x < 32 || abort "Group name '${x}' is longer than 31 characters which is not allowed!"); x;
96 default = "nogroup";
97 description = "The user's primary group.";
98 };
99
100 extraGroups = mkOption {
101 type = types.listOf types.str;
102 default = [];
103 description = "The user's auxiliary groups.";
104 };
105
106 home = mkOption {
107 type = types.path;
108 default = "/var/empty";
109 description = "The user's home directory.";
110 };
111
112 cryptHomeLuks = mkOption {
113 type = with types; nullOr str;
114 default = null;
115 description = ''
116 Path to encrypted luks device that contains
117 the user's home directory.
118 '';
119 };
120
121 shell = mkOption {
122 type = types.either types.shellPackage types.path;
123 default = pkgs.shadow;
124 defaultText = "pkgs.shadow";
125 example = literalExample "pkgs.bashInteractive";
126 description = ''
127 The path to the user's shell. Can use shell derivations,
128 like <literal>pkgs.bashInteractive</literal>. Don’t
129 forget to enable your shell in
130 <literal>programs</literal> if necessary,
131 like <code>programs.zsh.enable = true;</code>.
132 '';
133 };
134
135 subUidRanges = mkOption {
136 type = with types; listOf (submodule subordinateUidRange);
137 default = [];
138 example = [
139 { startUid = 1000; count = 1; }
140 { startUid = 100001; count = 65534; }
141 ];
142 description = ''
143 Subordinate user ids that user is allowed to use.
144 They are set into <filename>/etc/subuid</filename> and are used
145 by <literal>newuidmap</literal> for user namespaces.
146 '';
147 };
148
149 subGidRanges = mkOption {
150 type = with types; listOf (submodule subordinateGidRange);
151 default = [];
152 example = [
153 { startGid = 100; count = 1; }
154 { startGid = 1001; count = 999; }
155 ];
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 packages = mkOption {
250 type = types.listOf types.package;
251 default = [];
252 example = literalExample "[ pkgs.firefox pkgs.thunderbird ]";
253 description = ''
254 The set of packages that should be made availabe to the user.
255 This is in contrast to <option>environment.systemPackages</option>,
256 which adds packages to all users.
257 '';
258 };
259
260 };
261
262 config = mkMerge
263 [ { name = mkDefault name;
264 shell = mkIf config.useDefaultShell (mkDefault cfg.defaultUserShell);
265 }
266 (mkIf config.isNormalUser {
267 group = mkDefault "users";
268 createHome = mkDefault true;
269 home = mkDefault "/home/${name}";
270 useDefaultShell = mkDefault true;
271 isSystemUser = mkDefault false;
272 })
273 # If !mutableUsers, setting ‘initialPassword’ is equivalent to
274 # setting ‘password’ (and similarly for hashed passwords).
275 (mkIf (!cfg.mutableUsers && config.initialPassword != null) {
276 password = mkDefault config.initialPassword;
277 })
278 (mkIf (!cfg.mutableUsers && config.initialHashedPassword != null) {
279 hashedPassword = mkDefault config.initialHashedPassword;
280 })
281 ];
282
283 };
284
285 groupOpts = { name, ... }: {
286
287 options = {
288
289 name = mkOption {
290 type = types.str;
291 description = ''
292 The name of the group. If undefined, the name of the attribute set
293 will be used.
294 '';
295 };
296
297 gid = mkOption {
298 type = with types; nullOr int;
299 default = null;
300 description = ''
301 The group GID. If the GID is null, a free GID is picked on
302 activation.
303 '';
304 };
305
306 members = mkOption {
307 type = with types; listOf string;
308 default = [];
309 description = ''
310 The user names of the group members, added to the
311 <literal>/etc/group</literal> file.
312 '';
313 };
314
315 };
316
317 config = {
318 name = mkDefault name;
319 };
320
321 };
322
323 subordinateUidRange = {
324 options = {
325 startUid = mkOption {
326 type = types.int;
327 description = ''
328 Start of the range of subordinate user ids that user is
329 allowed to use.
330 '';
331 };
332 count = mkOption {
333 type = types.int;
334 default = 1;
335 description = ''Count of subordinate user ids'';
336 };
337 };
338 };
339
340 subordinateGidRange = {
341 options = {
342 startGid = mkOption {
343 type = types.int;
344 description = ''
345 Start of the range of subordinate group ids that user is
346 allowed to use.
347 '';
348 };
349 count = mkOption {
350 type = types.int;
351 default = 1;
352 description = ''Count of subordinate group ids'';
353 };
354 };
355 };
356
357 mkSubuidEntry = user: concatStrings (
358 map (range: "${user.name}:${toString range.startUid}:${toString range.count}\n")
359 user.subUidRanges);
360
361 subuidFile = concatStrings (map mkSubuidEntry (attrValues cfg.users));
362
363 mkSubgidEntry = user: concatStrings (
364 map (range: "${user.name}:${toString range.startGid}:${toString range.count}\n")
365 user.subGidRanges);
366
367 subgidFile = concatStrings (map mkSubgidEntry (attrValues cfg.users));
368
369 idsAreUnique = set: idAttr: !(fold (name: args@{ dup, acc }:
370 let
371 id = builtins.toString (builtins.getAttr idAttr (builtins.getAttr name set));
372 exists = builtins.hasAttr id acc;
373 newAcc = acc // (builtins.listToAttrs [ { name = id; value = true; } ]);
374 in if dup then args else if exists
375 then builtins.trace "Duplicate ${idAttr} ${id}" { dup = true; acc = null; }
376 else { dup = false; acc = newAcc; }
377 ) { dup = false; acc = {}; } (builtins.attrNames set)).dup;
378
379 uidsAreUnique = idsAreUnique (filterAttrs (n: u: u.uid != null) cfg.users) "uid";
380 gidsAreUnique = idsAreUnique (filterAttrs (n: g: g.gid != null) cfg.groups) "gid";
381
382 spec = pkgs.writeText "users-groups.json" (builtins.toJSON {
383 inherit (cfg) mutableUsers;
384 users = mapAttrsToList (_: u:
385 { inherit (u)
386 name uid group description home createHome isSystemUser
387 password passwordFile hashedPassword
388 initialPassword initialHashedPassword;
389 shell = utils.toShellPath u.shell;
390 }) cfg.users;
391 groups = mapAttrsToList (n: g:
392 { inherit (g) name gid;
393 members = g.members ++ (mapAttrsToList (n: u: u.name) (
394 filterAttrs (n: u: elem g.name u.extraGroups) cfg.users
395 ));
396 }) cfg.groups;
397 });
398
399 systemShells =
400 let
401 shells = mapAttrsToList (_: u: u.shell) cfg.users;
402 in
403 filter types.shellPackage.check shells;
404
405in {
406
407 ###### interface
408
409 options = {
410
411 users.mutableUsers = mkOption {
412 type = types.bool;
413 default = true;
414 description = ''
415 If set to <literal>true</literal>, you are free to add new users and groups to the system
416 with the ordinary <literal>useradd</literal> and
417 <literal>groupadd</literal> commands. On system activation, the
418 existing contents of the <literal>/etc/passwd</literal> and
419 <literal>/etc/group</literal> files will be merged with the
420 contents generated from the <literal>users.users</literal> and
421 <literal>users.groups</literal> options.
422 The initial password for a user will be set
423 according to <literal>users.users</literal>, but existing passwords
424 will not be changed.
425
426 <warning><para>
427 If set to <literal>false</literal>, the contents of the user and
428 group files will simply be replaced on system activation. This also
429 holds for the user passwords; all changed
430 passwords will be reset according to the
431 <literal>users.users</literal> configuration on activation.
432 </para></warning>
433 '';
434 };
435
436 users.enforceIdUniqueness = mkOption {
437 type = types.bool;
438 default = true;
439 description = ''
440 Whether to require that no two users/groups share the same uid/gid.
441 '';
442 };
443
444 users.users = mkOption {
445 default = {};
446 type = with types; loaOf (submodule userOpts);
447 example = {
448 alice = {
449 uid = 1234;
450 description = "Alice Q. User";
451 home = "/home/alice";
452 createHome = true;
453 group = "users";
454 extraGroups = ["wheel"];
455 shell = "/bin/sh";
456 };
457 };
458 description = ''
459 Additional user accounts to be created automatically by the system.
460 This can also be used to set options for root.
461 '';
462 };
463
464 users.groups = mkOption {
465 default = {};
466 example =
467 { students.gid = 1001;
468 hackers = { };
469 };
470 type = with types; loaOf (submodule groupOpts);
471 description = ''
472 Additional groups to be created automatically by the system.
473 '';
474 };
475
476 # FIXME: obsolete - will remove.
477 security.initialRootPassword = mkOption {
478 type = types.str;
479 default = "!";
480 example = "";
481 visible = false;
482 };
483
484 };
485
486
487 ###### implementation
488
489 config = {
490
491 users.users = {
492 root = {
493 uid = ids.uids.root;
494 description = "System administrator";
495 home = "/root";
496 shell = mkDefault cfg.defaultUserShell;
497 group = "root";
498 initialHashedPassword = mkDefault config.security.initialRootPassword;
499 };
500 nobody = {
501 uid = ids.uids.nobody;
502 description = "Unprivileged account (don't use!)";
503 group = "nogroup";
504 };
505 };
506
507 users.groups = {
508 root.gid = ids.gids.root;
509 wheel.gid = ids.gids.wheel;
510 disk.gid = ids.gids.disk;
511 kmem.gid = ids.gids.kmem;
512 tty.gid = ids.gids.tty;
513 floppy.gid = ids.gids.floppy;
514 uucp.gid = ids.gids.uucp;
515 lp.gid = ids.gids.lp;
516 cdrom.gid = ids.gids.cdrom;
517 tape.gid = ids.gids.tape;
518 audio.gid = ids.gids.audio;
519 video.gid = ids.gids.video;
520 dialout.gid = ids.gids.dialout;
521 nogroup.gid = ids.gids.nogroup;
522 users.gid = ids.gids.users;
523 nixbld.gid = ids.gids.nixbld;
524 utmp.gid = ids.gids.utmp;
525 adm.gid = ids.gids.adm;
526 input.gid = ids.gids.input;
527 kvm.gid = ids.gids.kvm;
528 render.gid = ids.gids.render;
529 };
530
531 system.activationScripts.users = stringAfter [ "stdio" ]
532 ''
533 install -m 0700 -d /root
534 install -m 0755 -d /home
535
536 ${pkgs.perl}/bin/perl -w \
537 -I${pkgs.perlPackages.FileSlurp}/lib/perl5/site_perl \
538 -I${pkgs.perlPackages.JSON}/lib/perl5/site_perl \
539 ${./update-users-groups.pl} ${spec}
540 '';
541
542 # for backwards compatibility
543 system.activationScripts.groups = stringAfter [ "users" ] "";
544
545 # Install all the user shells
546 environment.systemPackages = systemShells;
547
548 environment.etc = {
549 "subuid" = {
550 text = subuidFile;
551 mode = "0644";
552 };
553 "subgid" = {
554 text = subgidFile;
555 mode = "0644";
556 };
557 } // (mapAttrs' (name: { packages, ... }: {
558 name = "profiles/per-user/${name}";
559 value.source = pkgs.buildEnv {
560 name = "user-environment";
561 paths = packages;
562 inherit (config.environment) pathsToLink extraOutputsToInstall;
563 inherit (config.system.path) ignoreCollisions postBuild;
564 };
565 }) (filterAttrs (_: u: u.packages != []) cfg.users));
566
567 environment.profiles = [ "/etc/profiles/per-user/$USER" ];
568
569 assertions = [
570 { assertion = !cfg.enforceIdUniqueness || (uidsAreUnique && gidsAreUnique);
571 message = "UIDs and GIDs must be unique!";
572 }
573 { # If mutableUsers is false, to prevent users creating a
574 # configuration that locks them out of the system, ensure that
575 # there is at least one "privileged" account that has a
576 # password or an SSH authorized key. Privileged accounts are
577 # root and users in the wheel group.
578 assertion = !cfg.mutableUsers ->
579 any id (mapAttrsToList (name: cfg:
580 (name == "root"
581 || cfg.group == "wheel"
582 || elem "wheel" cfg.extraGroups)
583 &&
584 ((cfg.hashedPassword != null && cfg.hashedPassword != "!")
585 || cfg.password != null
586 || cfg.passwordFile != null
587 || cfg.openssh.authorizedKeys.keys != []
588 || cfg.openssh.authorizedKeys.keyFiles != [])
589 ) cfg.users);
590 message = ''
591 Neither the root account nor any wheel user has a password or SSH authorized key.
592 You must set one to prevent being locked out of your system.'';
593 }
594 ];
595
596 };
597
598}