[Pkg-shadow-devel] [Git][debian/adduser][wip/various-random-things] 15 commits: Give chpasswd test values that it will accept
Marc Haber (@zugschlus)
gitlab at salsa.debian.org
Sun Jan 11 07:09:21 GMT 2026
Marc Haber pushed to branch wip/various-random-things at Debian / adduser
Commits:
8e67780f by Marc Haber at 2026-01-11T07:41:25+01:00
Give chpasswd test values that it will accept
This is to work around the more picky chpasswd in new src:shadow
- - - - -
13c16e7e by Marc Haber at 2026-01-11T07:52:45+01:00
rework EXISTING_ and existing_user_status
this redefines the EXISTING variables
simplifies the EXISTING states (they were overengineered and redundant)
reworks existing_user_status, improving logic and output
The new variable values are also read by the test suites and need to be
in sync
Thanks: Matt Barry
- - - - -
f1a50e4f by Marc Haber at 2026-01-11T07:52:45+01:00
move Marc's test10.pl to test11.pl to make Room
Git-Dch: ignore
- - - - -
5d590845 by Marc Haber at 2026-01-11T07:52:45+01:00
add vim helper line to testsuite files
The files need to be reflowed when working on them next time
Git-Dch: ignore
- - - - -
4edf8108 by Marc Haber at 2026-01-11T07:52:45+01:00
prepare upstream testsuite for EXISTING_
Git-Dch: ignore
- - - - -
8e2114ab by Marc Haber at 2026-01-11T07:52:45+01:00
various fixes in lib_test.pm
Git-Dch: ignore
- - - - -
c3352ba9 by Marc Haber at 2026-01-11T07:52:45+01:00
rework runsuite.sh
This now allows running a single test, and cleans up better
Git-Dch: ignore
- - - - -
87897be5 by Marc Haber at 2026-01-11T07:52:45+01:00
make more clear that failure was expected
Git-Dch: ignore
- - - - -
b664e466 by Marc Haber at 2026-01-11T08:01:27+01:00
improve log level handling
Thanks: Matt Barry
Git-Dch: ignore
- - - - -
6747a884 by Marc Haber at 2026-01-11T08:04:43+01:00
clarify documentation of exit value 31
Git-Dch: ignore
- - - - -
a7efe29c by Marc Haber at 2026-01-11T08:04:43+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
- - - - -
f543541e by Marc Haber at 2026-01-11T08:04:43+01:00
move interactive command loops to a function
This is more streamlined and handles running on no terminal better
- - - - -
fc25dc72 by Marc Haber at 2026-01-11T08:04:43+01:00
re-work logic around remove-home etc
Git-Dch: ignore
- - - - -
cfe4f515 by Marc Haber at 2026-01-11T08:04:43+01:00
prepare testsuite libraries to properly handle EXISTING_
this brings the simplifications to the test suite libraries
Git-Dch: ignore
- - - - -
28c9faeb by Marc Haber at 2026-01-11T08:04:43+01:00
add assert_path_is_a_file to debian/tests/lib
Git-Dch: ignore
- - - - -
19 changed files:
- AdduserCommon.pm
- AdduserRetvalues.pm
- adduser
- debian/tests/f/adduser_system.t
- debian/tests/lib/AdduserTestsCommon.pm
- deluser
- doc/adduser.8
- 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 → testsuite/test11.pl
Changes:
=====================================
AdduserCommon.pm
=====================================
@@ -102,6 +102,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 +146,11 @@ use constant {
'EXISTING_ID_MISMATCH',
'EXISTING_LOCKED',
'EXISTING_HAS_PASSWORD',
+ 'EXISTING_EXPIRED',
+ 'EXISTING_NOLOGIN',
+ 'STDOUTDEFLEVEL',
+ 'STDERRDEFLEVEL',
+ 'LOGMSGDEFLEVEL',
'existing_user_status',
'existing_group_status',
);
@@ -590,35 +603,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 ($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 =~ /^[!*]/;
+
+ $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,23 +659,37 @@ 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;
$ret |= EXISTING_ID_MISMATCH if (defined($new_gid) && $gid != $new_gid);
$ret |= EXISTING_SYSTEM if
(($gid >= $config->{"first_system_gid"}) && ($gid <= $config->{"last_system_gid"}));
- } elsif ($new_gid && getgrgid($new_gid)) {
+ } 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
=====================================
adduser
=====================================
@@ -103,9 +103,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?
@@ -199,10 +199,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 +224,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 +241,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 );
}
}
@@ -364,7 +371,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 +411,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 );
}
@@ -942,49 +950,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 +971,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"}) ) {
@@ -1309,21 +1280,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 +1304,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 +1321,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 +1331,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);
@@ -1466,6 +1437,64 @@ sub user_is_member {
}
+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) {
log_info( mtx("Removing directory `%s' ..."), $undohome);
=====================================
debian/tests/f/adduser_system.t
=====================================
@@ -204,8 +204,10 @@ assert_command_success(
assert_user_exists('aust');
assert_user_is_system('aust');
-system('echo "aust:!foobar" | chpasswd --encrypted');
-# with #1099734 fixed, this should fail
+# $ 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");
assert_command_result_silent(RET_WRONG_OBJECT_PROPERTIES,
'/usr/sbin/adduser',
'--stdoutmsglevel=error', '--stderrmsglevel=error',
@@ -216,23 +218,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');
+# #1124835
+# $ 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");
+#assert_command_result_silent(RET_WRONG_OBJECT_PROPERTIES,
+# '/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
=====================================
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)");
@@ -294,6 +309,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 +475,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
=====================================
@@ -265,21 +265,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,14 +294,9 @@ 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 );
}
=====================================
doc/adduser.8
=====================================
@@ -613,8 +613,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.
=====================================
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;
}
@@ -228,5 +234,182 @@ sub check_user_has_gid {
return 1;
}
+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
+ # 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;
+}
+
+
+
+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
=====================================
@@ -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 → testsuite/test11.pl
=====================================
View it on GitLab: https://salsa.debian.org/debian/adduser/-/compare/432432f70ef45e7ed595edb63d89f24be34957c2...28c9faebb2b6fd6d660baf128b2b65094c7aa25a
--
View it on GitLab: https://salsa.debian.org/debian/adduser/-/compare/432432f70ef45e7ed595edb63d89f24be34957c2...28c9faebb2b6fd6d660baf128b2b65094c7aa25a
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/20260111/7a6eb70e/attachment-0001.htm>
More information about the Pkg-shadow-devel
mailing list