e0e65cbf8e
This hopefully fixes a regression introduced by08b214a
. Inbf129a2
, it was already fixed for normal uid/gid values and it got reintroduced by sub-uid/gid-handling again, so I've refactored it a bit into a filterNull function which takes care of also the filtering introduced bybf129a2
. I have not tested this extensively, but master is already broken for systems with `mutableUsers = true` and no uid values set. Signed-off-by: aszlig <aszlig@redmoonstudios.org>
604 lines
19 KiB
Nix
604 lines
19 KiB
Nix
{ config, lib, pkgs, ... }:
|
|
|
|
with lib;
|
|
|
|
let
|
|
|
|
ids = config.ids;
|
|
cfg = config.users;
|
|
|
|
nonUidUsers = filterAttrs (n: u: u.createUser && u.uid == null) cfg.extraUsers;
|
|
nonGidGroups = filterAttrs (n: g: g.gid == null) cfg.extraGroups;
|
|
|
|
passwordDescription = ''
|
|
The options <literal>hashedPassword</literal>,
|
|
<literal>password</literal> and <literal>passwordFile</literal>
|
|
controls what password is set for the user.
|
|
<literal>hashedPassword</literal> overrides both
|
|
<literal>password</literal> and <literal>passwordFile</literal>.
|
|
<literal>password</literal> overrides <literal>passwordFile</literal>.
|
|
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 <literal>users.mutableUsers</literal> 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
|
|
<literal>users.mutableUsers</literal> is false, you cannot change
|
|
user passwords, they will always be set according to the password
|
|
options.
|
|
'';
|
|
|
|
userOpts = { name, config, ... }: {
|
|
|
|
options = {
|
|
|
|
name = mkOption {
|
|
type = types.str;
|
|
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 <option>mutableUsers</option> option
|
|
is false, the UID cannot be null. Otherwise, the UID might be
|
|
null, in which case a free UID is picked on activation (by the
|
|
useradd command).
|
|
'';
|
|
};
|
|
|
|
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>mutableUsers</option> is
|
|
<literal>true</literal> and <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).
|
|
'';
|
|
};
|
|
|
|
group = mkOption {
|
|
type = types.str;
|
|
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.str;
|
|
default = "/var/empty";
|
|
description = "The user's home directory.";
|
|
};
|
|
|
|
shell = mkOption {
|
|
type = types.str;
|
|
default = "/run/current-system/sw/sbin/nologin";
|
|
description = "The path to the user's shell.";
|
|
};
|
|
|
|
subUidRanges = mkOption {
|
|
type = types.listOf types.optionSet;
|
|
default = [];
|
|
example = [
|
|
{ startUid = 1000; count = 1; }
|
|
{ startUid = 100001; count = 65534; }
|
|
];
|
|
options = [ subordinateUidRange ];
|
|
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 = types.listOf types.optionSet;
|
|
default = [];
|
|
example = [
|
|
{ startGid = 100; count = 1; }
|
|
{ startGid = 1001; count = 999; }
|
|
];
|
|
options = [ subordinateGidRange ];
|
|
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
|
|
<literal>cfg.defaultUserShell</literal>.
|
|
'';
|
|
};
|
|
|
|
hashedPassword = mkOption {
|
|
type = with types; uniq (nullOr str);
|
|
default = null;
|
|
description = ''
|
|
Specifies the (hashed) password for the user.
|
|
${passwordDescription}
|
|
'';
|
|
};
|
|
|
|
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 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}
|
|
'';
|
|
};
|
|
|
|
createUser = mkOption {
|
|
type = types.bool;
|
|
default = true;
|
|
description = ''
|
|
Indicates if the user should be created automatically as a local user.
|
|
Set this to false if the user for instance is an LDAP user. NixOS will
|
|
then not modify any of the basic properties for the user account.
|
|
'';
|
|
};
|
|
};
|
|
|
|
config = {
|
|
name = mkDefault name;
|
|
shell = mkIf config.useDefaultShell (mkDefault cfg.defaultUserShell);
|
|
};
|
|
|
|
};
|
|
|
|
groupOpts = { name, config, ... }: {
|
|
|
|
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 <literal>mutableUsers</literal> option
|
|
is false, the GID cannot be null. Otherwise, the GID might be
|
|
null, in which case a free GID is picked on activation (by the
|
|
groupadd command).
|
|
'';
|
|
};
|
|
|
|
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 = {
|
|
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 = {
|
|
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'';
|
|
};
|
|
};
|
|
|
|
getGroup = gname:
|
|
let
|
|
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
|
|
];
|
|
|
|
filterNull = a: filter (x: hasAttr a x && getAttr a x != null);
|
|
|
|
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" (filterNull "gid" (attrValues cfg.extraGroups))
|
|
))
|
|
);
|
|
|
|
passwdFile = pkgs.writeText "passwd" (
|
|
concatStringsSep "\n" (map (u: mkPasswdEntry u.name) (
|
|
sortOn "uid" (filterNull "uid" (attrValues cfg.extraUsers))
|
|
))
|
|
);
|
|
|
|
mkSubuidEntry = user: concatStrings (
|
|
map (range: "${user.name}:${toString range.startUid}:${toString range.count}\n")
|
|
user.subUidRanges);
|
|
|
|
subuidFile = concatStrings (map mkSubuidEntry (
|
|
sortOn "uid" (filterNull "uid" (attrValues cfg.extraUsers))));
|
|
|
|
mkSubgidEntry = user: concatStrings (
|
|
map (range: "${user.name}:${toString range.startGid}:${toString range.count}\n")
|
|
user.subGidRanges);
|
|
|
|
subgidFile = concatStrings (map mkSubgidEntry (
|
|
sortOn "uid" (filterNull "uid" (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
|
|
'';
|
|
|
|
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.extraUsers) "uid";
|
|
gidsAreUnique = idsAreUnique (filterAttrs (n: g: g.gid != null) cfg.extraGroups) "gid";
|
|
|
|
in {
|
|
|
|
###### interface
|
|
|
|
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 <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.extraUsers</literal> and
|
|
<literal>users.extraGroups</literal> options. If
|
|
<literal>mutableUsers</literal> 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
|
|
<literal>users.extraUsers</literal> configuration on activation. If
|
|
this option is true, the initial password for a user will be set
|
|
according to <literal>users.extraUsers</literal>, but existing passwords
|
|
will not be changed.
|
|
'';
|
|
};
|
|
|
|
users.enforceIdUniqueness = mkOption {
|
|
type = types.bool;
|
|
default = true;
|
|
description = ''
|
|
Whether to require that no two users/groups share the same uid/gid.
|
|
'';
|
|
};
|
|
|
|
users.extraUsers = mkOption {
|
|
default = {};
|
|
type = types.loaOf types.optionSet;
|
|
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.
|
|
'';
|
|
options = [ userOpts ];
|
|
};
|
|
|
|
users.extraGroups = mkOption {
|
|
default = {};
|
|
example =
|
|
{ students.gid = 1001;
|
|
hackers = { };
|
|
};
|
|
type = types.loaOf types.optionSet;
|
|
description = ''
|
|
Additional groups to be created automatically by the system.
|
|
'';
|
|
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 <command>su</command> or
|
|
<command>sudo</command>). The string <literal>!</literal>
|
|
prevents root from logging in using a password.
|
|
Note that setting this option sets
|
|
<literal>users.extraUsers.root.hashedPassword</literal>.
|
|
Also, if <literal>users.mutableUsers</literal> is false
|
|
you cannot change the root password manually, so in that case
|
|
the name of this option is a bit misleading, since it will define
|
|
the root password beyond the user initialisation phase.
|
|
'';
|
|
};
|
|
|
|
};
|
|
|
|
|
|
###### implementation
|
|
|
|
config = {
|
|
|
|
users.extraUsers = {
|
|
root = {
|
|
uid = ids.uids.root;
|
|
description = "System administrator";
|
|
home = "/root";
|
|
shell = mkDefault cfg.defaultUserShell;
|
|
group = "root";
|
|
extraGroups = [ "grsecurity" ];
|
|
hashedPassword = mkDefault config.security.initialRootPassword;
|
|
};
|
|
nobody = {
|
|
uid = ids.uids.nobody;
|
|
description = "Unprivileged account (don't use!)";
|
|
group = "nogroup";
|
|
};
|
|
};
|
|
|
|
users.extraGroups = {
|
|
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;
|
|
grsecurity.gid = ids.gids.grsecurity;
|
|
};
|
|
|
|
system.activationScripts.users =
|
|
let
|
|
mkhomeUsers = filterAttrs (n: u: u.createHome) cfg.extraUsers;
|
|
setpwUsers = filterAttrs (n: u: u.createUser) cfg.extraUsers;
|
|
pwFile = u: if !(isNull u.hashedPassword)
|
|
then pkgs.writeTextFile { name = "password-file"; text = u.hashedPassword; }
|
|
else if !(isNull u.password)
|
|
then pkgs.runCommand "password-file" { pw = u.password; } ''
|
|
echo -n "$pw" | ${pkgs.mkpasswd}/bin/mkpasswd -s > $out
|
|
'' else u.passwordFile;
|
|
setpw = n: u: ''
|
|
setpw=yes
|
|
${optionalString cfg.mutableUsers ''
|
|
test "$(getent shadow '${u.name}' | cut -d: -f2)" != "x" && setpw=no
|
|
''}
|
|
if [ "$setpw" == "yes" ]; then
|
|
${if !(isNull (pwFile u))
|
|
then ''
|
|
echo -n "${u.name}:" | cat - "${pwFile u}" | \
|
|
${pkgs.shadow}/sbin/chpasswd -e
|
|
''
|
|
else "passwd -l '${u.name}' &>/dev/null"
|
|
}
|
|
fi
|
|
'';
|
|
mkhome = n: u: ''
|
|
uid="$(id -u ${u.name})"
|
|
gid="$(id -g ${u.name})"
|
|
h="${u.home}"
|
|
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
|
|
'';
|
|
groupadd = n: g: ''
|
|
if [ -z "$(getent group "${g.name}")" ]; then
|
|
${pkgs.shadow}/sbin/groupadd "${g.name}"
|
|
fi
|
|
'';
|
|
useradd = n: u: ''
|
|
if ! id "${u.name}" &>/dev/null; then
|
|
${pkgs.shadow}/sbin/useradd \
|
|
-g "${u.group}" \
|
|
-G "${concatStringsSep "," u.extraGroups}" \
|
|
-s "${u.shell}" \
|
|
-d "${u.home}" \
|
|
${optionalString u.isSystemUser "--system"} \
|
|
"${u.name}"
|
|
echo "${u.name}:x" | ${pkgs.shadow}/sbin/chpasswd -e
|
|
fi
|
|
'';
|
|
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 groupadd nonGidGroups)}
|
|
${concatStrings (mapAttrsToList useradd nonUidUsers)}
|
|
${concatStrings (mapAttrsToList mkhome mkhomeUsers)}
|
|
${concatStrings (mapAttrsToList setpw setpwUsers)}
|
|
'';
|
|
|
|
# for backwards compatibility
|
|
system.activationScripts.groups = stringAfter [ "users" ] "";
|
|
|
|
environment.etc."subuid" = {
|
|
text = subuidFile;
|
|
mode = "0644";
|
|
};
|
|
environment.etc."subgid" = {
|
|
text = subgidFile;
|
|
mode = "0644";
|
|
};
|
|
|
|
assertions = [
|
|
{ assertion = !cfg.enforceIdUniqueness || (uidsAreUnique && gidsAreUnique);
|
|
message = "uids and gids must be unique!";
|
|
}
|
|
{ assertion = cfg.mutableUsers || (nonUidUsers == {});
|
|
message = "When mutableUsers is false, no uid can be null: ${toString (attrNames nonUidUsers)}";
|
|
}
|
|
{ assertion = cfg.mutableUsers || (nonGidGroups == {});
|
|
message = "When mutableUsers is false, no gid can be null";
|
|
}
|
|
];
|
|
|
|
};
|
|
|
|
}
|