at 16.09-beta 18 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 = types.listOf types.optionSet; 135 default = []; 136 example = [ 137 { startUid = 1000; count = 1; } 138 { startUid = 100001; count = 65534; } 139 ]; 140 options = [ subordinateUidRange ]; 141 description = '' 142 Subordinate user ids that user is allowed to use. 143 They are set into <filename>/etc/subuid</filename> and are used 144 by <literal>newuidmap</literal> for user namespaces. 145 ''; 146 }; 147 148 subGidRanges = mkOption { 149 type = types.listOf types.optionSet; 150 default = []; 151 example = [ 152 { startGid = 100; count = 1; } 153 { startGid = 1001; count = 999; } 154 ]; 155 options = [ subordinateGidRange ]; 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 }; 250 251 config = mkMerge 252 [ { name = mkDefault name; 253 shell = mkIf config.useDefaultShell (mkDefault cfg.defaultUserShell); 254 } 255 (mkIf config.isNormalUser { 256 group = mkDefault "users"; 257 createHome = mkDefault true; 258 home = mkDefault "/home/${name}"; 259 useDefaultShell = mkDefault true; 260 isSystemUser = mkDefault false; 261 }) 262 # If !mutableUsers, setting ‘initialPassword’ is equivalent to 263 # setting ‘password’ (and similarly for hashed passwords). 264 (mkIf (!cfg.mutableUsers && config.initialPassword != null) { 265 password = mkDefault config.initialPassword; 266 }) 267 (mkIf (!cfg.mutableUsers && config.initialHashedPassword != null) { 268 hashedPassword = mkDefault config.initialHashedPassword; 269 }) 270 ]; 271 272 }; 273 274 groupOpts = { name, config, ... }: { 275 276 options = { 277 278 name = mkOption { 279 type = types.str; 280 description = '' 281 The name of the group. If undefined, the name of the attribute set 282 will be used. 283 ''; 284 }; 285 286 gid = mkOption { 287 type = with types; nullOr int; 288 default = null; 289 description = '' 290 The group GID. If the GID is null, a free GID is picked on 291 activation. 292 ''; 293 }; 294 295 members = mkOption { 296 type = with types; listOf string; 297 default = []; 298 description = '' 299 The user names of the group members, added to the 300 <literal>/etc/group</literal> file. 301 ''; 302 }; 303 304 }; 305 306 config = { 307 name = mkDefault name; 308 }; 309 310 }; 311 312 subordinateUidRange = { 313 startUid = mkOption { 314 type = types.int; 315 description = '' 316 Start of the range of subordinate user ids that user is 317 allowed to use. 318 ''; 319 }; 320 count = mkOption { 321 type = types.int; 322 default = 1; 323 description = ''Count of subordinate user ids''; 324 }; 325 }; 326 327 subordinateGidRange = { 328 startGid = mkOption { 329 type = types.int; 330 description = '' 331 Start of the range of subordinate group ids that user is 332 allowed to use. 333 ''; 334 }; 335 count = mkOption { 336 type = types.int; 337 default = 1; 338 description = ''Count of subordinate group ids''; 339 }; 340 }; 341 342 mkSubuidEntry = user: concatStrings ( 343 map (range: "${user.name}:${toString range.startUid}:${toString range.count}\n") 344 user.subUidRanges); 345 346 subuidFile = concatStrings (map mkSubuidEntry (attrValues cfg.users)); 347 348 mkSubgidEntry = user: concatStrings ( 349 map (range: "${user.name}:${toString range.startGid}:${toString range.count}\n") 350 user.subGidRanges); 351 352 subgidFile = concatStrings (map mkSubgidEntry (attrValues cfg.users)); 353 354 idsAreUnique = set: idAttr: !(fold (name: args@{ dup, acc }: 355 let 356 id = builtins.toString (builtins.getAttr idAttr (builtins.getAttr name set)); 357 exists = builtins.hasAttr id acc; 358 newAcc = acc // (builtins.listToAttrs [ { name = id; value = true; } ]); 359 in if dup then args else if exists 360 then builtins.trace "Duplicate ${idAttr} ${id}" { dup = true; acc = null; } 361 else { dup = false; acc = newAcc; } 362 ) { dup = false; acc = {}; } (builtins.attrNames set)).dup; 363 364 uidsAreUnique = idsAreUnique (filterAttrs (n: u: u.uid != null) cfg.users) "uid"; 365 gidsAreUnique = idsAreUnique (filterAttrs (n: g: g.gid != null) cfg.groups) "gid"; 366 367 spec = pkgs.writeText "users-groups.json" (builtins.toJSON { 368 inherit (cfg) mutableUsers; 369 users = mapAttrsToList (_: u: 370 { inherit (u) 371 name uid group description home createHome isSystemUser 372 password passwordFile hashedPassword 373 initialPassword initialHashedPassword; 374 shell = utils.toShellPath u.shell; 375 }) cfg.users; 376 groups = mapAttrsToList (n: g: 377 { inherit (g) name gid; 378 members = g.members ++ (mapAttrsToList (n: u: u.name) ( 379 filterAttrs (n: u: elem g.name u.extraGroups) cfg.users 380 )); 381 }) cfg.groups; 382 }); 383 384 systemShells = 385 let 386 shells = mapAttrsToList (_: u: u.shell) cfg.users; 387 in 388 filter types.shellPackage.check shells; 389 390in { 391 392 ###### interface 393 394 options = { 395 396 users.mutableUsers = mkOption { 397 type = types.bool; 398 default = true; 399 description = '' 400 If set to <literal>true</literal>, you are free to add new users and groups to the system 401 with the ordinary <literal>useradd</literal> and 402 <literal>groupadd</literal> commands. On system activation, the 403 existing contents of the <literal>/etc/passwd</literal> and 404 <literal>/etc/group</literal> files will be merged with the 405 contents generated from the <literal>users.users</literal> and 406 <literal>users.groups</literal> options. 407 The initial password for a user will be set 408 according to <literal>users.users</literal>, but existing passwords 409 will not be changed. 410 411 <warning><para> 412 If set to <literal>false</literal>, the contents of the user and 413 group files will simply be replaced on system activation. This also 414 holds for the user passwords; all changed 415 passwords will be reset according to the 416 <literal>users.users</literal> configuration on activation. 417 </para></warning> 418 ''; 419 }; 420 421 users.enforceIdUniqueness = mkOption { 422 type = types.bool; 423 default = true; 424 description = '' 425 Whether to require that no two users/groups share the same uid/gid. 426 ''; 427 }; 428 429 users.users = mkOption { 430 default = {}; 431 type = types.loaOf types.optionSet; 432 example = { 433 alice = { 434 uid = 1234; 435 description = "Alice Q. User"; 436 home = "/home/alice"; 437 createHome = true; 438 group = "users"; 439 extraGroups = ["wheel"]; 440 shell = "/bin/sh"; 441 }; 442 }; 443 description = '' 444 Additional user accounts to be created automatically by the system. 445 This can also be used to set options for root. 446 ''; 447 options = [ userOpts ]; 448 }; 449 450 users.groups = mkOption { 451 default = {}; 452 example = 453 { students.gid = 1001; 454 hackers = { }; 455 }; 456 type = types.loaOf types.optionSet; 457 description = '' 458 Additional groups to be created automatically by the system. 459 ''; 460 options = [ groupOpts ]; 461 }; 462 463 # FIXME: obsolete - will remove. 464 security.initialRootPassword = mkOption { 465 type = types.str; 466 default = "!"; 467 example = ""; 468 visible = false; 469 }; 470 471 }; 472 473 474 ###### implementation 475 476 config = { 477 478 users.users = { 479 root = { 480 uid = ids.uids.root; 481 description = "System administrator"; 482 home = "/root"; 483 shell = mkDefault cfg.defaultUserShell; 484 group = "root"; 485 initialHashedPassword = mkDefault config.security.initialRootPassword; 486 }; 487 nobody = { 488 uid = ids.uids.nobody; 489 description = "Unprivileged account (don't use!)"; 490 group = "nogroup"; 491 }; 492 }; 493 494 # Install all the user shells 495 environment.systemPackages = systemShells; 496 497 users.groups = { 498 root.gid = ids.gids.root; 499 wheel.gid = ids.gids.wheel; 500 disk.gid = ids.gids.disk; 501 kmem.gid = ids.gids.kmem; 502 tty.gid = ids.gids.tty; 503 floppy.gid = ids.gids.floppy; 504 uucp.gid = ids.gids.uucp; 505 lp.gid = ids.gids.lp; 506 cdrom.gid = ids.gids.cdrom; 507 tape.gid = ids.gids.tape; 508 audio.gid = ids.gids.audio; 509 video.gid = ids.gids.video; 510 dialout.gid = ids.gids.dialout; 511 nogroup.gid = ids.gids.nogroup; 512 users.gid = ids.gids.users; 513 nixbld.gid = ids.gids.nixbld; 514 utmp.gid = ids.gids.utmp; 515 adm.gid = ids.gids.adm; 516 input.gid = ids.gids.input; 517 }; 518 519 system.activationScripts.users = stringAfter [ "etc" ] 520 '' 521 ${pkgs.perl}/bin/perl -w \ 522 -I${pkgs.perlPackages.FileSlurp}/lib/perl5/site_perl \ 523 -I${pkgs.perlPackages.JSON}/lib/perl5/site_perl \ 524 ${./update-users-groups.pl} ${spec} 525 ''; 526 527 # for backwards compatibility 528 system.activationScripts.groups = stringAfter [ "users" ] ""; 529 530 environment.etc."subuid" = { 531 text = subuidFile; 532 mode = "0644"; 533 }; 534 environment.etc."subgid" = { 535 text = subgidFile; 536 mode = "0644"; 537 }; 538 539 assertions = [ 540 { assertion = !cfg.enforceIdUniqueness || (uidsAreUnique && gidsAreUnique); 541 message = "UIDs and GIDs must be unique!"; 542 } 543 { # If mutableUsers is false, to prevent users creating a 544 # configuration that locks them out of the system, ensure that 545 # there is at least one "privileged" account that has a 546 # password or an SSH authorized key. Privileged accounts are 547 # root and users in the wheel group. 548 assertion = !cfg.mutableUsers -> 549 any id (mapAttrsToList (name: cfg: 550 (name == "root" 551 || cfg.group == "wheel" 552 || elem "wheel" cfg.extraGroups) 553 && 554 ((cfg.hashedPassword != null && cfg.hashedPassword != "!") 555 || cfg.password != null 556 || cfg.passwordFile != null 557 || cfg.openssh.authorizedKeys.keys != [] 558 || cfg.openssh.authorizedKeys.keyFiles != []) 559 ) cfg.users); 560 message = '' 561 Neither the root account nor any wheel user has a password or SSH authorized key. 562 You must set one to prevent being locked out of your system.''; 563 } 564 ]; 565 566 }; 567 568 imports = 569 [ (mkAliasOptionModule [ "users" "extraUsers" ] [ "users" "users" ]) 570 (mkAliasOptionModule [ "users" "extraGroups" ] [ "users" "groups" ]) 571 ]; 572}