nixos/users-groups: Add dry mode

This commit is contained in:
Janne Heß 2021-09-03 17:21:36 +02:00
parent 3156730402
commit a851b4d20e
No known key found for this signature in database
GPG key ID: 69165158F05265DF
3 changed files with 60 additions and 17 deletions

View file

@ -1,11 +1,10 @@
use strict; use strict;
use warnings;
use File::Path qw(make_path); use File::Path qw(make_path);
use File::Slurp; use File::Slurp;
use Getopt::Long;
use JSON; use JSON;
make_path("/var/lib/nixos", { mode => 0755 });
# Keep track of deleted uids and gids. # Keep track of deleted uids and gids.
my $uidMapFile = "/var/lib/nixos/uid-map"; my $uidMapFile = "/var/lib/nixos/uid-map";
my $uidMap = -e $uidMapFile ? decode_json(read_file($uidMapFile)) : {}; my $uidMap = -e $uidMapFile ? decode_json(read_file($uidMapFile)) : {};
@ -13,12 +12,19 @@ my $uidMap = -e $uidMapFile ? decode_json(read_file($uidMapFile)) : {};
my $gidMapFile = "/var/lib/nixos/gid-map"; my $gidMapFile = "/var/lib/nixos/gid-map";
my $gidMap = -e $gidMapFile ? decode_json(read_file($gidMapFile)) : {}; my $gidMap = -e $gidMapFile ? decode_json(read_file($gidMapFile)) : {};
my $is_dry = ($ENV{'NIXOS_ACTION'} // "") eq "dry-activate";
GetOptions("dry-activate" => \$is_dry);
make_path("/var/lib/nixos", { mode => 0755 }) unless $is_dry;
sub updateFile { sub updateFile {
my ($path, $contents, $perms) = @_; my ($path, $contents, $perms) = @_;
return if $is_dry;
write_file($path, { atomic => 1, binmode => ':utf8', perms => $perms // 0644 }, $contents) or die; write_file($path, { atomic => 1, binmode => ':utf8', perms => $perms // 0644 }, $contents) or die;
} }
sub nscdInvalidate {
system("nscd", "--invalidate", $_[0]) unless $is_dry;
}
sub hashPassword { sub hashPassword {
my ($password) = @_; my ($password) = @_;
@ -28,6 +34,14 @@ sub hashPassword {
return crypt($password, '$6$' . $salt . '$'); return crypt($password, '$6$' . $salt . '$');
} }
sub dry_print {
if ($is_dry) {
print STDERR ("$_[1] $_[2]\n")
} else {
print STDERR ("$_[0] $_[2]\n")
}
}
# Functions for allocating free GIDs/UIDs. FIXME: respect ID ranges in # Functions for allocating free GIDs/UIDs. FIXME: respect ID ranges in
# /etc/login.defs. # /etc/login.defs.
@ -51,7 +65,7 @@ sub allocGid {
my ($name) = @_; my ($name) = @_;
my $prevGid = $gidMap->{$name}; my $prevGid = $gidMap->{$name};
if (defined $prevGid && !defined $gidsUsed{$prevGid}) { if (defined $prevGid && !defined $gidsUsed{$prevGid}) {
print STDERR "reviving group '$name' with GID $prevGid\n"; dry_print("reviving", "would revive", "group '$name' with GID $prevGid");
$gidsUsed{$prevGid} = 1; $gidsUsed{$prevGid} = 1;
return $prevGid; return $prevGid;
} }
@ -63,15 +77,14 @@ sub allocUid {
my ($min, $max, $up) = $isSystemUser ? (400, 999, 0) : (1000, 29999, 1); my ($min, $max, $up) = $isSystemUser ? (400, 999, 0) : (1000, 29999, 1);
my $prevUid = $uidMap->{$name}; my $prevUid = $uidMap->{$name};
if (defined $prevUid && $prevUid >= $min && $prevUid <= $max && !defined $uidsUsed{$prevUid}) { if (defined $prevUid && $prevUid >= $min && $prevUid <= $max && !defined $uidsUsed{$prevUid}) {
print STDERR "reviving user '$name' with UID $prevUid\n"; dry_print("reviving", "would revive", "user '$name' with UID $prevUid");
$uidsUsed{$prevUid} = 1; $uidsUsed{$prevUid} = 1;
return $prevUid; return $prevUid;
} }
return allocId(\%uidsUsed, \%uidsPrevUsed, $min, $max, $up, sub { my ($uid) = @_; getpwuid($uid) }); return allocId(\%uidsUsed, \%uidsPrevUsed, $min, $max, $up, sub { my ($uid) = @_; getpwuid($uid) });
} }
# Read the declared users/groups
# Read the declared users/groups.
my $spec = decode_json(read_file($ARGV[0])); my $spec = decode_json(read_file($ARGV[0]));
# Don't allocate UIDs/GIDs that are manually assigned. # Don't allocate UIDs/GIDs that are manually assigned.
@ -134,7 +147,7 @@ foreach my $g (@{$spec->{groups}}) {
if (defined $existing) { if (defined $existing) {
$g->{gid} = $existing->{gid} if !defined $g->{gid}; $g->{gid} = $existing->{gid} if !defined $g->{gid};
if ($g->{gid} != $existing->{gid}) { if ($g->{gid} != $existing->{gid}) {
warn "warning: not applying GID change of group $name ($existing->{gid} -> $g->{gid})\n"; dry_print("warning: not applying", "warning: would not apply", "GID change of group $name ($existing->{gid} -> $g->{gid})");
$g->{gid} = $existing->{gid}; $g->{gid} = $existing->{gid};
} }
$g->{password} = $existing->{password}; # do we want this? $g->{password} = $existing->{password}; # do we want this?
@ -163,7 +176,7 @@ foreach my $name (keys %groupsCur) {
my $g = $groupsCur{$name}; my $g = $groupsCur{$name};
next if defined $groupsOut{$name}; next if defined $groupsOut{$name};
if (!$spec->{mutableUsers} || defined $declGroups{$name}) { if (!$spec->{mutableUsers} || defined $declGroups{$name}) {
print STDERR "removing group $name\n"; dry_print("removing group", "would remove group", "$name");
} else { } else {
$groupsOut{$name} = $g; $groupsOut{$name} = $g;
} }
@ -175,7 +188,7 @@ my @lines = map { join(":", $_->{name}, $_->{password}, $_->{gid}, $_->{members}
(sort { $a->{gid} <=> $b->{gid} } values(%groupsOut)); (sort { $a->{gid} <=> $b->{gid} } values(%groupsOut));
updateFile($gidMapFile, to_json($gidMap)); updateFile($gidMapFile, to_json($gidMap));
updateFile("/etc/group", \@lines); updateFile("/etc/group", \@lines);
system("nscd --invalidate group"); nscdInvalidate("group");
# Generate a new /etc/passwd containing the declared users. # Generate a new /etc/passwd containing the declared users.
my %usersOut; my %usersOut;
@ -196,7 +209,7 @@ foreach my $u (@{$spec->{users}}) {
if (defined $existing) { if (defined $existing) {
$u->{uid} = $existing->{uid} if !defined $u->{uid}; $u->{uid} = $existing->{uid} if !defined $u->{uid};
if ($u->{uid} != $existing->{uid}) { if ($u->{uid} != $existing->{uid}) {
warn "warning: not applying UID change of user $name ($existing->{uid} -> $u->{uid})\n"; dry_print("warning: not applying", "warning: would not apply", "UID change of user $name ($existing->{uid} -> $u->{uid})");
$u->{uid} = $existing->{uid}; $u->{uid} = $existing->{uid};
} }
} else { } else {
@ -211,7 +224,7 @@ foreach my $u (@{$spec->{users}}) {
# Ensure home directory incl. ownership and permissions. # Ensure home directory incl. ownership and permissions.
if ($u->{createHome}) { if ($u->{createHome}) {
make_path($u->{home}, { mode => 0700 }) if ! -e $u->{home}; make_path($u->{home}, { mode => 0700 }) if ! -e $u->{home} and ! $is_dry;
chown $u->{uid}, $u->{gid}, $u->{home}; chown $u->{uid}, $u->{gid}, $u->{home};
chmod 0700, $u->{home}; chmod 0700, $u->{home};
} }
@ -250,7 +263,7 @@ foreach my $name (keys %usersCur) {
my $u = $usersCur{$name}; my $u = $usersCur{$name};
next if defined $usersOut{$name}; next if defined $usersOut{$name};
if (!$spec->{mutableUsers} || defined $declUsers{$name}) { if (!$spec->{mutableUsers} || defined $declUsers{$name}) {
print STDERR "removing user $name\n"; dry_print("removing user", "would remove user", "$name");
} else { } else {
$usersOut{$name} = $u; $usersOut{$name} = $u;
} }
@ -261,7 +274,7 @@ foreach my $name (keys %usersCur) {
(sort { $a->{uid} <=> $b->{uid} } (values %usersOut)); (sort { $a->{uid} <=> $b->{uid} } (values %usersOut));
updateFile($uidMapFile, to_json($uidMap)); updateFile($uidMapFile, to_json($uidMap));
updateFile("/etc/passwd", \@lines); updateFile("/etc/passwd", \@lines);
system("nscd --invalidate passwd"); nscdInvalidate("passwd");
# Rewrite /etc/shadow to add new accounts or remove dead ones. # Rewrite /etc/shadow to add new accounts or remove dead ones.
@ -293,7 +306,7 @@ updateFile("/etc/shadow", \@shadowNew, 0640);
my $uid = getpwnam "root"; my $uid = getpwnam "root";
my $gid = getgrnam "shadow"; my $gid = getgrnam "shadow";
my $path = "/etc/shadow"; my $path = "/etc/shadow";
chown($uid, $gid, $path) || die "Failed to change ownership of $path: $!"; (chown($uid, $gid, $path) || die "Failed to change ownership of $path: $!") unless $is_dry;
} }
# Rewrite /etc/subuid & /etc/subgid to include default container mappings # Rewrite /etc/subuid & /etc/subgid to include default container mappings

View file

@ -561,14 +561,16 @@ in {
shadow.gid = ids.gids.shadow; shadow.gid = ids.gids.shadow;
}; };
system.activationScripts.users = stringAfter [ "stdio" ] system.activationScripts.users = {
'' supportsDryActivation = true;
text = ''
install -m 0700 -d /root install -m 0700 -d /root
install -m 0755 -d /home install -m 0755 -d /home
${pkgs.perl.withPackages (p: [ p.FileSlurp p.JSON ])}/bin/perl \ ${pkgs.perl.withPackages (p: [ p.FileSlurp p.JSON ])}/bin/perl \
-w ${./update-users-groups.pl} ${spec} -w ${./update-users-groups.pl} ${spec}
''; '';
};
# for backwards compatibility # for backwards compatibility
system.activationScripts.groups = stringAfter [ "users" ] ""; system.activationScripts.groups = stringAfter [ "users" ] "";

View file

@ -12,6 +12,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
}; };
mutable = { ... }: { mutable = { ... }: {
users.mutableUsers = true; users.mutableUsers = true;
users.users.dry-test.isNormalUser = true;
}; };
}; };
@ -41,5 +42,32 @@ import ./make-test-python.nix ({ pkgs, ...} : {
"${mutableSystem}/bin/switch-to-configuration test" "${mutableSystem}/bin/switch-to-configuration test"
) )
assert "/run/wrappers/" in machine.succeed("which passwd") assert "/run/wrappers/" in machine.succeed("which passwd")
with subtest("dry-activation does not change files"):
machine.succeed('test -e /home/dry-test') # home was created
machine.succeed('rm -rf /home/dry-test')
files_to_check = ['/etc/group',
'/etc/passwd',
'/etc/shadow',
'/etc/subuid',
'/etc/subgid',
'/var/lib/nixos/uid-map',
'/var/lib/nixos/gid-map',
'/var/lib/nixos/declarative-groups',
'/var/lib/nixos/declarative-users'
]
expected_hashes = {}
expected_stats = {}
for file in files_to_check:
expected_hashes[file] = machine.succeed(f"sha256sum {file}")
expected_stats[file] = machine.succeed(f"stat {file}")
machine.succeed("/run/current-system/bin/switch-to-configuration dry-activate")
machine.fail('test -e /home/dry-test') # home was not recreated
for file in files_to_check:
assert machine.succeed(f"sha256sum {file}") == expected_hashes[file]
assert machine.succeed(f"stat {file}") == expected_stats[file]
''; '';
}) })