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