[Pkg-shadow-devel] [Git][debian/adduser][debian/latest] 9 commits: add AdduserStatefile.pm to maintain state file

Marc Haber (@zugschlus) gitlab at salsa.debian.org
Mon Jan 19 07:33:25 GMT 2026



Marc Haber pushed to branch debian/latest at Debian / adduser


Commits:
26e35d01 by Marc Haber at 2026-01-17T15:15:05+01:00
add AdduserStatefile.pm to maintain state file

- - - - -
9bf89c5d by Marc Haber at 2026-01-17T15:15:05+01:00
install AdduserStatefile, remove state file on postrm

Git-Dch: ignore

- - - - -
c462807a by Marc Haber at 2026-01-17T15:15:05+01:00
use AdduserStatefile

Git-Dch: ignore

- - - - -
1fa65cad by Marc Haber at 2026-01-17T15:15:05+01:00
fix EXISTING_HAS_PASSWORD to correctly handle !

This now also handles !something in the password field

- - - - -
223579fc by Matt Barry at 2026-01-17T15:15:05+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).

- - - - -
c23643a0 by Marc Haber at 2026-01-17T15:15:05+01:00
adduser --unlock [--system]

- - - - -
9e761834 by Matt Barry at 2026-01-17T15:15:05+01:00
the documentation commit

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

- - - - -
69283b89 by Matt Barry at 2026-01-17T15:15:05+01:00
add some basic tests

- - - - -
5937f07b by Matt Barry at 2026-01-17T15:15:05+01:00
locked.account.tests

- - - - -


17 changed files:

- AdduserCommon.pm
- + AdduserStatefile.pm
- adduser
- adduser.conf
- debian/postrm
- debian/rules
- + 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/deluser-delete.conf
- testsuite/lib_test.pm
- + testsuite/test10.pl


Changes:

=====================================
AdduserCommon.pm
=====================================
@@ -31,6 +31,7 @@ my $codeset;
 
 use Debian::AdduserLogging 3.139;
 use Debian::AdduserRetvalues 3.139;
+use Debian::AdduserStatefile 3.139;
 BEGIN {
     if ( Debian::AdduserLogging->VERSION != version->declare('3.139') ||
          Debian::AdduserRetvalues->VERSION != version->declare('3.139') ) {
@@ -520,6 +521,7 @@ sub preseed_config {
         no_del_paths => "^/bin\$ ^/boot\$ ^/dev\$ ^/etc\$ ^/initrd ^/lib ^/lost+found\$ ^/media\$ ^/mnt\$ ^/opt\$ ^/proc\$ ^/root\$ ^/run\$ ^/sbin\$ ^/srv\$ ^/sys\$ ^/tmp\$ ^/usr\$ ^/var\$ ^/vmlinu",
         name_regex     => def_name_regex,
         sys_name_regex => def_sys_name_regex,
+        sys_delete_action => "delete",
         exclude_fstypes => "(proc|sysfs|usbfs|devpts|devtmpfs|devfs|afs)",
         skel_ignore_regex => "\.(dpkg|ucf)-(old|new|dist)\$",
         extra_groups => "users",
@@ -629,7 +631,7 @@ sub existing_user_status {
             (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_LOCKED if $password_field =~ /^[!*]/ && (get_state_value($user_name, "locked") // "") eq "1";
 
         $ret |= EXISTING_NOLOGIN if ($egpwn_shell =~ /bin\/nologin/);
 


=====================================
AdduserStatefile.pm
=====================================
@@ -0,0 +1,293 @@
+package Debian::AdduserStatefile 3.139;
+use 5.36.0;
+use utf8;
+
+# AdduserStatefile.pm: Manage persistent state for deleted user accounts
+#
+# This module provides a simple key-value store for tracking information
+# about deleted user accounts. The state is stored in a text file at
+# /var/lib/adduser/state in a format similar to /etc/passwd.
+#
+# File format: username:key1=value1:key2=value2:key3=value3
+#
+# Copyright (C) 2026 Marc Haber
+#
+# License: GPL-2+
+
+use parent qw(Exporter);
+use vars qw(@EXPORT);
+
+ at EXPORT = (
+    'set_state_value',
+    'get_state_value',
+    'get_state_user_data',
+    'delete_state_user',
+);
+
+use strict;
+use warnings;
+
+our $STATE_DIR = '/var/lib/adduser';
+our $STATE_FILE = "$STATE_DIR/state";
+our $LOCK_FILE = "$STATE_DIR/state.lock";
+
+# init_state_file()
+#
+# Initialize the state file and its parent directory if they don't exist.
+# Creates /var/lib/adduser with mode 0755 and the state file with mode 0644.
+# Dies on error.
+sub init_state_file {
+    # Create the directory if it doesn't exist
+    unless (-d $STATE_DIR) {
+        mkdir($STATE_DIR, 0755) or die "Cannot create $STATE_DIR: $!";
+    }
+
+    # Create the state file if it doesn't exist
+    unless (-f $STATE_FILE) {
+        open(my $fh, '>', $STATE_FILE) or die "Cannot create $STATE_FILE: $!";
+        close($fh);
+        chmod(0644, $STATE_FILE);
+    }
+}
+
+# set_value($username, $key, $value)
+#
+# Set a key-value pair for the specified username in the state file.
+# If the username doesn't exist, it will be created.
+# If the key already exists for that user, it will be overwritten.
+# If the value is empty or undefined, the key will be deleted instead.
+#
+# Parameters:
+#   $username - The username to store data for
+#   $key      - The key name (cannot contain '=', ':', or newlines)
+#   $value    - The value to store (cannot contain ':' or newlines)
+#               If empty or undef, the key is deleted
+#
+# Dies if the username, key, or value contain invalid characters.
+sub set_state_value {
+    my ($username, $key, $value) = @_;
+
+    my $lock_fh = _acquire_lock();
+    my %data = _read_state();
+
+    $data{$username} ||= {};
+    if (!defined $value || $value eq '') {
+        delete $data{$username}{$key};
+        if (keys %{$data{$username}} == 0) {
+            delete $data{$username};
+        }
+    } else {
+        die "Invalid value" if $value =~ /[:\n]/;
+        $data{$username}{$key} = $value;
+    }
+
+    _write_state(\%data);
+    _release_lock($lock_fh);
+}
+
+# get_value($username, $key)
+#
+# Retrieve a value for a given username and key (or all values if key is undef).
+# This is a lock-free read operation. Due to atomic file replacement,
+# readers will always see a consistent (though possibly slightly stale)
+# version of the state file.
+#
+# Parameters:
+#   $username - The username to look up
+#   $key      - The key name to retrieve (optional)
+#
+# Returns:
+#   If $key is provided: the value if found, undef otherwise
+#   If $key is undef: a hash reference containing all key-value pairs for the user
+sub get_state_value {
+    my ($username, $key) = @_;
+
+    # Return undef/empty hash if state file doesn't exist yet
+    return undef unless -f $STATE_FILE;
+
+    my %data = _read_state();
+
+    # Return undef/empty hash if user doesn't exist
+    if (!exists $data{$username}) {
+        return defined $key ? undef : {};
+    }
+
+    # Return specific key or all data
+    return defined $key ? $data{$username}{$key} : $data{$username};
+}
+
+# get_user_data($username)
+#
+# Retrieve all key-value pairs for a given username.
+# This is a convenience wrapper around get_value($username, undef).
+#
+# Parameters:
+#   $username - The username to look up
+#
+# Returns:
+#   A hash reference containing all key-value pairs for the user,
+#   or an empty hash reference if the user doesn't exist
+sub get_state_user_data {
+    my ($username) = @_;
+    return get_value($username, undef);
+}
+
+# delete_user($username)
+#
+# Remove all data for a user from the state file.
+# This completely removes the user's entry from the state file.
+#
+# Parameters:
+#   $username - The username to delete
+#
+# Returns silently if the state file doesn't exist or the user isn't found.
+sub delete_state_user {
+    my ($username) = @_;
+
+    # Nothing to do if state file doesn't exist
+    return unless -f $STATE_FILE;
+
+    # Acquire lock, read, modify, write, release lock
+    my $lock_fh = _acquire_lock();
+    my %data = _read_state();
+    delete $data{$username};
+    _write_state(\%data);
+    _release_lock($lock_fh);
+}
+
+# _acquire_lock()
+#
+# Internal function to acquire an exclusive lock on the state file.
+# Uses a separate lock file to avoid interfering with atomic renames.
+#
+# Returns:
+#   File handle to the lock file (caller must pass to _release_lock)
+#
+# Dies if the lock file cannot be opened.
+sub _acquire_lock {
+    # Ensure the directory exists
+    unless (-d $STATE_DIR) {
+        mkdir($STATE_DIR, 0755) or die "Cannot create $STATE_DIR: $!";
+    }
+
+    # Open (or create) the lock file
+    open(my $lock_fh, '>>', $LOCK_FILE) or die "Cannot open $LOCK_FILE: $!";
+
+    # Acquire exclusive lock (blocks until available)
+    flock($lock_fh, 2) or die "Cannot acquire lock on $LOCK_FILE: $!";  # LOCK_EX
+
+    return $lock_fh;
+}
+
+# _release_lock($lock_fh)
+#
+# Internal function to release the lock and close the lock file.
+#
+# Parameters:
+#   $lock_fh - File handle returned by _acquire_lock()
+sub _release_lock {
+    my ($lock_fh) = @_;
+
+    # Closing the file handle releases the flock automatically
+    close($lock_fh);
+}
+
+# _read_state()
+#
+# Internal function to read and parse the entire state file.
+# For write operations, this MUST be called while holding the lock.
+# For read-only operations, this can be called without a lock due to
+# atomic file replacement via rename().
+#
+# File format: Each line contains:
+#   username:key1=value1:key2=value2:...
+#
+# Empty lines and lines starting with '#' are ignored.
+#
+# Returns:
+#   A hash where keys are usernames and values are hash references
+#   containing the key-value pairs for that user
+#
+# Dies if the file cannot be opened.
+sub _read_state {
+    my %data;
+
+    # Return empty hash if state file doesn't exist yet
+    return %data unless -f $STATE_FILE;
+
+    open(my $fh, '<', $STATE_FILE) or die "Cannot read $STATE_FILE: $!";
+
+    while (my $line = <$fh>) {
+        chomp $line;
+
+        # Skip empty lines and comments
+        next if $line =~ /^\s*$/;
+        next if $line =~ /^\s*#/;
+
+        # Split on colons - first field is username, rest are key=value pairs
+        # Untaint the line by validating it matches our expected format
+        next unless $line =~ /^([^:\n]+(?::[^:\n=]+=[^:\n]*)*?)$/;
+        $line = $1;  # Now untainted
+        my @fields = split(/:/, $line);
+        next unless @fields >= 1;
+
+        my $username = shift @fields;
+        $data{$username} = {};
+
+        # Parse each key=value pair
+        foreach my $pair (@fields) {
+            my ($key, $value) = split(/=/, $pair, 2);
+            $data{$username}{$key} = $value if defined $key && defined $value;
+        }
+    }
+
+    close($fh);
+
+    return %data;
+}
+
+# _write_state(\%data)
+#
+# Internal function to write the entire state file atomically.
+# MUST be called while holding the lock via _acquire_lock().
+# Uses a temporary file and rename() to ensure atomicity.
+#
+# Parameters:
+#   $data_ref - A hash reference in the format returned by _read_state()
+#
+# The output is sorted by username and by key within each username
+# for consistent, diff-friendly output.
+#
+# Dies if the file cannot be written or renamed.
+sub _write_state {
+    my ($data_ref) = @_;
+
+    # Use a process-specific temporary file name
+    my $temp_file = "$STATE_FILE.tmp.$$";
+
+    open(my $fh, '>', $temp_file) or die "Cannot write to $temp_file: $!";
+
+    # Write each user's data, sorted for consistency
+    foreach my $username (sort keys %$data_ref) {
+        my @pairs;
+
+        # Build key=value pairs, sorted by key
+        foreach my $key (sort keys %{$data_ref->{$username}}) {
+            my $value = $data_ref->{$username}{$key};
+            push @pairs, "$key=$value";
+        }
+
+        # Write the line: username:key1=value1:key2=value2:...
+        print $fh "$username:", join(':', @pairs), "\n";
+    }
+
+    close($fh);
+
+    # Atomically replace the old state file with the new one
+    # This happens while still holding the lock, so no race condition
+    rename($temp_file, $STATE_FILE) or die "Cannot rename $temp_file: $!";
+    chmod(0644, $STATE_FILE);
+}
+
+# Module must return true
+1;


=====================================
adduser
=====================================
@@ -34,6 +34,7 @@ use Getopt::Long;
 use Debian::AdduserCommon 3.139;
 use Debian::AdduserLogging 3.139;
 use Debian::AdduserRetvalues 3.139;
+use Debian::AdduserStatefile 3.139;
 BEGIN {
     if ( Debian::AdduserCommon->VERSION != version->declare('3.139') ||
          Debian::AdduserLogging->VERSION != version->declare('3.139') ||
@@ -114,6 +115,7 @@ our @configfiles;
 our @defaults = undef;
 our $found_group_opt = undef;
 our $found_sys_opt = undef;
+our $found_unlock_opt = undef;
 our $ingroup_name = undef;
 our $new_firstgid = undef;
 our $new_firstuid = undef;
@@ -186,6 +188,7 @@ GetOptions(
     'shell=s' => \$special_shell,
     'system' => \$found_sys_opt,
     'uid=i' => \$new_uid,
+    'unlock' => \$found_unlock_opt,
     'verbose' => sub { $verbose = 1; },
     'version|v' => sub { &version; exit },
 ) or &usage_error;
@@ -320,7 +323,6 @@ if ($action ne "addgroup" &&
     exit( RET_EXCLUSIVE_PARAMETERS );
 }
 
-
 if ($found_group_opt) {
     if ($action eq "addsysuser") {
         $make_group_also = 1;
@@ -333,6 +335,10 @@ if ($found_group_opt) {
     }
 }
 
+if ($found_unlock_opt) {
+    $action = "unlockuser";
+}
+
 # $new_firstuid = $new_firstuid || $config{"first_uid"} || 1000;
 # $new_lastuid = $new_lastuid || $config{"last_uid"} || 59999;
 # $new_firstgid = $new_firstgid || $config{"first_gid"} || 1000;
@@ -439,6 +445,8 @@ $SIG{'INT'} = $SIG{'QUIT'} = $SIG{'HUP'} = 'handler';
 # $action = "addusertogroup"
 #    $existing_user           - the user to be added
 #    $existing_group          - the group to add her to
+# $action = "unlockuser"
+#    $new_name                - the user to be unlocked
 #####
 
 
@@ -597,6 +605,9 @@ if ($action eq "addsysuser") {
         exit( RET_WRONG_OBJECT_PROPERTIES );
     }
     if ($ret & EXISTING_FOUND) {
+        if ($ret & EXISTING_LOCKED) {
+            unlock_user($new_name, 1);
+        }
         log_info( mtx("The system user `%s' already exists. Exiting.\n"), $new_name );
         exit( RET_OK );
     }
@@ -1022,6 +1033,15 @@ if ($action eq "adduser") {
     exit( $returnvalue );
 }
 
+if ($action eq "unlockuser") {
+    log_trace( "unlockuser %s", $new_name );
+    acquire_lock();
+    unlock_user($new_name, 0);
+    release_lock(0);
+
+    exit( $returnvalue );
+}
+
 #
 # we never go here
 #
@@ -1142,6 +1162,13 @@ sub check_user_group {
     log_debug( "check_user_group %s called, make_group_also %s", $system, $make_group_also );
 
     my $ustat = existing_user_status(\%config, $new_name, $new_uid);
+    if ($ustat & EXISTING_FOUND) {
+        if ($ustat & EXISTING_LOCKED) {
+            # this must be a non-system user, addsysuser handles the locked case before we get called
+            log_err( mtx("User `%s' already exists and is locked. use adduser --unlock explicitly to unlock. Exiting.\n"), $new_name );
+            exit( RET_OBJECT_EXISTS );
+        }
+    }
     if ($system) {
         if (($ustat & EXISTING_FOUND) && !($ustat & EXISTING_SYSTEM)) {
             log_fatal( mtx("The user `%s' already exists, and is not a system user."), $new_name);
@@ -1436,6 +1463,54 @@ sub user_is_member {
     return 0;
 }
 
+# unlock user
+sub unlock_user {
+    my ($user_name, $system) = @_;
+    log_debug( "unlock_user %s called, system %s", $user_name, $system );
+    my $ret = existing_user_status(\%config, $user_name, undef);
+    if ($ret & EXISTING_FOUND) {
+        if ($ret & EXISTING_LOCKED) {
+            my $default_shell = $system ? "/usr/sbin/nologin" : "/bin/bash";
+
+            # Get current shell state if available
+            my $shell = get_state_value($user_name, "shell") // $default_shell;
+
+            # Build usermod argument list
+            my @usermod_args = (
+                '-e', '-1',       # set account expiry to never
+                '-s', $shell,     # set shell
+                $user_name        # username
+            );
+
+            # Add -U only if account has a password
+            if ($ret & EXISTING_HAS_PASSWORD) {
+                push @usermod_args, '-U';
+            } else {
+                log_info( mtx(
+                    "Unlocking password `%s' would result in a passwordless account. Leaving password locked\n",
+                    $user_name
+                ));
+            }
+
+            log_info( mtx("Unlocking user `%s' ...", $user_name) );
+            log_trace( "usermod `%s' ...", join(" ", @usermod_args) );
+            my $unlock_ret = systemcall_useradd($name_check_level, 'usermod', @usermod_args);
+
+            if( $unlock_ret == 0 ) {
+                log_info( mtx("user `%s' successfully unlocked.\n", $user_name) );
+                set_state_value($user_name, "shell", undef);
+                set_state_value($user_name, "locked", undef);
+            } else {
+                log_fatal( mtx("error %s while unlocking user `%s'. Exiting.\n", $unlock_ret, $user_name) );
+                exit( RET_SYSTEMCALL_ERROR );
+            }
+        }
+        log_info( mtx("User `%s' is already unlocked.\n"), $user_name );
+    } else {
+        log_fatal( mtx("User `%s' does not exist\n"), $user_name );
+        exit( RET_OBJECT_DOES_NOT_EXIST );
+    }
+}
 
 sub interactive_command_with_confirmation {
     my ($cmd, $user_name, $error_map, $always_confirm) = @_;
@@ -1552,6 +1627,11 @@ sub usage {
         user
     Add a regular user
 
+adduser --unlock
+        [--system]
+        user
+    Unlock an existing locked user account
+
 adduser --system
         [--uid id] [--group] [--ingroup group] [--gid id]
         [--shell shell] [--comment comment] [--home dir] [--no-create-home]


=====================================
adduser.conf
=====================================
@@ -113,4 +113,3 @@
 # EXTRA_GROUPS.
 # Default: ADD_EXTRA_GROUPS=0
 #ADD_EXTRA_GROUPS=0
-


=====================================
debian/postrm
=====================================
@@ -5,7 +5,7 @@ set -eu
 
 case $1 in
   purge)
-    rm -fv /etc/adduser.conf /etc/adduser.conf.dpkg-save /etc/adduser.conf.update-old
+    rm -fv /etc/adduser.conf /etc/adduser.conf.dpkg-save /etc/adduser.conf.update-old /var/lib/adduser/state /var/lib/adduser
     ;;
 esac
 


=====================================
debian/rules
=====================================
@@ -24,6 +24,7 @@ override_dh_install:
 	sed -e s/DVERSION/$(cversion)/g AdduserCommon.pm > debian/adduser/usr/share/perl5/Debian/AdduserCommon.pm
 	sed -e s/DVERSION/$(cversion)/g AdduserLogging.pm > debian/adduser/usr/share/perl5/Debian/AdduserLogging.pm
 	sed -e s/DVERSION/$(cversion)/g AdduserRetvalues.pm > debian/adduser/usr/share/perl5/Debian/AdduserRetvalues.pm
+	sed -e s/DVERSION/$(cversion)/g AdduserStatefile.pm > debian/adduser/usr/share/perl5/Debian/AdduserStatefile.pm
 	ln -s adduser debian/adduser/usr/sbin/addgroup
 	ln -s deluser debian/adduser/usr/sbin/delgroup
 


=====================================
debian/tests/f/account_locks.t
=====================================
@@ -0,0 +1,103 @@
+#! /usr/bin/perl -Idebian/tests/lib
+
+use diagnostics;
+use strict;
+use warnings;
+
+use AdduserTestsCommon;
+
+my $prefix = "lockedtest";
+my $un;
+
+END {
+	remove_tree("/home/$prefix-user");
+	remove_tree("/var/mail/$prefix-user");
+}
+
+## system user
+
+$un = "${prefix}-sys";
+
+assert_user_does_not_exist($un);
+assert_command_success('/usr/sbin/adduser',
+	'--stdoutmsglevel=error', '--stderrmsglevel=error',
+	'--disabled-password',
+    '--system',
+	$un);
+assert_user_exists($un);
+assert_user_status($un, EXISTING_LOCKED,       "locked");
+assert_user_status($un, EXISTING_HAS_PASSWORD, "not has password");
+assert_user_status($un, EXISTING_NOLOGIN,      "set to nologin");
+assert_user_status($un, EXISTING_EXPIRED,      "not expired");
+assert_command_success('/usr/sbin/deluser',
+	'--stdoutmsglevel=error', '--stderrmsglevel=error',
+    '--system', "--lock",
+	$un);
+assert_user_exists($un);
+assert_user_status($un, EXISTING_LOCKED,       "locked");
+assert_user_status($un, EXISTING_HAS_PASSWORD, "not has password");
+assert_user_status($un, EXISTING_NOLOGIN,      "set to nologin");
+assert_user_status($un, EXISTING_EXPIRED,      "expired");
+assert_command_success('/usr/sbin/adduser',
+	'--stdoutmsglevel=error', '--stderrmsglevel=error',
+    '--system',
+	$un);
+assert_user_status($un, EXISTING_LOCKED,       "locked");
+assert_user_status($un, EXISTING_HAS_PASSWORD, "not has password");
+assert_user_status($un, EXISTING_NOLOGIN,      "set to nologin");
+assert_user_status($un, EXISTING_EXPIRED,      "not expired");
+assert_user_exists($un);
+assert_command_success('/usr/sbin/deluser',
+	'--stdoutmsglevel=error', '--stderrmsglevel=error',
+    '--system', '--force-delete',
+	$un);
+assert_user_does_not_exist($un);
+
+## normal user
+
+$un = "${prefix}-user";
+
+assert_user_does_not_exist($un);
+assert_command_success('/usr/sbin/adduser',
+    '--stdoutmsglevel=error', '--stderrmsglevel=error',
+    '--comment', "",
+    '--disabled-password',
+    $un);
+assert_user_exists($un);
+assert_user_status($un, EXISTING_LOCKED,       "locked");
+assert_user_status($un, EXISTING_HAS_PASSWORD, "not has password");
+assert_user_status($un, EXISTING_NOLOGIN,      "not set to nologin");
+assert_user_status($un, EXISTING_EXPIRED,      "not expired");
+assert_command_success('/usr/sbin/deluser',
+    '--stdoutmsglevel=error', '--stderrmsglevel=error',
+    "--lock",
+    $un);
+assert_user_exists($un);
+assert_user_status($un, EXISTING_LOCKED,       "locked");
+assert_user_status($un, EXISTING_HAS_PASSWORD, "not has password");
+assert_user_status($un, EXISTING_NOLOGIN,      "set to nologin");
+assert_user_status($un, EXISTING_EXPIRED,      "expired");
+assert_command_failure('/usr/sbin/adduser',
+    '--stdoutmsglevel=fatal', '--stderrmsglevel=fatal',
+    $un);
+assert_user_exists($un);
+assert_user_status($un, EXISTING_LOCKED,       "locked");
+assert_user_status($un, EXISTING_HAS_PASSWORD, "not has password");
+assert_user_status($un, EXISTING_NOLOGIN,      "set to nologin");
+assert_user_status($un, EXISTING_EXPIRED,      "expired");
+assert_command_success('/usr/sbin/adduser',
+    '--stdoutmsglevel=error', '--stderrmsglevel=error',
+    '--unlock',
+    $un);
+assert_user_exists($un);
+assert_user_status($un, EXISTING_LOCKED,       "locked");
+assert_user_status($un, EXISTING_HAS_PASSWORD, "not has password");
+assert_user_status($un, EXISTING_NOLOGIN,      "not set to nologin");
+assert_user_status($un, EXISTING_EXPIRED,      "not expired");
+assert_command_success('/usr/sbin/deluser',
+    '--stdoutmsglevel=error', '--stderrmsglevel=error',
+    '--force-delete', '--remove-home',
+    $un);
+assert_user_does_not_exist($un);
+
+# vim: tabstop=4 shiftwidth=4 expandtab


=====================================
debian/tests/lib/AdduserTestsCommon.pm
=====================================
@@ -497,7 +497,6 @@ sub assert_user_status {
     ok($ok, $message);
 }
 
-
 sub existing_user_status {
     my ($user_name,$user_uid) = @_;
     my $ret = EXISTING_NOT_FOUND;


=====================================
deluser
=====================================
@@ -28,6 +28,7 @@ use Getopt::Long;
 use Debian::AdduserCommon 3.139;
 use Debian::AdduserLogging 3.139;
 use Debian::AdduserRetvalues 3.139;
+use Debian::AdduserStatefile 3.139;
 BEGIN {
     if ( Debian::AdduserCommon->VERSION != version->declare('3.139') ||
          Debian::AdduserLogging->VERSION != version->declare('3.139') ||
@@ -122,22 +123,24 @@ our @defaults = undef;
 our @names;
 
 GetOptions (
+    'backup' => \$pconfig{'backup'},
+    'backup-to=s' => \$pconfig{'backup_to'},
+    'backup-suffix=s' => \$pconfig{'backup_suffix'},
     'conf|c=s' => \@configfiles,
     'debug' => sub { $verbose = 2; },
-    'stdoutmsglevel=s' => \$stdoutmsglevel,
-    'stderrmsglevel=s' => \$stderrmsglevel,
-    'logmsglevel=s' => \$logmsglevel,
-    'help|h' => sub { &usage(); exit 0; },
+    'force-delete' => \$pconfig{'force_delete'},
     'group' => sub { $action = 'delgroup'; },
-    'system' => \$pconfig{'system'},
+    'help|h' => sub { &usage(); exit 0; },
+    'lock|L' => \$pconfig{'lock'},
+    'logmsglevel=s' => \$logmsglevel,
+    'no-preserve-root' => \$no_preserve_root,
     'only-if-empty' => \$pconfig{'only_if_empty'},
+    'quiet|q' => sub { $verbose = 0; },
     'remove-home' => \$pconfig{'remove_home'},
     'remove-all-files' => \$pconfig{'remove_all_files'},
-    'backup' => \$pconfig{'backup'},
-    'backup-to=s' => \$pconfig{'backup_to'},
-    'backup-suffix=s' => \$pconfig{'backup_suffix'},
-    'no-preserve-root' => \$no_preserve_root,
-    'quiet|q' => sub { $verbose = 0; },
+    'stdoutmsglevel=s' => \$stdoutmsglevel,
+    'stderrmsglevel=s' => \$stderrmsglevel,
+    'system' => \$pconfig{'system'},
     'verbose' => sub { $verbose = 1; },
     'version|v' => sub { &version; exit },
 ) or &usage_error;
@@ -302,7 +305,7 @@ if($action eq "deluser") {
     }
 
     # Warn in any case if you want to remove the root account
-    if ((defined($pw_uid)) && ($pw_uid == 0) && (!defined($no_preserve_root)))  {
+    if ((defined($egpwn_uid)) && ($egpwn_uid == 0) && (!defined($no_preserve_root)))  {
         log_fatal( mtx("WARNING: You are just about to delete the root account (uid 0). Usually this is never required as it may render the whole system unusable. If you really want this, call deluser with parameter --no-preserve-root. Stopping now without having performed any action.") );
         exit( RET_DONT_REMOVE_ROOT );
     }
@@ -314,6 +317,42 @@ if($action eq "deluser") {
         $config{"backup"} = 1;
     }
 
+    # command line           sys_delete_action      action
+    # deluser                lock                   lock
+    # deluser                delete                 delete
+    # deluser --lock         any                    lock
+    # deluser --force-delete any                   delete
+
+    # decide whether to lock or to delete the account
+    # command line takes precedence, otherwise sys_delete_action wins
+    my $do_lock = ($config{sys_delete_action} eq 'lock');
+    $do_lock = 1 if $config{lock};
+    $do_lock = 0 if $config{force_delete};
+    log_trace( "Config: sys_delete_action=%s, lock=%s, force_delete=%s → do_lock=%s", $config{sys_delete_action}, $config{lock}, $config{force_delete}, $do_lock );
+
+    # if account is to be locked (not deleted), leave files alone
+    if ($do_lock) {
+        if ($config{"backup"} or $config{"remove"} or $config{"remove_all_files"}) {
+            log_warn( mtx("Account will be locked; files will not be backed up or removed.") );
+            log_warn( mtx("Hint: Use --force-delete to delete the account as normal.") )
+        }
+
+        log_info( mtx("Locking user account `%s' ...", $user));
+        acquire_lock();
+        set_state_value($user, "shell", $egpwn_shell);
+        set_state_value($user, "locked", 1);
+        my $lock_ret = &systemcall('usermod', "-e", 1, "-f", 0, "-L", "-s", "/usr/sbin/nologin", $user);
+        if( $lock_ret == 0 ) {
+            log_info( mtx("user `%s' successfully locked.\n", $user) );
+        } else {
+            log_fatal( mtx("error %s while unlocking user `%s'. Exiting.\n", $lock_ret, $user) );
+            exit( RET_SYSTEMCALL_ERROR );
+        }
+        release_lock();
+
+        exit( RET_OK )
+    }
+
     if($config{"remove_home"} || $config{"remove_all_files"}) {
         log_trace( mtx("remove_home or remove_all_files beginning") );
         log_info( mtx("Looking for files to backup/remove ...") );
@@ -446,6 +485,7 @@ if($action eq "deluser") {
     acquire_lock();
     systemcall('/usr/sbin/userdel', $user);
     release_lock();
+    delete_state_user($user);
 
     systemcall('/usr/local/sbin/deluser.local', $user, $pw_uid,
         $pw_gid, $pw_homedir) if (-x "/usr/local/sbin/deluser.local");
@@ -497,8 +537,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 );
@@ -564,7 +603,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
 
@@ -631,3 +670,4 @@ sub check_backup_suffix {
 # End:
 
 # vim: tabstop=4 shiftwidth=4 expandtab
+0


=====================================
deluser.conf
=====================================
@@ -39,3 +39,8 @@
 # be excluded when looking for files of a user to be deleted.
 # Default: EXCLUDE_FSTYPES = "(proc|sysfs|usbfs|devpts|tmpfs|afs)"
 #EXCLUDE_FSTYPES = "(proc|sysfs|usbfs|devpts|tmpfs|afs)"
+
+# What action to take when deleting a system account
+# Options: lock|delete
+# Default: SYS_DELETE_ACTION=delete
+#SYS_DELETE_ACTION=delete


=====================================
doc/adduser.8
=====================================
@@ -65,6 +65,11 @@ adduser, addgroup \- add or manipulate users or groups
 .B user
 .YS
 .SY adduser
+.B \-\-unlock
+.OP \-\-system
+.B user
+.YS
+.SY adduser
 .B \-\-group
 .OP \-\-conf file
 .OP \-\-debug
@@ -162,6 +167,11 @@ that means a
 in the sense of Debian Policy.
 This is commonly referred to in \fBadduser\fP as a \fInon-system user.\fP
 .PP
+If \fBadduser\fP is called with the \fB\-\-unlock\fP option, it will
+search for an existing account, and ensure it is unlocked.  If the
+user is not found, \fBadduser\fP will return an error.  See 
+\fBUnlock an account\fP below for more details.
+.PP
 \fBadduser\fP will choose the first available UID
 from the range specified by
 \fBFIRST_UID\fP and \fBLAST_UID\fP
@@ -224,6 +234,10 @@ often abbreviated as
 \fIsystem user\fP
 in the context of the \fBadduser\fP package.
 .PP
+If the user already exists and is a system account, then
+\fBadduser\fP will ensure the account is unlocked and return
+successfully.  See \fBUnlock a user account\fP below.
+.PP
 \fBadduser\fP will choose the first available UID
 from the range specified by
 \fBFIRST_SYSTEM_UID\fP and \fBLAST_SYSTEM_UID\fP
@@ -266,6 +280,13 @@ Skeletal configuration files are not copied.
 Other options will behave as for the creation of a regular user.
 The files referenced by \fBUID_POOL\fP and \fBGID_POOL\fP are also honored.
 
+.SS "Unlock a user account"
+If called with the \fB\-\-unlock\fP option, \fBadduser\fP will
+unlock an existing, previously locked account.  If found, the
+user's password will be re-enabled, and expiry restrictions will
+be removed.  Any previous expiry settings will have to be
+added manually after unlocking the account.
+
 .SS "Add a group"
 If \fBadduser\fP is called with the \fB\-\-group\fP option and
 without the \fB\-\-system\fP option, or


=====================================
doc/deluser.8
=====================================
@@ -27,6 +27,7 @@ deluser, delgroup \- remove a user or group from the system
 .OP \-\-stdoutmsglevel prio
 .OP \-\-stderrmsglevel prio
 .OP \-\-logmsglevel prio
+.OP \-\-lock
 .B user
 .YS
 
@@ -44,6 +45,8 @@ deluser, delgroup \- remove a user or group from the system
 .OP \-\-stdoutmsglevel prio
 .OP \-\-stderrmsglevel prio
 .OP \-\-logmsglevel prio
+.OP \-\-lock
+.OP \-\-force\-delete
 .B user
 .YS
 
@@ -115,6 +118,10 @@ If called with one non-option argument and
 without the \fB\-\-group\fP option,
 \fBdeluser\fP will remove a non-system user.
 .PP
+Note: If \fBdeluser\fP is called with the \fB\-\-lock\fP option,
+the user account will be locked rather than removed as described
+here; see \fB\-\-lock\fP, below.
+.PP
 By default,
 \fBdeluser\fP will remove the user
 without removing the home directory,
@@ -263,6 +270,19 @@ That allows the local admin to control \fBadduser\fP's chattiness
 on the console and in the log independently, keeping probably confusing
 information to itself while still leaving helpful information in the log.
 .TP
+.B \-\-lock
+If \fBdeluser\fP is called with the \fB\-\-lock\fP option,
+the user account will be locked rather than removed.  In this
+case, the user's password is invalidated and the account is
+set to expired; no other action is taken.  A locked account may be
+subsequently unlocked using \fBadduser\fP \fB\-\-unlock\fP.
+
+System users may be set to lock upon delete by default by setting
+\fBSYS_DELETE_ACTION\fP=\fBlock\fP in \fB/etc/deluser.conf\fP.
+.B \-\-force\-delete
+If deleting a system user, and the default \fBSYS_DELETE_ACTION\fP
+would lock the account, instead delete it normally.
+.TP
 .B \-\-version
 Display version and copyright information.
 


=====================================
doc/deluser.conf.5
=====================================
@@ -78,6 +78,12 @@ Values may be 0 or 1. Defaults to \fI0\fP.
 .B REMOVE_HOME
 Removes the home directory and mail spool of the user to be removed.
 Value may be 0 (don't delete) or 1 (do delete). Defaults to \fI0\fP.
+.TP
+.B SYS_DELETE_ACTION
+The default action to take when deleting system accounts.  Valid options
+are "lock" and "delete" (the default).  If set to "lock" and not
+overridden by \fB\-\-force\-delete\fP, behave as if \fB\-\-lock\fP were
+passed and lock the account.  See \fBdeluser(8)\fP for specific details.
 
 .SH FILES
 .I /etc/deluser.conf


=====================================
notes.100808x.md
=====================================
@@ -0,0 +1,28 @@
+#1008082: deluser --system(?) --lock
+
+- leaves the account intact but makes login impossible (by setting an invalid
+password, leaving existing password recoverable, and setting shell to
+/usr/sbin/nologin)
+- only system accounts?
+- adding state (/var/lib/adduser)?
+    - or set to nologin, reset shell to default and WARN on unlock
+
+#1008083: deluser --system
+
+- /etc/deluser.conf: DELUSER_SYS_ACTION = (lock*|delete)  *default
+- delgroup --system honors the above (lock == NOOP)
+
+- basically: extends --lock to /etc/deluser.conf for system users
+
+#1008084: adduser --system behavior if trying to create existing locked account
+  - if system account already exists, just unlock and set shell
+  - addgroup --system silently ignore existing also (?)
+
+#?: --homeless
+  - couldn't find a bug for this but saw it mentioned; is this something we
+    still want to do?
+
+blocked above:
+#1006912: is it time to have account deletion in policy?
+- anything relevant policy-wise?  (i have not read the whole thread,
+  or any list discussion)


=====================================
testsuite/deluser-delete.conf
=====================================
@@ -0,0 +1,47 @@
+# /etc/deluser.conf: `deluser' configuration.
+# See deluser(8) and deluser.conf(5) for full documentation.
+
+# A commented out setting indicates that this is the default in the
+# code. If you need to change those settings, remove the comment and
+# make your intended change.
+
+# Remove home directory and mail spool when user is removed
+# Default: REMOVE_HOME = 0
+#REMOVE_HOME = 0
+
+# Remove all files on the system owned by the user to be removed
+# Default: REMOVE_ALL_FILES = 0
+#REMOVE_ALL_FILES = 0
+
+# Backup files before removing them. This options has only an effect if
+# REMOVE_HOME or REMOVE_ALL_FILES is set.
+# Default: BACKUP = 0
+#BACKUP = 0
+
+# Target directory for the backup file
+# Default: BACKUP_TO = "."
+#BACKUP_TO = "."
+
+# Select compression (from tar --auto-compress) for backups
+# Default: BACKUP_SUFFIX = .gz
+#BACKUP_SUFFIX = .gz
+
+# Space-Separated list of regular expressions. Do not delete files
+# matching any of these.
+# Default: NO_DEL_PATHS="^/bin\$ ^/boot\$ ^/dev\$ ^/etc\$ ^/initrd ^/lib ^/lost+found\$ ^/media\$ ^/mnt\$ ^/opt\$ ^/proc\$ ^/root\$ ^/run\$ ^/sbin\$ ^/srv\$ ^/sys\$ ^/tmp\$ ^/usr\$ ^/var\$ ^/vmlinu"
+#NO_DEL_PATHS="^/bin\$ ^/boot\$ ^/dev\$ ^/etc\$ ^/initrd ^/lib ^/lost+found\$ ^/media\$ ^/mnt\$ ^/opt\$ ^/proc\$ ^/root\$ ^/run\$ ^/sbin\$ ^/srv\$ ^/sys\$ ^/tmp\$ ^/usr\$ ^/var\$ ^/vmlinu"
+
+# Only delete a group if there are no users belonging to this group.
+# Default: ONLY_IF_EMPTY = 0
+#ONLY_IF_EMPTY = 0
+
+# Single regular expression which describes filesystems types which should
+# be excluded when looking for files of a user to be deleted.
+# Default: EXCLUDE_FSTYPES = "(proc|sysfs|usbfs|devpts|tmpfs|afs)"
+#EXCLUDE_FSTYPES = "(proc|sysfs|usbfs|devpts|tmpfs|afs)"
+
+# What action to take when deleting a system account
+# Options: lock|delete
+# Default: SYS_DELETE_ACTION=delete
+#SYS_DELETE_ACTION=delete
+SYS_DELETE_ACTION=lock


=====================================
testsuite/lib_test.pm
=====================================
@@ -410,6 +410,94 @@ sub check_user_status {
 
 
 
+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/test10.pl
=====================================
@@ -0,0 +1,447 @@
+#!/usr/bin/perl -w
+
+# there is a deluser.conf in the same directory as the tests
+# that has SYS_DELETE_ACTION=lock so that we can choose the behavior
+
+use strict;
+
+use lib_test;
+
+my $error;
+my $output;
+
+my $cmd;
+my $username;
+my $susername;
+my $num;
+
+$username = find_unused_name();
+$num = 0;
+
+assert(check_user_not_exist ($username));
+# unlock a non-existing account
+$cmd = "adduser --unlock $username";
+++$num && print "Testing failing (10.$num) $cmd... ";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+if (!$error) {
+    print "failed\n  $cmd returned errorcode ($error)\n  $output\n";
+    exit 1;
+}
+print "ok.\n";
+assert(check_user_not_exist ($username));
+
+# add the account
+$cmd = "adduser --no-create-home --comment '' --disabled-password $username";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+    print "failed\n  $cmd returned errorcode ($error)\n  $output\n";
+    exit 1;
+}
+print "ok.\n";
+assert(check_user_exist ($username));
+assert(check_user_status ($username, "locked", 1));
+assert(check_user_status ($username, "not_haspasswd", 1));
+assert(check_user_status ($username, "not_nologin", 1));
+assert(check_user_status ($username, "not_expired", 1));
+
+# unlock the account
+$cmd = "adduser --unlock $username";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+    print "failed\n  $cmd returned errorcode ($error)\n  $output\n";
+    exit 1;
+}
+print "ok.\n";
+assert(check_user_exist ($username));
+assert(check_user_status ($username, "locked", 1));
+assert(check_user_status ($username, "not_haspasswd", 1));
+assert(check_user_status ($username, "not_nologin", 1));
+assert(check_user_status ($username, "not_expired", 1));
+
+# lock the account
+$cmd = "deluser --lock $username";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+    print "failed\n  $cmd returned errorcode ($error)\n  $output\n";
+    exit 1;
+}
+print "ok.\n";
+assert(check_user_exist ($username));
+assert(check_user_status ($username, "locked", 1));
+assert(check_user_status ($username, "not_haspasswd", 1));
+assert(check_user_status ($username, "nologin", 1));
+assert(check_user_status ($username, "expired", 1));
+
+# unlock the account
+$cmd = "adduser --unlock $username";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+    print "failed\n  $cmd returned errorcode ($error)\n  $output\n";
+    exit 1;
+}
+print "ok.\n";
+assert(check_user_exist ($username));
+assert(check_user_status ($username, "locked", 1));
+assert(check_user_status ($username, "not_haspasswd", 1));
+assert(check_user_status ($username, "not_nologin", 1));
+assert(check_user_status ($username, "not_expired", 1));
+
+# set a password
+$cmd = "usermod --password \$y\$j9T\$6KrIYfSdT/O2rBLrkyzcF/\$pMxfrOqQgNn/jlZZVjSs1ELUZjpFRyjZ5ahXKZ84115 $username";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+    print "failed\n  $cmd returned errorcode ($error)\n  $output\n";
+    exit 1;
+}
+print "ok.\n";
+assert(check_user_exist ($username));
+assert(check_user_status ($username, "not_locked", 1));
+assert(check_user_status ($username, "haspasswd", 1));
+assert(check_user_status ($username, "not_nologin", 1));
+assert(check_user_status ($username, "not_expired", 1));
+
+# unlock the account
+$cmd = "adduser --unlock $username";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+    print "failed\n  $cmd returned errorcode ($error)\n  $output\n";
+    exit 1;
+}
+print "ok.\n";
+assert(check_user_exist ($username));
+assert(check_user_status ($username, "not_locked", 1));
+assert(check_user_status ($username, "haspasswd", 1));
+assert(check_user_status ($username, "not_nologin", 1));
+assert(check_user_status ($username, "not_expired", 1));
+
+# lock the account
+$cmd = "deluser --lock $username";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+    print "failed\n  $cmd returned errorcode ($error)\n  $output\n";
+    exit 1;
+}
+print "ok.\n";
+assert(check_user_exist ($username));
+assert(check_user_status ($username, "locked", 1));
+assert(check_user_status ($username, "haspasswd", 1));
+assert(check_user_status ($username, "nologin", 1));
+assert(check_user_status ($username, "expired", 1));
+
+# add the account (should fail)
+$cmd = "adduser --no-create-home --comment '' --disabled-password $username";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing failing (10.$num) $cmd... ";
+if (!$error) {
+    print "failed\n  $cmd returned errorcode ($error)\n  $output\n";
+    exit 1;
+}
+print "ok.\n";
+assert(check_user_exist ($username));
+assert(check_user_status ($username, "locked", 1));
+assert(check_user_status ($username, "haspasswd", 1));
+assert(check_user_status ($username, "nologin", 1));
+assert(check_user_status ($username, "expired", 1));
+
+# unlock the account
+$cmd = "adduser --unlock $username";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+    print "failed\n  $cmd returned errorcode ($error)\n  $output\n";
+    exit 1;
+}
+print "ok.\n";
+assert(check_user_exist ($username));
+assert(check_user_status ($username, "not_locked", 1));
+assert(check_user_status ($username, "haspasswd", 1));
+assert(check_user_status ($username, "not_nologin", 1));
+assert(check_user_status ($username, "not_expired", 1));
+
+# deluser with SYS_DELETE_ACTION=lock
+$cmd = "deluser --conf ./deluser-delete.conf $username";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+    print "failed\n  $cmd returned errorcode ($error)\n  $output\n";
+    exit 1;
+}
+print "ok.\n";
+assert(check_user_exist ($username));
+assert(check_user_status ($username, "locked", 1));
+assert(check_user_status ($username, "haspasswd", 1));
+assert(check_user_status ($username, "nologin", 1));
+assert(check_user_status ($username, "expired", 1));
+
+# unlock the account
+$cmd = "adduser --unlock $username";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+    print "failed\n  $cmd returned errorcode ($error)\n  $output\n";
+    exit 1;
+}
+print "ok.\n";
+assert(check_user_exist ($username));
+assert(check_user_status ($username, "not_locked", 1));
+assert(check_user_status ($username, "haspasswd", 1));
+assert(check_user_status ($username, "not_nologin", 1));
+assert(check_user_status ($username, "not_expired", 1));
+
+# regular deluser
+$cmd = "deluser $username";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+    print "failed\n  $cmd returned errorcode ($error)\n  $output\n";
+    exit 1;
+}
+print "ok.\n";
+assert(check_user_not_exist ($username));
+
+
+
+#=======================
+# system user
+$susername = find_unused_name();
+
+assert(check_user_not_exist ($susername));
+# unlock a non-existing system ccount
+$cmd = "adduser --unlock --system $susername";
+++$num && print "Testing failing (10.$num) $cmd... ";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+if (!$error) {
+    print "failed\n  $cmd returned errorcode ($error)\n  $output\n";
+    exit 1;
+}
+assert(check_user_not_exist ($susername));
+print "ok\n";
+
+# add system account
+$cmd = "adduser --system $susername";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+    print "failed\n  $cmd returned errorcode ($error)\n  $output\n";
+    exit 1;
+}
+print "ok\n";
+assert(check_user_exist ($susername));
+assert(check_user_status ($susername, "locked", 1));
+assert(check_user_status ($susername, "not_haspasswd", 1));
+assert(check_user_status ($susername, "nologin", 1));
+assert(check_user_status ($susername, "not_expired", 1));
+
+
+# unlock the account
+$cmd = "adduser --system --unlock $susername";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing failing (10.$num) $cmd... ";
+if ($error) {
+    print "failed\n  $cmd returned errorcode ($error)\n  $output\n";
+    exit 1;
+}
+print "ok\n";
+assert(check_user_exist ($susername));
+assert(check_user_status ($susername, "locked", 1));
+assert(check_user_status ($susername, "not_haspasswd", 1));
+assert(check_user_status ($susername, "nologin", 1));
+assert(check_user_status ($susername, "not_expired", 1));
+
+# lock the account
+$cmd = "deluser --system --lock $susername";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+    print "failed\n  $cmd returned errorcode ($error)\n  $output\n";
+    exit 1;
+}
+print "ok\n";
+assert(check_user_exist ($susername));
+assert(check_user_status ($susername, "locked", 1));
+assert(check_user_status ($susername, "not_haspasswd", 1));
+assert(check_user_status ($susername, "nologin", 1));
+assert(check_user_status ($susername, "expired", 1));
+
+# unlock the account
+$cmd = "adduser --system --unlock $susername";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+    print "failed\n  $cmd returned errorcode ($error)\n  $output\n";
+    exit 1;
+}
+print "ok\n";
+assert(check_user_exist ($susername));
+assert(check_user_status ($susername, "locked", 1));
+assert(check_user_status ($susername, "not_haspasswd", 1));
+assert(check_user_status ($susername, "nologin", 1));
+assert(check_user_status ($susername, "not_expired", 1));
+
+# set a password
+$cmd = "usermod --password \$y\$j9T\$6KrIYfSdT/O2rBLrkyzcF/\$pMxfrOqQgNn/jlZZVjSs1ELUZjpFRyjZ5ahXKZ84115 $susername";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+    print "failed\n  $cmd returned errorcode ($error)\n  $output\n";
+    exit 1;
+}
+print "ok\n";
+assert(check_user_exist ($susername));
+assert(check_user_status ($susername, "not_locked", 1));
+assert(check_user_status ($susername, "haspasswd", 1));
+assert(check_user_status ($susername, "nologin", 1));
+assert(check_user_status ($susername, "not_expired", 1));
+
+# unlock the account
+$cmd = "adduser --system --unlock $susername";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+    print "failed\n  $cmd returned errorcode ($error)\n  $output\n";
+    exit 1;
+}
+print "ok\n";
+assert(check_user_exist ($susername));
+assert(check_user_status ($susername, "not_locked", 1));
+assert(check_user_status ($susername, "haspasswd", 1));
+assert(check_user_status ($susername, "nologin", 1));
+assert(check_user_status ($susername, "not_expired", 1));
+
+# lock the account
+$cmd = "deluser --system --lock $susername";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+    print "failed\n  $cmd returned errorcode ($error)\n  $output\n";
+    exit 1;
+}
+print "ok\n";
+assert(check_user_exist ($susername));
+assert(check_user_status ($susername, "locked", 1));
+assert(check_user_status ($susername, "haspasswd", 1));
+assert(check_user_status ($susername, "nologin", 1));
+assert(check_user_status ($susername, "expired", 1));
+
+# re-enable via add (fails, already exists and has a password)
+$cmd = "adduser --system $susername";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if (!$error) {
+    print "failed\n  $cmd returned errorcode ($error)\n  $output\n";
+    exit 1;
+}
+print "ok\n";
+assert(check_user_exist ($susername));
+assert(check_user_status ($susername, "locked", 1));
+assert(check_user_status ($susername, "haspasswd", 1));
+assert(check_user_status ($susername, "nologin", 1));
+assert(check_user_status ($susername, "expired", 1));
+
+# unlock the account
+$cmd = "adduser --system --unlock $susername";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+    print "failed\n  $cmd returned errorcode ($error)\n  $output\n";
+    exit 1;
+}
+print "ok\n";
+assert(check_user_exist ($susername));
+assert(check_user_status ($susername, "not_locked", 1));
+assert(check_user_status ($susername, "haspasswd", 1));
+assert(check_user_status ($susername, "nologin", 1));
+assert(check_user_status ($susername, "not_expired", 1));
+
+# unlock already-unlocked
+$cmd = "adduser --unlock $susername";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+    print "failed\n  $cmd returned errorcode ($error)\n  $output\n";
+    exit 1;
+}
+print "ok\n";
+assert(check_user_exist ($susername));
+assert(check_user_status ($susername, "not_locked", 1));
+assert(check_user_status ($susername, "haspasswd", 1));
+assert(check_user_status ($susername, "nologin", 1));
+assert(check_user_status ($susername, "not_expired", 1));
+
+# deluser with SYS_DELETE_ACTION=lock
+$cmd = "deluser --conf ./deluser-delete.conf --system $susername";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+    print "failed\n  $cmd returned errorcode ($error)\n  $output\n";
+    exit 1;
+}
+assert(check_user_exist ($susername));
+print "ok\n";
+assert(check_user_exist ($susername));
+assert(check_user_status ($susername, "locked", 1));
+assert(check_user_status ($susername, "haspasswd", 1));
+assert(check_user_status ($susername, "nologin", 1));
+assert(check_user_status ($susername, "expired", 1));
+
+# unlock the account
+$cmd = "adduser --system --unlock $susername";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+    print "failed\n  $cmd returned errorcode ($error)\n  $output\n";
+    exit 1;
+}
+print "ok\n";
+assert(check_user_exist ($susername));
+assert(check_user_status ($susername, "not_locked", 1));
+assert(check_user_status ($susername, "haspasswd", 1));
+assert(check_user_status ($susername, "nologin", 1));
+assert(check_user_status ($susername, "not_expired", 1));
+
+# regular deluser
+$cmd = "deluser --system $susername";
+$output=`$cmd 2>&1`;
+$error = ($?>>8);
+++$num && print "Testing (10.$num) $cmd... ";
+if ($error) {
+    print "failed\n  $cmd returned errorcode ($error)\n  $output\n";
+    exit 1;
+}
+print "ok\n";
+assert(check_user_not_exist ($susername));
+
+# vim: tabstop=4 shiftwidth=4 expandtab



View it on GitLab: https://salsa.debian.org/debian/adduser/-/compare/67c314d0a72efeb4bbc748f1fcd3f2f174aed853...5937f07b1cb4a3267fffe051c647c3e218a9fc2b

-- 
View it on GitLab: https://salsa.debian.org/debian/adduser/-/compare/67c314d0a72efeb4bbc748f1fcd3f2f174aed853...5937f07b1cb4a3267fffe051c647c3e218a9fc2b
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/20260119/d09a0594/attachment-0001.htm>


More information about the Pkg-shadow-devel mailing list