#! /usr/bin/perl -T # vim: set filetype=perl ts=4 sw=4 sts=4 et: # NEEDGROUP osh-accountDelete # SUDOERS %osh-accountDelete ALL=(root) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountDelete * # FILEMODE 0700 # FILEOWN 0 0 #>HEADER use common::sense; use Getopt::Long; use File::Copy qw(move); use File::Basename; use lib dirname(__FILE__) . '/../../lib/perl'; use OVH::Bastion; local $| = 1; # # Globals # $SIG{'HUP'} = 'IGNORE'; # continue even when attached terminal is closed (we're called with setsid on supported systems anyway) $SIG{'PIPE'} = 'IGNORE'; # continue even if osh_info gets a SIGPIPE because there's no longer a terminal $ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/pkg/bin'; my ($self) = $ENV{'SUDO_USER'} =~ m{^([a-zA-Z0-9._-]+)$}; if (not defined $self) { if ($< == 0) { $self = 'root'; } else { HEXIT('ERR_SUDO_NEEDED', msg => 'This command must be run under sudo'); } } # Fetch command options my $fnret; my ($result, @optwarns); my ($account, $type); eval { local $SIG{__WARN__} = sub { push @optwarns, shift }; $result = GetOptions( "account=s" => sub { $account //= $_[1] }, "type=s" => sub { $type //= $_[1] }, ); }; if ($@) { die $@ } if (!$result) { local $" = ", "; HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns"); } if (!$account || !$type) { HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'account' or 'type'"); } #
RIGHTSCHECK if ($self eq 'root') { osh_debug "Real root, skipping checks of permissions"; } else { # need to perform another security check $fnret = OVH::Bastion::is_user_in_group(user => $self, group => "osh-accountDelete"); if (!$fnret) { HEXIT('ERR_SECURITY_VIOLATION', msg => "You're not allowed to run this, dear $self"); } } #PARAMS:TYPE osh_debug("Checking type"); if (not grep { $type eq $_ } qw{ normal realm }) { HEXIT('ERR_INVALID_PARAMETER', "Expected type 'normal' or 'realm'"); } #PARAMS:ACCOUNT osh_debug("Checking account"); $fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account, accountType => $type); $fnret or HEXIT($fnret); # get returned untainted value $account = $fnret->value->{'account'}; #>CODE # don't allow a non-admin deleting an admin if (OVH::Bastion::is_admin(account => $account, sudo => 1) && !OVH::Bastion::is_admin(account => $self, sudo => 1)) { HEXIT('ERR_SECURITY_VIOLATION', msg => "You can't delete an admin without being admin yourself"); } # Try to find the user -tty group my $ttygroup = "$account-tty"; if (!getgrnam($ttygroup)) { $ttygroup = substr($account, 0, 5) . '-tty'; if (!getgrnam($ttygroup)) { osh_warn("Couldn't find out which is the tty group of this account, will still delete it anyway"); warn_syslog("Couldn't find out the name of the tty group of acount $account ($ttygroup doesn't exist), deleting anyway"); $ttygroup = undef; } } # last security check if ((getpwnam($account))[8] !~ m{/osh\.pl$}) { HEXIT('ERR_INVALID_ACCOUNT', msg => "Account $account doesn't seem to be a legit bastion account"); } # kill all user processes, if any # GNU and BSD compliant $fnret = OVH::Bastion::execute(cmd => ['ps', '-U', $account, '-o', 'pid'], noisy_stderr => 1); if ($fnret->err ne 'OK' || ref $fnret->value->{'stdout'} ne 'ARRAY') { ; # don't warn, we can get an return code of 1 just because there are no processes matching } else { # don't check kill return because it may fail if the process died since, # and it's not a big issue, we'll still delete the account # we have to untaint what `ps` gave us however my @pids; foreach my $pid (@{$fnret->value->{'stdout'}}) { push @pids, $1 if ($pid =~ m{(\d+)}); } kill 'KILL', @pids if @pids; } # do the stuff if (!-d "/home/oldkeeper") { mkdir "/home/oldkeeper"; } chown 0, 0, "/home/oldkeeper"; chmod 0700, "/home/oldkeeper"; if (!-d "/home/oldkeeper/accounts") { mkdir "/home/oldkeeper/accounts"; } chown 0, 0, "/home/oldkeeper/accounts"; chmod 0700, "/home/oldkeeper/accounts"; my $suffix = 'at-' . time() . '.by-' . $self; my $fulldir = "/home/oldkeeper/accounts/$account.$suffix"; if (-e $fulldir) { HEXIT('ERR_BACKUP_DIR_COLLISION', msg => "This shouldn't happen, $fulldir already exists!"); } mkdir $fulldir; chown 0, 0, $fulldir; chmod 0700, $fulldir; move("/home/$account", "$fulldir/$account-home"); move("/home/allowkeeper/$account", "$fulldir/allowkeeper"); # remove +a or tar won't be able to rm files, don't check if it succeeded if we're on a system without chattr $fnret = OVH::Bastion::execute(cmd => ['find', "$fulldir/$account-home", '-maxdepth', '1', '-name', "*.log", '-exec', 'chattr', '-a', '{}', ';']); # remove sudoers if it's there $fnret = OVH::Bastion::execute(cmd => [$OVH::Bastion::BASEPATH . '/bin/sudogen/generate-sudoers.sh', 'delete', 'account', $account], must_succeed => 1, noisy_stdout => 1); if (!$fnret) { warn_syslog("Error during account deletion of '$account', couldn't delete sudoers file: " . $fnret->msg); } # add a text file with all the groups the user was a member of $fnret = OVH::Bastion::get_user_groups(account => $account, extra => 1); if ($fnret) { if (open(my $txtfile, '>', "$fulldir/groups.txt")) { print $txtfile join("\n", @{$fnret->value}); close($txtfile); } else { osh_warn("Couldn't open the groups.txt file to save the group list of this account ($!)"); } } # now tar.gz the directory, this is important because inside we'll keep the # old UID of the user, and we don't want UID-orphaned on our filesystem, it's # not a problem to have those inside a tarfile however. my @tarcmd = qw{ tar czf }; push @tarcmd, $fulldir . '.tar.gz'; push @tarcmd, '--acls' if OVH::Bastion::has_acls(); push @tarcmd, '--one-file-system', '-p', '--remove-files', $fulldir; osh_info("Backing up home directory..."); $fnret = OVH::Bastion::execute(cmd => \@tarcmd, must_succeed => 1); if (!$fnret) { osh_warn("Couldn't tar the backup homedir of this account (" . $fnret->msg . "), proceeding anyway."); chmod 0000, $fulldir; } else { chmod 0000, $fulldir . '.tar.gz'; unlink($fulldir); } osh_info("Backup done"); osh_info "Removing '$account' group membership from 'keyreader' user"; $fnret = OVH::Bastion::sys_delmemberfromgroup(user => "keyreader", group => $account); $fnret or HEXIT($fnret); osh_info "Deleting system user '$account'..."; $fnret = OVH::Bastion::sys_userdel(user => $account); $fnret or HEXIT($fnret); # some systems don't delete the primary group with userdel (suse at least) $fnret = OVH::Bastion::execute(cmd => ['getent', 'group', $account]); if ($fnret && $fnret->value->{'sysret'} == 0) { osh_info "Deleting account main group '$account'..."; $fnret = OVH::Bastion::sys_groupdel(group => $account); $fnret or HEXIT($fnret); } if (defined $ttygroup) { osh_info "Deleting group $ttygroup..."; $fnret = OVH::Bastion::sys_groupdel(group => $ttygroup); $fnret or HEXIT($fnret); } OVH::Bastion::syslogFormatted( severity => 'info', type => 'account', fields => [['action', 'delete'], ['account', $account], ['tty_group', $ttygroup],] ); HEXIT('OK', value => {account => $account, ttygroup => $ttygroup, operation => 'deleted'});