at 17.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 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>. Dont 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}