diff options
Diffstat (limited to 'nixpkgs/nixos/modules/config/users-groups.nix')
-rw-r--r-- | nixpkgs/nixos/modules/config/users-groups.nix | 598 |
1 files changed, 598 insertions, 0 deletions
diff --git a/nixpkgs/nixos/modules/config/users-groups.nix b/nixpkgs/nixos/modules/config/users-groups.nix new file mode 100644 index 000000000000..c3f228c9bcc4 --- /dev/null +++ b/nixpkgs/nixos/modules/config/users-groups.nix @@ -0,0 +1,598 @@ +{ config, lib, utils, pkgs, ... }: + +with lib; + +let + ids = config.ids; + cfg = config.users; + + passwordDescription = '' + The options <option>hashedPassword</option>, + <option>password</option> and <option>passwordFile</option> + controls what password is set for the user. + <option>hashedPassword</option> overrides both + <option>password</option> and <option>passwordFile</option>. + <option>password</option> overrides <option>passwordFile</option>. + If none of these three options are set, no password is assigned to + the user, and the user will not be able to do password logins. + If the option <option>users.mutableUsers</option> is true, the + password defined in one of the three options will only be set when + the user is created for the first time. After that, you are free to + change the password with the ordinary user management commands. If + <option>users.mutableUsers</option> is false, you cannot change + user passwords, they will always be set according to the password + options. + ''; + + hashedPasswordDescription = '' + To generate hashed password install <literal>mkpasswd</literal> + package and run <literal>mkpasswd -m sha-512</literal>. + ''; + + userOpts = { name, config, ... }: { + + options = { + + name = mkOption { + type = types.str; + apply = x: assert (builtins.stringLength x < 32 || abort "Username '${x}' is longer than 31 characters which is not allowed!"); x; + description = '' + The name of the user account. If undefined, the name of the + attribute set will be used. + ''; + }; + + description = mkOption { + type = types.str; + default = ""; + example = "Alice Q. User"; + description = '' + A short description of the user account, typically the + user's full name. This is actually the “GECOS” or “comment” + field in <filename>/etc/passwd</filename>. + ''; + }; + + uid = mkOption { + type = with types; nullOr int; + default = null; + description = '' + The account UID. If the UID is null, a free UID is picked on + activation. + ''; + }; + + isSystemUser = mkOption { + type = types.bool; + default = false; + description = '' + Indicates if the user is a system user or not. This option + only has an effect if <option>uid</option> is + <option>null</option>, in which case it determines whether + the user's UID is allocated in the range for system users + (below 500) or in the range for normal users (starting at + 1000). + ''; + }; + + isNormalUser = mkOption { + type = types.bool; + default = false; + description = '' + Indicates whether this is an account for a “real” user. This + automatically sets <option>group</option> to + <literal>users</literal>, <option>createHome</option> to + <literal>true</literal>, <option>home</option> to + <filename>/home/<replaceable>username</replaceable></filename>, + <option>useDefaultShell</option> to <literal>true</literal>, + and <option>isSystemUser</option> to + <literal>false</literal>. + ''; + }; + + group = mkOption { + type = types.str; + apply = x: assert (builtins.stringLength x < 32 || abort "Group name '${x}' is longer than 31 characters which is not allowed!"); x; + default = "nogroup"; + description = "The user's primary group."; + }; + + extraGroups = mkOption { + type = types.listOf types.str; + default = []; + description = "The user's auxiliary groups."; + }; + + home = mkOption { + type = types.path; + default = "/var/empty"; + description = "The user's home directory."; + }; + + cryptHomeLuks = mkOption { + type = with types; nullOr str; + default = null; + description = '' + Path to encrypted luks device that contains + the user's home directory. + ''; + }; + + shell = mkOption { + type = types.either types.shellPackage types.path; + default = pkgs.shadow; + defaultText = "pkgs.shadow"; + example = literalExample "pkgs.bashInteractive"; + description = '' + The path to the user's shell. Can use shell derivations, + like <literal>pkgs.bashInteractive</literal>. Don’t + forget to enable your shell in + <literal>programs</literal> if necessary, + like <code>programs.zsh.enable = true;</code>. + ''; + }; + + subUidRanges = mkOption { + type = with types; listOf (submodule subordinateUidRange); + default = []; + example = [ + { startUid = 1000; count = 1; } + { startUid = 100001; count = 65534; } + ]; + description = '' + Subordinate user ids that user is allowed to use. + They are set into <filename>/etc/subuid</filename> and are used + by <literal>newuidmap</literal> for user namespaces. + ''; + }; + + subGidRanges = mkOption { + type = with types; listOf (submodule subordinateGidRange); + default = []; + example = [ + { startGid = 100; count = 1; } + { startGid = 1001; count = 999; } + ]; + description = '' + Subordinate group ids that user is allowed to use. + They are set into <filename>/etc/subgid</filename> and are used + by <literal>newgidmap</literal> for user namespaces. + ''; + }; + + createHome = mkOption { + type = types.bool; + default = false; + description = '' + If true, the home directory will be created automatically. If this + option is true and the home directory already exists but is not + owned by the user, directory owner and group will be changed to + match the user. + ''; + }; + + useDefaultShell = mkOption { + type = types.bool; + default = false; + description = '' + If true, the user's shell will be set to + <option>users.defaultUserShell</option>. + ''; + }; + + hashedPassword = mkOption { + type = with types; uniq (nullOr str); + default = null; + description = '' + Specifies the hashed password for the user. + ${passwordDescription} + ${hashedPasswordDescription} + ''; + }; + + password = mkOption { + type = with types; uniq (nullOr str); + default = null; + description = '' + Specifies the (clear text) password for the user. + Warning: do not set confidential information here + because it is world-readable in the Nix store. This option + should only be used for public accounts. + ${passwordDescription} + ''; + }; + + passwordFile = mkOption { + type = with types; uniq (nullOr string); + default = null; + description = '' + The full path to a file that contains the user's password. The password + file is read on each system activation. The file should contain + exactly one line, which should be the password in an encrypted form + that is suitable for the <literal>chpasswd -e</literal> command. + ${passwordDescription} + ''; + }; + + initialHashedPassword = mkOption { + type = with types; uniq (nullOr str); + default = null; + description = '' + Specifies the initial hashed password for the user, i.e. the + hashed password assigned if the user does not already + exist. If <option>users.mutableUsers</option> is true, the + password can be changed subsequently using the + <command>passwd</command> command. Otherwise, it's + equivalent to setting the <option>hashedPassword</option> option. + + ${hashedPasswordDescription} + ''; + }; + + initialPassword = mkOption { + type = with types; uniq (nullOr str); + default = null; + description = '' + Specifies the initial password for the user, i.e. the + password assigned if the user does not already exist. If + <option>users.mutableUsers</option> is true, the password + can be changed subsequently using the + <command>passwd</command> command. Otherwise, it's + equivalent to setting the <option>password</option> + option. The same caveat applies: the password specified here + is world-readable in the Nix store, so it should only be + used for guest accounts or passwords that will be changed + promptly. + ''; + }; + + packages = mkOption { + type = types.listOf types.package; + default = []; + example = literalExample "[ pkgs.firefox pkgs.thunderbird ]"; + description = '' + The set of packages that should be made availabe to the user. + This is in contrast to <option>environment.systemPackages</option>, + which adds packages to all users. + ''; + }; + + }; + + config = mkMerge + [ { name = mkDefault name; + shell = mkIf config.useDefaultShell (mkDefault cfg.defaultUserShell); + } + (mkIf config.isNormalUser { + group = mkDefault "users"; + createHome = mkDefault true; + home = mkDefault "/home/${config.name}"; + useDefaultShell = mkDefault true; + isSystemUser = mkDefault false; + }) + # If !mutableUsers, setting ‘initialPassword’ is equivalent to + # setting ‘password’ (and similarly for hashed passwords). + (mkIf (!cfg.mutableUsers && config.initialPassword != null) { + password = mkDefault config.initialPassword; + }) + (mkIf (!cfg.mutableUsers && config.initialHashedPassword != null) { + hashedPassword = mkDefault config.initialHashedPassword; + }) + ]; + + }; + + groupOpts = { name, ... }: { + + options = { + + name = mkOption { + type = types.str; + description = '' + The name of the group. If undefined, the name of the attribute set + will be used. + ''; + }; + + gid = mkOption { + type = with types; nullOr int; + default = null; + description = '' + The group GID. If the GID is null, a free GID is picked on + activation. + ''; + }; + + members = mkOption { + type = with types; listOf string; + default = []; + description = '' + The user names of the group members, added to the + <literal>/etc/group</literal> file. + ''; + }; + + }; + + config = { + name = mkDefault name; + }; + + }; + + subordinateUidRange = { + options = { + startUid = mkOption { + type = types.int; + description = '' + Start of the range of subordinate user ids that user is + allowed to use. + ''; + }; + count = mkOption { + type = types.int; + default = 1; + description = ''Count of subordinate user ids''; + }; + }; + }; + + subordinateGidRange = { + options = { + startGid = mkOption { + type = types.int; + description = '' + Start of the range of subordinate group ids that user is + allowed to use. + ''; + }; + count = mkOption { + type = types.int; + default = 1; + description = ''Count of subordinate group ids''; + }; + }; + }; + + mkSubuidEntry = user: concatStrings ( + map (range: "${user.name}:${toString range.startUid}:${toString range.count}\n") + user.subUidRanges); + + subuidFile = concatStrings (map mkSubuidEntry (attrValues cfg.users)); + + mkSubgidEntry = user: concatStrings ( + map (range: "${user.name}:${toString range.startGid}:${toString range.count}\n") + user.subGidRanges); + + subgidFile = concatStrings (map mkSubgidEntry (attrValues cfg.users)); + + idsAreUnique = set: idAttr: !(fold (name: args@{ dup, acc }: + let + id = builtins.toString (builtins.getAttr idAttr (builtins.getAttr name set)); + exists = builtins.hasAttr id acc; + newAcc = acc // (builtins.listToAttrs [ { name = id; value = true; } ]); + in if dup then args else if exists + then builtins.trace "Duplicate ${idAttr} ${id}" { dup = true; acc = null; } + else { dup = false; acc = newAcc; } + ) { dup = false; acc = {}; } (builtins.attrNames set)).dup; + + uidsAreUnique = idsAreUnique (filterAttrs (n: u: u.uid != null) cfg.users) "uid"; + gidsAreUnique = idsAreUnique (filterAttrs (n: g: g.gid != null) cfg.groups) "gid"; + + spec = pkgs.writeText "users-groups.json" (builtins.toJSON { + inherit (cfg) mutableUsers; + users = mapAttrsToList (_: u: + { inherit (u) + name uid group description home createHome isSystemUser + password passwordFile hashedPassword + initialPassword initialHashedPassword; + shell = utils.toShellPath u.shell; + }) cfg.users; + groups = mapAttrsToList (n: g: + { inherit (g) name gid; + members = g.members ++ (mapAttrsToList (n: u: u.name) ( + filterAttrs (n: u: elem g.name u.extraGroups) cfg.users + )); + }) cfg.groups; + }); + + systemShells = + let + shells = mapAttrsToList (_: u: u.shell) cfg.users; + in + filter types.shellPackage.check shells; + +in { + + ###### interface + + options = { + + users.mutableUsers = mkOption { + type = types.bool; + default = true; + description = '' + If set to <literal>true</literal>, you are free to add new users and groups to the system + with the ordinary <literal>useradd</literal> and + <literal>groupadd</literal> commands. On system activation, the + existing contents of the <literal>/etc/passwd</literal> and + <literal>/etc/group</literal> files will be merged with the + contents generated from the <literal>users.users</literal> and + <literal>users.groups</literal> options. + The initial password for a user will be set + according to <literal>users.users</literal>, but existing passwords + will not be changed. + + <warning><para> + If set to <literal>false</literal>, the contents of the user and + group files will simply be replaced on system activation. This also + holds for the user passwords; all changed + passwords will be reset according to the + <literal>users.users</literal> configuration on activation. + </para></warning> + ''; + }; + + users.enforceIdUniqueness = mkOption { + type = types.bool; + default = true; + description = '' + Whether to require that no two users/groups share the same uid/gid. + ''; + }; + + users.users = mkOption { + default = {}; + type = with types; loaOf (submodule userOpts); + example = { + alice = { + uid = 1234; + description = "Alice Q. User"; + home = "/home/alice"; + createHome = true; + group = "users"; + extraGroups = ["wheel"]; + shell = "/bin/sh"; + }; + }; + description = '' + Additional user accounts to be created automatically by the system. + This can also be used to set options for root. + ''; + }; + + users.groups = mkOption { + default = {}; + example = + { students.gid = 1001; + hackers = { }; + }; + type = with types; loaOf (submodule groupOpts); + description = '' + Additional groups to be created automatically by the system. + ''; + }; + + # FIXME: obsolete - will remove. + security.initialRootPassword = mkOption { + type = types.str; + default = "!"; + example = ""; + visible = false; + }; + + }; + + + ###### implementation + + config = { + + users.users = { + root = { + uid = ids.uids.root; + description = "System administrator"; + home = "/root"; + shell = mkDefault cfg.defaultUserShell; + group = "root"; + initialHashedPassword = mkDefault config.security.initialRootPassword; + }; + nobody = { + uid = ids.uids.nobody; + description = "Unprivileged account (don't use!)"; + group = "nogroup"; + }; + }; + + users.groups = { + root.gid = ids.gids.root; + wheel.gid = ids.gids.wheel; + disk.gid = ids.gids.disk; + kmem.gid = ids.gids.kmem; + tty.gid = ids.gids.tty; + floppy.gid = ids.gids.floppy; + uucp.gid = ids.gids.uucp; + lp.gid = ids.gids.lp; + cdrom.gid = ids.gids.cdrom; + tape.gid = ids.gids.tape; + audio.gid = ids.gids.audio; + video.gid = ids.gids.video; + dialout.gid = ids.gids.dialout; + nogroup.gid = ids.gids.nogroup; + users.gid = ids.gids.users; + nixbld.gid = ids.gids.nixbld; + utmp.gid = ids.gids.utmp; + adm.gid = ids.gids.adm; + input.gid = ids.gids.input; + kvm.gid = ids.gids.kvm; + render.gid = ids.gids.render; + }; + + system.activationScripts.users = stringAfter [ "stdio" ] + '' + install -m 0700 -d /root + install -m 0755 -d /home + + ${pkgs.perl}/bin/perl -w \ + -I${pkgs.perlPackages.FileSlurp}/${pkgs.perl.libPrefix} \ + -I${pkgs.perlPackages.JSON}/${pkgs.perl.libPrefix} \ + ${./update-users-groups.pl} ${spec} + ''; + + # for backwards compatibility + system.activationScripts.groups = stringAfter [ "users" ] ""; + + # Install all the user shells + environment.systemPackages = systemShells; + + environment.etc = { + "subuid" = { + text = subuidFile; + mode = "0644"; + }; + "subgid" = { + text = subgidFile; + mode = "0644"; + }; + } // (mapAttrs' (name: { packages, ... }: { + name = "profiles/per-user/${name}"; + value.source = pkgs.buildEnv { + name = "user-environment"; + paths = packages; + inherit (config.environment) pathsToLink extraOutputsToInstall; + inherit (config.system.path) ignoreCollisions postBuild; + }; + }) (filterAttrs (_: u: u.packages != []) cfg.users)); + + environment.profiles = [ "/etc/profiles/per-user/$USER" ]; + + assertions = [ + { assertion = !cfg.enforceIdUniqueness || (uidsAreUnique && gidsAreUnique); + message = "UIDs and GIDs must be unique!"; + } + { # If mutableUsers is false, to prevent users creating a + # configuration that locks them out of the system, ensure that + # there is at least one "privileged" account that has a + # password or an SSH authorized key. Privileged accounts are + # root and users in the wheel group. + assertion = !cfg.mutableUsers -> + any id (mapAttrsToList (name: cfg: + (name == "root" + || cfg.group == "wheel" + || elem "wheel" cfg.extraGroups) + && + ((cfg.hashedPassword != null && cfg.hashedPassword != "!") + || cfg.password != null + || cfg.passwordFile != null + || cfg.openssh.authorizedKeys.keys != [] + || cfg.openssh.authorizedKeys.keyFiles != []) + ) cfg.users); + message = '' + Neither the root account nor any wheel user has a password or SSH authorized key. + You must set one to prevent being locked out of your system.''; + } + ]; + + }; + +} |