[Pkg-shadow-devel] [Git][debian/adduser][wip/fixup_existing] 8 commits: Give chpasswd test values that it will accept
Marc Haber (@zugschlus)
gitlab at salsa.debian.org
Sun Jan 11 06:55:51 GMT 2026
Marc Haber pushed to branch wip/fixup_existing 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
- - - - -
14 changed files:
- AdduserCommon.pm
- debian/tests/f/adduser_system.t
- 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:
=====================================
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
=====================================
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,94 @@ 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;
+}
+
+
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/4da77685fd171e653de087605635c064639b5b1f...87897be5805e2956b7fe323e13d2597cc73da040
--
View it on GitLab: https://salsa.debian.org/debian/adduser/-/compare/4da77685fd171e653de087605635c064639b5b1f...87897be5805e2956b7fe323e13d2597cc73da040
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/014de386/attachment-0001.htm>
More information about the Pkg-shadow-devel
mailing list