diff --git a/nixos/modules/config/users-groups.nix b/nixos/modules/config/users-groups.nix
index 714de646eb7..97bf6726279 100644
--- a/nixos/modules/config/users-groups.nix
+++ b/nixos/modules/config/users-groups.nix
@@ -5,7 +5,7 @@ with pkgs.lib;
let
ids = config.ids;
- users = config.users;
+ cfg = config.users;
userOpts = { name, config, ... }: {
@@ -28,9 +28,8 @@ let
};
uid = mkOption {
- type = with types; uniq (nullOr int);
- default = null;
- description = "The account UID. If undefined, NixOS will select a free UID.";
+ type = with types; uniq int;
+ description = "The account UID.";
};
group = mkOption {
@@ -60,13 +59,21 @@ let
createHome = mkOption {
type = types.bool;
default = false;
- description = "If true, the home directory will be created automatically.";
+ 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 users.defaultUserShell.";
+ description = ''
+ If true, the user's shell will be set to
+ cfg.defaultUserShell.
+ '';
};
password = mkOption {
@@ -78,13 +85,29 @@ let
because it is world-readable in the Nix store. This option
should only be used for public accounts such as
guest.
+ The option password overrides
+ passwordFile, if both are specified.
+ If none of the options password or
+ passwordFile are specified, the user account will
+ be locked for password logins. This is the default behavior except
+ for the root account, which has an empty password by default. If you
+ want to lock the root account for password logins, set
+ users.extraUsers.root.password to
+ null.
'';
};
- isSystemUser = mkOption {
- type = types.bool;
- default = true;
- description = "Indicates if the user is a system user or not.";
+ passwordFile = mkOption {
+ type = with types; uniq (nullOr string);
+ default = null;
+ description = ''
+ The 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 chpasswd -e command.
+ See the password for more details on how passwords
+ are assigned.
+ '';
};
createUser = mkOption {
@@ -96,19 +119,11 @@ let
then not modify any of the basic properties for the user account.
'';
};
-
- isAlias = mkOption {
- type = types.bool;
- default = false;
- description = "If true, the UID of this user is not required to be unique and can thus alias another user.";
- };
-
};
config = {
name = mkDefault name;
- uid = mkDefault (attrByPath [name] null ids.uids);
- shell = mkIf config.useDefaultShell (mkDefault users.defaultUserShell);
+ shell = mkIf config.useDefaultShell (mkDefault cfg.defaultUserShell);
};
};
@@ -123,28 +138,100 @@ let
};
gid = mkOption {
- type = with types; uniq (nullOr int);
- default = null;
- description = "The GID of the group. If undefined, NixOS will select a free GID.";
+ type = with types; uniq int;
+ description = "The GID of the group.";
+ };
+
+ members = mkOption {
+ type = with types; listOf string;
+ default = [];
+ description = ''
+ '';
};
};
config = {
name = mkDefault name;
- gid = mkDefault (attrByPath [name] null ids.gids);
};
};
- # Note: the 'X' in front of the password is to distinguish between
- # having an empty password, and not having a password.
- serializedUser = u: "${u.name}\n${u.description}\n${if u.uid != null then toString u.uid else ""}\n${u.group}\n${toString (concatStringsSep "," u.extraGroups)}\n${u.home}\n${u.shell}\n${toString u.createHome}\n${if u.password != null then "X" + u.password else ""}\n${toString u.isSystemUser}\n${toString u.createUser}\n${toString u.isAlias}\n";
-
- usersFile = pkgs.writeText "users" (
+ getGroup = gname:
let
- p = partition (u: u.isAlias) (attrValues config.users.extraUsers);
- in concatStrings (map serializedUser p.wrong ++ map serializedUser p.right));
+ groups = mapAttrsToList (n: g: g) (
+ filterAttrs (n: g: g.name == gname) cfg.extraGroups
+ );
+ in
+ if length groups == 1 then head groups
+ else if groups == [] then throw "Group ${gname} not defined"
+ else throw "Group ${gname} has multiple definitions";
+
+ getUser = uname:
+ let
+ users = mapAttrsToList (n: u: u) (
+ filterAttrs (n: u: u.name == uname) cfg.extraUsers
+ );
+ in
+ if length users == 1 then head users
+ else if users == [] then throw "User ${uname} not defined"
+ else throw "User ${uname} has multiple definitions";
+
+ mkGroupEntry = gname:
+ let
+ g = getGroup gname;
+ users = mapAttrsToList (n: u: u.name) (
+ filterAttrs (n: u: elem g.name u.extraGroups) cfg.extraUsers
+ );
+ in concatStringsSep ":" [
+ g.name "x" (toString g.gid)
+ (concatStringsSep "," (users ++ (filter (u: !(elem u users)) g.members)))
+ ];
+
+ mkPasswdEntry = uname: let u = getUser uname; in
+ concatStringsSep ":" [
+ u.name "x" (toString u.uid)
+ (toString (getGroup u.group).gid)
+ u.description u.home u.shell
+ ];
+
+ sortOn = a: sort (as1: as2: lessThan (getAttr a as1) (getAttr a as2));
+
+ groupFile = pkgs.writeText "group" (
+ concatStringsSep "\n" (map (g: mkGroupEntry g.name) (
+ sortOn "gid" (attrValues cfg.extraGroups)
+ ))
+ );
+
+ passwdFile = pkgs.writeText "passwd" (
+ concatStringsSep "\n" (map (u: mkPasswdEntry u.name) (
+ sortOn "uid" (filter (u: u.createUser) (attrValues cfg.extraUsers))
+ ))
+ );
+
+ # If mutableUsers is true, this script adds all users/groups defined in
+ # users.extra{Users,Groups} to /etc/{passwd,group} iff there isn't any
+ # existing user/group with the same name in those files.
+ # If mutableUsers is false, the /etc/{passwd,group} files will simply be
+ # replaced with the users/groups defined in the NixOS configuration.
+ # The merging procedure could certainly be improved, and instead of just
+ # keeping the lines as-is from /etc/{passwd,group} they could be combined
+ # in some way with the generated content from the NixOS configuration.
+ merger = src: pkgs.writeScript "merger" ''
+ #!${pkgs.bash}/bin/bash
+
+ PATH=${pkgs.gawk}/bin:${pkgs.gnugrep}/bin:$PATH
+
+ ${if !cfg.mutableUsers
+ then ''cp ${src} $1.tmp''
+ else ''awk -F: '{ print "^"$1":.*" }' $1 | egrep -vf - ${src} | cat $1 - > $1.tmp''
+ }
+
+ # set mtime to +1, otherwise change might go unnoticed (vipw/vigr only looks at mtime)
+ touch -m -t $(date -d @$(($(stat -c %Y $1)+1)) +%Y%m%d%H%M.%S) $1.tmp
+
+ mv -f $1.tmp $1
+ '';
in
@@ -154,6 +241,28 @@ in
options = {
+ users.mutableUsers = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ If true, you are free to add new users and groups to the system
+ with the ordinary useradd and
+ groupadd commands. On system activation, the
+ existing contents of the /etc/passwd and
+ /etc/group files will be merged with the
+ contents generated from the users.extraUsers and
+ users.extraGroups options. If
+ mutableUsers is false, the contents of the user and
+ group files will simply be replaced on system activation. This also
+ holds for the user passwords; if this option is false, all changed
+ passwords will be reset according to the
+ users.extraUsers configuration on activation. If
+ this option is true, the initial password for a user will be set
+ according to users.extraUsers, but existing passwords
+ will not be changed.
+ '';
+ };
+
users.extraUsers = mkOption {
default = {};
type = types.loaOf types.optionSet;
@@ -188,20 +297,6 @@ in
options = [ groupOpts ];
};
- security.initialRootPassword = mkOption {
- type = types.str;
- default = "";
- example = "!";
- description = ''
- The (hashed) password for the root account set on initial
- installation. The empty string denotes that root can login
- locally without a password (but not via remote services such
- as SSH, or indirectly via su or
- sudo). The string !
- prevents root from logging in using a password.
- '';
- };
-
};
@@ -211,144 +306,88 @@ in
users.extraUsers = {
root = {
+ uid = ids.uids.root;
description = "System administrator";
home = "/root";
- shell = config.users.defaultUserShell;
+ shell = cfg.defaultUserShell;
group = "root";
+ password = mkDefault "";
};
nobody = {
+ uid = ids.uids.nobody;
description = "Unprivileged account (don't use!)";
+ group = "nogroup";
};
};
users.extraGroups = {
- root = { };
- wheel = { };
- disk = { };
- kmem = { };
- tty = { };
- floppy = { };
- uucp = { };
- lp = { };
- cdrom = { };
- tape = { };
- audio = { };
- video = { };
- dialout = { };
- nogroup = { };
- users = { };
- nixbld = { };
- utmp = { };
- adm = { }; # expected by journald
+ 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;
};
- system.activationScripts.rootPasswd = stringAfter [ "etc" ]
- ''
- # If there is no password file yet, create a root account with an
- # empty password.
- if ! test -e /etc/passwd; then
- rootHome=/root
- touch /etc/passwd; chmod 0644 /etc/passwd
- touch /etc/group; chmod 0644 /etc/group
- touch /etc/shadow; chmod 0600 /etc/shadow
- # Can't use useradd, since it complains that it doesn't know us
- # (bootstrap problem!).
- echo "root:x:0:0:System administrator:$rootHome:${config.users.defaultUserShell}" >> /etc/passwd
- echo "root:${config.security.initialRootPassword}:::::::" >> /etc/shadow
- fi
+ system.activationScripts.users =
+ let
+ mkhomeUsers = filterAttrs (n: u: u.createHome) cfg.extraUsers;
+ setpwUsers = filterAttrs (n: u: u.createUser) cfg.extraUsers;
+ setpw = n: u: ''
+ setpw=yes
+ ${optionalString cfg.mutableUsers ''
+ test "$(getent shadow '${u.name}' | cut -d: -f2)" != "x" && setpw=no
+ ''}
+ if [ "$setpw" == "yes" ]; then
+ ${if u.password == ""
+ then "passwd -d '${u.name}' &>/dev/null"
+ else if (isNull u.password && isNull u.passwordFile)
+ then "passwd -l '${u.name}' &>/dev/null"
+ else if !(isNull u.password)
+ then ''
+ echo "${u.name}:${u.password}" | ${pkgs.shadow}/sbin/chpasswd''
+ else ''
+ echo -n "${u.name}:" | cat - "${u.passwordFile}" | \
+ ${pkgs.shadow}/sbin/chpasswd -e
+ ''
+ }
+ fi
+ '';
+ mkhome = n: u:
+ let
+ uid = toString u.uid;
+ gid = toString ((getGroup u.group).gid);
+ h = u.home;
+ in ''
+ test -a "${h}" || mkdir -p "${h}" || true
+ test "$(stat -c %u "${h}")" = ${uid} || chown ${uid} "${h}" || true
+ test "$(stat -c %g "${h}")" = ${gid} || chgrp ${gid} "${h}" || true
+ '';
+ in stringAfter [ "etc" ] ''
+ touch /etc/group
+ touch /etc/passwd
+ VISUAL=${merger groupFile} ${pkgs.shadow}/sbin/vigr &>/dev/null
+ VISUAL=${merger passwdFile} ${pkgs.shadow}/sbin/vipw &>/dev/null
+ ${pkgs.shadow}/sbin/grpconv
+ ${pkgs.shadow}/sbin/pwconv
+ ${concatStrings (mapAttrsToList mkhome mkhomeUsers)}
+ ${concatStrings (mapAttrsToList setpw setpwUsers)}
'';
- # Print a reminder for users to set a root password.
- environment.interactiveShellInit =
- ''
- if [ "$UID" = 0 ]; then
- read _l < /etc/shadow
- if [ "''${_l:0:6}" = root:: ]; then
- cat >&2 <