package Debian::AdduserCommon 3.139; use 5.36.0; use utf8; # Subroutines shared by the "adduser" and "deluser" utilities. # # Copyright (C) 2000-2004 Roland Bauerschmidt # 2004-2025 Marc Haber # 2005-2009 Joerg Hoh # 2006-2008 Stephen Gran # 2016 Nis Martensen # 2016 Afif Elghraoui # 2021-2022 Jason Franklin # 2022 Matt Barry # 2023 Guillem Jover # # Someo of the subroutines here are adopted from Debian's # original "adduser" program. # # Copyright (C) 1997-1999 Guy Maor # # Copyright (C) 1995 Ted Hajek # Ian A. Murdock # # License: GPL-2+ use parent qw(Exporter); use Fcntl qw(:flock SEEK_END); my $codeset; use Debian::AdduserLogging 3.139; use Debian::AdduserRetvalues 3.139; BEGIN { if ( Debian::AdduserLogging->VERSION != version->declare('3.139') || Debian::AdduserRetvalues->VERSION != version->declare('3.139') ) { die "wrong module version in adduser, check your packaging or path"; } local $ENV{PERL_DL_NONLAZY}=1; eval { require Encode; Encode->import(qw(encode decode)); }; if ($@) { *encode = sub { return $_[1]; }; *decode = sub { return $_[1]; }; } $codeset="US-ASCII"; eval { require I18N::Langinfo; I18N::Langinfo->import(qw(langinfo CODESET YESEXPR NOEXPR)); $codeset = I18N::Langinfo->CODESET; }; if ($@) { *langinfo = sub { return shift; }; *YESEXPR = sub { "^[yY]" }; *NOEXPR = sub { "^[nN]" }; } } use vars qw(@EXPORT $VAR1); my $charset = langinfo($codeset); BEGIN { local $ENV{PERL_DL_NONLAZY}=1; eval { require Locale::gettext; Locale::gettext->import(qw(gettext textdomain LC_MESSAGES)); }; if ($@) { *gettext = sub { shift }; *textdomain = sub { "" }; *LC_MESSAGES = sub { 5 }; } else { Locale::gettext::textdomain("adduser"); } } my $lockfile; my $lockfile_path = '/run/adduser'; use constant { filenamere => qr/[-_\.+!\$%&()\]\[;0-9a-zA-Z]+/, simplefilenamere => qr/[-_\.0-9a-zA-Z]+/, pathre => qr/[- \p{Graph}_\.+!\$%&()\]\[;0-9a-zA-Z\/{}>*'@]+/, simplepathre => qr/[-_\$\.0-9a-zA-Z\/]+/, commentre => qr/[-"_\.+!\$%&()\]\[;\/'’ A-Za-z0-9ß\x{a1}-\x{ac}\x{ae}-\x{ff}\p{L}\p{Nd}\p{Zs}]*/, numberre => qr/[0-9]+/, namere => qr/^([^-+~:,\s\/][^:,\s\/]*)$/aa, anynamere => qr/^([^-+~:,\s\/][^:,\s\/]*)$/aa, def_name_regex => qr/^[a-zA-Z][a-zA-Z0-9_-]*\$?$/aa, def_sys_name_regex => qr/^[a-zA-Z_][a-zA-Z0-9_-]*\$?$/aa, def_ieee_name_regex => qr/^[a-zA-Z0-9_.][a-zA-Z0-9_.-]*\$?$/aa, def_min_regex => qr(^[^-+~:,\s/][^:,\s/]*$)aa, }; @EXPORT = ( 'get_group_members', 'read_config', 'read_pool', 'systemcall_useradd', 'systemcall', 'systemcall_or_warn', 'systemcall_silent', 'systemcall_silent_error', 'acquire_lock', 'release_lock', 'sanitize_string', 'egetgrnam', 'egetpwnam', 'preseed_config', 'which', "filenamere", "simplefilenamere", "pathre", "simplepathre", "commentre", "numberre", 'namere', 'anynamere', 'def_name_regex', 'def_sys_name_regex', 'def_ieee_name_regex', 'def_min_regex', ); sub sanitize_string { my ($input, $pattern) = @_; # Set a default pattern to allow alphanumeric characters, # spaces, and underscores. $pattern //= qr/[a-zA-Z0-9 _]*/; log_trace("sanitize_string %s against %s", $input, $pattern); # If the input matches the pattern, # extract and return the untainted value. if ($input =~ qr/^($pattern)$/ ) { log_trace("sanitize_string returning %s", "$1"); return $1; # $1 is the captured, untainted portion of the string. } else { #die "invalid characters in $input"; # this sometimes hangs the perl interpreter, see #1104726 die "invalid characters in input string, see trace output for more details"; } } sub egetgrnam { my ($name) = @_; log_trace("egetgrnam called with %s", $name); $name = encode($charset, $name); return getgrnam($name); } sub egetpwnam { my ($name) = @_; log_trace("egetpwnam called with %s", $name); $name = encode($charset, $name); return getpwnam($name); } # parse the configuration file # parameters: # -- filename of the configuration file # -- a hash for the configuration data sub read_config { my ($conf_file, $configref) = @_; my ($var, $lcvar, $val); $conf_file = sanitize_string( $conf_file, simplepathre ); if (! -f $conf_file) { log_warn( mtx("`%s' does not exist. Using defaults."), $conf_file ); return; } my $conffh; unless( open ($conffh, q{<}, $conf_file) ) { log_fatal( mtx("cannot open configuration file %s: `%s'\n"), $conf_file, $! ); exit( RET_CONFFILE ); } while (<$conffh>) { chomp; $_ = decode($charset, $_); next if /^#/ || /^\s*$/; log_trace("read from config file: %s", $_); if ((($var, $val) = m/^\s*([_a-zA-Z0-9]+)\s*=\s*([-a-zA-Z0-9_\/\.^\$\]\[*?+\|@\\^":\)\(~,\s]*)/) != 2) { log_warn( mtx("Couldn't parse `%s', line %d."), $conf_file, $. ); next; } $lcvar = lc $var; if (!exists($configref->{$lcvar})) { log_warn( mtx("Unknown variable `%s' at `%s', line %d."), $var, $conf_file, $. ); next; } log_trace("lcvar, val: %s, %s", $lcvar, $val); if( $lcvar =~ /^(first|last).*_[ug]id$/ ) { $val = sanitize_string( $val, qr/[0-9]*/ ); } $val =~ s/^"(.*)"$/$1/; $val =~ s/^'(.*)'$/$1/; log_debug("importing config value for %s: %s", $lcvar, $val); $configref->{$lcvar} = $val; } close $conffh || die "$!"; } # read names and IDs from a pool file # parameters: # -- filename of the pool file, or directory containing files # -- a hash for the pool data sub read_pool { my ($pool_file, $type, $poolref) = @_; my ($name, $id, $comment, $home, $shell); my %ids = (); my %new; $pool_file = decode($charset, $pool_file); if (-d $pool_file) { my $dir; unless( opendir( $dir, $pool_file) ) { log_fatal( mtx("Cannot read directory `%s'"), $pool_file ); exit( RET_POOLFILE ); } my @files = readdir ($dir); closedir ($dir); foreach (sort @files) { next if (/^\./); next if (!/\.conf$/); my $file = "$pool_file/$_"; next if (! -f $file); read_pool ($file, $type, $poolref); } return; } if (! -f $pool_file) { log_warn( mtx("`%s' does not exist."), $pool_file ); return; } my $pool; unless( open( $pool, q{<}, $pool_file) ) { log_fatal( mtx("Cannot open pool file %s: `%s'"), $pool_file, $!); exit( RET_POOLFILE ); } while (<$pool>) { chomp; $_ = decode($charset, $_); next if /^#/ || /^\s*$/; my $new; if ($type eq "uid") { ($name, $id, $comment, $home, $shell) = split (/:/); if (!$name || $name !~ /^([_a-zA-Z0-9-]+)$/ || !defined($id) || $id !~ /^(\d+)$/) { log_warn( mtx("Couldn't parse `%s', line %d."), $pool_file, $.); next; } if( defined $name ) { $name = sanitize_string($name, namere); } if( defined $id ) { $id = sanitize_string($id, numberre); } if( defined $comment ) { $comment = sanitize_string($comment, commentre); } if( defined $home ) { $home = sanitize_string($home, simplepathre); } if( defined $shell ) { $shell = sanitize_string($shell, simplepathre); } $new = { 'id' => $id, 'comment' => $comment, 'home' => $home, 'shell' => $shell }; } elsif ($type eq "gid") { ($name, $id) = split (/:/); if (!$name || $name !~ /^([_a-zA-Z0-9-]+)$/ || !defined($id) || $id !~ /^(\d+)$/) { log_warn( mtx("Couldn't parse `%s', line %d."), $pool_file, $. ); next; } if( defined $name ) { $name = sanitize_string($name, namere); } if( defined $id ) { $id = sanitize_string($id, numberre); } $new = { 'id' => $id, }; } else { log_fatal( mtx("Illegal pool type `%s' reading `%s'."), $type, $pool_file ); exit( RET_POOLFILE_FORMAT ); } if (defined($poolref->{$name})) { log_fatal( mtx("Duplicate name `%s' at `%s', line %d."), $name, $pool_file, $. ); exit( RET_POOLFILE_FORMAT ); } if (defined($ids{$id})) { log_fatal( mtx("Duplicate ID `%s' at `%s', line %d."), $id, $pool_file, $. ); exit( RET_POOLFILE_FORMAT ); } $poolref->{$name} = $new; } close $pool || die "$!"; } sub get_group_members { my $group = shift; my @members; foreach my $member (split(/ /, (egetgrnam($group))[3])) { push(@members, $member) if defined(egetpwnam($member)); } return @members; } sub systemcall_useradd { my $name_check_level = shift; my $command = join(' ', @_); my $ret; log_debug( "executing systemcall_useradd (%s): %s", $name_check_level, $command ); $ret = system(@_); if ($ret != 0) { my $exitcode = $ret>>8; if ($exitcode != 0) { if ($exitcode == 19 ) { # we should never get here. It is our expectation that we catch # an invalid user name before useradd gets to reject it. So # we consider getting here a bug in our regexps. We can safely # bomb out here. if( $name_check_level == 2 ) { log_warn( mtx("`%s' refused the given user name, but --allow-all-names is given. Continueing."), $command ); return( RET_INVALID_NAME_FROM_USERADD ); } else { log_err( mtx("`%s' refused the given user name. This is a bug in adduser. Please file a bug report."), $command ); exit( RET_INVALID_NAME_FROM_USERADD ); }; } else { log_fatal( mtx("`%s' returned error code %d. Exiting."), $command, $exitcode ); exit( RET_SYSTEMCALL_ERROR ); } } log_fatal( mtx("`%s' exited from signal %d. Exiting."), $command, $ret&127 ); exit( RET_SYSTEMCALL_SIGNAL ); } } sub systemcall { my $command = join(' ', @_); log_debug( "executing systemcall: %s", $command ); if (system(@_)) { if ($?>>8) { log_fatal( mtx("`%s' returned error code %d. Exiting."), $command, $?>>8 ); exit( RET_SYSTEMCALL_ERROR ); } log_fatal( mtx("`%s' exited from signal %d. Exiting."), $command, $?&127 ); exit( RET_SYSTEMCALL_SIGNAL ); } } sub systemcall_or_warn { my $command = join(' ', @_); log_debug( "executing systemcall_or_warn: %s", $command ); system(@_); my $ret = $?; if ($ret == -1) { log_warn( mtx("`%s' failed to execute. %s. Continuing."), $command, $! ); } elsif ($? & 127) { log_warn( mtx("`%s' killed by signal %d. Continuing."), $command, ($ret & 127) ); } elsif ($? >> 8) { log_warn( mtx("`%s' failed with status %d. Continuing."), $command, ($ret >> 8) ); } return $ret; } sub systemcall_silent { my $command = join(' ', @_); log_debug( "executing systemcall_silent: %s", $command ); my $pid = fork(); if( !defined($pid) ) { return -1; } if ($pid) { wait; return $?; } open(STDOUT, '>>', '/dev/null'); open(STDERR, '>>', '/dev/null'); # TODO: report exec() failure to parent exec(@_) or exit(1); } sub systemcall_silent_error { my $command = join(' ', @_); log_debug( "executing systemcall_silent_error: %s", $command ); my $output = `$command >/dev/null 2>&1`; return $?; } sub which { my ($progname, $nonfatal) = @_ ; for my $dir (split /:/, $ENV{"PATH"}) { if (-x "$dir/$progname" ) { return sanitize_string( "$dir/$progname", simplepathre ); } } unless( $nonfatal ) { log_fatal( mtx("Could not find program named `%s' in \$PATH."), $progname ); exit( RET_EXEC_NOT_FOUND ); } return 0; } # preseed the configuration variables # then read the config file /etc/adduser and overwrite the data hardcoded here # we cannot give defaults for users_gid and users_group here since this will # probably lead to double defined users_gid and users_group. sub preseed_config { my ($conflistref, $configref) = @_; my %config_defaults = ( system => 0, only_if_empty => 0, remove_home => 0, home => "", remove_all_files => 0, backup => 0, backup_to => ".", dshell => "/bin/bash", first_system_uid => 100, last_system_uid => 999, first_uid => 1000, last_uid => 59999, first_system_gid => 100, last_system_gid => 999, first_gid => 1000, last_gid => 59999, dhome => "/home", skel => "/etc/skel", usergroups => "yes", users_gid => undef, users_group => undef, grouphomes => "no", letterhomes => "no", quotauser => "", dir_mode => "0700", sys_dir_mode => "0755", setgid_home => "no", 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, exclude_fstypes => "(proc|sysfs|usbfs|devpts|devtmpfs|devfs|afs)", skel_ignore_regex => "\.(dpkg|ucf)-(old|new|dist)\$", extra_groups => "users", add_extra_groups => 0, uid_pool => "", gid_pool => "", reserve_uid_pool => "yes", reserve_gid_pool => "yes", loggerparms => "", stdoutmsglevel => "warn", stderrmsglevel => "warn", logmsglevel => "info", ); # Initialize to the set of known variables. foreach (keys %config_defaults) { log_debug("importing default value for %s: %s", $_, $config_defaults{$_}); $configref->{$_} = $config_defaults{$_}; } # Read the configuration files foreach( @$conflistref ) { my $configfile = sanitize_string($_, simplepathre); log_debug( "read configuration file %s\n", $configfile ); read_config($configfile ,$configref); } } sub acquire_lock { my @notify_secs = (1, 3, 8, 18, 28); my ($wait_secs, $timeout_secs) = (0, 30); unless( open($lockfile, '>>', $lockfile_path) ) { log_fatal( mtx("could not open lock file %s!"), $lockfile_path ); exit( RET_LOCKFILE ); } while (!flock($lockfile, LOCK_EX | LOCK_NB)) { if ($wait_secs == $timeout_secs) { log_fatal( mtx("Could not obtain exclusive lock, please try again shortly!") ); exit( RET_LOCKFILE ); } elsif (grep @notify_secs, $wait_secs) { log_warn( mtx("Waiting for lock to become available...") ); } sleep 1; $wait_secs++; } unless( seek($lockfile, 0, SEEK_END) ) { log_fatal( mtx("could not seek - %s!"), $lockfile_path ); exit( RET_LOCKFILE ); } } sub release_lock { my $nonfatal = shift || 0; return if ($nonfatal && !$lockfile); unless( $lockfile ) { log_fatal( mtx("could not find lock file!") ); exit( RET_LOCKFILE ); } if( defined(fileno($lockfile)) ) { unless( flock($lockfile, LOCK_UN) or $nonfatal ) { log_fatal( mtx("could not unlock file %s: %s"), $lockfile_path, $! ); exit( RET_LOCKFILE ); } } unless( close($lockfile) or $nonfatal ) { log_fatal( mtx("could not close lock file %s: %s"), $lockfile_path, $! ); exit( RET_LOCKFILE ); } } END { release_lock(1); } 1; # Local Variables: # mode:cperl # End: # vim: tabstop=4 shiftwidth=4 expandtab