3 use File
::Path
qw(make_path);
9 # Keep track of deleted uids and gids.
10 my $uidMapFile = "/var/lib/nixos/uid-map";
11 my $uidMap = -e
$uidMapFile ? decode_json
(read_file
($uidMapFile)) : {};
13 my $gidMapFile = "/var/lib/nixos/gid-map";
14 my $gidMap = -e
$gidMapFile ? decode_json
(read_file
($gidMapFile)) : {};
16 my $is_dry = ($ENV{'NIXOS_ACTION'} // "") eq "dry-activate";
17 GetOptions
("dry-activate" => \
$is_dry);
18 make_path
("/var/lib/nixos", { mode
=> 0755 }) unless $is_dry;
21 my ($path, $contents, $perms) = @_;
23 write_file
($path, { atomic
=> 1, binmode => ':utf8', perms
=> $perms // 0644 }, $contents) or die;
26 # Converts an ISO date to number of days since 1970-01-01
29 my $time = Time
::Piece
->strptime($date, "%Y-%m-%d");
30 return $time->epoch / 60 / 60 / 24;
34 system("nscd", "--invalidate", $_[0]) unless $is_dry;
40 my @chars = ('.', '/', 0..9, 'A'..'Z', 'a'..'z');
41 $salt .= $chars[rand 64] for (1..8);
42 return crypt($password, '$6$' . $salt . '$');
47 print STDERR
("$_[1] $_[2]\n")
49 print STDERR
("$_[0] $_[2]\n")
54 # Functions for allocating free GIDs/UIDs. FIXME: respect ID ranges in
57 my ($used, $prevUsed, $idMin, $idMax, $up, $getid) = @_;
58 my $id = $up ?
$idMin : $idMax;
59 while ($id >= $idMin && $id <= $idMax) {
60 if (!$used->{$id} && !$prevUsed->{$id} && !defined &$getid($id)) {
65 if ($up) { $id++; } else { $id--; }
67 die "$0: out of free UIDs or GIDs\n";
70 my (%gidsUsed, %uidsUsed, %gidsPrevUsed, %uidsPrevUsed);
74 my $prevGid = $gidMap->{$name};
75 if (defined $prevGid && !defined $gidsUsed{$prevGid}) {
76 dry_print
("reviving", "would revive", "group '$name' with GID $prevGid");
77 $gidsUsed{$prevGid} = 1;
80 return allocId
(\
%gidsUsed, \
%gidsPrevUsed, 400, 999, 0, sub { my ($gid) = @_; getgrgid($gid) });
84 my ($name, $isSystemUser) = @_;
85 my ($min, $max, $up) = $isSystemUser ?
(400, 999, 0) : (1000, 29999, 1);
86 my $prevUid = $uidMap->{$name};
87 if (defined $prevUid && $prevUid >= $min && $prevUid <= $max && !defined $uidsUsed{$prevUid}) {
88 dry_print
("reviving", "would revive", "user '$name' with UID $prevUid");
89 $uidsUsed{$prevUid} = 1;
92 return allocId
(\
%uidsUsed, \
%uidsPrevUsed, $min, $max, $up, sub { my ($uid) = @_; getpwuid($uid) });
95 # Read the declared users/groups
96 my $spec = decode_json
(read_file
($ARGV[0]));
98 # Don't allocate UIDs/GIDs that are manually assigned.
99 foreach my $g (@
{$spec->{groups
}}) {
100 $gidsUsed{$g->{gid
}} = 1 if defined $g->{gid
};
103 foreach my $u (@
{$spec->{users
}}) {
104 $uidsUsed{$u->{uid
}} = 1 if defined $u->{uid
};
107 # Likewise for previously used but deleted UIDs/GIDs.
108 $uidsPrevUsed{$_} = 1 foreach values %{$uidMap};
109 $gidsPrevUsed{$_} = 1 foreach values %{$gidMap};
112 # Read the current /etc/group.
115 my @f = split(':', $_, -4);
116 my $gid = $f[2] eq "" ?
undef : int($f[2]);
117 $gidsUsed{$gid} = 1 if defined $gid;
118 return ($f[0], { name
=> $f[0], password
=> $f[1], gid
=> $gid, members
=> $f[3] });
121 my %groupsCur = -f
"/etc/group" ?
map { parseGroup
} read_file
("/etc/group", { binmode => ":utf8" }) : ();
123 # Read the current /etc/passwd.
126 my @f = split(':', $_, -7);
127 my $uid = $f[2] eq "" ?
undef : int($f[2]);
128 $uidsUsed{$uid} = 1 if defined $uid;
129 return ($f[0], { name
=> $f[0], fakePassword
=> $f[1], uid
=> $uid,
130 gid
=> $f[3], description
=> $f[4], home
=> $f[5], shell
=> $f[6] });
132 my %usersCur = -f
"/etc/passwd" ?
map { parseUser
} read_file
("/etc/passwd", { binmode => ":utf8" }) : ();
134 # Read the groups that were created declaratively (i.e. not by groups)
135 # in the past. These must be removed if they are no longer in the
137 my $declGroupsFile = "/var/lib/nixos/declarative-groups";
139 $declGroups{$_} = 1 foreach split / /, -e
$declGroupsFile ? read_file
($declGroupsFile, { binmode => ":utf8" }) : "";
141 # Idem for the users.
142 my $declUsersFile = "/var/lib/nixos/declarative-users";
144 $declUsers{$_} = 1 foreach split / /, -e
$declUsersFile ? read_file
($declUsersFile, { binmode => ":utf8" }) : "";
147 # Generate a new /etc/group containing the declared groups.
149 foreach my $g (@
{$spec->{groups
}}) {
150 my $name = $g->{name
};
151 my $existing = $groupsCur{$name};
153 my %members = map { ($_, 1) } @
{$g->{members
}};
155 if (defined $existing) {
156 $g->{gid
} = $existing->{gid
} if !defined $g->{gid
};
157 if ($g->{gid
} != $existing->{gid
}) {
158 dry_print
("warning: not applying", "warning: would not apply", "GID change of group ‘$name’ ($existing->{gid} -> $g->{gid}) in /etc/group");
159 $g->{gid
} = $existing->{gid
};
161 $g->{password
} = $existing->{password
}; # do we want this?
162 if ($spec->{mutableUsers
}) {
163 # Merge in non-declarative group members.
164 foreach my $uname (split /,/, $existing->{members
} // "") {
165 $members{$uname} = 1 if !defined $declUsers{$uname};
169 $g->{gid
} = allocGid
($name) if !defined $g->{gid
};
170 $g->{password
} = "x";
173 $g->{members
} = join ",", sort(keys(%members));
174 $groupsOut{$name} = $g;
176 $gidMap->{$name} = $g->{gid
};
179 # Update the persistent list of declarative groups.
180 updateFile
($declGroupsFile, join(" ", sort(keys %groupsOut)));
182 # Merge in the existing /etc/group.
183 foreach my $name (keys %groupsCur) {
184 my $g = $groupsCur{$name};
185 next if defined $groupsOut{$name};
186 if (!$spec->{mutableUsers
} || defined $declGroups{$name}) {
187 dry_print
("removing group", "would remove group", "‘$name’");
189 $groupsOut{$name} = $g;
194 # Rewrite /etc/group. FIXME: acquire lock.
195 my @lines = map { join(":", $_->{name
}, $_->{password
}, $_->{gid
}, $_->{members
}) . "\n" }
196 (sort { $a->{gid
} <=> $b->{gid
} } values(%groupsOut));
197 updateFile
($gidMapFile, to_json
($gidMap, {canonical
=> 1}));
198 updateFile
("/etc/group", \
@lines);
199 nscdInvalidate
("group");
201 # Generate a new /etc/passwd containing the declared users.
203 foreach my $u (@
{$spec->{users
}}) {
204 my $name = $u->{name
};
206 # Resolve the gid of the user.
207 if ($u->{group
} =~ /^[0-9]$/) {
208 $u->{gid
} = $u->{group
};
209 } elsif (defined $groupsOut{$u->{group
}}) {
210 $u->{gid
} = $groupsOut{$u->{group
}}->{gid
} // die;
212 warn "warning: user ‘$name’ has unknown group ‘$u->{group}’\n";
216 my $existing = $usersCur{$name};
217 if (defined $existing) {
218 $u->{uid
} = $existing->{uid
} if !defined $u->{uid
};
219 if ($u->{uid
} != $existing->{uid
}) {
220 dry_print
("warning: not applying", "warning: would not apply", "UID change of user ‘$name’ ($existing->{uid} -> $u->{uid}) in /etc/passwd");
221 $u->{uid
} = $existing->{uid
};
224 $u->{uid
} = allocUid
($name, $u->{isSystemUser
}) if !defined $u->{uid
};
226 if (!defined $u->{hashedPassword
}) {
227 if (defined $u->{initialPassword
}) {
228 $u->{hashedPassword
} = hashPassword
($u->{initialPassword
});
229 } elsif (defined $u->{initialHashedPassword
}) {
230 $u->{hashedPassword
} = $u->{initialHashedPassword
};
235 # Ensure home directory incl. ownership and permissions.
236 if ($u->{createHome
} and !$is_dry) {
237 make_path
($u->{home
}, { mode
=> 0755 }) if ! -e
$u->{home
};
238 chown $u->{uid
}, $u->{gid
}, $u->{home
};
239 chmod oct($u->{homeMode
}), $u->{home
};
242 if (defined $u->{hashedPasswordFile
}) {
243 if (-e
$u->{hashedPasswordFile
}) {
244 $u->{hashedPassword
} = read_file
($u->{hashedPasswordFile
});
245 chomp $u->{hashedPassword
};
247 warn "warning: password file ‘$u->{hashedPasswordFile}’ does not exist\n";
249 } elsif (defined $u->{password
}) {
250 $u->{hashedPassword
} = hashPassword
($u->{password
});
253 if (!defined $u->{shell
}) {
254 if (defined $existing) {
255 $u->{shell
} = $existing->{shell
};
257 warn "warning: no declarative or previous shell for ‘$name’, setting shell to nologin\n";
258 $u->{shell
} = "/run/current-system/sw/bin/nologin";
262 $u->{fakePassword
} = $existing->{fakePassword
} // "x";
263 $usersOut{$name} = $u;
265 $uidMap->{$name} = $u->{uid
};
268 # Update the persistent list of declarative users.
269 updateFile
($declUsersFile, join(" ", sort(keys %usersOut)));
271 # Merge in the existing /etc/passwd.
272 foreach my $name (keys %usersCur) {
273 my $u = $usersCur{$name};
274 next if defined $usersOut{$name};
275 if (!$spec->{mutableUsers
} || defined $declUsers{$name}) {
276 dry_print
("removing user", "would remove user", "‘$name’");
278 $usersOut{$name} = $u;
282 # Rewrite /etc/passwd. FIXME: acquire lock.
283 @lines = map { join(":", $_->{name
}, $_->{fakePassword
}, $_->{uid
}, $_->{gid
}, $_->{description
}, $_->{home
}, $_->{shell
}) . "\n" }
284 (sort { $a->{uid
} <=> $b->{uid
} } (values %usersOut));
285 updateFile
($uidMapFile, to_json
($uidMap, {canonical
=> 1}));
286 updateFile
("/etc/passwd", \
@lines);
287 nscdInvalidate
("passwd");
290 # Rewrite /etc/shadow to add new accounts or remove dead ones.
294 foreach my $line (-f
"/etc/shadow" ? read_file
("/etc/shadow", { binmode => ":utf8" }) : ()) {
296 # struct name copied from `man 3 shadow`
297 my ($sp_namp, $sp_pwdp, $sp_lstch, $sp_min, $sp_max, $sp_warn, $sp_inact, $sp_expire, $sp_flag) = split(':', $line, -9);
298 my $u = $usersOut{$sp_namp};;
300 $sp_pwdp = "!" if !$spec->{mutableUsers
};
301 $sp_pwdp = $u->{hashedPassword
} if defined $u->{hashedPassword
} && !$spec->{mutableUsers
}; # FIXME
302 $sp_expire = dateToDays
($u->{expires
}) if defined $u->{expires
};
304 push @shadowNew, join(":", $sp_namp, $sp_pwdp, $sp_lstch, $sp_min, $sp_max, $sp_warn, $sp_inact, $sp_expire, $sp_flag) . "\n";
305 $shadowSeen{$sp_namp} = 1;
308 foreach my $u (values %usersOut) {
309 next if defined $shadowSeen{$u->{name
}};
310 my $hashedPassword = "!";
311 $hashedPassword = $u->{hashedPassword
} if defined $u->{hashedPassword
};
313 $expires = dateToDays
($u->{expires
}) if defined $u->{expires
};
314 # FIXME: set correct value for sp_lstchg.
315 push @shadowNew, join(":", $u->{name
}, $hashedPassword, "1::::", $expires, "") . "\n";
318 updateFile
("/etc/shadow", \
@shadowNew, 0640);
320 my $uid = getpwnam "root";
321 my $gid = getgrnam "shadow";
322 my $path = "/etc/shadow";
323 (chown($uid, $gid, $path) || die "Failed to change ownership of $path: $!") unless $is_dry;
326 # Rewrite /etc/subuid & /etc/subgid to include default container mappings
328 my $subUidMapFile = "/var/lib/nixos/auto-subuid-map";
329 my $subUidMap = -e
$subUidMapFile ? decode_json
(read_file
($subUidMapFile)) : {};
331 my (%subUidsUsed, %subUidsPrevUsed);
333 $subUidsPrevUsed{$_} = 1 foreach values %{$subUidMap};
336 my ($name, @rest) = @_;
338 # TODO: No upper bounds?
339 my ($min, $max, $up) = (100000, 100000 * 100, 1);
340 my $prevId = $subUidMap->{$name};
341 if (defined $prevId && !defined $subUidsUsed{$prevId}) {
342 $subUidsUsed{$prevId} = 1;
346 my $id = allocId
(\
%subUidsUsed, \
%subUidsPrevUsed, $min, $max, $up, sub { my ($uid) = @_; getpwuid($uid) });
347 my $offset = $id - 100000;
348 my $count = $offset * 65536;
349 my $subordinate = 100000 + $count;
355 foreach my $u (values %usersOut) {
356 my $name = $u->{name
};
358 foreach my $range (@
{$u->{subUidRanges
}}) {
359 my $value = join(":", ($name, $range->{startUid
}, $range->{count
}));
360 push @subUids, $value;
363 foreach my $range (@
{$u->{subGidRanges
}}) {
364 my $value = join(":", ($name, $range->{startGid
}, $range->{count
}));
365 push @subGids, $value;
368 if($u->{autoSubUidGidRange
}) {
369 my $subordinate = allocSubUid
($name);
370 $subUidMap->{$name} = $subordinate;
371 my $value = join(":", ($name, $subordinate, 65536));
372 push @subUids, $value;
373 push @subGids, $value;
377 updateFile
("/etc/subuid", join("\n", @subUids) . "\n");
378 updateFile
("/etc/subgid", join("\n", @subGids) . "\n");
379 updateFile
($subUidMapFile, encode_json
($subUidMap) . "\n");