Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nixos/update-users-groups: add support for account expiry #246772

Merged
merged 3 commits into from
Aug 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 29 additions & 8 deletions nixos/modules/config/update-users-groups.pl
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use File::Slurp;
use Getopt::Long;
use JSON;
use DateTime;

# Keep track of deleted uids and gids.
my $uidMapFile = "/var/lib/nixos/uid-map";
Expand All @@ -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;
}
Expand Down Expand Up @@ -283,22 +300,26 @@ sub parseUser {

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);
Expand Down
15 changes: 13 additions & 2 deletions nixos/modules/config/users-groups.nix
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,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
Expand Down Expand Up @@ -433,7 +444,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;
Expand Down Expand Up @@ -587,7 +598,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}
'';
};
Expand Down
1 change: 1 addition & 0 deletions nixos/tests/all-tests.nix
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,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 {};
Expand Down
70 changes: 70 additions & 0 deletions nixos/tests/user-expiry.nix
Original file line number Diff line number Diff line change
@@ -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")
'';
}