From 9da75fdaf147dc525861e8a534780d563c897dba Mon Sep 17 00:00:00 2001 From: zimbatm Date: Wed, 30 Nov 2022 14:54:59 +0100 Subject: [PATCH 1/3] nixos/update-users-groups: add support for account expiry Introduce a `users.users..expires` option to allows setting an expiry date to user accounts. This is useful when members should gain temporary access and you don't want to have to roll out another system update to disable them. --- nixos/modules/config/update-users-groups.pl | 33 ++++++++++++++++----- nixos/modules/config/users-groups.nix | 15 ++++++++-- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/nixos/modules/config/update-users-groups.pl b/nixos/modules/config/update-users-groups.pl index 4368ec24ea9e9..7ec4235db51a1 100644 --- a/nixos/modules/config/update-users-groups.pl +++ b/nixos/modules/config/update-users-groups.pl @@ -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"; @@ -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; } @@ -283,14 +300,16 @@ 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) { diff --git a/nixos/modules/config/users-groups.nix b/nixos/modules/config/users-groups.nix index b538a0119c06d..69fd04a8c06fa 100644 --- a/nixos/modules/config/users-groups.nix +++ b/nixos/modules/config/users-groups.nix @@ -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 @@ -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; @@ -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} ''; }; From 127e2ed645137ccbcbedb3ba316e1c8bf3ab9ae1 Mon Sep 17 00:00:00 2001 From: r-vdp Date: Wed, 2 Aug 2023 13:51:06 +0200 Subject: [PATCH 2/3] nixos/update-users-groups: add nixos test for the expires option --- nixos/tests/all-tests.nix | 1 + nixos/tests/user-expiry.nix | 70 +++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 nixos/tests/user-expiry.nix diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 59b8c81fb0b57..97226f4834481 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -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 {}; diff --git a/nixos/tests/user-expiry.nix b/nixos/tests/user-expiry.nix new file mode 100644 index 0000000000000..bcaed7a0ccb0b --- /dev/null +++ b/nixos/tests/user-expiry.nix @@ -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") + ''; +} From 44a7059bf287366e96f8188cf834a0a63932e220 Mon Sep 17 00:00:00 2001 From: r-vdp Date: Wed, 2 Aug 2023 13:51:41 +0200 Subject: [PATCH 3/3] nixos/update-users-groups: set expiry correctly for new users --- nixos/modules/config/update-users-groups.pl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nixos/modules/config/update-users-groups.pl b/nixos/modules/config/update-users-groups.pl index 7ec4235db51a1..fea31ab280f44 100644 --- a/nixos/modules/config/update-users-groups.pl +++ b/nixos/modules/config/update-users-groups.pl @@ -316,8 +316,10 @@ sub parseUser { 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);