[Pkg-shadow-devel] [Git][debian/adduser][wip/feature-system-locks] 39 commits: deluser --lock [--system]
Marc Haber (@zugschlus)
gitlab at salsa.debian.org
Thu Jan 8 19:22:38 GMT 2026
Marc Haber pushed to branch wip/feature-system-locks at Debian / adduser
Commits:
748d0501 by Matt Barry at 2026-01-08T20:05:59+01:00
deluser --lock [--system]
* Add --lock/--unlock options for dealing with disabled accounts.
See documentation. Should probably have a notice specifically
about behavioral changes (eg. adding/removing system accounts).
- - - - -
1c9f7b96 by Matt Barry at 2026-01-08T20:06:02+01:00
the documentation commit
this contains documentation changes related to account locking.
might still need some work.
- - - - -
eba2579e by Matt Barry at 2026-01-08T20:06:02+01:00
add some basic tests
- - - - -
9940ec5a by Marc Haber at 2026-01-08T20:06:02+01:00
improve log level handling
Thanks: Matt Barry
Git-Dch: ignore
- - - - -
a1f52465 by Marc Haber at 2026-01-08T20:06:02+01:00
add new constants for password handling
Git-Dch: ignore
Thanks: Matt Barry
- - - - -
857d4432 by Marc Haber at 2026-01-08T20:06:02+01:00
introduce password handling logic
Git-Dch: ignore
Thanks: Matt Barry
- - - - -
27adefcb by Marc Haber at 2026-01-08T20:06:02+01:00
introduce new function existing_value_desc
Git-Dch: ignore
Thanks: Matt Barry
- - - - -
ac9f7847 by Marc Haber at 2026-01-08T20:06:02+01:00
use existing_value_desc
Thanks: Matt Barry
Git-Dch: ignore
- - - - -
7482a186 by Matt Barry at 2026-01-08T20:06:53+01:00
locked.account.tests
- - - - -
5343a23f by Marc Haber at 2026-01-08T20:06:54+01:00
prepare AdduserTestsCommon for the new tests
Git-Dch: ignore
- - - - -
e066556d by Matt Barry at 2026-01-08T20:07:18+01:00
user locked account tests
Git-Dch: ignore
- - - - -
b25955ae by Marc Haber at 2026-01-08T20:07:19+01:00
fix testsuite warning in test08.pl
Git-Dch: ignore
- - - - -
e5dd5190 by Marc Haber at 2026-01-08T20:07:19+01:00
run upstream testsuite with redirected stdin
otherwise tests might hang invisibly when the software
goes interactive
Git-Dch: ignore
- - - - -
7e4ae6f5 by Marc Haber at 2026-01-08T20:07:19+01:00
--unlock makes sense also for system accounts
This is consisten with the documentation
Git-Dch: ignore
- - - - -
1bf2a3a7 by Marc Haber at 2026-01-08T20:07:19+01:00
fix EXISTING_HAS_PASSWORD to correctly handle !
This now also handles !something in the password field
- - - - -
5f496bb3 by Marc Haber at 2026-01-08T20:07:19+01:00
add unlock_user function
Git-Dch: ignore
- - - - -
cc36a19b by Marc Haber at 2026-01-08T20:07:19+01:00
error out if asked to add an already locked non-ystem user
Git-Dch: ignore
- - - - -
d7d4199a by Marc Haber at 2026-01-08T20:07:19+01:00
unlock existing system user with new function
Git-Dch: ignore
- - - - -
ff29e6d5 by Marc Haber at 2026-01-08T20:07:19+01:00
unlocking an existing system user is info
Git-Dch: ignore
- - - - -
23068fc6 by Marc Haber at 2026-01-08T20:07:19+01:00
implement adduser --unlock
Git-Dch: ignore
- - - - -
b37bab5b by Marc Haber at 2026-01-08T20:07:19+01:00
add vim helper line to testsuite files
The files need to be reflowed when working on them next time
Git-Dch: ignore
- - - - -
c1899ce0 by Marc Haber at 2026-01-08T20:07:19+01:00
add AdduserStatefile.pm to maintain state file
- - - - -
1dc00baf by Marc Haber at 2026-01-08T20:07:19+01:00
install AdduserStatefile, remove state file on postrm
Git-Dch: ignore
- - - - -
59e0cba3 by Marc Haber at 2026-01-08T20:07:19+01:00
clarify documentation of exit value 31
Git-Dch: ignore
- - - - -
d490d934 by Marc Haber at 2026-01-08T20:07:19+01:00
use AdduserStatefile
Git-Dch: ignore
- - - - -
c237e974 by Marc Haber at 2026-01-08T20:07:19+01:00
rename to RET_INVALID_CHARS_IN_INPUT, apply to comment as well
That was RET_INVALID_CHARS_IN_INPUT previously. The check is now
applied to the comment as well and the error message adapted.
Git-Dch: ignore
- - - - -
a7ab290c by Marc Haber at 2026-01-08T20:07:19+01:00
move interactive command loops to a function
This is more streamlined and handles running on no terminal better
- - - - -
744a46f8 by Marc Haber at 2026-01-08T20:08:03+01:00
re-work logic around remove-home etc
Git-Dch: ignore
- - - - -
a115c992 by Marc Haber at 2026-01-08T20:08:03+01:00
prepare testsuite libraries to properly handle EXISTING_
this brings the simplifications to the test suite libraries
Git-Dch: ignore
- - - - -
820a82f5 by Marc Haber at 2026-01-08T20:08:03+01:00
add assert_path_is_a_file to debian/tests/lib
Git-Dch: ignore
- - - - -
cf736092 by Marc Haber at 2026-01-08T20:08:03+01:00
remove ",,," from comment in test suite expectations
this must have been lost during some rebase.
Git-Dch: ignore
- - - - -
f3b40660 by Marc Haber at 2026-01-08T20:08:03+01:00
adapt test to new more picky src:shadow
Git-Dch: ignore
- - - - -
a381b625 by Marc Haber at 2026-01-08T20:08:03+01:00
test whether account is really gone.
Git-Dch: ignore
- - - - -
4ba5c2bc by Marc Haber at 2026-01-08T20:08:03+01:00
replace cronjack.t with a no-op
Git-Dch: ignore
this is no longer possible since new src:shadow won't accept the bobby
tables user name any more
- - - - -
e35108f4 by Marc Haber at 2026-01-08T20:08:03+01:00
fix wrong message in lib_test
Git-Dch: ignore
- - - - -
cbf4d3c3 by Marc Haber at 2026-01-08T20:08:03+01:00
lib_test.pm: don't exit, return
Git-Dch: ignore
- - - - -
efc89ce7 by Marc Haber at 2026-01-08T20:08:03+01:00
make testsuite functions more verbose
Git-Dch: ignore
- - - - -
84503239 by Marc Haber at 2026-01-08T20:08:03+01:00
rework runsuite.sh
This now allows running a single test, and cleans up better
Git-Dch: ignore
- - - - -
ae6e8bd0 by Marc Haber at 2026-01-08T20:08:03+01:00
make more clear that failure was expected
Git-Dch: ignore
- - - - -
30 changed files:
- AdduserCommon.pm
- AdduserRetvalues.pm
- + AdduserStatefile.pm
- adduser
- adduser.conf
- debian/postrm
- debian/rules
- + debian/tests/f/account_locks.t
- debian/tests/f/adduser_system.t
- debian/tests/f/cronjack.t
- debian/tests/lib/AdduserTestsCommon.pm
- deluser
- deluser.conf
- doc/adduser.8
- doc/deluser.8
- doc/deluser.conf.5
- + notes.100808x.md
- + testsuite/deluser-delete.conf
- testsuite/lib_test.pm
- testsuite/runsuite.sh
- testsuite/test01.pl
- testsuite/test02.pl
- testsuite/test03.pl
- testsuite/test04.pl
- testsuite/test05.pl
- testsuite/test06.pl
- testsuite/test07.pl
- testsuite/test08.pl
- testsuite/test09.pl
- + testsuite/test10.pl
Changes:
=====================================
AdduserCommon.pm
=====================================
@@ -31,6 +31,7 @@ my $codeset;
use Debian::AdduserLogging 3.139;
use Debian::AdduserRetvalues 3.139;
+use Debian::AdduserStatefile 3.139;
BEGIN {
if ( Debian::AdduserLogging->VERSION != version->declare('3.139') ||
Debian::AdduserRetvalues->VERSION != version->declare('3.139') ) {
@@ -102,6 +103,14 @@ use constant {
EXISTING_ID_MISMATCH => 4,
EXISTING_LOCKED => 8,
EXISTING_HAS_PASSWORD => 16,
+ EXISTING_EXPIRED => 32,
+ EXISTING_NOLOGIN => 64,
+};
+
+use constant {
+ STDOUTDEFLEVEL => "warn",
+ STDERRDEFLEVEL => "warn",
+ LOGMSGDEFLEVEL => "info",
};
@EXPORT = (
@@ -138,6 +147,11 @@ use constant {
'EXISTING_ID_MISMATCH',
'EXISTING_LOCKED',
'EXISTING_HAS_PASSWORD',
+ 'EXISTING_EXPIRED',
+ 'EXISTING_NOLOGIN',
+ 'STDOUTDEFLEVEL',
+ 'STDERRDEFLEVEL',
+ 'LOGMSGDEFLEVEL',
'existing_user_status',
'existing_group_status',
);
@@ -507,6 +521,7 @@ sub preseed_config {
no_del_paths => "^/bin\$ ^/boot\$ ^/dev\$ ^/etc\$ ^/initrd ^/lib ^/lost+found\$ ^/media\$ ^/mnt\$ ^/opt\$ ^/proc\$ ^/root\$ ^/run\$ ^/sbin\$ ^/srv\$ ^/sys\$ ^/tmp\$ ^/usr\$ ^/var\$ ^/vmlinu",
name_regex => def_name_regex,
sys_name_regex => def_sys_name_regex,
+ sys_delete_action => "delete",
exclude_fstypes => "(proc|sysfs|usbfs|devpts|devtmpfs|devfs|afs)",
skel_ignore_regex => "\.(dpkg|ucf)-(old|new|dist)\$",
extra_groups => "users",
@@ -590,35 +605,47 @@ END {
# new_name: the name of the user to check
# new_uid : the UID of the user
# return value:
-# bitwise combination of these constants:
-# EXISTING_NOT_FOUND => 0
-# EXISTING_FOUND => 1
-# EXISTING_SYSTEM => 2
-# EXISTING_ID_MISMATCH => 4
-# EXISTING_LOCKED => 8
-# EXISTING_HAS_PASSWORD => 16
-# e.g. if the requested account name exists as a locked system user,
-# return 8|2|1 == 11
+# bitwise combination of the EXISTING_ constants
sub existing_user_status {
- my ($config, $new_name,$new_uid) = @_;
- my ($dummy1,$pw,$uid);
+ my ($config, $user_name,$user_uid) = @_;
my $ret = EXISTING_NOT_FOUND;
- log_trace( "existing_user_status called with new_name %s, new_uid %s, first_system_uid %s, last_system_uid %s", $new_name, $new_uid, $config->{"first_system_uid"}, $config->{"last_system_uid"} );
- if (($dummy1,$pw,$uid) = egetpwnam($new_name)) {
+ log_trace( "existing_user_status called with user_name %s, user_uid %s, first_system_uid %s, last_system_uid %s", $user_name, $user_uid, $config->{"first_system_uid"}, $config->{"last_system_uid"} );
+
+ # collect user data
+ my (
+ $egpwn_name, $egpwn_passwd, $egpwn_uid, $egpwn_gid, $egpwn_quota,
+ $egpwn_comment, $egpwn_gcos, $egpwn_dir, $egpwn_shell, $egpwn_expire,
+ $egpwn_rest
+ ) = egetpwnam($user_name);
+ my $shadow_line = `getent shadow $user_name`;
+ chomp $shadow_line;
+ my @shadow_fields = split /:/, $shadow_line;
+ if (defined $egpwn_uid) {
# user with the name exists
- log_trace( "egetpwnam(%s) returns %s, %s, %s", $new_name, $dummy1, $pw, $uid );
+ log_trace( "egetpwnam(%s) returns %s, %s, %s, %s", $user_name, $egpwn_passwd, $egpwn_uid, $egpwn_dir, $egpwn_shell );
$ret |= EXISTING_FOUND;
- $ret |= EXISTING_ID_MISMATCH if (defined($new_uid) && $uid != $new_uid);
+ $ret |= EXISTING_ID_MISMATCH if (defined($user_uid) && $egpwn_uid != $user_uid);
$ret |= EXISTING_SYSTEM if
- (($uid >= $config->{"first_system_uid"}) && ($uid <= $config->{"last_system_uid"}));
+ (($egpwn_uid >= $config->{"first_system_uid"}) && ($egpwn_uid <= $config->{"last_system_uid"}));
$ret |= EXISTING_HAS_PASSWORD if
- (defined $pw && $pw ne '' && $pw ne '!' && $pw !~ /^\*/);
- $ret |= EXISTING_LOCKED if (substr($pw,0,1) eq "!"); # TODO: also check expiry?
- } elsif (defined($new_uid) && getpwuid($new_uid)) {
+ (defined $egpwn_passwd && $egpwn_passwd ne '' && ($egpwn_passwd =~ s/^[!*]+//r ne ''));
+
+ my $password_field = $shadow_fields[1] // '';
+ $ret |= EXISTING_LOCKED if $password_field =~ /^[!*]/ && (get_state_value($user_name, "locked") // "") eq "1";
+
+ $ret |= EXISTING_NOLOGIN if ($egpwn_shell =~ /bin\/nologin/);
+
+ my $acct_exp = $shadow_fields[7] // '';
+ if ($acct_exp && $acct_exp > 0) {
+ my $today_days = int(time / 86400);
+ $ret |= EXISTING_EXPIRED if $acct_exp < $today_days;
+ }
+
+ } elsif (defined($user_uid) && getpwuid($user_uid)) {
# user with the uid exists
$ret |= EXISTING_ID_MISMATCH;
}
- log_trace( "existing_user_status( %s, %s ) returns %s", $new_name, $new_uid, $ret );
+ log_trace( "existing_user_status( %s, %s ) returns %s (%s)", $user_name, $user_uid, $ret, existing_value_desc($ret) );
return $ret;
}
@@ -634,10 +661,10 @@ sub existing_user_status {
# EXISTING_ID_MISMATCH => 4
sub existing_group_status {
my ($config, $new_name,$new_gid) = @_;
- my ($dummy1,$dummy2,$gid);
+ my ($gid);
my $ret = EXISTING_NOT_FOUND;
log_trace( "existing_group_status called with new_name %s, new_gid %s", $new_name, $new_gid );
- if (($dummy1,$dummy2,$gid) = egetgrnam($new_name)) {
+ if ((undef,undef,$gid) = egetgrnam($new_name)) {
# group with the name exists
log_trace("egetgrnam %s returned successfully, gid = %s", $new_name, $gid);
$ret |= EXISTING_FOUND;
@@ -647,10 +674,24 @@ sub existing_group_status {
} elsif (defined($new_gid) && getgrgid($new_gid)) {
$ret |= EXISTING_ID_MISMATCH;
}
- log_trace( "existing_group_status( %s, %s ) returns %s", $new_name, $new_gid, $ret );
+ log_trace( "existing_group_status( %s, %s ) returns %s (%s)", $new_name, $new_gid, $ret, existing_value_desc($ret) );
return $ret;
}
+sub existing_value_desc {
+ my ($val) = @_;
+ my @flags = ();
+ push @flags, "found" if ($val & EXISTING_FOUND);
+ push @flags, "wrongid" if $val & EXISTING_ID_MISMATCH;
+ push @flags, "system" if ($val & EXISTING_SYSTEM);
+ push @flags, "locked" if $val & EXISTING_LOCKED;
+ push @flags, "haspass" if $val & EXISTING_HAS_PASSWORD;
+ push @flags, "nologin" if $val & EXISTING_NOLOGIN;
+ push @flags, "expired" if $val & EXISTING_EXPIRED;
+ push @flags, "notfound" unless $#flags > 0;
+ return join '|', at flags
+}
+
1;
# Local Variables:
=====================================
AdduserRetvalues.pm
=====================================
@@ -20,7 +20,7 @@ use vars qw(@EXPORT $VAR1);
'RET_ID_IN_USE',
'RET_NO_ID_IN_RANGE',
'RET_NO_PRIMARY_GID',
- 'RET_INVALID_CHARS_IN_NAME',
+ 'RET_INVALID_CHARS_IN_INPUT',
'RET_INVALID_HOME_DIRECTORY',
'RET_INVALID_NAME_FROM_USERADD',
'RET_GROUP_NOT_EMPTY',
@@ -61,8 +61,8 @@ use constant RET_NO_PRIMARY_GID => 23; # requested primary GID does not exist
# object name errors
-use constant RET_INVALID_CHARS_IN_NAME => 31; # the provided name contains invalid characters
-use constant RET_INVALID_HOME_DIRECTORY => 32; # the provided name contains invalid characters
+use constant RET_INVALID_CHARS_IN_INPUT => 31; # provided input (name/comment) contains invalid characters
+use constant RET_INVALID_HOME_DIRECTORY => 32; # the provided home directory is invalid
use constant RET_INVALID_NAME_FROM_USERADD => 32; # useradd returned 19 "invalid user or group name"
# group membership errors
=====================================
AdduserStatefile.pm
=====================================
@@ -0,0 +1,293 @@
+package Debian::AdduserStatefile 3.139;
+use 5.36.0;
+use utf8;
+
+# AdduserStatefile.pm: Manage persistent state for deleted user accounts
+#
+# This module provides a simple key-value store for tracking information
+# about deleted user accounts. The state is stored in a text file at
+# /var/lib/adduser/state in a format similar to /etc/passwd.
+#
+# File format: username:key1=value1:key2=value2:key3=value3
+#
+# Copyright (C) 2026 Marc Haber
+#
+# License: GPL-2+
+
+use parent qw(Exporter);
+use vars qw(@EXPORT);
+
+ at EXPORT = (
+ 'set_state_value',
+ 'get_state_value',
+ 'get_state_user_data',
+ 'delete_state_user',
+);
+
+use strict;
+use warnings;
+
+our $STATE_DIR = '/var/lib/adduser';
+our $STATE_FILE = "$STATE_DIR/state";
+our $LOCK_FILE = "$STATE_DIR/state.lock";
+
+# init_state_file()
+#
+# Initialize the state file and its parent directory if they don't exist.
+# Creates /var/lib/adduser with mode 0755 and the state file with mode 0644.
+# Dies on error.
+sub init_state_file {
+ # Create the directory if it doesn't exist
+ unless (-d $STATE_DIR) {
+ mkdir($STATE_DIR, 0755) or die "Cannot create $STATE_DIR: $!";
+ }
+
+ # Create the state file if it doesn't exist
+ unless (-f $STATE_FILE) {
+ open(my $fh, '>', $STATE_FILE) or die "Cannot create $STATE_FILE: $!";
+ close($fh);
+ chmod(0644, $STATE_FILE);
+ }
+}
+
+# set_value($username, $key, $value)
+#
+# Set a key-value pair for the specified username in the state file.
+# If the username doesn't exist, it will be created.
+# If the key already exists for that user, it will be overwritten.
+# If the value is empty or undefined, the key will be deleted instead.
+#
+# Parameters:
+# $username - The username to store data for
+# $key - The key name (cannot contain '=', ':', or newlines)
+# $value - The value to store (cannot contain ':' or newlines)
+# If empty or undef, the key is deleted
+#
+# Dies if the username, key, or value contain invalid characters.
+sub set_state_value {
+ my ($username, $key, $value) = @_;
+
+ my $lock_fh = _acquire_lock();
+ my %data = _read_state();
+
+ $data{$username} ||= {};
+ if (!defined $value || $value eq '') {
+ delete $data{$username}{$key};
+ if (keys %{$data{$username}} == 0) {
+ delete $data{$username};
+ }
+ } else {
+ die "Invalid value" if $value =~ /[:\n]/;
+ $data{$username}{$key} = $value;
+ }
+
+ _write_state(\%data);
+ _release_lock($lock_fh);
+}
+
+# get_value($username, $key)
+#
+# Retrieve a value for a given username and key (or all values if key is undef).
+# This is a lock-free read operation. Due to atomic file replacement,
+# readers will always see a consistent (though possibly slightly stale)
+# version of the state file.
+#
+# Parameters:
+# $username - The username to look up
+# $key - The key name to retrieve (optional)
+#
+# Returns:
+# If $key is provided: the value if found, undef otherwise
+# If $key is undef: a hash reference containing all key-value pairs for the user
+sub get_state_value {
+ my ($username, $key) = @_;
+
+ # Return undef/empty hash if state file doesn't exist yet
+ return undef unless -f $STATE_FILE;
+
+ my %data = _read_state();
+
+ # Return undef/empty hash if user doesn't exist
+ if (!exists $data{$username}) {
+ return defined $key ? undef : {};
+ }
+
+ # Return specific key or all data
+ return defined $key ? $data{$username}{$key} : $data{$username};
+}
+
+# get_user_data($username)
+#
+# Retrieve all key-value pairs for a given username.
+# This is a convenience wrapper around get_value($username, undef).
+#
+# Parameters:
+# $username - The username to look up
+#
+# Returns:
+# A hash reference containing all key-value pairs for the user,
+# or an empty hash reference if the user doesn't exist
+sub get_state_user_data {
+ my ($username) = @_;
+ return get_value($username, undef);
+}
+
+# delete_user($username)
+#
+# Remove all data for a user from the state file.
+# This completely removes the user's entry from the state file.
+#
+# Parameters:
+# $username - The username to delete
+#
+# Returns silently if the state file doesn't exist or the user isn't found.
+sub delete_state_user {
+ my ($username) = @_;
+
+ # Nothing to do if state file doesn't exist
+ return unless -f $STATE_FILE;
+
+ # Acquire lock, read, modify, write, release lock
+ my $lock_fh = _acquire_lock();
+ my %data = _read_state();
+ delete $data{$username};
+ _write_state(\%data);
+ _release_lock($lock_fh);
+}
+
+# _acquire_lock()
+#
+# Internal function to acquire an exclusive lock on the state file.
+# Uses a separate lock file to avoid interfering with atomic renames.
+#
+# Returns:
+# File handle to the lock file (caller must pass to _release_lock)
+#
+# Dies if the lock file cannot be opened.
+sub _acquire_lock {
+ # Ensure the directory exists
+ unless (-d $STATE_DIR) {
+ mkdir($STATE_DIR, 0755) or die "Cannot create $STATE_DIR: $!";
+ }
+
+ # Open (or create) the lock file
+ open(my $lock_fh, '>>', $LOCK_FILE) or die "Cannot open $LOCK_FILE: $!";
+
+ # Acquire exclusive lock (blocks until available)
+ flock($lock_fh, 2) or die "Cannot acquire lock on $LOCK_FILE: $!"; # LOCK_EX
+
+ return $lock_fh;
+}
+
+# _release_lock($lock_fh)
+#
+# Internal function to release the lock and close the lock file.
+#
+# Parameters:
+# $lock_fh - File handle returned by _acquire_lock()
+sub _release_lock {
+ my ($lock_fh) = @_;
+
+ # Closing the file handle releases the flock automatically
+ close($lock_fh);
+}
+
+# _read_state()
+#
+# Internal function to read and parse the entire state file.
+# For write operations, this MUST be called while holding the lock.
+# For read-only operations, this can be called without a lock due to
+# atomic file replacement via rename().
+#
+# File format: Each line contains:
+# username:key1=value1:key2=value2:...
+#
+# Empty lines and lines starting with '#' are ignored.
+#
+# Returns:
+# A hash where keys are usernames and values are hash references
+# containing the key-value pairs for that user
+#
+# Dies if the file cannot be opened.
+sub _read_state {
+ my %data;
+
+ # Return empty hash if state file doesn't exist yet
+ return %data unless -f $STATE_FILE;
+
+ open(my $fh, '<', $STATE_FILE) or die "Cannot read $STATE_FILE: $!";
+
+ while (my $line = <$fh>) {
+ chomp $line;
+
+ # Skip empty lines and comments
+ next if $line =~ /^\s*$/;
+ next if $line =~ /^\s*#/;
+
+ # Split on colons - first field is username, rest are key=value pairs
+ # Untaint the line by validating it matches our expected format
+ next unless $line =~ /^([^:\n]+(?::[^:\n=]+=[^:\n]*)*?)$/;
+ $line = $1; # Now untainted
+ my @fields = split(/:/, $line);
+ next unless @fields >= 1;
+
+ my $username = shift @fields;
+ $data{$username} = {};
+
+ # Parse each key=value pair
+ foreach my $pair (@fields) {
+ my ($key, $value) = split(/=/, $pair, 2);
+ $data{$username}{$key} = $value if defined $key && defined $value;
+ }
+ }
+
+ close($fh);
+
+ return %data;
+}
+
+# _write_state(\%data)
+#
+# Internal function to write the entire state file atomically.
+# MUST be called while holding the lock via _acquire_lock().
+# Uses a temporary file and rename() to ensure atomicity.
+#
+# Parameters:
+# $data_ref - A hash reference in the format returned by _read_state()
+#
+# The output is sorted by username and by key within each username
+# for consistent, diff-friendly output.
+#
+# Dies if the file cannot be written or renamed.
+sub _write_state {
+ my ($data_ref) = @_;
+
+ # Use a process-specific temporary file name
+ my $temp_file = "$STATE_FILE.tmp.$$";
+
+ open(my $fh, '>', $temp_file) or die "Cannot write to $temp_file: $!";
+
+ # Write each user's data, sorted for consistency
+ foreach my $username (sort keys %$data_ref) {
+ my @pairs;
+
+ # Build key=value pairs, sorted by key
+ foreach my $key (sort keys %{$data_ref->{$username}}) {
+ my $value = $data_ref->{$username}{$key};
+ push @pairs, "$key=$value";
+ }
+
+ # Write the line: username:key1=value1:key2=value2:...
+ print $fh "$username:", join(':', @pairs), "\n";
+ }
+
+ close($fh);
+
+ # Atomically replace the old state file with the new one
+ # This happens while still holding the lock, so no race condition
+ rename($temp_file, $STATE_FILE) or die "Cannot rename $temp_file: $!";
+ chmod(0644, $STATE_FILE);
+}
+
+# Module must return true
+1;
=====================================
adduser
=====================================
@@ -34,6 +34,7 @@ use Getopt::Long;
use Debian::AdduserCommon 3.139;
use Debian::AdduserLogging 3.139;
use Debian::AdduserRetvalues 3.139;
+use Debian::AdduserStatefile 3.139;
BEGIN {
if ( Debian::AdduserCommon->VERSION != version->declare('3.139') ||
Debian::AdduserLogging->VERSION != version->declare('3.139') ||
@@ -103,9 +104,9 @@ $0 =~ s+.*/++;
our $action;
our $verbose; # should we be verbose?
my $name_check_level = 0; # should we allow bad names?
-our $stdoutmsglevel = "warn";
-our $stderrmsglevel = "warn";
-our $logmsglevel = "info";
+our $stdoutmsglevel = undef;
+our $stderrmsglevel = undef;
+our $logmsglevel = undef;
my $allow_badname = 0; # should we allow bad names?
my $ask_passwd = 1; # ask for a passwd?
my $disabled_login = 0; # leave the new account disabled?
@@ -114,6 +115,7 @@ our @configfiles;
our @defaults = undef;
our $found_group_opt = undef;
our $found_sys_opt = undef;
+our $found_unlock_opt = undef;
our $ingroup_name = undef;
our $new_firstgid = undef;
our $new_firstuid = undef;
@@ -186,6 +188,7 @@ GetOptions(
'shell=s' => \$special_shell,
'system' => \$found_sys_opt,
'uid=i' => \$new_uid,
+ 'unlock' => \$found_unlock_opt,
'verbose' => sub { $verbose = 1; },
'version|v' => sub { &version; exit },
) or &usage_error;
@@ -199,10 +202,11 @@ if (!@configfiles) {
# make sure that message levels apply for reading configuration
# this will be overridden again after reading configuration
-$stdoutmsglevel = sanitize_string($stdoutmsglevel);
-$stderrmsglevel = sanitize_string($stderrmsglevel);
-$logmsglevel = sanitize_string($logmsglevel);
-set_msglevel( $stderrmsglevel, $stdoutmsglevel, $logmsglevel );
+set_msglevel(
+ sanitize_string($stderrmsglevel or STDERRDEFLEVEL),
+ sanitize_string($stdoutmsglevel or STDOUTDEFLEVEL),
+ sanitize_string($logmsglevel or LOGMSGDEFLEVEL),
+);
log_trace("ARGV %s", join(@ARGV,"-"));
log_trace("special_home %s", $special_home);
log_trace("special_home %s", encode($charset, $special_home));
@@ -223,8 +227,13 @@ if( $> != 0) {
log_fatal( mtx("Only root may add a user or group to the system.") );
exit( RET_ROOT_NEEDED );
}
-
-# TODO: Handle configuration file input, allow bare input there.
+# ARGV > adduser.conf > (default)
+$stdoutmsglevel //= $config{'stdoutmsglevel'};
+$stdoutmsglevel //= STDOUTDEFLEVEL;
+$stderrmsglevel //= $config{'stderrmsglevel'};
+$stderrmsglevel //= STDERRDEFLEVEL;
+$logmsglevel //= $config{'logmsglevel'};
+$logmsglevel //= LOGMSGDEFLEVEL;
$stdoutmsglevel = sanitize_string($stdoutmsglevel);
$stderrmsglevel = sanitize_string($stderrmsglevel);
$logmsglevel = sanitize_string($logmsglevel);
@@ -235,6 +244,7 @@ if( defined $verbose ) {
} elsif( $verbose == 1 ) {
set_msglevel( $stderrmsglevel, "info", $logmsglevel );
} elsif( $verbose == 2 ) {
+ set_msglevel( $stdoutmsglevel, "debug", $logmsglevel );
set_msglevel( $stderrmsglevel, "debug", $logmsglevel );
}
}
@@ -313,7 +323,6 @@ if ($action ne "addgroup" &&
exit( RET_EXCLUSIVE_PARAMETERS );
}
-
if ($found_group_opt) {
if ($action eq "addsysuser") {
$make_group_also = 1;
@@ -326,6 +335,10 @@ if ($found_group_opt) {
}
}
+if ($found_unlock_opt) {
+ $action = "unlockuser";
+}
+
# $new_firstuid = $new_firstuid || $config{"first_uid"} || 1000;
# $new_lastuid = $new_lastuid || $config{"last_uid"} || 59999;
# $new_firstgid = $new_firstgid || $config{"first_gid"} || 1000;
@@ -364,7 +377,8 @@ if ( defined $comment_tainted ) {
log_trace("check comment %s for unwanted chars", $special_home);
# do not sanitize, can't be done without libperl
if ( $comment_tainted !~ qr/^([^\x00-\x1F\x7F:]*)$/ ) {
- die( "unwanted chars in comment" );
+ log_fatal( mtx("unwanted chars in comment") );
+ exit( RET_INVALID_CHARS_IN_INPUT );
}
}
if( defined $special_shell ) {
@@ -403,8 +417,8 @@ if( defined $gid_option ) {
}
if ((defined($special_home)) && ($special_home !~ m+^/+ )) {
- log_fatal( mtx("The home dir must be an absolute path.") );
- exit( RET_INVALID_HOME_DIRECTORY );
+ log_fatal( mtx("The home dir must be an absolute path.") );
+ exit( RET_INVALID_HOME_DIRECTORY );
}
@@ -431,6 +445,8 @@ $SIG{'INT'} = $SIG{'QUIT'} = $SIG{'HUP'} = 'handler';
# $action = "addusertogroup"
# $existing_user - the user to be added
# $existing_group - the group to add her to
+# $action = "unlockuser"
+# $new_name - the user to be unlocked
#####
@@ -589,6 +605,9 @@ if ($action eq "addsysuser") {
exit( RET_WRONG_OBJECT_PROPERTIES );
}
if ($ret & EXISTING_FOUND) {
+ if ($ret & EXISTING_LOCKED) {
+ unlock_user($new_name, 1);
+ }
log_info( mtx("The system user `%s' already exists. Exiting.\n"), $new_name );
exit( RET_OK );
}
@@ -942,49 +961,20 @@ if ($action eq "adduser") {
create_homedir ($no_copy_skel ? 0 : 1, 0); # copy skeleton data
# useradd without -p has left the account disabled (password string is '!')
- my $yesexpr = langinfo(YESEXPR());
- my $noexpr = langinfo(NOEXPR());
if ($ask_passwd) {
- PASSWD: for (;;) {
- my $passwd = which('passwd');
- my $ok = systemcall_or_warn($passwd, $new_name);
- $ok = $ok >> 8;
- log_debug( "systemcall_or_warn %s %s return value %s", $passwd, $new_name, $ok);
- if ($ok != 0) {
- my $answer;
- # hm, error, should we break now?
- if ($ok == 1) {
- log_warn( mtx("Permission denied"));
- } elsif ($ok == 2) {
- log_warn( mtx("invalid combination of options"));
- } elsif ($ok == 3) {
- log_warn( mtx("unexpected failure, nothing done"));
- } elsif ($ok == 4) {
- log_warn( mtx("unexpected failure, passwd file missing"));
- } elsif ($ok == 5) {
- log_warn( mtx("passwd file busy, try again"));
- } elsif ($ok == 6) {
- log_warn( mtx("invalid argument to option"));
- } elsif ($ok == 10) {
- log_warn( mtx("wrong password given or password retyped incorrectly"));
- } else {
- log_warn( mtx("unexpected return code %s given from passwd"), $ok );
- }
+ my %passwd_errors = (
+ 1 => "Permission denied",
+ 2 => "Invalid combination of options",
+ 3 => "Unexpected failure, nothing done",
+ 4 => "Passwd file missing",
+ 5 => "Passwd file busy, try again",
+ 6 => "Invalid argument to option",
+ 10 => "Wrong password given or retyped incorrectly",
+ );
- # Translators: [y/N] has to be replaced by values defined in your
- # locale. You can see by running "locale noexpr" which regular
- # expression will be checked to find positive answer.
- PROMPT: for (;;) {
- print (gtx("Try again? [y/N] "));
- chop ($answer=<STDIN>);
- last PROMPT if ($answer =~ m/$yesexpr/o);
- last PASSWD if ($answer =~ m/$noexpr/o);
- last PASSWD if (!$answer);
- }
- } else {
- last; ## passwd ok
- }
- }
+ interactive_command_with_confirmation(
+ 'passwd', $new_name, \%passwd_errors
+ );
}
if (defined($comment_tainted)) {
@@ -992,22 +982,14 @@ if ($action eq "adduser") {
} elsif ($uid_pool{$new_name}{'comment'}) {
ch_comment($new_name, $uid_pool{$new_name}{'comment'});
} else {
- my $noexpr = langinfo(NOEXPR());
- my $yesexpr = langinfo(YESEXPR());
- CHFN: for (;;) {
- my $chfn = &which('chfn');
- systemcall($chfn, $new_name);
- # Translators: [y/N] has to be replaced by values defined in your
- # locale. You can see by running "locale yesexpr" which regular
- # expression will be checked to find positive answer.
- PROMPT: for (;;) {
- print (gtx("Is the information correct? [Y/n] "));
- chop (my $answer=<STDIN>);
- last PROMPT if ($answer =~ m/$noexpr/o);
- last CHFN if ($answer =~ m/$yesexpr/o);
- last CHFN if (!$answer);
- }
- }
+ my %chfn_errors = (
+ 1 => "Permission denied",
+ 2 => "Invalid combination of options",
+ );
+
+ interactive_command_with_confirmation(
+ 'chfn', $new_name, \%chfn_errors, 1
+ );
}
if ( ( $add_extra_groups || $config{"add_extra_groups"} ) && defined($config{"extra_groups"}) ) {
@@ -1051,6 +1033,15 @@ if ($action eq "adduser") {
exit( $returnvalue );
}
+if ($action eq "unlockuser") {
+ log_trace( "unlockuser %s", $new_name );
+ acquire_lock();
+ unlock_user($new_name, 0);
+ release_lock(0);
+
+ exit( $returnvalue );
+}
+
#
# we never go here
#
@@ -1171,6 +1162,13 @@ sub check_user_group {
log_debug( "check_user_group %s called, make_group_also %s", $system, $make_group_also );
my $ustat = existing_user_status(\%config, $new_name, $new_uid);
+ if ($ustat & EXISTING_FOUND) {
+ if ($ustat & EXISTING_LOCKED) {
+ # this must be a non-system user, addsysuser handles the locked case before we get called
+ log_err( mtx("User `%s' already exists and is locked. use adduser --unlock explicitly to unlock. Exiting.\n"), $new_name );
+ exit( RET_OBJECT_EXISTS );
+ }
+ }
if ($system) {
if (($ustat & EXISTING_FOUND) && !($ustat & EXISTING_SYSTEM)) {
log_fatal( mtx("The user `%s' already exists, and is not a system user."), $new_name);
@@ -1309,21 +1307,21 @@ sub sanitize_name {
# this check cannot be turned off
log_err( mtx("To avoid ambiguity with numerical UIDs, usernames which" .
"resemble numbers or negative numbers are not allowed.") );
- exit( RET_INVALID_CHARS_IN_NAME );
+ exit( RET_INVALID_CHARS_IN_INPUT );
}
log_trace("sanitize_name testing single or double period");
if ( $name =~ qr/^\.\.?$/ ) {
# this check cannot be turned off
log_err( mtx("Usernames must not be a single or a double period.") );
- exit( RET_INVALID_CHARS_IN_NAME );
+ exit( RET_INVALID_CHARS_IN_INPUT );
}
log_trace("sanitize_name testing > 32 chars");
if (length( encode($charset, $name) ) > 32) {
# this check cannot be turned off
log_err( mtx("Usernames must be no more than 32 bytes in length.") );
- exit( RET_INVALID_CHARS_IN_NAME );
+ exit( RET_INVALID_CHARS_IN_INPUT );
}
log_trace("sanitize_name testing %s against insane chars %s", $name, def_min_regex);
@@ -1333,7 +1331,7 @@ sub sanitize_name {
"dash, plus sign, or tilde, and it must not contain any of the" .
"following: colon, comma, slash, or any whitespace characters" .
"including spaces, tabs, and newlines.") );
- exit( RET_INVALID_CHARS_IN_NAME );
+ exit( RET_INVALID_CHARS_IN_INPUT );
}
log_trace("sanitize_name checking %s against %s (%s)", $name, $name_regex, $name_regex_var);
@@ -1350,7 +1348,7 @@ sub sanitize_name {
"compatibility with Samba machine accounts, \$ is also supported" .
"at the end of the username. (Use the `--allow-all-names' option" .
"to bypass this restriction.)") );
- exit( RET_INVALID_CHARS_IN_NAME );
+ exit( RET_INVALID_CHARS_IN_INPUT );
}
if ($name_check_level) {
@@ -1360,7 +1358,7 @@ sub sanitize_name {
"configured via the %s configuration variable. Use the" .
"`--allow-bad-names' option to relax this check or reconfigure" .
"%s in configuration."), $name_regex_var, $name_regex_var );
- exit( RET_INVALID_CHARS_IN_NAME );
+ exit( RET_INVALID_CHARS_IN_INPUT );
}
log_trace("sanitize_name checking %s against %s", $name, anynamere);
@@ -1465,6 +1463,112 @@ sub user_is_member {
return 0;
}
+# unlock user
+sub unlock_user {
+ my ($user_name, $system) = @_;
+ log_debug( "unlock_user %s called, system %s", $user_name, $system );
+ my $ret = existing_user_status(\%config, $user_name, undef);
+ if ($ret & EXISTING_FOUND) {
+ if ($ret & EXISTING_LOCKED) {
+ my $default_shell = $system ? "/usr/sbin/nologin" : "/bin/bash";
+
+ # Get current shell state if available
+ my $shell = get_state_value($user_name, "shell") // $default_shell;
+
+ # Build usermod argument list
+ my @usermod_args = (
+ '-e', '-1', # set account expiry to never
+ '-s', $shell, # set shell
+ $user_name # username
+ );
+
+ # Add -U only if account has a password
+ if ($ret & EXISTING_HAS_PASSWORD) {
+ push @usermod_args, '-U';
+ } else {
+ log_info( mtx(
+ "Unlocking password `%s' would result in a passwordless account. Leaving password locked\n",
+ $user_name
+ ));
+ }
+
+ log_info( mtx("Unlocking user `%s' ...", $user_name) );
+ log_trace( "usermod `%s' ...", join(" ", @usermod_args) );
+ my $unlock_ret = systemcall_useradd($name_check_level, 'usermod', @usermod_args);
+
+ if( $unlock_ret == 0 ) {
+ log_info( mtx("user `%s' successfully unlocked.\n", $user_name) );
+ set_state_value($user_name, "shell", undef);
+ set_state_value($user_name, "locked", undef);
+ } else {
+ log_fatal( mtx("error %s while unlocking user `%s'. Exiting.\n", $unlock_ret, $user_name) );
+ exit( RET_SYSTEMCALL_ERROR );
+ }
+ }
+ log_info( mtx("User `%s' is already unlocked.\n"), $user_name );
+ } else {
+ log_fatal( mtx("User `%s' does not exist\n"), $user_name );
+ exit( RET_OBJECT_DOES_NOT_EXIST );
+ }
+}
+
+sub interactive_command_with_confirmation {
+ my ($cmd, $user_name, $error_map, $always_confirm) = @_;
+
+ $always_confirm //= 0; # default to false if not provided
+ return unless -t STDIN; # skip non-interactive
+
+ my $yesexpr = langinfo(YESEXPR);
+ my $noexpr = langinfo(NOEXPR);
+
+ my $retry = 1;
+
+ while ($retry) {
+ $retry = 0;
+
+ my $full_cmd = which($cmd);
+ my $ok = systemcall_or_warn($full_cmd, $user_name);
+ $ok = $ok >> 8; # extract exit code
+
+ log_debug("systemcall_or_warn %s %s return value %s", $full_cmd, $user_name, $ok);
+
+ if ($ok != 0 && $error_map) {
+ if (exists $error_map->{$ok}) {
+ log_warn($error_map->{$ok});
+ } else {
+ log_warn("Unexpected return code %s from %s", $ok, $cmd);
+ }
+ }
+
+ # Prompt for confirmation if requested
+ if ($always_confirm) {
+ my $answer;
+ do {
+ print gtx("Is the information correct? [Y/n] ");
+ $answer = <STDIN>;
+
+ unless (defined $answer) {
+ warn "No input available, assuming 'yes'\n";
+ last;
+ }
+
+ chomp($answer);
+ $answer =~ s/^\s+|\s+$//g;
+
+ if ($answer =~ m/$yesexpr/o || $answer eq '') {
+ $retry = 0; # accepted
+ last;
+ } elsif ($answer =~ m/$noexpr/o) {
+ $retry = 1; # retry command
+ } else {
+ print "Please answer yes or no.\n";
+ $answer = undef; # repeat prompt
+ }
+
+ } while (!defined $answer);
+ }
+ }
+}
sub cleanup {
if ($undohome) {
@@ -1523,6 +1627,11 @@ sub usage {
user
Add a regular user
+adduser --unlock
+ [--system]
+ user
+ Unlock an existing locked user account
+
adduser --system
[--uid id] [--group] [--ingroup group] [--gid id]
[--shell shell] [--comment comment] [--home dir] [--no-create-home]
=====================================
adduser.conf
=====================================
@@ -113,4 +113,3 @@
# EXTRA_GROUPS.
# Default: ADD_EXTRA_GROUPS=0
#ADD_EXTRA_GROUPS=0
-
=====================================
debian/postrm
=====================================
@@ -5,7 +5,7 @@ set -eu
case $1 in
purge)
- rm -fv /etc/adduser.conf /etc/adduser.conf.dpkg-save /etc/adduser.conf.update-old
+ rm -fv /etc/adduser.conf /etc/adduser.conf.dpkg-save /etc/adduser.conf.update-old /var/lib/adduser/state /var/lib/adduser
;;
esac
=====================================
debian/rules
=====================================
@@ -24,6 +24,7 @@ override_dh_install:
sed -e s/DVERSION/$(cversion)/g AdduserCommon.pm > debian/adduser/usr/share/perl5/Debian/AdduserCommon.pm
sed -e s/DVERSION/$(cversion)/g AdduserLogging.pm > debian/adduser/usr/share/perl5/Debian/AdduserLogging.pm
sed -e s/DVERSION/$(cversion)/g AdduserRetvalues.pm > debian/adduser/usr/share/perl5/Debian/AdduserRetvalues.pm
+ sed -e s/DVERSION/$(cversion)/g AdduserStatefile.pm > debian/adduser/usr/share/perl5/Debian/AdduserStatefile.pm
ln -s adduser debian/adduser/usr/sbin/addgroup
ln -s deluser debian/adduser/usr/sbin/delgroup
=====================================
debian/tests/f/account_locks.t
=====================================
@@ -0,0 +1,103 @@
+#! /usr/bin/perl -Idebian/tests/lib
+
+use diagnostics;
+use strict;
+use warnings;
+
+use AdduserTestsCommon;
+
+my $prefix = "lockedtest";
+my $un;
+
+END {
+ remove_tree("/home/$prefix-user");
+ remove_tree("/var/mail/$prefix-user");
+}
+
+## system user
+
+$un = "${prefix}-sys";
+
+assert_user_does_not_exist($un);
+assert_command_success('/usr/sbin/adduser',
+ '--stdoutmsglevel=error', '--stderrmsglevel=error',
+ '--disabled-password',
+ '--system',
+ $un);
+assert_user_exists($un);
+assert_user_status($un, EXISTING_LOCKED, "locked");
+assert_user_status($un, EXISTING_HAS_PASSWORD, "not has password");
+assert_user_status($un, EXISTING_NOLOGIN, "set to nologin");
+assert_user_status($un, EXISTING_EXPIRED, "not expired");
+assert_command_success('/usr/sbin/deluser',
+ '--stdoutmsglevel=error', '--stderrmsglevel=error',
+ '--system', "--lock",
+ $un);
+assert_user_exists($un);
+assert_user_status($un, EXISTING_LOCKED, "locked");
+assert_user_status($un, EXISTING_HAS_PASSWORD, "not has password");
+assert_user_status($un, EXISTING_NOLOGIN, "set to nologin");
+assert_user_status($un, EXISTING_EXPIRED, "expired");
+assert_command_success('/usr/sbin/adduser',
+ '--stdoutmsglevel=error', '--stderrmsglevel=error',
+ '--system',
+ $un);
+assert_user_status($un, EXISTING_LOCKED, "locked");
+assert_user_status($un, EXISTING_HAS_PASSWORD, "not has password");
+assert_user_status($un, EXISTING_NOLOGIN, "set to nologin");
+assert_user_status($un, EXISTING_EXPIRED, "not expired");
+assert_user_exists($un);
+assert_command_success('/usr/sbin/deluser',
+ '--stdoutmsglevel=error', '--stderrmsglevel=error',
+ '--system', '--force-delete',
+ $un);
+assert_user_does_not_exist($un);
+
+## normal user
+
+$un = "${prefix}-user";
+
+assert_user_does_not_exist($un);
+assert_command_success('/usr/sbin/adduser',
+ '--stdoutmsglevel=error', '--stderrmsglevel=error',
+ '--comment', "",
+ '--disabled-password',
+ $un);
+assert_user_exists($un);
+assert_user_status($un, EXISTING_LOCKED, "locked");
+assert_user_status($un, EXISTING_HAS_PASSWORD, "not has password");
+assert_user_status($un, EXISTING_NOLOGIN, "not set to nologin");
+assert_user_status($un, EXISTING_EXPIRED, "not expired");
+assert_command_success('/usr/sbin/deluser',
+ '--stdoutmsglevel=error', '--stderrmsglevel=error',
+ "--lock",
+ $un);
+assert_user_exists($un);
+assert_user_status($un, EXISTING_LOCKED, "locked");
+assert_user_status($un, EXISTING_HAS_PASSWORD, "not has password");
+assert_user_status($un, EXISTING_NOLOGIN, "set to nologin");
+assert_user_status($un, EXISTING_EXPIRED, "expired");
+assert_command_failure('/usr/sbin/adduser',
+ '--stdoutmsglevel=fatal', '--stderrmsglevel=fatal',
+ $un);
+assert_user_exists($un);
+assert_user_status($un, EXISTING_LOCKED, "locked");
+assert_user_status($un, EXISTING_HAS_PASSWORD, "not has password");
+assert_user_status($un, EXISTING_NOLOGIN, "set to nologin");
+assert_user_status($un, EXISTING_EXPIRED, "expired");
+assert_command_success('/usr/sbin/adduser',
+ '--stdoutmsglevel=error', '--stderrmsglevel=error',
+ '--unlock',
+ $un);
+assert_user_exists($un);
+assert_user_status($un, EXISTING_LOCKED, "locked");
+assert_user_status($un, EXISTING_HAS_PASSWORD, "not has password");
+assert_user_status($un, EXISTING_NOLOGIN, "not set to nologin");
+assert_user_status($un, EXISTING_EXPIRED, "not expired");
+assert_command_success('/usr/sbin/deluser',
+ '--stdoutmsglevel=error', '--stderrmsglevel=error',
+ '--force-delete', '--remove-home',
+ $un);
+assert_user_does_not_exist($un);
+
+# vim: tabstop=4 shiftwidth=4 expandtab
=====================================
debian/tests/f/adduser_system.t
=====================================
@@ -204,7 +204,10 @@ assert_command_success(
assert_user_exists('aust');
assert_user_is_system('aust');
-system('echo "aust:!foobar" | chpasswd --encrypted');
+# $ mkpasswd --hash=yescrypt foobar
+# $y$j9T$dDqPXxXOCZL14/3jiuscW.$P8VGTWHqO1.qLOJs5Mas7Vzj3Ni9Es3QhACrVa0Z5Z3
+system(qq{echo 'aust:!\$y\$j9T\$dDqPXxXOCZL14/3jiuscW.\$P8VGTWHqO1.qLOJs5Mas7Vzj3Ni9Es3QhACrVa0Z5Z3' | chpasswd --encrypted});
+ok(1, "set passwd to !foobar");
# with #1099734 fixed, this should fail
assert_command_result_silent(RET_WRONG_OBJECT_PROPERTIES,
'/usr/sbin/adduser',
@@ -216,24 +219,28 @@ assert_command_result_silent(RET_WRONG_OBJECT_PROPERTIES,
assert_user_exists('aust');
assert_user_is_system('aust');
-system('echo "aust:*foobar" | chpasswd --encrypted');
-ok(1, "set passwd to *foobar");
-assert_command_success(
- '/usr/sbin/adduser',
- '--stdoutmsglevel=error', '--stderrmsglevel=error',
- '--system',
- '--disabled-login',
- 'aust'
-);
-assert_user_exists('aust');
-assert_user_is_system('aust');
+# $ mkpasswd --hash=yescrypt foobar
+# $y$j9T$dDqPXxXOCZL14/3jiuscW.$P8VGTWHqO1.qLOJs5Mas7Vzj3Ni9Es3QhACrVa0Z5Z3
+# * plus a valid hash is not accepted by chpasswd any more
+#system(qq{echo 'aust:*\$y\$j9T\$dDqPXxXOCZL14/3jiuscW.\$P8VGTWHqO1.qLOJs5Mas7Vzj3Ni9Es3QhACrVa0Z5Z3' | chpasswd --encrypted});
+#ok(1, "set passwd to *foobar");
+#assert_command_success(
+# '/usr/sbin/adduser',
+# '--stdoutmsglevel=error', '--stderrmsglevel=error',
+# '--system',
+# '--disabled-login',
+# 'aust'
+#);
+#assert_user_exists('aust');
+#assert_user_is_system('aust');
assert_command_success(
'/usr/sbin/deluser',
'--stdoutmsglevel=error', '--stderrmsglevel=error',
'--system',
'aust'
);
-
+assert_user_does_not_exist('aust');
+#
# ref #100032
# test --home
# we are testing with stdoutmsglevel warn so that we can catch
=====================================
debian/tests/f/cronjack.t
=====================================
@@ -9,18 +9,22 @@ use warnings;
use AdduserTestsCommon;
-
-END {
- remove_tree('/hacked');
- remove_tree('/home/bob');
-}
-
-assert_command_success('sh', '-c', q{/usr/sbin/useradd --badname -d /home/bob -m 'bob;>/hacked' 2>/dev/null});
-
-assert_path_does_not_exist('/hacked');
-
-`/usr/sbin/deluser 'bob;>/hacked' >/dev/null 2>&1`;
-
-assert_path_does_not_exist('/hacked');
+ok("test no longer possible and disabled");
+
+#
+#
+#END {
+# remove_tree('/hacked');
+# remove_tree('/home/bob');
+#}
+#
+# this is no longer allowed by passwd 4.19.0
+#assert_command_success('sh', '-c', q{/usr/sbin/useradd --badname -d /home/bob -m 'bob;>/hacked' 2>/dev/null});
+#
+#assert_path_does_not_exist('/hacked');
+#
+#`/usr/sbin/deluser 'bob;>/hacked' >/dev/null 2>&1`;
+#
+#assert_path_does_not_exist('/hacked');
# vim: tabstop=4 shiftwidth=4 expandtab
=====================================
debian/tests/lib/AdduserTestsCommon.pm
=====================================
@@ -30,6 +30,21 @@ END {
if (-f '/var/cache/adduser/tests/state.tar');
}
+use constant {
+ EXISTING_NOT_FOUND => 0,
+ EXISTING_FOUND => 1,
+ EXISTING_SYSTEM => 2,
+ EXISTING_ID_MISMATCH => 4,
+ EXISTING_LOCKED => 8,
+ EXISTING_HAS_PASSWORD => 16,
+ EXISTING_EXPIRED => 32,
+ EXISTING_NOLOGIN => 64,
+ SYS_MIN => 100,
+ SYS_MAX => 999,
+ USER_MIN => 1000,
+ USER_MAX => 9999,
+};
+
my $charset = langinfo(CODESET);
binmode(STDOUT, ":encoding($charset)");
binmode(STDERR, ":encoding($charset)");
@@ -293,7 +308,7 @@ sub assert_path_has_ownership {
sub assert_path_is_a_file {
my $path = shift;
- ok(-f $path, "path is a file $path");
+ ok(-f $path, "path is a file: $path");
}
sub assert_path_is_a_directory {
@@ -459,6 +474,83 @@ sub apply_config_hash {
close(CONF);
}
+sub assert_user_status {
+ my ($username, $mask, $desc, $invert) = @_;
+
+ # Determine inversion from description if it starts with 'NOT '
+ if ($desc =~ /^NOT\s+(.*)/i) {
+ die "Cannot set invert=1 if description starts with NOT" if $invert;
+ $desc = $1; # remove 'NOT ' prefix
+ $invert = 1; # automatically invert
+ }
+
+ $invert //= 0; # default to positive assertion
+
+ my $status = existing_user_status($username);
+ my $ok = ($status & $mask) == $mask;
+ $ok = !$ok if $invert;
+
+ my $message = $invert
+ ? "User '$username' is NOT $desc (status $status)"
+ : "User '$username' $desc (status $status)";
+
+ ok($ok, $message);
+}
+
+
+sub existing_user_status {
+ my ($user_name,$user_uid) = @_;
+ my $ret = EXISTING_NOT_FOUND;
+ my (
+ $egpwn_name, $egpwn_passwd, $egpwn_uid, $egpwn_gid, $egpwn_quota,
+ $egpwn_comment, $egpwn_gcos, $egpwn_dir, $egpwn_shell, $egpwn_expire,
+ $egpwn_rest
+ ) = getpwnam($user_name);
+
+ if (defined $egpwn_uid) {
+ $ret |= EXISTING_FOUND;
+ $ret |= EXISTING_ID_MISMATCH if (defined($user_uid) && $egpwn_uid != $user_uid);
+ $ret |= EXISTING_SYSTEM if \
+ ($egpwn_uid >= SYS_MIN && $egpwn_uid <= SYS_MAX);
+
+ $ret |= EXISTING_NOLOGIN if ($egpwn_shell =~ /bin\/nologin/);
+ $ret |= EXISTING_HAS_PASSWORD if
+ (defined $egpwn_passwd && $egpwn_passwd ne '' && ($egpwn_passwd =~ s/^[!*]+//r ne ''));
+ $ret |= EXISTING_LOCKED if
+ (defined $egpwn_passwd && $egpwn_passwd =~ /^[!*]/);
+
+ my $age = `chage -l $user_name`;
+ if ($age =~ /Account expires\s*:\s*(.+)/i) {
+ my $exp = $1;
+ use POSIX qw(strftime);
+ use Time::Local;
+ if ($exp ne 'never') {
+ my $expiry_epoch = eval { `date -d "$exp" +%s` };
+ my $now = time;
+ $ret |= EXISTING_EXPIRED if ($expiry_epoch < $now);
+ }
+ }
+ } elsif ($user_uid && getpwuid($user_uid)) {
+ $ret |= EXISTING_ID_MISMATCH;
+ }
+ return $ret;
+}
+
+sub existing_group_status {
+ my ($group_name,$group_gid) = @_;
+ my $gid;
+ my $ret = EXISTING_NOT_FOUND;
+ if ((undef,undef,$gid) = egetgrnam($group_name)) {
+ $ret |= EXISTING_FOUND;
+ $ret |= EXISTING_ID_MISMATCH if (defined($group_gid) && $gid != $group_gid);
+ $ret |= EXISTING_SYSTEM if \
+ ($gid >= SYS_MIN && $gid <= SYS_MAX);
+ } elsif ($group_gid && getgrgid($group_gid)) {
+ $ret |= EXISTING_ID_MISMATCH;
+ }
+ return $ret;
+}
+
1;
# vim: tabstop=4 shiftwidth=4 expandtab
=====================================
deluser
=====================================
@@ -28,6 +28,7 @@ use Getopt::Long;
use Debian::AdduserCommon 3.139;
use Debian::AdduserLogging 3.139;
use Debian::AdduserRetvalues 3.139;
+use Debian::AdduserStatefile 3.139;
BEGIN {
if ( Debian::AdduserCommon->VERSION != version->declare('3.139') ||
Debian::AdduserLogging->VERSION != version->declare('3.139') ||
@@ -122,22 +123,24 @@ our @defaults = undef;
our @names;
GetOptions (
+ 'backup' => \$pconfig{'backup'},
+ 'backup-to=s' => \$pconfig{'backup_to'},
+ 'backup-suffix=s' => \$pconfig{'backup_suffix'},
'conf|c=s' => \@configfiles,
'debug' => sub { $verbose = 2; },
- 'stdoutmsglevel=s' => \$stdoutmsglevel,
- 'stderrmsglevel=s' => \$stderrmsglevel,
- 'logmsglevel=s' => \$logmsglevel,
- 'help|h' => sub { &usage(); exit 0; },
+ 'force-delete' => \$pconfig{'force_delete'},
'group' => sub { $action = 'delgroup'; },
- 'system' => \$pconfig{'system'},
+ 'help|h' => sub { &usage(); exit 0; },
+ 'lock|L' => \$pconfig{'lock'},
+ 'logmsglevel=s' => \$logmsglevel,
+ 'no-preserve-root' => \$no_preserve_root,
'only-if-empty' => \$pconfig{'only_if_empty'},
+ 'quiet|q' => sub { $verbose = 0; },
'remove-home' => \$pconfig{'remove_home'},
'remove-all-files' => \$pconfig{'remove_all_files'},
- 'backup' => \$pconfig{'backup'},
- 'backup-to=s' => \$pconfig{'backup_to'},
- 'backup-suffix=s' => \$pconfig{'backup_suffix'},
- 'no-preserve-root' => \$no_preserve_root,
- 'quiet|q' => sub { $verbose = 0; },
+ 'stdoutmsglevel=s' => \$stdoutmsglevel,
+ 'stderrmsglevel=s' => \$stderrmsglevel,
+ 'system' => \$pconfig{'system'},
'verbose' => sub { $verbose = 1; },
'version|v' => sub { &version; exit },
) or &usage_error;
@@ -265,21 +268,28 @@ if(defined($group)) {
if($action eq "deluser") {
my($name, $passwd, ,$uid, $rest);
+ if (($config{remove_home} || $config{remove_all_files} || $config{backup}) && ($install_more_packages)) {
+ log_warn( mtx("In order to use the --remove-home, --remove-all-files, and --backup features, you need to install the `perl' package. To accomplish that, run apt-get install perl.") );
+ $config{remove_home}=undef;
+ $config{remove_all_files}=undef;
+ $config{backup}=undef;
+ $config{backup_to}=undef;
+ exit ( RET_MORE_PACKAGES) unless( $config{"system"} );
+ }
+
+ my (
+ $egpwn_name, $egpwn_passwd, $egpwn_uid, $egpwn_gid, $egpwn_quota,
+ $egpwn_comment, $egpwn_gcos, $egpwn_dir, $egpwn_shell, $egpwn_expire,
+ $egpwn_rest
+ ) = egetpwnam(encode($charset, $user));
+
# Don't allow a non-system user to be deleted when --system is given
# Also, "user does not exist" is only a warning with --system, but an
# error without --system.
if( $config{"system"} ) {
- if (($config{remove_home} || $config{remove_all_files} || $config{backup}) && ($install_more_packages)) {
- log_warn( mtx("In order to use the --remove-home, --remove-all-files, and --backup features, you need to install the `perl' package. To accomplish that, run apt-get install perl.") );
- $config{remove_home}=undef;
- $config{remove_all_files}=undef;
- $config{backup}=undef;
- $config{backup_to}=undef;
- }
-
- if( ($name, $passwd, $uid, $rest) = egetpwnam(encode($charset, $user)) ) {
- if ( ($uid < $config{"first_system_uid"} ||
- $uid > $config{"last_system_uid" } ) ) {
+ if( defined $egpwn_uid ) {
+ if ( ($egpwn_uid < $config{"first_system_uid"} ||
+ $egpwn_uid > $config{"last_system_uid" } ) ) {
log_warn( mtx("The user `%s' is not a system user. Exiting."), $user);
exit( RET_WRONG_OBJECT_PROPERTIES );
}
@@ -287,20 +297,14 @@ if($action eq "deluser") {
log_info( mtx("The user `%s' does not exist, but --system was given. Exiting."), $user);
exit( RET_OK );
}
- } else {
- if (($config{remove_home} || $config{remove_all_files} || $config{backup}) && ($install_more_packages)) {
- log_fatal( mtx("In order to use the --remove-home, --remove-all-files, and --backup features, you need to install the `perl' package. To accomplish that, run apt-get install perl.") );
- exit( RET_MORE_PACKAGES );
- }
}
-
- unless(exist_user($user)) {
+ unless( defined $egpwn_uid ) {
log_fatal( mtx("The user `%s' does not exist."), $user );
exit( RET_OBJECT_DOES_NOT_EXIST );
}
# Warn in any case if you want to remove the root account
- if ((defined($pw_uid)) && ($pw_uid == 0) && (!defined($no_preserve_root))) {
+ if ((defined($egpwn_uid)) && ($egpwn_uid == 0) && (!defined($no_preserve_root))) {
log_fatal( mtx("WARNING: You are just about to delete the root account (uid 0). Usually this is never required as it may render the whole system unusable. If you really want this, call deluser with parameter --no-preserve-root. Stopping now without having performed any action.") );
exit( RET_DONT_REMOVE_ROOT );
}
@@ -312,6 +316,42 @@ if($action eq "deluser") {
$config{"backup"} = 1;
}
+ # command line sys_delete_action action
+ # deluser lock lock
+ # deluser delete delete
+ # deluser --lock any lock
+ # deluser --force-delete any delete
+
+ # decide whether to lock or to delete the account
+ # command line takes precedence, otherwise sys_delete_action wins
+ my $do_lock = ($config{sys_delete_action} eq 'lock');
+ $do_lock = 1 if $config{lock};
+ $do_lock = 0 if $config{force_delete};
+ log_trace( "Config: sys_delete_action=%s, lock=%s, force_delete=%s → do_lock=%s", $config{sys_delete_action}, $config{lock}, $config{force_delete}, $do_lock );
+
+ # if account is to be locked (not deleted), leave files alone
+ if ($do_lock) {
+ if ($config{"backup"} or $config{"remove"} or $config{"remove_all_files"}) {
+ log_warn( mtx("Account will be locked; files will not be backed up or removed.") );
+ log_warn( mtx("Hint: Use --force-delete to delete the account as normal.") )
+ }
+
+ log_info( mtx("Locking user account `%s' ...", $user));
+ acquire_lock();
+ set_state_value($user, "shell", $egpwn_shell);
+ set_state_value($user, "locked", 1);
+ my $lock_ret = &systemcall('usermod', "-e", 1, "-f", 0, "-L", "-s", "/usr/sbin/nologin", $user);
+ if( $lock_ret == 0 ) {
+ log_info( mtx("user `%s' successfully locked.\n", $user) );
+ } else {
+ log_fatal( mtx("error %s while unlocking user `%s'. Exiting.\n", $lock_ret, $user) );
+ exit( RET_SYSTEMCALL_ERROR );
+ }
+ release_lock();
+
+ exit( RET_OK )
+ }
+
if($config{"remove_home"} || $config{"remove_all_files"}) {
log_trace( mtx("remove_home or remove_all_files beginning") );
log_info( mtx("Looking for files to backup/remove ...") );
@@ -444,6 +484,7 @@ if($action eq "deluser") {
acquire_lock();
systemcall('/usr/sbin/userdel', $user);
release_lock();
+ delete_state_user($user);
systemcall('/usr/local/sbin/deluser.local', $user, $pw_uid,
$pw_gid, $pw_homedir) if (-x "/usr/local/sbin/deluser.local");
@@ -495,8 +536,7 @@ if ($action eq 'delgroup') {
}
-if($action eq 'deluserfromgroup')
-{
+if($action eq 'deluserfromgroup') {
unless(exist_user($user)) {
log_fatal( mtx("The user `%s' does not exist.\n"), $user );
exit( RET_OBJECT_DOES_NOT_EXIST );
@@ -562,7 +602,7 @@ sub usage {
printf( gtx(
"deluser [--system] [--remove-home] [--remove-all-files] [--backup]
[--backup-to dir] [--backup-suffix str] [--conf file]
- [--quiet] [--verbose] [--debug] user
+ [--quiet] [--verbose] [--debug] [--lock] user
remove a regular user from the system
@@ -629,3 +669,4 @@ sub check_backup_suffix {
# End:
# vim: tabstop=4 shiftwidth=4 expandtab
+0
=====================================
deluser.conf
=====================================
@@ -39,3 +39,8 @@
# be excluded when looking for files of a user to be deleted.
# Default: EXCLUDE_FSTYPES = "(proc|sysfs|usbfs|devpts|tmpfs|afs)"
#EXCLUDE_FSTYPES = "(proc|sysfs|usbfs|devpts|tmpfs|afs)"
+
+# What action to take when deleting a system account
+# Options: lock|delete
+# Default: SYS_DELETE_ACTION=delete
+#SYS_DELETE_ACTION=delete
=====================================
doc/adduser.8
=====================================
@@ -65,6 +65,11 @@ adduser, addgroup \- add or manipulate users or groups
.B user
.YS
.SY adduser
+.B \-\-unlock
+.OP \-\-system
+.B user
+.YS
+.SY adduser
.B \-\-group
.OP \-\-conf file
.OP \-\-debug
@@ -162,6 +167,11 @@ that means a
in the sense of Debian Policy.
This is commonly referred to in \fBadduser\fP as a \fInon-system user.\fP
.PP
+If \fBadduser\fP is called with the \fB\-\-unlock\fP option, it will
+search for an existing account, and ensure it is unlocked. If the
+user is not found, \fBadduser\fP will return an error. See
+\fBUnlock an account\fP below for more details.
+.PP
\fBadduser\fP will choose the first available UID
from the range specified by
\fBFIRST_UID\fP and \fBLAST_UID\fP
@@ -224,6 +234,10 @@ often abbreviated as
\fIsystem user\fP
in the context of the \fBadduser\fP package.
.PP
+If the user already exists and is a system account, then
+\fBadduser\fP will ensure the account is unlocked and return
+successfully. See \fBUnlock a user account\fP below.
+.PP
\fBadduser\fP will choose the first available UID
from the range specified by
\fBFIRST_SYSTEM_UID\fP and \fBLAST_SYSTEM_UID\fP
@@ -266,6 +280,13 @@ Skeletal configuration files are not copied.
Other options will behave as for the creation of a regular user.
The files referenced by \fBUID_POOL\fP and \fBGID_POOL\fP are also honored.
+.SS "Unlock a user account"
+If called with the \fB\-\-unlock\fP option, \fBadduser\fP will
+unlock an existing, previously locked account. If found, the
+user's password will be re-enabled, and expiry restrictions will
+be removed. Any previous expiry settings will have to be
+added manually after unlocking the account.
+
.SS "Add a group"
If \fBadduser\fP is called with the \fB\-\-group\fP option and
without the \fB\-\-system\fP option, or
@@ -613,8 +634,8 @@ There is no group with the requested GID for the primary group
for a new user.
.TP
.B 31
-The chosen name for a new user or a new group does not conform to
-the selected naming rules.
+The chosen name or comment for a new user or a new group
+does not conform to the selected naming rules.
.TP
.B 32
The home directory of a new user must be an absolute path.
=====================================
doc/deluser.8
=====================================
@@ -27,6 +27,7 @@ deluser, delgroup \- remove a user or group from the system
.OP \-\-stdoutmsglevel prio
.OP \-\-stderrmsglevel prio
.OP \-\-logmsglevel prio
+.OP \-\-lock
.B user
.YS
@@ -44,6 +45,8 @@ deluser, delgroup \- remove a user or group from the system
.OP \-\-stdoutmsglevel prio
.OP \-\-stderrmsglevel prio
.OP \-\-logmsglevel prio
+.OP \-\-lock
+.OP \-\-force\-delete
.B user
.YS
@@ -115,6 +118,10 @@ If called with one non-option argument and
without the \fB\-\-group\fP option,
\fBdeluser\fP will remove a non-system user.
.PP
+Note: If \fBdeluser\fP is called with the \fB\-\-lock\fP option,
+the user account will be locked rather than removed as described
+here; see \fB\-\-lock\fP, below.
+.PP
By default,
\fBdeluser\fP will remove the user
without removing the home directory,
@@ -263,6 +270,19 @@ That allows the local admin to control \fBadduser\fP's chattiness
on the console and in the log independently, keeping probably confusing
information to itself while still leaving helpful information in the log.
.TP
+.B \-\-lock
+If \fBdeluser\fP is called with the \fB\-\-lock\fP option,
+the user account will be locked rather than removed. In this
+case, the user's password is invalidated and the account is
+set to expired; no other action is taken. A locked account may be
+subsequently unlocked using \fBadduser\fP \fB\-\-unlock\fP.
+
+System users may be set to lock upon delete by default by setting
+\fBSYS_DELETE_ACTION\fP=\fBlock\fP in \fB/etc/deluser.conf\fP.
+.B \-\-force\-delete
+If deleting a system user, and the default \fBSYS_DELETE_ACTION\fP
+would lock the account, instead delete it normally.
+.TP
.B \-\-version
Display version and copyright information.
=====================================
doc/deluser.conf.5
=====================================
@@ -78,6 +78,12 @@ Values may be 0 or 1. Defaults to \fI0\fP.
.B REMOVE_HOME
Removes the home directory and mail spool of the user to be removed.
Value may be 0 (don't delete) or 1 (do delete). Defaults to \fI0\fP.
+.TP
+.B SYS_DELETE_ACTION
+The default action to take when deleting system accounts. Valid options
+are "lock" and "delete" (the default). If set to "lock" and not
+overridden by \fB\-\-force\-delete\fP, behave as if \fB\-\-lock\fP were
+passed and lock the account. See \fBdeluser(8)\fP for specific details.
.SH FILES
.I /etc/deluser.conf
=====================================
notes.100808x.md
=====================================
@@ -0,0 +1,28 @@
+#1008082: deluser --system(?) --lock
+
+- leaves the account intact but makes login impossible (by setting an invalid
+password, leaving existing password recoverable, and setting shell to
+/usr/sbin/nologin)
+- only system accounts?
+- adding state (/var/lib/adduser)?
+ - or set to nologin, reset shell to default and WARN on unlock
+
+#1008083: deluser --system
+
+- /etc/deluser.conf: DELUSER_SYS_ACTION = (lock*|delete) *default
+- delgroup --system honors the above (lock == NOOP)
+
+- basically: extends --lock to /etc/deluser.conf for system users
+
+#1008084: adduser --system behavior if trying to create existing locked account
+ - if system account already exists, just unlock and set shell
+ - addgroup --system silently ignore existing also (?)
+
+#?: --homeless
+ - couldn't find a bug for this but saw it mentioned; is this something we
+ still want to do?
+
+blocked above:
+#1006912: is it time to have account deletion in policy?
+- anything relevant policy-wise? (i have not read the whole thread,
+ or any list discussion)
=====================================
testsuite/deluser-delete.conf
=====================================
@@ -0,0 +1,47 @@
+# /etc/deluser.conf: `deluser' configuration.
+# See deluser(8) and deluser.conf(5) for full documentation.
+
+# A commented out setting indicates that this is the default in the
+# code. If you need to change those settings, remove the comment and
+# make your intended change.
+
+# Remove home directory and mail spool when user is removed
+# Default: REMOVE_HOME = 0
+#REMOVE_HOME = 0
+
+# Remove all files on the system owned by the user to be removed
+# Default: REMOVE_ALL_FILES = 0
+#REMOVE_ALL_FILES = 0
+
+# Backup files before removing them. This options has only an effect if
+# REMOVE_HOME or REMOVE_ALL_FILES is set.
+# Default: BACKUP = 0
+#BACKUP = 0
+
+# Target directory for the backup file
+# Default: BACKUP_TO = "."
+#BACKUP_TO = "."
+
+# Select compression (from tar --auto-compress) for backups
+# Default: BACKUP_SUFFIX = .gz
+#BACKUP_SUFFIX = .gz
+
+# Space-Separated list of regular expressions. Do not delete files
+# matching any of these.
+# Default: NO_DEL_PATHS="^/bin\$ ^/boot\$ ^/dev\$ ^/etc\$ ^/initrd ^/lib ^/lost+found\$ ^/media\$ ^/mnt\$ ^/opt\$ ^/proc\$ ^/root\$ ^/run\$ ^/sbin\$ ^/srv\$ ^/sys\$ ^/tmp\$ ^/usr\$ ^/var\$ ^/vmlinu"
+#NO_DEL_PATHS="^/bin\$ ^/boot\$ ^/dev\$ ^/etc\$ ^/initrd ^/lib ^/lost+found\$ ^/media\$ ^/mnt\$ ^/opt\$ ^/proc\$ ^/root\$ ^/run\$ ^/sbin\$ ^/srv\$ ^/sys\$ ^/tmp\$ ^/usr\$ ^/var\$ ^/vmlinu"
+
+# Only delete a group if there are no users belonging to this group.
+# Default: ONLY_IF_EMPTY = 0
+#ONLY_IF_EMPTY = 0
+
+# Single regular expression which describes filesystems types which should
+# be excluded when looking for files of a user to be deleted.
+# Default: EXCLUDE_FSTYPES = "(proc|sysfs|usbfs|devpts|tmpfs|afs)"
+#EXCLUDE_FSTYPES = "(proc|sysfs|usbfs|devpts|tmpfs|afs)"
+
+# What action to take when deleting a system account
+# Options: lock|delete
+# Default: SYS_DELETE_ACTION=delete
+#SYS_DELETE_ACTION=delete
+SYS_DELETE_ACTION=lock
=====================================
testsuite/lib_test.pm
=====================================
@@ -16,12 +16,15 @@ preseed_config(\@deluserconf,\%del_config);
my $user_prefix = "addusertest";
-
+use constant {
+ SYS_MIN => 100,
+ SYS_MAX => 999,
+};
sub assert {
my ($cond) = @_;
if ($cond) {
- print "Test failed; aborting test suite\n";
+ print "Test failed\n";
exit 1;
}
}
@@ -46,7 +49,7 @@ sub find_unused_uid {
}
else {
print "Haven't found a unused uid in range ($low_uid - $high_uid)\nExiting ...\n";
- exit 1;
+ return 1;
}
}
@@ -90,7 +93,7 @@ sub find_unused_gid {
}
else {
print "Haven't found a unused gid in range ($low_gid - $high_gid)\nExiting ...\n";
- exit 1;
+ return 1;
}
}
@@ -102,12 +105,13 @@ sub check_user_exist {
my @ent = getpwnam ($username);
if (!@ent) {
print "user $username does not exist\n";
- exit 1;
+ return 1;
}
if (( defined($uid)) && ($ent[2] != $uid)) {
printf "uid $uid does not match %s\n",$ent[2];
return 1;
}
+ print "user $username exists\n";
return 0;
}
@@ -115,8 +119,10 @@ sub check_user_not_exist {
my ($username) = @_;
if (defined(getpwnam($username))) {
+ print "user $username exists\n";
return 1;
}
+ print "user $username does not exist\n";
return 0;
}
@@ -229,4 +235,94 @@ sub check_user_has_gid {
}
+sub testsuite_existing_user_status {
+ my ($user_name,$user_uid) = @_;
+ my $ret = EXISTING_NOT_FOUND;
+
+ my (
+ $egpwn_name, $egpwn_passwd, $egpwn_uid, $egpwn_gid, $egpwn_quota,
+ $egpwn_comment, $egpwn_gcos, $egpwn_dir, $egpwn_shell, $egpwn_expire,
+ $egpwn_rest
+ ) = getpwnam($user_name);
+
+ if (defined $egpwn_uid) {
+ $ret |= EXISTING_FOUND;
+ $ret |= EXISTING_ID_MISMATCH if (defined($user_uid) && $egpwn_uid != $user_uid);
+ $ret |= EXISTING_SYSTEM if \
+ ($egpwn_uid >= SYS_MIN && $egpwn_uid <= SYS_MAX);
+
+ $ret |= EXISTING_NOLOGIN if ($egpwn_shell =~ /bin\/nologin/);
+ $ret |= EXISTING_HAS_PASSWORD if
+ (defined $egpwn_passwd && $egpwn_passwd ne '' && ($egpwn_passwd =~ s/^[!*]+//r ne ''));
+ $ret |= EXISTING_LOCKED if
+ (defined $egpwn_passwd && $egpwn_passwd =~ /^[!*]/);
+
+ # this is deliberately implemented differently from the actual program
+ my $age = `chage -l $user_name`;
+
+ if ($age =~ /Account expires\s*:\s*(.+)/i) {
+ my $exp = $1;
+ if ($exp ne 'never') {
+ chomp $exp;
+ # Convert to epoch using GNU date
+ my $expiry_epoch = `date -d "$exp" +%s 2>/dev/null`;
+ chomp $expiry_epoch;
+
+ if (defined $expiry_epoch && $expiry_epoch =~ /^\d+$/) {
+ $ret |= EXISTING_EXPIRED if ($expiry_epoch < time);
+ } else {
+ warn "Failed to parse expiry date '$exp' with date command\n";
+ }
+ }
+ }
+ } elsif ($user_uid && getpwuid($user_uid)) {
+ $ret |= EXISTING_ID_MISMATCH;
+ }
+ return $ret;
+}
+
+# Map human-readable status names to bitmask constants
+my %USER_STATUS_MASK = (
+ locked => EXISTING_LOCKED,
+ haspasswd => EXISTING_HAS_PASSWORD,
+ nologin => EXISTING_NOLOGIN,
+ expired => EXISTING_EXPIRED,
+);
+
+sub check_user_status {
+ my ($username, $check, $do_print) = @_;
+ $do_print //= 0;
+
+ my $invert = 0;
+ my $result;
+
+ # Check for negative prefix "not_"
+ if ($check =~ /^not_(.+)$/) {
+ $invert = 1;
+ $check = $1;
+ }
+
+ my $mask = $USER_STATUS_MASK{$check}
+ or die "Unknown user status '$check'";
+
+ my $status = testsuite_existing_user_status($username);
+ # returns 0 if status is as desired so that it can be used in assertion
+ $result = (($status & $mask) == $mask) ? 0 : 1;
+
+ if ($do_print) {
+ my $msg = $result
+ ? "User '$username' $check"
+ : "User '$username' NOT $check";
+ print "$msg";
+ }
+
+ $result = !$result if $invert;
+ print " (status $status, returning ", $result ? 1 : 0, ")\n";
+ return $result;
+}
+
+
+
return 1
+
+# vim: tabstop=4 shiftwidth=4 expandtab
=====================================
testsuite/runsuite.sh
=====================================
@@ -1,39 +1,63 @@
#!/bin/bash
+set -euo pipefail
-FAILED=""
+# Backup directory
+BACKUP_DIR="./etc_backup"
+mkdir -p "$BACKUP_DIR"
-PASSWD_BAK="./passwd.backup"
+FILES=(passwd shadow group gshadow)
+FAILED=()
-
-if [ "$(id -u)" != "0" ]; then
- echo "root needed"
+# Ensure root
+if [ "$(id -u)" -ne 0 ]; then
+ echo "root needed" >&2
exit 1
fi
-cp /etc/passwd $PASSWD_BAK
+# Backup critical files
+for f in "${FILES[@]}"; do
+ cp "/etc/$f" "$BACKUP_DIR/$f"
+done
+# Restore on exit
+cleanup() {
+ echo "Restoring original /etc files..."
+ for f in "${FILES[@]}"; do
+ cp "$BACKUP_DIR/$f" "/etc/$f"
+ done
+ rm -rf $BACKUP_DIR
+}
+trap cleanup EXIT
+
+# Determine tests to run
+if [ $# -ge 1 ]; then
+ TESTS=("$1")
+else
+ TESTS=(./test*.pl)
+fi
+
+# Run tests with shadowconfig set to "on" only
for a in on; do
- for i in ./test*.pl ; do
- if ! shadowconfig $a > /dev/null; then
- echo "shadowconfig $a failed"
- exit 1
- fi
+ if ! shadowconfig "$a" >/dev/null; then
+ echo "shadowconfig $a failed" >&2
+ exit 1
+ fi
+
+ for i in "${TESTS[@]}"; do
echo
echo "Starting $i (shadow $a)"
- /usr/bin/perl -I. $i
- if [ "$?" != "0" ]; then
- FAILED="$FAILED $i($a)"
+ if ! perl -I. "$i" < /dev/null; then
+ FAILED+=("$i($a)")
fi
done
done
-if [ -z "$FAILED" ]; then
+# Success/failure reporting
+if [ "${#FAILED[@]}" -eq 0 ]; then
echo "All tests passed successfully"
- rm $PASSWD_BAK
- exit 0
else
- echo "tests $FAILED failed"
- echo "see $PASSWD_BAK for a copy of /etc/passwd before starting"
+ echo "Tests failed: ${FAILED[*]}"
+ echo "Original /etc files were restored from $BACKUP_DIR"
exit 1
fi
=====================================
testsuite/test01.pl
=====================================
@@ -42,3 +42,4 @@ if (defined (getpwnam($username))) {
print "ok\n";
}
+# vim: tabstop=4 shiftwidth=4 expandtab
=====================================
testsuite/test02.pl
=====================================
@@ -127,9 +127,9 @@ if (defined (getpwnam($username))) {
if( $error == 56 ) {
`deluser $username`;
} else {
- print "failed\n deluser (file::find not present) returned an errorcode != 0/56 ($error)\n";
+ print "failed (expected)\n deluser (file::find not present) returned an errorcode != 0/56 ($error)\n";
}
- print "failed\n deluser (file::find not present) returned an errorcode != 0 ($error)\n";
+ print "failed (expected)\n deluser (file::find not present) returned an errorcode != 0 ($error)\n";
$error=0;
`rm -rf $homedir`;
}
=====================================
testsuite/test03.pl
=====================================
@@ -30,4 +30,5 @@ if (!defined (getpwnam($username))) {
assert(check_homedir_not_exist($homedir));
print "ok\n";
}
-
+
+# vim: tabstop=4 shiftwidth=4 expandtab
=====================================
testsuite/test04.pl
=====================================
@@ -30,4 +30,5 @@ if (!defined (getpwnam($username))) {
assert(check_user_in_group($username,$groupname));
print "ok\n";
}
-
+
+# vim: tabstop=4 shiftwidth=4 expandtab
=====================================
testsuite/test05.pl
=====================================
@@ -29,4 +29,5 @@ if (!defined (getpwnam($username))) {
assert(check_user_in_group($username,$groupname));
print "ok\n";
}
-
+
+# vim: tabstop=4 shiftwidth=4 expandtab
=====================================
testsuite/test06.pl
=====================================
@@ -28,4 +28,5 @@ if (!defined (getpwnam($username))) {
assert(check_user_has_gid($username,$want_gid));
print "ok\n";
}
-
+
+# vim: tabstop=4 shiftwidth=4 expandtab
=====================================
testsuite/test07.pl
=====================================
@@ -30,4 +30,5 @@ if (!defined (getpwnam($username))) {
assert(check_user_homedir_not_exist ($username));
print "ok\n";
}
-
+
+# vim: tabstop=4 shiftwidth=4 expandtab
=====================================
testsuite/test08.pl
=====================================
@@ -91,7 +91,7 @@ unless (!defined getgrnam($newgroup)) {
print "ok\n";
}
-my $newgroup = find_unused_name();
+$newgroup = find_unused_name();
$cmd = "adduser --group $newgroup";
unless (defined getgrnam($newgroup)) {
@@ -137,3 +137,5 @@ if (!defined (getpwnam($sysusername))) {
}
print "ok\n";
}
+
+# vim: tabstop=4 shiftwidth=4 expandtab
=====================================
testsuite/test09.pl
=====================================
@@ -121,3 +121,4 @@ if ($output !~ /^fatal: The group `addusertest\d+' already exists\.$/ ) {
}
print "ok\n";
+# vim: tabstop=4 shiftwidth=4 expandtab
=====================================
testsuite/test10.pl
=====================================
@@ -0,0 +1,447 @@
+#!/usr/bin/perl -w
+
+# there is a deluser.conf in the same directory as the tests
+# that has SYS_DELETE_ACTION=lock so that we can choose the behavior
+
+use strict;
+
+use lib_test;
+
+my $error;
+my $output;
+
+my $cmd;
+my $username;
+my $susername;
+my $num;
+
+$username = find_unused_name();
+$num = 0;
+
+assert(check_user_not_exist ($username));
+# unlock a non-existing account
+$cmd = "adduser --unlock $username";
+++$num && print "Testing failing (10.$num) $cmd... ";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+if (!$error) {
+ print "failed\n $cmd returned errorcode ($error)\n $output\n";
+ exit 1;
+}
+print "ok.\n";
+assert(check_user_not_exist ($username));
+
+# add the account
+$cmd = "adduser --no-create-home --comment '' --disabled-password $username";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+ print "failed\n $cmd returned errorcode ($error)\n $output\n";
+ exit 1;
+}
+print "ok.\n";
+assert(check_user_exist ($username));
+assert(check_user_status ($username, "locked", 1));
+assert(check_user_status ($username, "not_haspasswd", 1));
+assert(check_user_status ($username, "not_nologin", 1));
+assert(check_user_status ($username, "not_expired", 1));
+
+# unlock the account
+$cmd = "adduser --unlock $username";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+ print "failed\n $cmd returned errorcode ($error)\n $output\n";
+ exit 1;
+}
+print "ok.\n";
+assert(check_user_exist ($username));
+assert(check_user_status ($username, "locked", 1));
+assert(check_user_status ($username, "not_haspasswd", 1));
+assert(check_user_status ($username, "not_nologin", 1));
+assert(check_user_status ($username, "not_expired", 1));
+
+# lock the account
+$cmd = "deluser --lock $username";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+ print "failed\n $cmd returned errorcode ($error)\n $output\n";
+ exit 1;
+}
+print "ok.\n";
+assert(check_user_exist ($username));
+assert(check_user_status ($username, "locked", 1));
+assert(check_user_status ($username, "not_haspasswd", 1));
+assert(check_user_status ($username, "nologin", 1));
+assert(check_user_status ($username, "expired", 1));
+
+# unlock the account
+$cmd = "adduser --unlock $username";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+ print "failed\n $cmd returned errorcode ($error)\n $output\n";
+ exit 1;
+}
+print "ok.\n";
+assert(check_user_exist ($username));
+assert(check_user_status ($username, "locked", 1));
+assert(check_user_status ($username, "not_haspasswd", 1));
+assert(check_user_status ($username, "not_nologin", 1));
+assert(check_user_status ($username, "not_expired", 1));
+
+# set a password
+$cmd = "usermod --password \$y\$j9T\$6KrIYfSdT/O2rBLrkyzcF/\$pMxfrOqQgNn/jlZZVjSs1ELUZjpFRyjZ5ahXKZ84115 $username";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+ print "failed\n $cmd returned errorcode ($error)\n $output\n";
+ exit 1;
+}
+print "ok.\n";
+assert(check_user_exist ($username));
+assert(check_user_status ($username, "not_locked", 1));
+assert(check_user_status ($username, "haspasswd", 1));
+assert(check_user_status ($username, "not_nologin", 1));
+assert(check_user_status ($username, "not_expired", 1));
+
+# unlock the account
+$cmd = "adduser --unlock $username";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+ print "failed\n $cmd returned errorcode ($error)\n $output\n";
+ exit 1;
+}
+print "ok.\n";
+assert(check_user_exist ($username));
+assert(check_user_status ($username, "not_locked", 1));
+assert(check_user_status ($username, "haspasswd", 1));
+assert(check_user_status ($username, "not_nologin", 1));
+assert(check_user_status ($username, "not_expired", 1));
+
+# lock the account
+$cmd = "deluser --lock $username";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+ print "failed\n $cmd returned errorcode ($error)\n $output\n";
+ exit 1;
+}
+print "ok.\n";
+assert(check_user_exist ($username));
+assert(check_user_status ($username, "locked", 1));
+assert(check_user_status ($username, "haspasswd", 1));
+assert(check_user_status ($username, "nologin", 1));
+assert(check_user_status ($username, "expired", 1));
+
+# add the account (should fail)
+$cmd = "adduser --no-create-home --comment '' --disabled-password $username";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing failing (10.$num) $cmd... ";
+if (!$error) {
+ print "failed\n $cmd returned errorcode ($error)\n $output\n";
+ exit 1;
+}
+print "ok.\n";
+assert(check_user_exist ($username));
+assert(check_user_status ($username, "locked", 1));
+assert(check_user_status ($username, "haspasswd", 1));
+assert(check_user_status ($username, "nologin", 1));
+assert(check_user_status ($username, "expired", 1));
+
+# unlock the account
+$cmd = "adduser --unlock $username";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+ print "failed\n $cmd returned errorcode ($error)\n $output\n";
+ exit 1;
+}
+print "ok.\n";
+assert(check_user_exist ($username));
+assert(check_user_status ($username, "not_locked", 1));
+assert(check_user_status ($username, "haspasswd", 1));
+assert(check_user_status ($username, "not_nologin", 1));
+assert(check_user_status ($username, "not_expired", 1));
+
+# deluser with SYS_DELETE_ACTION=lock
+$cmd = "deluser --conf ./deluser-delete.conf $username";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+ print "failed\n $cmd returned errorcode ($error)\n $output\n";
+ exit 1;
+}
+print "ok.\n";
+assert(check_user_exist ($username));
+assert(check_user_status ($username, "locked", 1));
+assert(check_user_status ($username, "haspasswd", 1));
+assert(check_user_status ($username, "nologin", 1));
+assert(check_user_status ($username, "expired", 1));
+
+# unlock the account
+$cmd = "adduser --unlock $username";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+ print "failed\n $cmd returned errorcode ($error)\n $output\n";
+ exit 1;
+}
+print "ok.\n";
+assert(check_user_exist ($username));
+assert(check_user_status ($username, "not_locked", 1));
+assert(check_user_status ($username, "haspasswd", 1));
+assert(check_user_status ($username, "not_nologin", 1));
+assert(check_user_status ($username, "not_expired", 1));
+
+# regular deluser
+$cmd = "deluser $username";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+ print "failed\n $cmd returned errorcode ($error)\n $output\n";
+ exit 1;
+}
+print "ok.\n";
+assert(check_user_not_exist ($username));
+
+
+
+#=======================
+# system user
+$susername = find_unused_name();
+
+assert(check_user_not_exist ($susername));
+# unlock a non-existing system ccount
+$cmd = "adduser --unlock --system $susername";
+++$num && print "Testing failing (10.$num) $cmd... ";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+if (!$error) {
+ print "failed\n $cmd returned errorcode ($error)\n $output\n";
+ exit 1;
+}
+assert(check_user_not_exist ($susername));
+print "ok\n";
+
+# add system account
+$cmd = "adduser --system $susername";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+ print "failed\n $cmd returned errorcode ($error)\n $output\n";
+ exit 1;
+}
+print "ok\n";
+assert(check_user_exist ($susername));
+assert(check_user_status ($susername, "locked", 1));
+assert(check_user_status ($susername, "not_haspasswd", 1));
+assert(check_user_status ($susername, "nologin", 1));
+assert(check_user_status ($susername, "not_expired", 1));
+
+
+# unlock the account
+$cmd = "adduser --system --unlock $susername";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing failing (10.$num) $cmd... ";
+if ($error) {
+ print "failed\n $cmd returned errorcode ($error)\n $output\n";
+ exit 1;
+}
+print "ok\n";
+assert(check_user_exist ($susername));
+assert(check_user_status ($susername, "locked", 1));
+assert(check_user_status ($susername, "not_haspasswd", 1));
+assert(check_user_status ($susername, "nologin", 1));
+assert(check_user_status ($susername, "not_expired", 1));
+
+# lock the account
+$cmd = "deluser --system --lock $susername";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+ print "failed\n $cmd returned errorcode ($error)\n $output\n";
+ exit 1;
+}
+print "ok\n";
+assert(check_user_exist ($susername));
+assert(check_user_status ($susername, "locked", 1));
+assert(check_user_status ($susername, "not_haspasswd", 1));
+assert(check_user_status ($susername, "nologin", 1));
+assert(check_user_status ($susername, "expired", 1));
+
+# unlock the account
+$cmd = "adduser --system --unlock $susername";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+ print "failed\n $cmd returned errorcode ($error)\n $output\n";
+ exit 1;
+}
+print "ok\n";
+assert(check_user_exist ($susername));
+assert(check_user_status ($susername, "locked", 1));
+assert(check_user_status ($susername, "not_haspasswd", 1));
+assert(check_user_status ($susername, "nologin", 1));
+assert(check_user_status ($susername, "not_expired", 1));
+
+# set a password
+$cmd = "usermod --password \$y\$j9T\$6KrIYfSdT/O2rBLrkyzcF/\$pMxfrOqQgNn/jlZZVjSs1ELUZjpFRyjZ5ahXKZ84115 $susername";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+ print "failed\n $cmd returned errorcode ($error)\n $output\n";
+ exit 1;
+}
+print "ok\n";
+assert(check_user_exist ($susername));
+assert(check_user_status ($susername, "not_locked", 1));
+assert(check_user_status ($susername, "haspasswd", 1));
+assert(check_user_status ($susername, "nologin", 1));
+assert(check_user_status ($susername, "not_expired", 1));
+
+# unlock the account
+$cmd = "adduser --system --unlock $susername";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+ print "failed\n $cmd returned errorcode ($error)\n $output\n";
+ exit 1;
+}
+print "ok\n";
+assert(check_user_exist ($susername));
+assert(check_user_status ($susername, "not_locked", 1));
+assert(check_user_status ($susername, "haspasswd", 1));
+assert(check_user_status ($susername, "nologin", 1));
+assert(check_user_status ($susername, "not_expired", 1));
+
+# lock the account
+$cmd = "deluser --system --lock $susername";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+ print "failed\n $cmd returned errorcode ($error)\n $output\n";
+ exit 1;
+}
+print "ok\n";
+assert(check_user_exist ($susername));
+assert(check_user_status ($susername, "locked", 1));
+assert(check_user_status ($susername, "haspasswd", 1));
+assert(check_user_status ($susername, "nologin", 1));
+assert(check_user_status ($susername, "expired", 1));
+
+# re-enable via add (fails, already exists and has a password)
+$cmd = "adduser --system $susername";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if (!$error) {
+ print "failed\n $cmd returned errorcode ($error)\n $output\n";
+ exit 1;
+}
+print "ok\n";
+assert(check_user_exist ($susername));
+assert(check_user_status ($susername, "locked", 1));
+assert(check_user_status ($susername, "haspasswd", 1));
+assert(check_user_status ($susername, "nologin", 1));
+assert(check_user_status ($susername, "expired", 1));
+
+# unlock the account
+$cmd = "adduser --system --unlock $susername";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+ print "failed\n $cmd returned errorcode ($error)\n $output\n";
+ exit 1;
+}
+print "ok\n";
+assert(check_user_exist ($susername));
+assert(check_user_status ($susername, "not_locked", 1));
+assert(check_user_status ($susername, "haspasswd", 1));
+assert(check_user_status ($susername, "nologin", 1));
+assert(check_user_status ($susername, "not_expired", 1));
+
+# unlock already-unlocked
+$cmd = "adduser --unlock $susername";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+ print "failed\n $cmd returned errorcode ($error)\n $output\n";
+ exit 1;
+}
+print "ok\n";
+assert(check_user_exist ($susername));
+assert(check_user_status ($susername, "not_locked", 1));
+assert(check_user_status ($susername, "haspasswd", 1));
+assert(check_user_status ($susername, "nologin", 1));
+assert(check_user_status ($susername, "not_expired", 1));
+
+# deluser with SYS_DELETE_ACTION=lock
+$cmd = "deluser --conf ./deluser-delete.conf --system $susername";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+ print "failed\n $cmd returned errorcode ($error)\n $output\n";
+ exit 1;
+}
+assert(check_user_exist ($susername));
+print "ok\n";
+assert(check_user_exist ($susername));
+assert(check_user_status ($susername, "locked", 1));
+assert(check_user_status ($susername, "haspasswd", 1));
+assert(check_user_status ($susername, "nologin", 1));
+assert(check_user_status ($susername, "expired", 1));
+
+# unlock the account
+$cmd = "adduser --system --unlock $susername";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+ print "failed\n $cmd returned errorcode ($error)\n $output\n";
+ exit 1;
+}
+print "ok\n";
+assert(check_user_exist ($susername));
+assert(check_user_status ($susername, "not_locked", 1));
+assert(check_user_status ($susername, "haspasswd", 1));
+assert(check_user_status ($susername, "nologin", 1));
+assert(check_user_status ($susername, "not_expired", 1));
+
+# regular deluser
+$cmd = "deluser --system $susername";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+ print "failed\n $cmd returned errorcode ($error)\n $output\n";
+ exit 1;
+}
+print "ok\n";
+assert(check_user_not_exist ($susername));
+
+# vim: tabstop=4 shiftwidth=4 expandtab
View it on GitLab: https://salsa.debian.org/debian/adduser/-/compare/86c3aa1de298fcaaaace7e1bf1867f5ebbb95709...ae6e8bd098bb0078e436c38fc0f7738362d312b8
--
View it on GitLab: https://salsa.debian.org/debian/adduser/-/compare/86c3aa1de298fcaaaace7e1bf1867f5ebbb95709...ae6e8bd098bb0078e436c38fc0f7738362d312b8
You're receiving this email because of your account on salsa.debian.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-shadow-devel/attachments/20260108/cc850fd9/attachment-0001.htm>
More information about the Pkg-shadow-devel
mailing list