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