[Pkg-shadow-devel] [Git][debian/adduser][wip/upstream-testsuite] 8 commits: Give chpasswd test values that it will accept

Marc Haber (@zugschlus) gitlab at salsa.debian.org
Sun Jan 11 06:53:51 GMT 2026



Marc Haber pushed to branch wip/upstream-testsuite 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/dd8204ff1ee95a970d69ef765386195154b259e9...87897be5805e2956b7fe323e13d2597cc73da040

-- 
View it on GitLab: https://salsa.debian.org/debian/adduser/-/compare/dd8204ff1ee95a970d69ef765386195154b259e9...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/a79c4824/attachment-0001.htm>


More information about the Pkg-shadow-devel mailing list