Merge pull request #246772 from R-VdP/nixos-user-expiry
nixos/update-users-groups: add support for account expiry
This commit is contained in:
commit
da37904672
4 changed files with 113 additions and 10 deletions
|
@ -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);
|
||||
|
|
|
@ -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}
|
||||
'';
|
||||
};
|
||||
|
|
|
@ -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 {};
|
||||
|
|
70
nixos/tests/user-expiry.nix
Normal file
70
nixos/tests/user-expiry.nix
Normal 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")
|
||||
'';
|
||||
}
|
Loading…
Reference in a new issue