at 18.09-beta 9.4 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.tmp", { binmode => ':utf8', perms => $perms // 0644 }, $contents); 20 rename("$path.tmp", $path) or die; 21} 22 23 24sub hashPassword { 25 my ($password) = @_; 26 my $salt = ""; 27 my @chars = ('.', '/', 0..9, 'A'..'Z', 'a'..'z'); 28 $salt .= $chars[rand 64] for (1..8); 29 return crypt($password, '$6$' . $salt . '$'); 30} 31 32 33# Functions for allocating free GIDs/UIDs. FIXME: respect ID ranges in 34# /etc/login.defs. 35sub allocId { 36 my ($used, $prevUsed, $idMin, $idMax, $up, $getid) = @_; 37 my $id = $up ? $idMin : $idMax; 38 while ($id >= $idMin && $id <= $idMax) { 39 if (!$used->{$id} && !$prevUsed->{$id} && !defined &$getid($id)) { 40 $used->{$id} = 1; 41 return $id; 42 } 43 $used->{$id} = 1; 44 if ($up) { $id++; } else { $id--; } 45 } 46 die "$0: out of free UIDs or GIDs\n"; 47} 48 49my (%gidsUsed, %uidsUsed, %gidsPrevUsed, %uidsPrevUsed); 50 51sub allocGid { 52 my ($name) = @_; 53 my $prevGid = $gidMap->{$name}; 54 if (defined $prevGid && !defined $gidsUsed{$prevGid}) { 55 print STDERR "reviving group '$name' with GID $prevGid\n"; 56 $gidsUsed{$prevGid} = 1; 57 return $prevGid; 58 } 59 return allocId(\%gidsUsed, \%gidsPrevUsed, 400, 499, 0, sub { my ($gid) = @_; getgrgid($gid) }); 60} 61 62sub allocUid { 63 my ($name, $isSystemUser) = @_; 64 my ($min, $max, $up) = $isSystemUser ? (400, 499, 0) : (1000, 29999, 1); 65 my $prevUid = $uidMap->{$name}; 66 if (defined $prevUid && $prevUid >= $min && $prevUid <= $max && !defined $uidsUsed{$prevUid}) { 67 print STDERR "reviving user '$name' with UID $prevUid\n"; 68 $uidsUsed{$prevUid} = 1; 69 return $prevUid; 70 } 71 return allocId(\%uidsUsed, \%uidsPrevUsed, $min, $max, $up, sub { my ($uid) = @_; getpwuid($uid) }); 72} 73 74 75# Read the declared users/groups. 76my $spec = decode_json(read_file($ARGV[0])); 77 78# Don't allocate UIDs/GIDs that are manually assigned. 79foreach my $g (@{$spec->{groups}}) { 80 $gidsUsed{$g->{gid}} = 1 if defined $g->{gid}; 81} 82 83foreach my $u (@{$spec->{users}}) { 84 $uidsUsed{$u->{uid}} = 1 if defined $u->{uid}; 85} 86 87# Likewise for previously used but deleted UIDs/GIDs. 88$uidsPrevUsed{$_} = 1 foreach values %{$uidMap}; 89$gidsPrevUsed{$_} = 1 foreach values %{$gidMap}; 90 91 92# Read the current /etc/group. 93sub parseGroup { 94 chomp; 95 my @f = split(':', $_, -4); 96 my $gid = $f[2] eq "" ? undef : int($f[2]); 97 $gidsUsed{$gid} = 1 if defined $gid; 98 return ($f[0], { name => $f[0], password => $f[1], gid => $gid, members => $f[3] }); 99} 100 101my %groupsCur = -f "/etc/group" ? map { parseGroup } read_file("/etc/group") : (); 102 103# Read the current /etc/passwd. 104sub parseUser { 105 chomp; 106 my @f = split(':', $_, -7); 107 my $uid = $f[2] eq "" ? undef : int($f[2]); 108 $uidsUsed{$uid} = 1 if defined $uid; 109 return ($f[0], { name => $f[0], fakePassword => $f[1], uid => $uid, 110 gid => $f[3], description => $f[4], home => $f[5], shell => $f[6] }); 111} 112 113my %usersCur = -f "/etc/passwd" ? map { parseUser } read_file("/etc/passwd") : (); 114 115# Read the groups that were created declaratively (i.e. not by groups) 116# in the past. These must be removed if they are no longer in the 117# current spec. 118my $declGroupsFile = "/var/lib/nixos/declarative-groups"; 119my %declGroups; 120$declGroups{$_} = 1 foreach split / /, -e $declGroupsFile ? read_file($declGroupsFile) : ""; 121 122# Idem for the users. 123my $declUsersFile = "/var/lib/nixos/declarative-users"; 124my %declUsers; 125$declUsers{$_} = 1 foreach split / /, -e $declUsersFile ? read_file($declUsersFile) : ""; 126 127 128# Generate a new /etc/group containing the declared groups. 129my %groupsOut; 130foreach my $g (@{$spec->{groups}}) { 131 my $name = $g->{name}; 132 my $existing = $groupsCur{$name}; 133 134 my %members = map { ($_, 1) } @{$g->{members}}; 135 136 if (defined $existing) { 137 $g->{gid} = $existing->{gid} if !defined $g->{gid}; 138 if ($g->{gid} != $existing->{gid}) { 139 warn "warning: not applying GID change of group ‘$name’ ($existing->{gid} -> $g->{gid})\n"; 140 $g->{gid} = $existing->{gid}; 141 } 142 $g->{password} = $existing->{password}; # do we want this? 143 if ($spec->{mutableUsers}) { 144 # Merge in non-declarative group members. 145 foreach my $uname (split /,/, $existing->{members} // "") { 146 $members{$uname} = 1 if !defined $declUsers{$uname}; 147 } 148 } 149 } else { 150 $g->{gid} = allocGid($name) if !defined $g->{gid}; 151 $g->{password} = "x"; 152 } 153 154 $g->{members} = join ",", sort(keys(%members)); 155 $groupsOut{$name} = $g; 156 157 $gidMap->{$name} = $g->{gid}; 158} 159 160# Update the persistent list of declarative groups. 161updateFile($declGroupsFile, join(" ", sort(keys %groupsOut))); 162 163# Merge in the existing /etc/group. 164foreach my $name (keys %groupsCur) { 165 my $g = $groupsCur{$name}; 166 next if defined $groupsOut{$name}; 167 if (!$spec->{mutableUsers} || defined $declGroups{$name}) { 168 print STDERR "removing group ‘$name’\n"; 169 } else { 170 $groupsOut{$name} = $g; 171 } 172} 173 174 175# Rewrite /etc/group. FIXME: acquire lock. 176my @lines = map { join(":", $_->{name}, $_->{password}, $_->{gid}, $_->{members}) . "\n" } 177 (sort { $a->{gid} <=> $b->{gid} } values(%groupsOut)); 178updateFile($gidMapFile, encode_json($gidMap)); 179updateFile("/etc/group", \@lines); 180system("nscd --invalidate group"); 181 182# Generate a new /etc/passwd containing the declared users. 183my %usersOut; 184foreach my $u (@{$spec->{users}}) { 185 my $name = $u->{name}; 186 187 # Resolve the gid of the user. 188 if ($u->{group} =~ /^[0-9]$/) { 189 $u->{gid} = $u->{group}; 190 } elsif (defined $groupsOut{$u->{group}}) { 191 $u->{gid} = $groupsOut{$u->{group}}->{gid} // die; 192 } else { 193 warn "warning: user ‘$name’ has unknown group ‘$u->{group}’\n"; 194 $u->{gid} = 65534; 195 } 196 197 my $existing = $usersCur{$name}; 198 if (defined $existing) { 199 $u->{uid} = $existing->{uid} if !defined $u->{uid}; 200 if ($u->{uid} != $existing->{uid}) { 201 warn "warning: not applying UID change of user ‘$name’ ($existing->{uid} -> $u->{uid})\n"; 202 $u->{uid} = $existing->{uid}; 203 } 204 } else { 205 $u->{uid} = allocUid($name, $u->{isSystemUser}) if !defined $u->{uid}; 206 207 if (defined $u->{initialPassword}) { 208 $u->{hashedPassword} = hashPassword($u->{initialPassword}); 209 } elsif (defined $u->{initialHashedPassword}) { 210 $u->{hashedPassword} = $u->{initialHashedPassword}; 211 } 212 } 213 214 # Create a home directory. 215 if ($u->{createHome}) { 216 make_path($u->{home}, { mode => 0700 }) if ! -e $u->{home}; 217 chown $u->{uid}, $u->{gid}, $u->{home}; 218 } 219 220 if (defined $u->{passwordFile}) { 221 if (-e $u->{passwordFile}) { 222 $u->{hashedPassword} = read_file($u->{passwordFile}); 223 chomp $u->{hashedPassword}; 224 } else { 225 warn "warning: password file ‘$u->{passwordFile}’ does not exist\n"; 226 } 227 } elsif (defined $u->{password}) { 228 $u->{hashedPassword} = hashPassword($u->{password}); 229 } 230 231 $u->{fakePassword} = $existing->{fakePassword} // "x"; 232 $usersOut{$name} = $u; 233 234 $uidMap->{$name} = $u->{uid}; 235} 236 237# Update the persistent list of declarative users. 238updateFile($declUsersFile, join(" ", sort(keys %usersOut))); 239 240# Merge in the existing /etc/passwd. 241foreach my $name (keys %usersCur) { 242 my $u = $usersCur{$name}; 243 next if defined $usersOut{$name}; 244 if (!$spec->{mutableUsers} || defined $declUsers{$name}) { 245 print STDERR "removing user ‘$name’\n"; 246 } else { 247 $usersOut{$name} = $u; 248 } 249} 250 251# Rewrite /etc/passwd. FIXME: acquire lock. 252@lines = map { join(":", $_->{name}, $_->{fakePassword}, $_->{uid}, $_->{gid}, $_->{description}, $_->{home}, $_->{shell}) . "\n" } 253 (sort { $a->{uid} <=> $b->{uid} } (values %usersOut)); 254updateFile($uidMapFile, encode_json($uidMap)); 255updateFile("/etc/passwd", \@lines); 256system("nscd --invalidate passwd"); 257 258 259# Rewrite /etc/shadow to add new accounts or remove dead ones. 260my @shadowNew; 261my %shadowSeen; 262 263foreach my $line (-f "/etc/shadow" ? read_file("/etc/shadow") : ()) { 264 chomp $line; 265 my ($name, $hashedPassword, @rest) = split(':', $line, -9); 266 my $u = $usersOut{$name};; 267 next if !defined $u; 268 $hashedPassword = "!" if !$spec->{mutableUsers}; 269 $hashedPassword = $u->{hashedPassword} if defined $u->{hashedPassword} && !$spec->{mutableUsers}; # FIXME 270 push @shadowNew, join(":", $name, $hashedPassword, @rest) . "\n"; 271 $shadowSeen{$name} = 1; 272} 273 274foreach my $u (values %usersOut) { 275 next if defined $shadowSeen{$u->{name}}; 276 my $hashedPassword = "!"; 277 $hashedPassword = $u->{hashedPassword} if defined $u->{hashedPassword}; 278 # FIXME: set correct value for sp_lstchg. 279 push @shadowNew, join(":", $u->{name}, $hashedPassword, "1::::::") . "\n"; 280} 281 282updateFile("/etc/shadow", \@shadowNew, 0600);