update-users-groups.pl: Keep track of deallocated UIDs/GIDs

When a user or group is revived, this allows it to be allocated the
UID/GID it had before.

A consequence is that UIDs and GIDs are no longer reused.

Fixes #24010.
This commit is contained in:
Eelco Dolstra 2017-03-29 18:10:20 +02:00
parent ffd29517dd
commit a57bcd38b4
No known key found for this signature in database
GPG Key ID: 8170B4726D7198DE

View File

@ -6,6 +6,21 @@ use JSON;
make_path("/var/lib/nixos", { mode => 0755 }); make_path("/var/lib/nixos", { mode => 0755 });
# Keep track of deleted uids and gids.
my $uidMapFile = "/var/lib/nixos/uid-map";
my $uidMap = -e $uidMapFile ? decode_json(read_file($uidMapFile)) : {};
my $gidMapFile = "/var/lib/nixos/gid-map";
my $gidMap = -e $gidMapFile ? decode_json(read_file($gidMapFile)) : {};
sub updateFile {
my ($path, $contents, $perms) = @_;
write_file("$path.tmp", { binmode => ':utf8', perms => $perms // 0644 }, $contents);
rename("$path.tmp", $path) or die;
}
sub hashPassword { sub hashPassword {
my ($password) = @_; my ($password) = @_;
my $salt = ""; my $salt = "";
@ -18,10 +33,10 @@ sub hashPassword {
# 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.
sub allocId { sub allocId {
my ($used, $idMin, $idMax, $up, $getid) = @_; my ($used, $prevUsed, $idMin, $idMax, $up, $getid) = @_;
my $id = $up ? $idMin : $idMax; my $id = $up ? $idMin : $idMax;
while ($id >= $idMin && $id <= $idMax) { while ($id >= $idMin && $id <= $idMax) {
if (!$used->{$id} && !defined &$getid($id)) { if (!$used->{$id} && !$prevUsed->{$id} && !defined &$getid($id)) {
$used->{$id} = 1; $used->{$id} = 1;
return $id; return $id;
} }
@ -31,23 +46,36 @@ sub allocId {
die "$0: out of free UIDs or GIDs\n"; die "$0: out of free UIDs or GIDs\n";
} }
my (%gidsUsed, %uidsUsed); my (%gidsUsed, %uidsUsed, %gidsPrevUsed, %uidsPrevUsed);
sub allocGid { sub allocGid {
return allocId(\%gidsUsed, 400, 499, 0, sub { my ($gid) = @_; getgrgid($gid) }); my ($name) = @_;
my $prevGid = $gidMap->{$name};
if (defined $prevGid && !defined $gidsUsed{$prevGid}) {
print STDERR "reviving group '$name' with GID $prevGid\n";
$gidsUsed{$prevGid} = 1;
return $prevGid;
}
return allocId(\%gidsUsed, \%gidsPrevUsed, 400, 499, 0, sub { my ($gid) = @_; getgrgid($gid) });
} }
sub allocUid { sub allocUid {
my ($isSystemUser) = @_; my ($name, $isSystemUser) = @_;
my ($min, $max, $up) = $isSystemUser ? (400, 499, 0) : (1000, 29999, 1); my ($min, $max, $up) = $isSystemUser ? (400, 499, 0) : (1000, 29999, 1);
return allocId(\%uidsUsed, $min, $max, $up, sub { my ($uid) = @_; getpwuid($uid) }); my $prevUid = $uidMap->{$name};
if (defined $prevUid && $prevUid >= $min && $prevUid <= $max && !defined $uidsUsed{$prevUid}) {
print STDERR "reviving user '$name' with UID $prevUid\n";
$uidsUsed{$prevUid} = 1;
return $prevUid;
}
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 already in use. # Don't allocate UIDs/GIDs that are manually assigned.
foreach my $g (@{$spec->{groups}}) { foreach my $g (@{$spec->{groups}}) {
$gidsUsed{$g->{gid}} = 1 if defined $g->{gid}; $gidsUsed{$g->{gid}} = 1 if defined $g->{gid};
} }
@ -56,6 +84,11 @@ foreach my $u (@{$spec->{users}}) {
$uidsUsed{$u->{uid}} = 1 if defined $u->{uid}; $uidsUsed{$u->{uid}} = 1 if defined $u->{uid};
} }
# Likewise for previously used but deleted UIDs/GIDs.
$uidsPrevUsed{$_} = 1 foreach values %{$uidMap};
$gidsPrevUsed{$_} = 1 foreach values %{$gidMap};
# Read the current /etc/group. # Read the current /etc/group.
sub parseGroup { sub parseGroup {
chomp; chomp;
@ -114,16 +147,18 @@ foreach my $g (@{$spec->{groups}}) {
} }
} }
} else { } else {
$g->{gid} = allocGid if !defined $g->{gid}; $g->{gid} = allocGid($name) if !defined $g->{gid};
$g->{password} = "x"; $g->{password} = "x";
} }
$g->{members} = join ",", sort(keys(%members)); $g->{members} = join ",", sort(keys(%members));
$groupsOut{$name} = $g; $groupsOut{$name} = $g;
$gidMap->{$name} = $g->{gid};
} }
# Update the persistent list of declarative groups. # Update the persistent list of declarative groups.
write_file($declGroupsFile, { binmode => ':utf8' }, join(" ", sort(keys %groupsOut))); updateFile($declGroupsFile, join(" ", sort(keys %groupsOut)));
# Merge in the existing /etc/group. # Merge in the existing /etc/group.
foreach my $name (keys %groupsCur) { foreach my $name (keys %groupsCur) {
@ -140,8 +175,8 @@ foreach my $name (keys %groupsCur) {
# Rewrite /etc/group. FIXME: acquire lock. # Rewrite /etc/group. FIXME: acquire lock.
my @lines = map { join(":", $_->{name}, $_->{password}, $_->{gid}, $_->{members}) . "\n" } my @lines = map { join(":", $_->{name}, $_->{password}, $_->{gid}, $_->{members}) . "\n" }
(sort { $a->{gid} <=> $b->{gid} } values(%groupsOut)); (sort { $a->{gid} <=> $b->{gid} } values(%groupsOut));
write_file("/etc/group.tmp", { binmode => ':utf8' }, @lines); updateFile($gidMapFile, encode_json($gidMap));
rename("/etc/group.tmp", "/etc/group") or die; updateFile("/etc/group", \@lines);
system("nscd --invalidate group"); system("nscd --invalidate group");
# Generate a new /etc/passwd containing the declared users. # Generate a new /etc/passwd containing the declared users.
@ -167,7 +202,7 @@ foreach my $u (@{$spec->{users}}) {
$u->{uid} = $existing->{uid}; $u->{uid} = $existing->{uid};
} }
} else { } else {
$u->{uid} = allocUid($u->{isSystemUser}) if !defined $u->{uid}; $u->{uid} = allocUid($name, $u->{isSystemUser}) if !defined $u->{uid};
if (defined $u->{initialPassword}) { if (defined $u->{initialPassword}) {
$u->{hashedPassword} = hashPassword($u->{initialPassword}); $u->{hashedPassword} = hashPassword($u->{initialPassword});
@ -195,10 +230,12 @@ foreach my $u (@{$spec->{users}}) {
$u->{fakePassword} = $existing->{fakePassword} // "x"; $u->{fakePassword} = $existing->{fakePassword} // "x";
$usersOut{$name} = $u; $usersOut{$name} = $u;
$uidMap->{$name} = $u->{uid};
} }
# Update the persistent list of declarative users. # Update the persistent list of declarative users.
write_file($declUsersFile, { binmode => ':utf8' }, join(" ", sort(keys %usersOut))); updateFile($declUsersFile, join(" ", sort(keys %usersOut)));
# Merge in the existing /etc/passwd. # Merge in the existing /etc/passwd.
foreach my $name (keys %usersCur) { foreach my $name (keys %usersCur) {
@ -214,8 +251,8 @@ foreach my $name (keys %usersCur) {
# Rewrite /etc/passwd. FIXME: acquire lock. # Rewrite /etc/passwd. FIXME: acquire lock.
@lines = map { join(":", $_->{name}, $_->{fakePassword}, $_->{uid}, $_->{gid}, $_->{description}, $_->{home}, $_->{shell}) . "\n" } @lines = map { join(":", $_->{name}, $_->{fakePassword}, $_->{uid}, $_->{gid}, $_->{description}, $_->{home}, $_->{shell}) . "\n" }
(sort { $a->{uid} <=> $b->{uid} } (values %usersOut)); (sort { $a->{uid} <=> $b->{uid} } (values %usersOut));
write_file("/etc/passwd.tmp", { binmode => ':utf8' }, @lines); updateFile($uidMapFile, encode_json($uidMap));
rename("/etc/passwd.tmp", "/etc/passwd") or die; updateFile("/etc/passwd", \@lines);
system("nscd --invalidate passwd"); system("nscd --invalidate passwd");
@ -242,5 +279,4 @@ foreach my $u (values %usersOut) {
push @shadowNew, join(":", $u->{name}, $hashedPassword, "1::::::") . "\n"; push @shadowNew, join(":", $u->{name}, $hashedPassword, "1::::::") . "\n";
} }
write_file("/etc/shadow.tmp", { binmode => ':utf8', perms => 0600 }, @shadowNew); updateFile("/etc/shadow", \@shadowNew, 0600);
rename("/etc/shadow.tmp", "/etc/shadow") or die;