at 21.11-pre 12 kB view raw
1use strict; 2use File::Path qw(make_path); 3use File::Slurp; 4use JSON; 5 6make_path("/var/lib/nixos", { mode => 0755 }); 7 8 9# Keep track of deleted uids and gids. 10my $uidMapFile = "/var/lib/nixos/uid-map"; 11my $uidMap = -e $uidMapFile ? decode_json(read_file($uidMapFile)) : {}; 12 13my $gidMapFile = "/var/lib/nixos/gid-map"; 14my $gidMap = -e $gidMapFile ? decode_json(read_file($gidMapFile)) : {}; 15 16 17sub updateFile { 18 my ($path, $contents, $perms) = @_; 19 write_file($path, { atomic => 1, binmode => ':utf8', perms => $perms // 0644 }, $contents) or die; 20} 21 22 23sub hashPassword { 24 my ($password) = @_; 25 my $salt = ""; 26 my @chars = ('.', '/', 0..9, 'A'..'Z', 'a'..'z'); 27 $salt .= $chars[rand 64] for (1..8); 28 return crypt($password, '$6$' . $salt . '$'); 29} 30 31 32# Functions for allocating free GIDs/UIDs. FIXME: respect ID ranges in 33# /etc/login.defs. 34sub allocId { 35 my ($used, $prevUsed, $idMin, $idMax, $up, $getid) = @_; 36 my $id = $up ? $idMin : $idMax; 37 while ($id >= $idMin && $id <= $idMax) { 38 if (!$used->{$id} && !$prevUsed->{$id} && !defined &$getid($id)) { 39 $used->{$id} = 1; 40 return $id; 41 } 42 $used->{$id} = 1; 43 if ($up) { $id++; } else { $id--; } 44 } 45 die "$0: out of free UIDs or GIDs\n"; 46} 47 48my (%gidsUsed, %uidsUsed, %gidsPrevUsed, %uidsPrevUsed); 49 50sub allocGid { 51 my ($name) = @_; 52 my $prevGid = $gidMap->{$name}; 53 if (defined $prevGid && !defined $gidsUsed{$prevGid}) { 54 print STDERR "reviving group '$name' with GID $prevGid\n"; 55 $gidsUsed{$prevGid} = 1; 56 return $prevGid; 57 } 58 return allocId(\%gidsUsed, \%gidsPrevUsed, 400, 999, 0, sub { my ($gid) = @_; getgrgid($gid) }); 59} 60 61sub allocUid { 62 my ($name, $isSystemUser) = @_; 63 my ($min, $max, $up) = $isSystemUser ? (400, 999, 0) : (1000, 29999, 1); 64 my $prevUid = $uidMap->{$name}; 65 if (defined $prevUid && $prevUid >= $min && $prevUid <= $max && !defined $uidsUsed{$prevUid}) { 66 print STDERR "reviving user '$name' with UID $prevUid\n"; 67 $uidsUsed{$prevUid} = 1; 68 return $prevUid; 69 } 70 return allocId(\%uidsUsed, \%uidsPrevUsed, $min, $max, $up, sub { my ($uid) = @_; getpwuid($uid) }); 71} 72 73 74# Read the declared users/groups. 75my $spec = decode_json(read_file($ARGV[0])); 76 77# Don't allocate UIDs/GIDs that are manually assigned. 78foreach my $g (@{$spec->{groups}}) { 79 $gidsUsed{$g->{gid}} = 1 if defined $g->{gid}; 80} 81 82foreach my $u (@{$spec->{users}}) { 83 $uidsUsed{$u->{uid}} = 1 if defined $u->{uid}; 84} 85 86# Likewise for previously used but deleted UIDs/GIDs. 87$uidsPrevUsed{$_} = 1 foreach values %{$uidMap}; 88$gidsPrevUsed{$_} = 1 foreach values %{$gidMap}; 89 90 91# Read the current /etc/group. 92sub parseGroup { 93 chomp; 94 my @f = split(':', $_, -4); 95 my $gid = $f[2] eq "" ? undef : int($f[2]); 96 $gidsUsed{$gid} = 1 if defined $gid; 97 return ($f[0], { name => $f[0], password => $f[1], gid => $gid, members => $f[3] }); 98} 99 100my %groupsCur = -f "/etc/group" ? map { parseGroup } read_file("/etc/group", { binmode => ":utf8" }) : (); 101 102# Read the current /etc/passwd. 103sub parseUser { 104 chomp; 105 my @f = split(':', $_, -7); 106 my $uid = $f[2] eq "" ? undef : int($f[2]); 107 $uidsUsed{$uid} = 1 if defined $uid; 108 return ($f[0], { name => $f[0], fakePassword => $f[1], uid => $uid, 109 gid => $f[3], description => $f[4], home => $f[5], shell => $f[6] }); 110} 111my %usersCur = -f "/etc/passwd" ? map { parseUser } read_file("/etc/passwd", { binmode => ":utf8" }) : (); 112 113# Read the groups that were created declaratively (i.e. not by groups) 114# in the past. These must be removed if they are no longer in the 115# current spec. 116my $declGroupsFile = "/var/lib/nixos/declarative-groups"; 117my %declGroups; 118$declGroups{$_} = 1 foreach split / /, -e $declGroupsFile ? read_file($declGroupsFile, { binmode => ":utf8" }) : ""; 119 120# Idem for the users. 121my $declUsersFile = "/var/lib/nixos/declarative-users"; 122my %declUsers; 123$declUsers{$_} = 1 foreach split / /, -e $declUsersFile ? read_file($declUsersFile, { binmode => ":utf8" }) : ""; 124 125 126# Generate a new /etc/group containing the declared groups. 127my %groupsOut; 128foreach my $g (@{$spec->{groups}}) { 129 my $name = $g->{name}; 130 my $existing = $groupsCur{$name}; 131 132 my %members = map { ($_, 1) } @{$g->{members}}; 133 134 if (defined $existing) { 135 $g->{gid} = $existing->{gid} if !defined $g->{gid}; 136 if ($g->{gid} != $existing->{gid}) { 137 warn "warning: not applying GID change of group ‘$name’ ($existing->{gid} -> $g->{gid})\n"; 138 $g->{gid} = $existing->{gid}; 139 } 140 $g->{password} = $existing->{password}; # do we want this? 141 if ($spec->{mutableUsers}) { 142 # Merge in non-declarative group members. 143 foreach my $uname (split /,/, $existing->{members} // "") { 144 $members{$uname} = 1 if !defined $declUsers{$uname}; 145 } 146 } 147 } else { 148 $g->{gid} = allocGid($name) if !defined $g->{gid}; 149 $g->{password} = "x"; 150 } 151 152 $g->{members} = join ",", sort(keys(%members)); 153 $groupsOut{$name} = $g; 154 155 $gidMap->{$name} = $g->{gid}; 156} 157 158# Update the persistent list of declarative groups. 159updateFile($declGroupsFile, join(" ", sort(keys %groupsOut))); 160 161# Merge in the existing /etc/group. 162foreach my $name (keys %groupsCur) { 163 my $g = $groupsCur{$name}; 164 next if defined $groupsOut{$name}; 165 if (!$spec->{mutableUsers} || defined $declGroups{$name}) { 166 print STDERR "removing group ‘$name’\n"; 167 } else { 168 $groupsOut{$name} = $g; 169 } 170} 171 172 173# Rewrite /etc/group. FIXME: acquire lock. 174my @lines = map { join(":", $_->{name}, $_->{password}, $_->{gid}, $_->{members}) . "\n" } 175 (sort { $a->{gid} <=> $b->{gid} } values(%groupsOut)); 176updateFile($gidMapFile, to_json($gidMap)); 177updateFile("/etc/group", \@lines); 178system("nscd --invalidate group"); 179 180# Generate a new /etc/passwd containing the declared users. 181my %usersOut; 182foreach my $u (@{$spec->{users}}) { 183 my $name = $u->{name}; 184 185 # Resolve the gid of the user. 186 if ($u->{group} =~ /^[0-9]$/) { 187 $u->{gid} = $u->{group}; 188 } elsif (defined $groupsOut{$u->{group}}) { 189 $u->{gid} = $groupsOut{$u->{group}}->{gid} // die; 190 } else { 191 warn "warning: user ‘$name’ has unknown group ‘$u->{group}’\n"; 192 $u->{gid} = 65534; 193 } 194 195 my $existing = $usersCur{$name}; 196 if (defined $existing) { 197 $u->{uid} = $existing->{uid} if !defined $u->{uid}; 198 if ($u->{uid} != $existing->{uid}) { 199 warn "warning: not applying UID change of user ‘$name’ ($existing->{uid} -> $u->{uid})\n"; 200 $u->{uid} = $existing->{uid}; 201 } 202 } else { 203 $u->{uid} = allocUid($name, $u->{isSystemUser}) if !defined $u->{uid}; 204 205 if (defined $u->{initialPassword}) { 206 $u->{hashedPassword} = hashPassword($u->{initialPassword}); 207 } elsif (defined $u->{initialHashedPassword}) { 208 $u->{hashedPassword} = $u->{initialHashedPassword}; 209 } 210 } 211 212 # Ensure home directory incl. ownership and permissions. 213 if ($u->{createHome}) { 214 make_path($u->{home}, { mode => 0700 }) if ! -e $u->{home}; 215 chown $u->{uid}, $u->{gid}, $u->{home}; 216 chmod 0700, $u->{home}; 217 } 218 219 if (defined $u->{passwordFile}) { 220 if (-e $u->{passwordFile}) { 221 $u->{hashedPassword} = read_file($u->{passwordFile}); 222 chomp $u->{hashedPassword}; 223 } else { 224 warn "warning: password file ‘$u->{passwordFile}’ does not exist\n"; 225 } 226 } elsif (defined $u->{password}) { 227 $u->{hashedPassword} = hashPassword($u->{password}); 228 } 229 230 if (!defined $u->{shell}) { 231 if (defined $existing) { 232 $u->{shell} = $existing->{shell}; 233 } else { 234 warn "warning: no declarative or previous shell for ‘$name’, setting shell to nologin\n"; 235 $u->{shell} = "/run/current-system/sw/bin/nologin"; 236 } 237 } 238 239 $u->{fakePassword} = $existing->{fakePassword} // "x"; 240 $usersOut{$name} = $u; 241 242 $uidMap->{$name} = $u->{uid}; 243} 244 245# Update the persistent list of declarative users. 246updateFile($declUsersFile, join(" ", sort(keys %usersOut))); 247 248# Merge in the existing /etc/passwd. 249foreach my $name (keys %usersCur) { 250 my $u = $usersCur{$name}; 251 next if defined $usersOut{$name}; 252 if (!$spec->{mutableUsers} || defined $declUsers{$name}) { 253 print STDERR "removing user ‘$name’\n"; 254 } else { 255 $usersOut{$name} = $u; 256 } 257} 258 259# Rewrite /etc/passwd. FIXME: acquire lock. 260@lines = map { join(":", $_->{name}, $_->{fakePassword}, $_->{uid}, $_->{gid}, $_->{description}, $_->{home}, $_->{shell}) . "\n" } 261 (sort { $a->{uid} <=> $b->{uid} } (values %usersOut)); 262updateFile($uidMapFile, to_json($uidMap)); 263updateFile("/etc/passwd", \@lines); 264system("nscd --invalidate passwd"); 265 266 267# Rewrite /etc/shadow to add new accounts or remove dead ones. 268my @shadowNew; 269my %shadowSeen; 270 271foreach my $line (-f "/etc/shadow" ? read_file("/etc/shadow", { binmode => ":utf8" }) : ()) { 272 chomp $line; 273 my ($name, $hashedPassword, @rest) = split(':', $line, -9); 274 my $u = $usersOut{$name};; 275 next if !defined $u; 276 $hashedPassword = "!" if !$spec->{mutableUsers}; 277 $hashedPassword = $u->{hashedPassword} if defined $u->{hashedPassword} && !$spec->{mutableUsers}; # FIXME 278 chomp $hashedPassword; 279 push @shadowNew, join(":", $name, $hashedPassword, @rest) . "\n"; 280 $shadowSeen{$name} = 1; 281} 282 283foreach my $u (values %usersOut) { 284 next if defined $shadowSeen{$u->{name}}; 285 my $hashedPassword = "!"; 286 $hashedPassword = $u->{hashedPassword} if defined $u->{hashedPassword}; 287 # FIXME: set correct value for sp_lstchg. 288 push @shadowNew, join(":", $u->{name}, $hashedPassword, "1::::::") . "\n"; 289} 290 291updateFile("/etc/shadow", \@shadowNew, 0640); 292{ 293 my $uid = getpwnam "root"; 294 my $gid = getgrnam "shadow"; 295 my $path = "/etc/shadow"; 296 chown($uid, $gid, $path) || die "Failed to change ownership of $path: $!"; 297} 298 299# Rewrite /etc/subuid & /etc/subgid to include default container mappings 300 301my $subUidMapFile = "/var/lib/nixos/auto-subuid-map"; 302my $subUidMap = -e $subUidMapFile ? decode_json(read_file($subUidMapFile)) : {}; 303 304my (%subUidsUsed, %subUidsPrevUsed); 305 306$subUidsPrevUsed{$_} = 1 foreach values %{$subUidMap}; 307 308sub allocSubUid { 309 my ($name, @rest) = @_; 310 311 # TODO: No upper bounds? 312 my ($min, $max, $up) = (100000, 100000 * 100, 1); 313 my $prevId = $subUidMap->{$name}; 314 if (defined $prevId && !defined $subUidsUsed{$prevId}) { 315 $subUidsUsed{$prevId} = 1; 316 return $prevId; 317 } 318 319 my $id = allocId(\%subUidsUsed, \%subUidsPrevUsed, $min, $max, $up, sub { my ($uid) = @_; getpwuid($uid) }); 320 my $offset = $id - 100000; 321 my $count = $offset * 65536; 322 my $subordinate = 100000 + $count; 323 return $subordinate; 324} 325 326my @subGids; 327my @subUids; 328foreach my $u (values %usersOut) { 329 my $name = $u->{name}; 330 331 foreach my $range (@{$u->{subUidRanges}}) { 332 my $value = join(":", ($name, $range->{startUid}, $range->{count})); 333 push @subUids, $value; 334 } 335 336 foreach my $range (@{$u->{subGidRanges}}) { 337 my $value = join(":", ($name, $range->{startGid}, $range->{count})); 338 push @subGids, $value; 339 } 340 341 if($u->{isNormalUser}) { 342 my $subordinate = allocSubUid($name); 343 $subUidMap->{$name} = $subordinate; 344 my $value = join(":", ($name, $subordinate, 65536)); 345 push @subUids, $value; 346 push @subGids, $value; 347 } 348} 349 350updateFile("/etc/subuid", join("\n", @subUids) . "\n"); 351updateFile("/etc/subgid", join("\n", @subGids) . "\n"); 352updateFile($subUidMapFile, encode_json($subUidMap) . "\n");