at 18.09-beta 19 kB view raw
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>. Dont 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}