[Pkg-shadow-devel] [Git][debian/adduser][wip/feature-system-locks] 20 commits: deluser --lock [--system]

Marc Haber (@zugschlus) gitlab at salsa.debian.org
Sun Jan 4 21:32:54 GMT 2026



Marc Haber pushed to branch wip/feature-system-locks at Debian / adduser


Commits:
963fdf5c by Matt Barry at 2026-01-04T22:19:53+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).

- - - - -
8c02ef1c by Matt Barry at 2026-01-04T22:19:53+01:00
the documentation commit

this contains documentation changes related to account locking.
might still need some work.

- - - - -
710e1cfc by Matt Barry at 2026-01-04T22:19:53+01:00
add some basic tests

- - - - -
fb43595f by Marc Haber at 2026-01-04T22:19:53+01:00
improve log level handling

Thanks: Matt Barry
Git-Dch: ignore

- - - - -
05cd99ad by Marc Haber at 2026-01-04T22:19:53+01:00
add new constants for password handling

Git-Dch: ignore
Thanks: Matt Barry

- - - - -
ec73f51e by Marc Haber at 2026-01-04T22:19:53+01:00
introduce password handling logic

Git-Dch: ignore
Thanks: Matt Barry

- - - - -
11799af8 by Marc Haber at 2026-01-04T22:19:53+01:00
introduce new function existing_value_desc

Git-Dch: ignore
Thanks: Matt Barry

- - - - -
3fc11534 by Marc Haber at 2026-01-04T22:19:53+01:00
use existing_value_desc

Thanks: Matt Barry
Git-Dch: ignore

- - - - -
e50261a2 by Matt Barry at 2026-01-04T22:19:53+01:00
locked.account.tests

- - - - -
d39f8d9d by Marc Haber at 2026-01-04T22:19:53+01:00
prepare AdduserTestsCommon for the new tests

Git-Dch: ignore

- - - - -
c089d7bb by Matt Barry at 2026-01-04T22:19:53+01:00
user locked account tests

Git-Dch: ignore

- - - - -
98bc92bb by Marc Haber at 2026-01-04T22:19:53+01:00
fix testsuite warning in test08.pl

Git-Dch: ignore

- - - - -
c95e564e by Marc Haber at 2026-01-04T22:19:53+01:00
run upstream testsuite with redirected stdin

otherwise tests might hang invisibly when the software
goes interactive

Git-Dch: ignore

- - - - -
ff7b07f9 by Marc Haber at 2026-01-04T22:19:53+01:00
--unlock makes sense also for system accounts

This is consisten with the documentation

Git-Dch: ignore

- - - - -
54112173 by Marc Haber at 2026-01-04T22:19:53+01:00
fix EXISTING_HAS_PASSWORD to correctly handle !

This now also handles !something in the password field

- - - - -
7c40fae2 by Marc Haber at 2026-01-04T22:19:53+01:00
add unlock_user function

Git-Dch: ignore

- - - - -
effaed98 by Marc Haber at 2026-01-04T22:19:53+01:00
error out if asked to add an already locked non-ystem user

Git-Dch: ignore

- - - - -
243947c9 by Marc Haber at 2026-01-04T22:19:53+01:00
unlock existing system user with new function

Git-Dch: ignore

- - - - -
054ef38a by Marc Haber at 2026-01-04T22:19:53+01:00
unlocking an existing system user is info

Git-Dch: ignore

- - - - -
e8e85ad1 by Marc Haber at 2026-01-04T22:19:53+01:00
implement adduser --unlock

Git-Dch: ignore

- - - - -


14 changed files:

- AdduserCommon.pm
- adduser
- adduser.conf
- + debian/tests/f/account_locks.t
- debian/tests/lib/AdduserTestsCommon.pm
- deluser
- deluser.conf
- doc/adduser.8
- doc/deluser.8
- doc/deluser.conf.5
- + notes.100808x.md
- testsuite/runsuite.sh
- testsuite/test08.pl
- + testsuite/test10.pl


Changes:

=====================================
AdduserCommon.pm
=====================================
@@ -102,6 +102,17 @@ use constant {
     EXISTING_ID_MISMATCH => 4,
     EXISTING_LOCKED => 8,
     EXISTING_HAS_PASSWORD => 16,
+    EXISTING_DISABLED_PASS => 40,        # 32 | EXISTING_LOCKED
+    EXISTING_INVALID_PASS => 72,    # 64 | EXISTING_LOCKED
+    EXISTING_EXPIRED => 136,         # 128 | EXISTING_LOCKED
+    EXISTING_NOLOGIN => 264,        # 256 | EXISTING_LOCKED
+    EXISTING_PASSWORDLESS => 512,
+};
+
+use constant {
+    STDOUTDEFLEVEL => "warn",
+    STDERRDEFLEVEL => "warn",
+    LOGMSGDEFLEVEL => "info",
 };
 
 @EXPORT = (
@@ -138,6 +149,14 @@ use constant {
     'EXISTING_ID_MISMATCH',
     'EXISTING_LOCKED',
     'EXISTING_HAS_PASSWORD',
+    'EXISTING_DISABLED_PASS',
+    'EXISTING_INVALID_PASS',
+    'EXISTING_EXPIRED',
+    'EXISTING_NOLOGIN',
+    'EXISTING_PASSWORDLESS',
+    'STDOUTDEFLEVEL',
+    'STDERRDEFLEVEL',
+    'LOGMSGDEFLEVEL',
     'existing_user_status',
     'existing_group_status',
 );
@@ -507,6 +526,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",
@@ -597,28 +617,41 @@ END {
 #       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
+#       EXISTING_DISABLED_PASS => 32
+#       EXISTING_INVALID_PASS => 64
+#       EXISTING_NOLOGIN => 128
+#       EXISTING_EXPIRED => 256
+#       EXISTING_PASSWORDLESS => 512
 sub existing_user_status {
     my ($config, $new_name,$new_uid) = @_;
-    my ($dummy1,$pw,$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)) {
+    if (my (undef,$pw,$uid,undef,undef,$home,$shell) = egetpwnam($new_name)) {
         # 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", $new_name, $pw, $uid, $home, $shell );
         $ret |= EXISTING_FOUND;
         $ret |= EXISTING_ID_MISMATCH if (defined($new_uid) && $uid != $new_uid);
         $ret |= EXISTING_SYSTEM if
             (($uid >= $config->{"first_system_uid"}) && ($uid <= $config->{"last_system_uid"}));
         $ret |= EXISTING_HAS_PASSWORD if
-            (defined $pw && $pw ne '' && $pw ne '!' && $pw !~ /^\*/);
+            (defined $pw && $pw ne '' && $pw !~ /^[!*]/);
         $ret |= EXISTING_LOCKED if (substr($pw,0,1) eq "!");  # TODO: also check expiry?
+
+        # Note: the following conditions will also be true against EXISTING_LOCKED
+        # iow, if $x & EXISTING_INVALID_PASS, then $x & EXISTING_LOCKED
+        $ret |= EXISTING_DISABLED_PASS if (substr($pw,0,1) eq "!");
+        $ret |= EXISTING_INVALID_PASS if (substr($pw,0,1) eq "*");
+        $ret |= EXISTING_PASSWORDLESS if ($pw eq '!');
+        $ret |= EXISTING_NOLOGIN if ($shell =~ /bin\/nologin/);
+
+        my $age = `chage -l $new_name`;
+        $ret |= EXISTING_EXPIRED if ($age =~ /password must be changed/);
+
     } elsif (defined($new_uid) && getpwuid($new_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)", $new_name, $new_uid, $ret, existing_value_desc($ret) );
     return $ret;
 }
 
@@ -634,10 +667,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 +680,26 @@ 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, "badpass" if $val & EXISTING_INVALID_PASS;
+    push @flags, "disabled" if $val & EXISTING_DISABLED_PASS;
+    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:


=====================================
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?
@@ -114,6 +114,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 +187,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 +201,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 +226,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 +243,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 +322,6 @@ if ($action ne "addgroup" &&
     exit( RET_EXCLUSIVE_PARAMETERS );
 }
 
-
 if ($found_group_opt) {
     if ($action eq "addsysuser") {
         $make_group_also = 1;
@@ -326,6 +334,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;
@@ -431,6 +443,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 +603,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 );
     }
@@ -1051,6 +1068,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 +1197,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_fatal( 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);
@@ -1465,6 +1498,33 @@ 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) {
+            log_info( mtx("unlocking user `%s' ...", $user_name) );
+            my $unlock_ret = systemcall_useradd($name_check_level, 
+                'usermod',
+                '-e',
+                '-1',
+                '-U',
+                $new_name);
+            if( $unlock_ret == 0 ) {
+                log_info( mtx("user `%s' successfully unlocked.\n", $user_name) );
+            } 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"), $new_name );
+    } else {
+        log_fatal( mtx("User `%s' does not exist\n"), $new_name );
+        exit( RET_OBJECT_DOES_NOT_EXIST );
+    }
+}
 
 sub cleanup {
     if ($undohome) {
@@ -1523,6 +1583,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/tests/f/account_locks.t
=====================================
@@ -0,0 +1,109 @@
+#! /usr/bin/perl -Idebian/tests/lib
+
+# Ref: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=701110
+
+
+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_command_success('/usr/sbin/deluser',
+	'--stdoutmsglevel=error', '--stderrmsglevel=error',
+    '--system', "--lock",
+	$un);
+assert_user_exists($un);
+assert_user_is_locked($un);
+assert_user_is_disabled($un);
+assert_user_is_invalid($un);
+assert_user_is_expired($un);
+assert_user_is_nologin($un);
+assert_command_success('/usr/sbin/deluser',
+	'--stdoutmsglevel=error', '--stderrmsglevel=error',
+    '--system', "--lock",
+	$un);
+assert_user_exists($un);
+assert_command_success('/usr/sbin/adduser',
+	'--stdoutmsglevel=error', '--stderrmsglevel=error',
+    '--system',
+	$un);
+assert_user_exists($un);
+assert_user_is_not_locked($un);
+assert_user_is_not_disabled($un);
+assert_user_is_not_invalid($un);
+assert_user_is_not_expired($un);
+assert_user_is_not_nologin($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',
+	'--disabled-password',
+	$un);
+assert_user_exists($un);
+assert_command_success('/usr/sbin/deluser',
+	'--stdoutmsglevel=error', '--stderrmsglevel=error',
+    "--lock",
+	$un);
+assert_user_exists($un);
+assert_user_is_locked($un);
+assert_user_is_disabled($un);
+assert_user_is_invalid($un);
+assert_user_is_expired($un);
+assert_user_is_nologin($un);
+assert_command_success('/usr/sbin/deluser',
+	'--stdoutmsglevel=error', '--stderrmsglevel=error',
+    "--lock",
+	$un);
+assert_user_exists($un);
+assert_command_failure('/usr/sbin/adduser',
+	'--stdoutmsglevel=error', '--stderrmsglevel=error',
+	$un);
+assert_user_exists($un);
+assert_user_is_locked($un);
+assert_command_success('/usr/sbin/adduser',
+	'--stdoutmsglevel=error', '--stderrmsglevel=error',
+	'--unlock',
+	$un);
+assert_user_exists($un);
+assert_user_is_not_locked($un);
+assert_user_is_not_disabled($un);
+assert_user_is_not_invalid($un);
+assert_user_is_not_expired($un);
+assert_user_is_not_nologin($un);
+assert_command_success('/usr/sbin/deluser',
+	'--stdoutmsglevel=error', '--stderrmsglevel=error',
+    '--system', '--force-delete', '--remove-home',
+	$un);
+assert_user_does_not_exist($un);
+
+
+# vim: tabstop=4 shiftwidth=4 expandtab


=====================================
debian/tests/lib/AdduserTestsCommon.pm
=====================================
@@ -30,6 +30,22 @@ 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_DISABLED_PASS => 24,        # 16 | EXISTING_LOCKED
+    EXISTING_INVALID_PASS => 40,    # 32 | EXISTING_LOCKED
+    EXISTING_EXPIRED => 72,         # 64 | EXISTING_LOCKED
+    EXISTING_NOLOGIN => 136,        # 128 | EXISTING_LOCKED
+    SYS_MIN => 100,
+    SYS_MAX => 999,
+    USER_MIN => 1000,
+    USER_MAX => 9999,
+};
+
 my $charset = langinfo(CODESET);
 binmode(STDOUT, ":encoding($charset)");
 binmode(STDERR, ":encoding($charset)");
@@ -291,11 +307,6 @@ sub assert_path_has_ownership {
     is(sprintf('%s:%s', $user, $group), $ownership, $name);
 }
 
-sub assert_path_is_a_file {
-    my $path = shift;
-    ok(-f $path, "path is a file $path");
-}
-
 sub assert_path_is_a_directory {
     my $path = shift;
     ok(-d $path, "path is a directory: $path");
@@ -387,6 +398,7 @@ sub assert_user_has_home_directory {
 
 sub assert_user_has_comment {
     my ($user, $comment) = @_;
+    $comment .= ',,,';
     is((egetpwnam($user))[6], $comment, "user has comment: ~$user is $comment");
 }
 
@@ -459,6 +471,65 @@ sub apply_config_hash {
     close(CONF);
 }
 
+sub assert_user_is_locked { 
+    return existing_user_status($1) & EXISTING_LOCKED; 
+}
+sub assert_user_is_disabled { 
+    return (existing_user_status($1) & EXISTING_DISABLED_PASS)==EXISTING_DISABLED_PASS;
+}
+sub assert_user_is_invalid {
+    return (existing_user_status($1) & EXISTING_INVALID_PASS)==EXISTING_INVALID_PASS;
+}
+sub assert_user_is_nologin {
+    return (existing_user_status($1) & EXISTING_NOLOGIN)==EXISTING_NOLOGIN;
+}
+sub assert_user_is_expired {
+    return (existing_user_status($1) & EXISTING_EXPIRED)==EXISTING_EXPIRED;
+}
+sub assert_user_is_not_locked { return !assert_user_is_locked($1) }
+sub assert_user_is_not_disabled { return !assert_user_is_disabled($1) }
+sub assert_user_is_not_invalid { return !assert_user_is_invalid($1) }
+sub assert_user_is_not_nologin { return !assert_user_is_nologin($1) }
+sub assert_user_is_not_expired { return !assert_user_is_expired($1) }
+
+sub existing_user_status {
+    my ($new_name,$new_uid) = @_;
+    my $ret = EXISTING_NOT_FOUND;
+    if (my (undef,$pw,$uid,undef,undef,$home,$shell) = egetpwnam($new_name)) {
+        $ret |= EXISTING_FOUND;
+        $ret |= EXISTING_ID_MISMATCH if (defined($new_uid) && $uid != $new_uid);
+        $ret |= EXISTING_SYSTEM if \
+            ($uid >= SYS_MIN && $uid <= SYS_MAX);
+        
+	# Note: the following conditions will also be true against EXISTING_LOCKED
+        # iow, if $x & EXISTING_INVALID_PASS, then $x & EXISTING_LOCKED
+        $ret |= EXISTING_DISABLED_PASS if (substr($pw,0,1) eq "!");
+        $ret |= EXISTING_INVALID_PASS if (substr($pw,0,1) eq "*");
+        $ret |= EXISTING_NOLOGIN if ($shell =~ /bin\/nologin/);
+
+        my $age = `chage -l $new_name`;
+        $ret |= EXISTING_EXPIRED if ($age =~ /password must be changed/);
+    } elsif ($new_uid && getpwuid($new_uid)) {
+        $ret |= EXISTING_ID_MISMATCH;
+    }
+    return $ret;
+}
+
+sub existing_group_status {
+    my ($new_name,$new_gid) = @_;
+    my $gid;
+    my $ret = EXISTING_NOT_FOUND;
+    if ((undef,undef,$gid) = egetgrnam($new_name)) {
+        $ret |= EXISTING_FOUND;
+        $ret |= EXISTING_ID_MISMATCH if (defined($new_gid) && $gid != $new_gid);
+        $ret |= EXISTING_SYSTEM if \
+            ($gid >= SYS_MIN && $gid <= SYS_MAX);
+    } elsif ($new_gid && getgrgid($new_gid)) {
+        $ret |= EXISTING_ID_MISMATCH;
+    }
+    return $ret;
+}
+
 1;
 
 # vim: tabstop=4 shiftwidth=4 expandtab


=====================================
deluser
=====================================
@@ -122,22 +122,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;
@@ -287,11 +289,6 @@ 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)) {
@@ -312,6 +309,39 @@ if($action eq "deluser") {
         $config{"backup"} = 1;
     }
 
+    # default behavior is to lock if deleting a system account;
+    # this can be changed via deluser.conf or --force-delete.
+    if(!$config{"lock"}) {
+        if ($config{"system"} && !($config{"sys_delete_action"} eq "delete")) {
+            $config{"lock"} = 1;
+        }
+    }
+    if ($config{"force_delete"}) {
+        $config{"lock"} = 0;
+    }
+
+    # if account is to be locked (not deleted), leave files alone
+    if ($config{"lock"}) {
+        # TODO: remove this section - any account can be locked
+        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();
+        my $lock_ret = &systemcall('usermod', "-e", 1, "-f", 0, "-L", $user);
+        if( $lock_ret == 0 ) {
+            log_info( mtx("user `%s' successfully locked.\n", $user_name) );
+        } else {↲
+            log_fatal( mtx("error %s while unlocking user `%s'. Exiting.\n", $unlock_ret, $user_name) );
+            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 ...") );
@@ -495,8 +525,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 +591,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 +658,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_LOCK_DELETE=lock
+#SYS_DELETE_ACTION=lock


=====================================
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


=====================================
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,13 @@ 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_LOCK_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/runsuite.sh
=====================================
@@ -20,7 +20,7 @@ for a in on; do
     fi
     echo
     echo "Starting $i (shadow $a)"
-    /usr/bin/perl -I. $i
+    < /dev/null /usr/bin/perl -I. $i
     if [ "$?" != "0" ]; then
       FAILED="$FAILED $i($a)"
     fi


=====================================
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)) {


=====================================
testsuite/test10.pl
=====================================
@@ -0,0 +1,288 @@
+#!/usr/bin/perl -w
+
+# expect:
+#  - a new non-system group $groupname
+#  - reading the group fails
+#  - reading the group as a system group fails
+#  - a new system group $groupname
+#  - reading the group succeeds
+#  - reading the group as a non-system group fails
+
+use strict;
+
+use lib_test;
+
+my $error;
+my $output;
+
+my $cmd;
+my $username;
+my $num;
+
+$username = find_unused_name();
+$num = 0;
+
+use strict;
+use warnings;
+
+sub get_user_status {
+    my ($username) = @_;
+    return 'absent' unless defined $username && length $username;
+
+    # Get passwd entry (returns actual password if run as root)
+    my @pw = getpwnam($username);
+    return 'absent' unless @pw;
+
+    my $pw_field = $pw[1];   # encrypted password field
+
+    # Locked if password starts with '!'
+    return ($pw_field =~ /^!/) ? 'locked' : 'unlocked';
+}
+
+# unlock a non-existing account
+$cmd = "adduser --unlock $username";
+++$num && print "Testing (10.$num) $cmd... ";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+if (!$error) {
+    print "failed\n  $cmd returned errorcode ($error)\n  $output\n";
+    exit 1;
+}
+if (get_user_status($username) ne "absent" ) {
+    print "failed\n  $username is not absent\n  $output}n";
+    exit 1;
+}
+print "ok\n";
+
+# 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;
+}
+if (get_user_status($username) ne "locked" ) {
+    print "failed\n  $username is not locked\n  $output}n";
+    exit 1;
+}
+print "ok\n";
+
+# unlock the account (should fail, no password set)
+$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;
+}
+if (get_user_status($username) ne "locked" ) {
+    print "failed\n  $username is not locked\n  $output}n";
+    exit 1;
+}
+print "ok\n";
+
+# 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;
+}
+if (get_user_status($username) ne "unlocked" ) {
+    print "failed\n  $username is not unlocked\n  $output}n";
+    exit 1;
+}
+print "ok\n";
+
+# 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;
+}
+if (get_user_status($username) ne "locked" ) {
+    print "failed\n  $username is not locked\n  $output}n";
+    exit 1;
+}
+print "ok\n";
+
+# add the account (should fail)
+$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;
+}
+if (get_user_status($username) ne "locked" ) {
+    print "failed\n  $username is not locked\n  $output}n";
+    exit 1;
+}
+print "ok\n";
+
+# 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;
+}
+if (get_user_status($username) ne "unlocked" ) {
+    print "failed\n  $username is not unlocked\n  $output}n";
+    exit 1;
+}
+print "ok\n";
+
+# system
+$username = find_unused_name();
+
+# unlock a non-existing system ccount
+$cmd = "adduser --unlock --system $username";
+++$num && print "Testing (10.$num) $cmd... ";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+if (!$error) {
+    print "failed\n  $cmd returned errorcode ($error)\n  $output\n";
+    exit 1;
+}
+if (get_user_status($username) ne "absent" ) {
+    print "failed\n  $username is not absent\n  $output}n";
+    exit 1;
+}
+print "ok\n";
+
+# add system account
+$cmd = "adduser --system $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;
+}
+if (get_user_status($username) ne "locked" ) {
+    print "failed\n  $username is not locked\n  $output}n";
+    exit 1;
+}
+print "ok\n";
+
+# unlock the account (should fail, no password set)
+$cmd = "adduser --system --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;
+}
+if (get_user_status($username) ne "locked" ) {
+    print "failed\n  $username is not locked\n  $output}n";
+    exit 1;
+}
+print "ok\n";
+
+# 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;
+}
+if (get_user_status($username) ne "unlocked" ) {
+    print "failed\n  $username is not unlocked\n  $output}n";
+    exit 1;
+}
+print "ok\n";
+
+# lock the account
+$cmd = "deluser --system --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;
+}
+if (get_user_status($username) ne "locked" ) {
+    print "failed\n  $username is not locked\n  $output}n";
+    exit 1;
+}
+print "ok\n";
+
+# re-enable via add
+$cmd = "adduser --system $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;
+}
+if (get_user_status($username) ne "unlocked" ) {
+    print "failed\n  $username is not unlocked\n  $output}n";
+    exit 1;
+}
+print "ok\n";
+
+# unlock already-unlocked
+$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;
+}
+if (get_user_status($username) ne "unlocked" ) {
+    print "failed\n  $username is not unlocked\n  $output}n";
+    exit 1;
+}
+print "ok\n";
+
+# lock the account
+$cmd = "deluser --system --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;
+}
+if (get_user_status($username) ne "locked" ) {
+    print "failed\n  $username is not locked\n  $output}n";
+    exit 1;
+}
+print "ok\n";
+
+# unlock the account
+$cmd = "adduser --system --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;
+}
+if (get_user_status($username) ne "unlocked" ) {
+    print "failed\n  $username is not unlocked\n  $output}n";
+    exit 1;
+}
+print "ok\n";
+
+# set configuration option and try to delete
+#
+print "not completely implemented yet\n";
+exit 1;



View it on GitLab: https://salsa.debian.org/debian/adduser/-/compare/64b8e77924dce922bbad50b06a37ed22a16a1094...e8e85ad145b02e60a3d3c95fbe19f0400a2f3d73

-- 
View it on GitLab: https://salsa.debian.org/debian/adduser/-/compare/64b8e77924dce922bbad50b06a37ed22a16a1094...e8e85ad145b02e60a3d3c95fbe19f0400a2f3d73
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/20260104/4fbd9abd/attachment-0001.htm>


More information about the Pkg-shadow-devel mailing list