1{ config, lib, utils, pkgs, ... }:
2
3with lib;
4
5let
6 ids = config.ids;
7 cfg = config.users;
8
9 # Check whether a password hash will allow login.
10 allowsLogin = hash:
11 hash == "" # login without password
12 || !(lib.elem hash
13 [ null # password login disabled
14 "!" # password login disabled
15 "!!" # a variant of "!"
16 "*" # password unset
17 ]);
18
19 passwordDescription = ''
20 The options {option}`hashedPassword`,
21 {option}`password` and {option}`passwordFile`
22 controls what password is set for the user.
23 {option}`hashedPassword` overrides both
24 {option}`password` and {option}`passwordFile`.
25 {option}`password` overrides {option}`passwordFile`.
26 If none of these three options are set, no password is assigned to
27 the user, and the user will not be able to do password logins.
28 If the option {option}`users.mutableUsers` is true, the
29 password defined in one of the three options will only be set when
30 the user is created for the first time. After that, you are free to
31 change the password with the ordinary user management commands. If
32 {option}`users.mutableUsers` is false, you cannot change
33 user passwords, they will always be set according to the password
34 options.
35 '';
36
37 hashedPasswordDescription = ''
38 To generate a hashed password run `mkpasswd`.
39
40 If set to an empty string (`""`), this user will
41 be able to log in without being asked for a password (but not via remote
42 services such as SSH, or indirectly via {command}`su` or
43 {command}`sudo`). This should only be used for e.g. bootable
44 live systems. Note: this is different from setting an empty password,
45 which can be achieved using {option}`users.users.<name?>.password`.
46
47 If set to `null` (default) this user will not
48 be able to log in using a password (i.e. via {command}`login`
49 command).
50 '';
51
52 userOpts = { name, config, ... }: {
53
54 options = {
55
56 name = mkOption {
57 type = types.passwdEntry types.str;
58 apply = x: assert (builtins.stringLength x < 32 || abort "Username '${x}' is longer than 31 characters which is not allowed!"); x;
59 description = lib.mdDoc ''
60 The name of the user account. If undefined, the name of the
61 attribute set will be used.
62 '';
63 };
64
65 description = mkOption {
66 type = types.passwdEntry types.str;
67 default = "";
68 example = "Alice Q. User";
69 description = lib.mdDoc ''
70 A short description of the user account, typically the
71 user's full name. This is actually the “GECOS” or “comment”
72 field in {file}`/etc/passwd`.
73 '';
74 };
75
76 uid = mkOption {
77 type = with types; nullOr int;
78 default = null;
79 description = lib.mdDoc ''
80 The account UID. If the UID is null, a free UID is picked on
81 activation.
82 '';
83 };
84
85 isSystemUser = mkOption {
86 type = types.bool;
87 default = false;
88 description = lib.mdDoc ''
89 Indicates if the user is a system user or not. This option
90 only has an effect if {option}`uid` is
91 {option}`null`, in which case it determines whether
92 the user's UID is allocated in the range for system users
93 (below 1000) or in the range for normal users (starting at
94 1000).
95 Exactly one of `isNormalUser` and
96 `isSystemUser` must be true.
97 '';
98 };
99
100 isNormalUser = mkOption {
101 type = types.bool;
102 default = false;
103 description = lib.mdDoc ''
104 Indicates whether this is an account for a “real” user.
105 This automatically sets {option}`group` to `users`,
106 {option}`createHome` to `true`,
107 {option}`home` to {file}`/home/«username»`,
108 {option}`useDefaultShell` to `true`,
109 and {option}`isSystemUser` to `false`.
110 Exactly one of `isNormalUser` and `isSystemUser` must be true.
111 '';
112 };
113
114 group = mkOption {
115 type = types.str;
116 apply = x: assert (builtins.stringLength x < 32 || abort "Group name '${x}' is longer than 31 characters which is not allowed!"); x;
117 default = "";
118 description = lib.mdDoc "The user's primary group.";
119 };
120
121 extraGroups = mkOption {
122 type = types.listOf types.str;
123 default = [];
124 description = lib.mdDoc "The user's auxiliary groups.";
125 };
126
127 home = mkOption {
128 type = types.passwdEntry types.path;
129 default = "/var/empty";
130 description = lib.mdDoc "The user's home directory.";
131 };
132
133 homeMode = mkOption {
134 type = types.strMatching "[0-7]{1,5}";
135 default = "700";
136 description = lib.mdDoc "The user's home directory mode in numeric format. See chmod(1). The mode is only applied if {option}`users.users.<name>.createHome` is true.";
137 };
138
139 cryptHomeLuks = mkOption {
140 type = with types; nullOr str;
141 default = null;
142 description = lib.mdDoc ''
143 Path to encrypted luks device that contains
144 the user's home directory.
145 '';
146 };
147
148 pamMount = mkOption {
149 type = with types; attrsOf str;
150 default = {};
151 description = lib.mdDoc ''
152 Attributes for user's entry in
153 {file}`pam_mount.conf.xml`.
154 Useful attributes might include `path`,
155 `options`, `fstype`, and `server`.
156 See <http://pam-mount.sourceforge.net/pam_mount.conf.5.html>
157 for more information.
158 '';
159 };
160
161 shell = mkOption {
162 type = types.nullOr (types.either types.shellPackage (types.passwdEntry types.path));
163 default = pkgs.shadow;
164 defaultText = literalExpression "pkgs.shadow";
165 example = literalExpression "pkgs.bashInteractive";
166 description = lib.mdDoc ''
167 The path to the user's shell. Can use shell derivations,
168 like `pkgs.bashInteractive`. Don’t
169 forget to enable your shell in
170 `programs` if necessary,
171 like `programs.zsh.enable = true;`.
172 '';
173 };
174
175 subUidRanges = mkOption {
176 type = with types; listOf (submodule subordinateUidRange);
177 default = [];
178 example = [
179 { startUid = 1000; count = 1; }
180 { startUid = 100001; count = 65534; }
181 ];
182 description = lib.mdDoc ''
183 Subordinate user ids that user is allowed to use.
184 They are set into {file}`/etc/subuid` and are used
185 by `newuidmap` for user namespaces.
186 '';
187 };
188
189 subGidRanges = mkOption {
190 type = with types; listOf (submodule subordinateGidRange);
191 default = [];
192 example = [
193 { startGid = 100; count = 1; }
194 { startGid = 1001; count = 999; }
195 ];
196 description = lib.mdDoc ''
197 Subordinate group ids that user is allowed to use.
198 They are set into {file}`/etc/subgid` and are used
199 by `newgidmap` for user namespaces.
200 '';
201 };
202
203 autoSubUidGidRange = mkOption {
204 type = types.bool;
205 default = false;
206 example = true;
207 description = lib.mdDoc ''
208 Automatically allocate subordinate user and group ids for this user.
209 Allocated range is currently always of size 65536.
210 '';
211 };
212
213 createHome = mkOption {
214 type = types.bool;
215 default = false;
216 description = lib.mdDoc ''
217 Whether to create the home directory and ensure ownership as well as
218 permissions to match the user.
219 '';
220 };
221
222 useDefaultShell = mkOption {
223 type = types.bool;
224 default = false;
225 description = lib.mdDoc ''
226 If true, the user's shell will be set to
227 {option}`users.defaultUserShell`.
228 '';
229 };
230
231 hashedPassword = mkOption {
232 type = with types; nullOr (passwdEntry str);
233 default = null;
234 description = lib.mdDoc ''
235 Specifies the hashed password for the user.
236 ${passwordDescription}
237 ${hashedPasswordDescription}
238 '';
239 };
240
241 password = mkOption {
242 type = with types; nullOr str;
243 default = null;
244 description = lib.mdDoc ''
245 Specifies the (clear text) password for the user.
246 Warning: do not set confidential information here
247 because it is world-readable in the Nix store. This option
248 should only be used for public accounts.
249 ${passwordDescription}
250 '';
251 };
252
253 passwordFile = mkOption {
254 type = with types; nullOr str;
255 default = null;
256 description = lib.mdDoc ''
257 The full path to a file that contains the user's password. The password
258 file is read on each system activation. The file should contain
259 exactly one line, which should be the password in an encrypted form
260 that is suitable for the `chpasswd -e` command.
261 ${passwordDescription}
262 '';
263 };
264
265 initialHashedPassword = mkOption {
266 type = with types; nullOr (passwdEntry str);
267 default = null;
268 description = lib.mdDoc ''
269 Specifies the initial hashed password for the user, i.e. the
270 hashed password assigned if the user does not already
271 exist. If {option}`users.mutableUsers` is true, the
272 password can be changed subsequently using the
273 {command}`passwd` command. Otherwise, it's
274 equivalent to setting the {option}`hashedPassword` option.
275
276 Note that the {option}`hashedPassword` option will override
277 this option if both are set.
278
279 ${hashedPasswordDescription}
280 '';
281 };
282
283 initialPassword = mkOption {
284 type = with types; nullOr str;
285 default = null;
286 description = lib.mdDoc ''
287 Specifies the initial password for the user, i.e. the
288 password assigned if the user does not already exist. If
289 {option}`users.mutableUsers` is true, the password
290 can be changed subsequently using the
291 {command}`passwd` command. Otherwise, it's
292 equivalent to setting the {option}`password`
293 option. The same caveat applies: the password specified here
294 is world-readable in the Nix store, so it should only be
295 used for guest accounts or passwords that will be changed
296 promptly.
297
298 Note that the {option}`password` option will override this
299 option if both are set.
300 '';
301 };
302
303 packages = mkOption {
304 type = types.listOf types.package;
305 default = [];
306 example = literalExpression "[ pkgs.firefox pkgs.thunderbird ]";
307 description = lib.mdDoc ''
308 The set of packages that should be made available to the user.
309 This is in contrast to {option}`environment.systemPackages`,
310 which adds packages to all users.
311 '';
312 };
313
314 };
315
316 config = mkMerge
317 [ { name = mkDefault name;
318 shell = mkIf config.useDefaultShell (mkDefault cfg.defaultUserShell);
319 }
320 (mkIf config.isNormalUser {
321 group = mkDefault "users";
322 createHome = mkDefault true;
323 home = mkDefault "/home/${config.name}";
324 homeMode = mkDefault "700";
325 useDefaultShell = mkDefault true;
326 isSystemUser = mkDefault false;
327 })
328 # If !mutableUsers, setting ‘initialPassword’ is equivalent to
329 # setting ‘password’ (and similarly for hashed passwords).
330 (mkIf (!cfg.mutableUsers && config.initialPassword != null) {
331 password = mkDefault config.initialPassword;
332 })
333 (mkIf (!cfg.mutableUsers && config.initialHashedPassword != null) {
334 hashedPassword = mkDefault config.initialHashedPassword;
335 })
336 (mkIf (config.isNormalUser && config.subUidRanges == [] && config.subGidRanges == []) {
337 autoSubUidGidRange = mkDefault true;
338 })
339 ];
340
341 };
342
343 groupOpts = { name, config, ... }: {
344
345 options = {
346
347 name = mkOption {
348 type = types.passwdEntry types.str;
349 description = lib.mdDoc ''
350 The name of the group. If undefined, the name of the attribute set
351 will be used.
352 '';
353 };
354
355 gid = mkOption {
356 type = with types; nullOr int;
357 default = null;
358 description = lib.mdDoc ''
359 The group GID. If the GID is null, a free GID is picked on
360 activation.
361 '';
362 };
363
364 members = mkOption {
365 type = with types; listOf (passwdEntry str);
366 default = [];
367 description = lib.mdDoc ''
368 The user names of the group members, added to the
369 `/etc/group` file.
370 '';
371 };
372
373 };
374
375 config = {
376 name = mkDefault name;
377
378 members = mapAttrsToList (n: u: u.name) (
379 filterAttrs (n: u: elem config.name u.extraGroups) cfg.users
380 );
381 };
382
383 };
384
385 subordinateUidRange = {
386 options = {
387 startUid = mkOption {
388 type = types.int;
389 description = lib.mdDoc ''
390 Start of the range of subordinate user ids that user is
391 allowed to use.
392 '';
393 };
394 count = mkOption {
395 type = types.int;
396 default = 1;
397 description = lib.mdDoc "Count of subordinate user ids";
398 };
399 };
400 };
401
402 subordinateGidRange = {
403 options = {
404 startGid = mkOption {
405 type = types.int;
406 description = lib.mdDoc ''
407 Start of the range of subordinate group ids that user is
408 allowed to use.
409 '';
410 };
411 count = mkOption {
412 type = types.int;
413 default = 1;
414 description = lib.mdDoc "Count of subordinate group ids";
415 };
416 };
417 };
418
419 idsAreUnique = set: idAttr: !(foldr (name: args@{ dup, acc }:
420 let
421 id = builtins.toString (builtins.getAttr idAttr (builtins.getAttr name set));
422 exists = builtins.hasAttr id acc;
423 newAcc = acc // (builtins.listToAttrs [ { name = id; value = true; } ]);
424 in if dup then args else if exists
425 then builtins.trace "Duplicate ${idAttr} ${id}" { dup = true; acc = null; }
426 else { dup = false; acc = newAcc; }
427 ) { dup = false; acc = {}; } (builtins.attrNames set)).dup;
428
429 uidsAreUnique = idsAreUnique (filterAttrs (n: u: u.uid != null) cfg.users) "uid";
430 gidsAreUnique = idsAreUnique (filterAttrs (n: g: g.gid != null) cfg.groups) "gid";
431 sdInitrdUidsAreUnique = idsAreUnique (filterAttrs (n: u: u.uid != null) config.boot.initrd.systemd.users) "uid";
432 sdInitrdGidsAreUnique = idsAreUnique (filterAttrs (n: g: g.gid != null) config.boot.initrd.systemd.groups) "gid";
433
434 spec = pkgs.writeText "users-groups.json" (builtins.toJSON {
435 inherit (cfg) mutableUsers;
436 users = mapAttrsToList (_: u:
437 { inherit (u)
438 name uid group description home homeMode createHome isSystemUser
439 password passwordFile hashedPassword
440 autoSubUidGidRange subUidRanges subGidRanges
441 initialPassword initialHashedPassword;
442 shell = utils.toShellPath u.shell;
443 }) cfg.users;
444 groups = attrValues cfg.groups;
445 });
446
447 systemShells =
448 let
449 shells = mapAttrsToList (_: u: u.shell) cfg.users;
450 in
451 filter types.shellPackage.check shells;
452
453in {
454 imports = [
455 (mkAliasOptionModuleMD [ "users" "extraUsers" ] [ "users" "users" ])
456 (mkAliasOptionModuleMD [ "users" "extraGroups" ] [ "users" "groups" ])
457 (mkRenamedOptionModule ["security" "initialRootPassword"] ["users" "users" "root" "initialHashedPassword"])
458 ];
459
460 ###### interface
461 options = {
462
463 users.mutableUsers = mkOption {
464 type = types.bool;
465 default = true;
466 description = lib.mdDoc ''
467 If set to `true`, you are free to add new users and groups to the system
468 with the ordinary `useradd` and
469 `groupadd` commands. On system activation, the
470 existing contents of the `/etc/passwd` and
471 `/etc/group` files will be merged with the
472 contents generated from the `users.users` and
473 `users.groups` options.
474 The initial password for a user will be set
475 according to `users.users`, but existing passwords
476 will not be changed.
477
478 ::: {.warning}
479 If set to `false`, the contents of the user and
480 group files will simply be replaced on system activation. This also
481 holds for the user passwords; all changed
482 passwords will be reset according to the
483 `users.users` configuration on activation.
484 :::
485 '';
486 };
487
488 users.enforceIdUniqueness = mkOption {
489 type = types.bool;
490 default = true;
491 description = lib.mdDoc ''
492 Whether to require that no two users/groups share the same uid/gid.
493 '';
494 };
495
496 users.users = mkOption {
497 default = {};
498 type = with types; attrsOf (submodule userOpts);
499 example = {
500 alice = {
501 uid = 1234;
502 description = "Alice Q. User";
503 home = "/home/alice";
504 createHome = true;
505 group = "users";
506 extraGroups = ["wheel"];
507 shell = "/bin/sh";
508 };
509 };
510 description = lib.mdDoc ''
511 Additional user accounts to be created automatically by the system.
512 This can also be used to set options for root.
513 '';
514 };
515
516 users.groups = mkOption {
517 default = {};
518 example =
519 { students.gid = 1001;
520 hackers = { };
521 };
522 type = with types; attrsOf (submodule groupOpts);
523 description = lib.mdDoc ''
524 Additional groups to be created automatically by the system.
525 '';
526 };
527
528
529 users.allowNoPasswordLogin = mkOption {
530 type = types.bool;
531 default = false;
532 description = lib.mdDoc ''
533 Disable checking that at least the `root` user or a user in the `wheel` group can log in using
534 a password or an SSH key.
535
536 WARNING: enabling this can lock you out of your system. Enable this only if you know what are you doing.
537 '';
538 };
539
540 # systemd initrd
541 boot.initrd.systemd.users = mkOption {
542 visible = false;
543 description = ''
544 Users to include in initrd.
545 '';
546 default = {};
547 type = types.attrsOf (types.submodule ({ name, ... }: {
548 options.uid = mkOption {
549 visible = false;
550 type = types.int;
551 description = ''
552 ID of the user in initrd.
553 '';
554 defaultText = literalExpression "config.users.users.\${name}.uid";
555 default = cfg.users.${name}.uid;
556 };
557 options.group = mkOption {
558 visible = false;
559 type = types.singleLineStr;
560 description = ''
561 Group the user belongs to in initrd.
562 '';
563 defaultText = literalExpression "config.users.users.\${name}.group";
564 default = cfg.users.${name}.group;
565 };
566 }));
567 };
568
569 boot.initrd.systemd.groups = mkOption {
570 visible = false;
571 description = ''
572 Groups to include in initrd.
573 '';
574 default = {};
575 type = types.attrsOf (types.submodule ({ name, ... }: {
576 options.gid = mkOption {
577 visible = false;
578 type = types.int;
579 description = ''
580 ID of the group in initrd.
581 '';
582 defaultText = literalExpression "config.users.groups.\${name}.gid";
583 default = cfg.groups.${name}.gid;
584 };
585 }));
586 };
587 };
588
589
590 ###### implementation
591
592 config = let
593 cryptSchemeIdPatternGroup = "(${lib.concatStringsSep "|" pkgs.libxcrypt.enabledCryptSchemeIds})";
594 in {
595
596 users.users = {
597 root = {
598 uid = ids.uids.root;
599 description = "System administrator";
600 home = "/root";
601 shell = mkDefault cfg.defaultUserShell;
602 group = "root";
603 initialHashedPassword = mkDefault "!";
604 };
605 nobody = {
606 uid = ids.uids.nobody;
607 isSystemUser = true;
608 description = "Unprivileged account (don't use!)";
609 group = "nogroup";
610 };
611 };
612
613 users.groups = {
614 root.gid = ids.gids.root;
615 wheel.gid = ids.gids.wheel;
616 disk.gid = ids.gids.disk;
617 kmem.gid = ids.gids.kmem;
618 tty.gid = ids.gids.tty;
619 floppy.gid = ids.gids.floppy;
620 uucp.gid = ids.gids.uucp;
621 lp.gid = ids.gids.lp;
622 cdrom.gid = ids.gids.cdrom;
623 tape.gid = ids.gids.tape;
624 audio.gid = ids.gids.audio;
625 video.gid = ids.gids.video;
626 dialout.gid = ids.gids.dialout;
627 nogroup.gid = ids.gids.nogroup;
628 users.gid = ids.gids.users;
629 nixbld.gid = ids.gids.nixbld;
630 utmp.gid = ids.gids.utmp;
631 adm.gid = ids.gids.adm;
632 input.gid = ids.gids.input;
633 kvm.gid = ids.gids.kvm;
634 render.gid = ids.gids.render;
635 sgx.gid = ids.gids.sgx;
636 shadow.gid = ids.gids.shadow;
637 };
638
639 system.activationScripts.users = {
640 supportsDryActivation = true;
641 text = ''
642 install -m 0700 -d /root
643 install -m 0755 -d /home
644
645 ${pkgs.perl.withPackages (p: [ p.FileSlurp p.JSON ])}/bin/perl \
646 -w ${./update-users-groups.pl} ${spec}
647 '';
648 };
649
650 # Warn about user accounts with deprecated password hashing schemes
651 system.activationScripts.hashes = {
652 deps = [ "users" ];
653 text = ''
654 users=()
655 while IFS=: read -r user hash tail; do
656 if [[ "$hash" = "$"* && ! "$hash" =~ ^\''$${cryptSchemeIdPatternGroup}\$ ]]; then
657 users+=("$user")
658 fi
659 done </etc/shadow
660
661 if (( "''${#users[@]}" )); then
662 echo "
663 WARNING: The following user accounts rely on password hashing algorithms
664 that have been removed. They need to be renewed as soon as possible, as
665 they do prevent their users from logging in."
666 printf ' - %s\n' "''${users[@]}"
667 fi
668 '';
669 };
670
671 # for backwards compatibility
672 system.activationScripts.groups = stringAfter [ "users" ] "";
673
674 # Install all the user shells
675 environment.systemPackages = systemShells;
676
677 environment.etc = mapAttrs' (_: { packages, name, ... }: {
678 name = "profiles/per-user/${name}";
679 value.source = pkgs.buildEnv {
680 name = "user-environment";
681 paths = packages;
682 inherit (config.environment) pathsToLink extraOutputsToInstall;
683 inherit (config.system.path) ignoreCollisions postBuild;
684 };
685 }) (filterAttrs (_: u: u.packages != []) cfg.users);
686
687 environment.profiles = [
688 "$HOME/.nix-profile"
689 "/etc/profiles/per-user/$USER"
690 ];
691
692 # systemd initrd
693 boot.initrd.systemd = lib.mkIf config.boot.initrd.systemd.enable {
694 contents = {
695 "/etc/passwd".text = ''
696 ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: { uid, group }: let
697 g = config.boot.initrd.systemd.groups.${group};
698 in "${n}:x:${toString uid}:${toString g.gid}::/var/empty:") config.boot.initrd.systemd.users)}
699 '';
700 "/etc/group".text = ''
701 ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: { gid }: "${n}:x:${toString gid}:") config.boot.initrd.systemd.groups)}
702 '';
703 };
704
705 users = {
706 root = {};
707 nobody = {};
708 };
709
710 groups = {
711 root = {};
712 nogroup = {};
713 systemd-journal = {};
714 tty = {};
715 dialout = {};
716 kmem = {};
717 input = {};
718 video = {};
719 render = {};
720 sgx = {};
721 audio = {};
722 video = {};
723 lp = {};
724 disk = {};
725 cdrom = {};
726 tape = {};
727 kvm = {};
728 };
729 };
730
731 assertions = [
732 { assertion = !cfg.enforceIdUniqueness || (uidsAreUnique && gidsAreUnique);
733 message = "UIDs and GIDs must be unique!";
734 }
735 { assertion = !cfg.enforceIdUniqueness || (sdInitrdUidsAreUnique && sdInitrdGidsAreUnique);
736 message = "systemd initrd UIDs and GIDs must be unique!";
737 }
738 { # If mutableUsers is false, to prevent users creating a
739 # configuration that locks them out of the system, ensure that
740 # there is at least one "privileged" account that has a
741 # password or an SSH authorized key. Privileged accounts are
742 # root and users in the wheel group.
743 # The check does not apply when users.disableLoginPossibilityAssertion
744 # The check does not apply when users.mutableUsers
745 assertion = !cfg.mutableUsers -> !cfg.allowNoPasswordLogin ->
746 any id (mapAttrsToList (name: cfg:
747 (name == "root"
748 || cfg.group == "wheel"
749 || elem "wheel" cfg.extraGroups)
750 &&
751 (allowsLogin cfg.hashedPassword
752 || cfg.password != null
753 || cfg.passwordFile != null
754 || cfg.openssh.authorizedKeys.keys != []
755 || cfg.openssh.authorizedKeys.keyFiles != [])
756 ) cfg.users ++ [
757 config.security.googleOsLogin.enable
758 ]);
759 message = ''
760 Neither the root account nor any wheel user has a password or SSH authorized key.
761 You must set one to prevent being locked out of your system.
762 If you really want to be locked out of your system, set users.allowNoPasswordLogin = true;
763 However you are most probably better off by setting users.mutableUsers = true; and
764 manually running passwd root to set the root password.
765 '';
766 }
767 ] ++ flatten (flip mapAttrsToList cfg.users (name: user:
768 [
769 {
770 assertion = (user.hashedPassword != null)
771 -> (builtins.match ".*:.*" user.hashedPassword == null);
772 message = ''
773 The password hash of user "${user.name}" contains a ":" character.
774 This is invalid and would break the login system because the fields
775 of /etc/shadow (file where hashes are stored) are colon-separated.
776 Please check the value of option `users.users."${user.name}".hashedPassword`.'';
777 }
778 {
779 assertion = let
780 xor = a: b: a && !b || b && !a;
781 isEffectivelySystemUser = user.isSystemUser || (user.uid != null && user.uid < 1000);
782 in xor isEffectivelySystemUser user.isNormalUser;
783 message = ''
784 Exactly one of users.users.${user.name}.isSystemUser and users.users.${user.name}.isNormalUser must be set.
785 '';
786 }
787 {
788 assertion = user.group != "";
789 message = ''
790 users.users.${user.name}.group is unset. This used to default to
791 nogroup, but this is unsafe. For example you can create a group
792 for this user with:
793 users.users.${user.name}.group = "${user.name}";
794 users.groups.${user.name} = {};
795 '';
796 }
797 ] ++ (map (shell: {
798 assertion = (user.shell == pkgs.${shell}) -> (config.programs.${shell}.enable == true);
799 message = ''
800 users.users.${user.name}.shell is set to ${shell}, but
801 programs.${shell}.enable is not true. This will cause the ${shell}
802 shell to lack the basic nix directories in its PATH and might make
803 logging in as that user impossible. You can fix it with:
804 programs.${shell}.enable = true;
805 '';
806 }) [
807 "fish"
808 "xonsh"
809 "zsh"
810 ])
811 ));
812
813 warnings =
814 builtins.filter (x: x != null) (
815 flip mapAttrsToList cfg.users (_: user:
816 # This regex matches a subset of the Modular Crypto Format (MCF)[1]
817 # informal standard. Since this depends largely on the OS or the
818 # specific implementation of crypt(3) we only support the (sane)
819 # schemes implemented by glibc and BSDs. In particular the original
820 # DES hash is excluded since, having no structure, it would validate
821 # common mistakes like typing the plaintext password.
822 #
823 # [1]: https://en.wikipedia.org/wiki/Crypt_(C)
824 let
825 sep = "\\$";
826 base64 = "[a-zA-Z0-9./]+";
827 id = cryptSchemeIdPatternGroup;
828 name = "[a-z0-9-]+";
829 value = "[a-zA-Z0-9/+.-]+";
830 options = "${name}(=${value})?(,${name}=${value})*";
831 scheme = "${id}(${sep}${options})?";
832 content = "${base64}${sep}${base64}(${sep}${base64})?";
833 mcf = "^${sep}${scheme}${sep}${content}$";
834 in
835 if (allowsLogin user.hashedPassword
836 && user.hashedPassword != "" # login without password
837 && builtins.match mcf user.hashedPassword == null)
838 then ''
839 The password hash of user "${user.name}" may be invalid. You must set a
840 valid hash or the user will be locked out of their account. Please
841 check the value of option `users.users."${user.name}".hashedPassword`.''
842 else null
843 ));
844
845 };
846
847}