Merge pull request #246772 from R-VdP/nixos-user-expiry

nixos/update-users-groups: add support for account expiry
This commit is contained in:
Franz Pletz 2023-08-26 02:41:34 +02:00 committed by GitHub
commit da37904672
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 113 additions and 10 deletions

View file

@ -4,6 +4,7 @@ use File::Path qw(make_path);
use File::Slurp;
use Getopt::Long;
use JSON;
use DateTime;
# Keep track of deleted uids and gids.
my $uidMapFile = "/var/lib/nixos/uid-map";
@ -22,6 +23,22 @@ sub updateFile {
write_file($path, { atomic => 1, binmode => ':utf8', perms => $perms // 0644 }, $contents) or die;
}
# Converts an ISO date to number of days since 1970-01-01
sub dateToDays {
my ($date) = @_;
my ($year, $month, $day) = split('-', $date, -3);
my $dt = DateTime->new(
year => $year,
month => $month,
day => $day,
hour => 0,
minute => 0,
second => 0,
time_zone => 'UTC',
);
return $dt->epoch / 86400;
}
sub nscdInvalidate {
system("nscd", "--invalidate", $_[0]) unless $is_dry;
}
@ -285,22 +302,26 @@ my %shadowSeen;
foreach my $line (-f "/etc/shadow" ? read_file("/etc/shadow", { binmode => ":utf8" }) : ()) {
chomp $line;
my ($name, $hashedPassword, @rest) = split(':', $line, -9);
my $u = $usersOut{$name};;
# struct name copied from `man 3 shadow`
my ($sp_namp, $sp_pwdp, $sp_lstch, $sp_min, $sp_max, $sp_warn, $sp_inact, $sp_expire, $sp_flag) = split(':', $line, -9);
my $u = $usersOut{$sp_namp};;
next if !defined $u;
$hashedPassword = "!" if !$spec->{mutableUsers};
$hashedPassword = $u->{hashedPassword} if defined $u->{hashedPassword} && !$spec->{mutableUsers}; # FIXME
chomp $hashedPassword;
push @shadowNew, join(":", $name, $hashedPassword, @rest) . "\n";
$shadowSeen{$name} = 1;
$sp_pwdp = "!" if !$spec->{mutableUsers};
$sp_pwdp = $u->{hashedPassword} if defined $u->{hashedPassword} && !$spec->{mutableUsers}; # FIXME
$sp_expire = dateToDays($u->{expires}) if defined $u->{expires};
chomp $sp_pwdp;
push @shadowNew, join(":", $sp_namp, $sp_pwdp, $sp_lstch, $sp_min, $sp_max, $sp_warn, $sp_inact, $sp_expire, $sp_flag) . "\n";
$shadowSeen{$sp_namp} = 1;
}
foreach my $u (values %usersOut) {
next if defined $shadowSeen{$u->{name}};
my $hashedPassword = "!";
$hashedPassword = $u->{hashedPassword} if defined $u->{hashedPassword};
my $expires = "";
$expires = dateToDays($u->{expires}) if defined $u->{expires};
# FIXME: set correct value for sp_lstchg.
push @shadowNew, join(":", $u->{name}, $hashedPassword, "1::::::") . "\n";
push @shadowNew, join(":", $u->{name}, $hashedPassword, "1::::", $expires, "") . "\n";
}
updateFile("/etc/shadow", \@shadowNew, 0640);

View file

@ -311,6 +311,17 @@ let
'';
};
expires = mkOption {
type = types.nullOr (types.strMatching "[[:digit:]]{4}-[[:digit:]]{2}-[[:digit:]]{2}");
default = null;
description = lib.mdDoc ''
Set the date on which the user's account will no longer be
accessible. The date is expressed in the format YYYY-MM-DD, or null
to disable the expiry.
A user whose account is locked must contact the system
administrator before being able to use the system again.
'';
};
};
config = mkMerge
@ -438,7 +449,7 @@ let
name uid group description home homeMode createHome isSystemUser
password passwordFile hashedPassword
autoSubUidGidRange subUidRanges subGidRanges
initialPassword initialHashedPassword;
initialPassword initialHashedPassword expires;
shell = utils.toShellPath u.shell;
}) cfg.users;
groups = attrValues cfg.groups;
@ -637,7 +648,7 @@ in {
install -m 0700 -d /root
install -m 0755 -d /home
${pkgs.perl.withPackages (p: [ p.FileSlurp p.JSON ])}/bin/perl \
${pkgs.perl.withPackages (p: [ p.FileSlurp p.JSON p.DateTime ])}/bin/perl \
-w ${./update-users-groups.pl} ${spec}
'';
};

View file

@ -831,6 +831,7 @@ in {
uptime-kuma = handleTest ./uptime-kuma.nix {};
usbguard = handleTest ./usbguard.nix {};
user-activation-scripts = handleTest ./user-activation-scripts.nix {};
user-expiry = runTest ./user-expiry.nix;
user-home-mode = handleTest ./user-home-mode.nix {};
uwsgi = handleTest ./uwsgi.nix {};
v2ray = handleTest ./v2ray.nix {};

View file

@ -0,0 +1,70 @@
let
alice = "alice";
bob = "bob";
eve = "eve";
passwd = "pass1";
in
{
name = "user-expiry";
nodes = {
machine = {
users.users = {
${alice} = {
initialPassword = passwd;
isNormalUser = true;
expires = "1990-01-01";
};
${bob} = {
initialPassword = passwd;
isNormalUser = true;
expires = "2990-01-01";
};
${eve} = {
initialPassword = passwd;
isNormalUser = true;
};
};
};
};
testScript = ''
def switch_to_tty(tty_number):
machine.fail(f"pgrep -f 'agetty.*tty{tty_number}'")
machine.send_key(f"alt-f{tty_number}")
machine.wait_until_succeeds(f"[ $(fgconsole) = {tty_number} ]")
machine.wait_for_unit(f"getty@tty{tty_number}.service")
machine.wait_until_succeeds(f"pgrep -f 'agetty.*tty{tty_number}'")
machine.wait_for_unit("multi-user.target")
machine.wait_for_unit("getty@tty1.service")
with subtest("${alice} cannot login"):
machine.wait_until_tty_matches("1", "login: ")
machine.send_chars("${alice}\n")
machine.wait_until_tty_matches("1", "Password: ")
machine.send_chars("${passwd}\n")
machine.wait_until_succeeds("journalctl --grep='account ${alice} has expired \\(account expired\\)'")
machine.wait_until_tty_matches("1", "login: ")
with subtest("${bob} can login"):
switch_to_tty(2)
machine.wait_until_tty_matches("2", "login: ")
machine.send_chars("${bob}\n")
machine.wait_until_tty_matches("2", "Password: ")
machine.send_chars("${passwd}\n")
machine.wait_until_succeeds("pgrep -u ${bob} bash")
with subtest("${eve} can login"):
switch_to_tty(3)
machine.wait_until_tty_matches("3", "login: ")
machine.send_chars("${eve}\n")
machine.wait_until_tty_matches("3", "Password: ")
machine.send_chars("${passwd}\n")
machine.wait_until_succeeds("pgrep -u ${eve} bash")
'';
}