#! /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 --type normal * # NEEDGROUP osh-realmDelete # SUDOERS %osh-realmDelete ALL=(root) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountDelete --type realm * # FILEMODE 0700 # FILEOWN 0 0 #>HEADER use common::sense; use Getopt::Long qw(:config no_auto_abbrev no_ignore_case); use File::Basename; use lib dirname(__FILE__) . '/../../lib/perl'; use OVH::Bastion; use OVH::Bastion::Helper; # 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"); } OVH::Bastion::Helper::check_spurious_args(); if (!$account || !$type) { HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'account' or 'type'"); } #
PARAMS:TYPE osh_debug("Checking type"); if (not grep { $type eq $_ } qw{ normal realm }) { HEXIT('ERR_INVALID_PARAMETER', "Expected type 'normal' or 'realm'"); } #RIGHTSCHECK if ($self eq 'root') { osh_debug "Real root, skipping checks of permissions"; } else { # need to perform another security check if ($type eq 'normal') { $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"); } } elsif ($type eq 'realm') { $fnret = OVH::Bastion::is_user_in_group(user => $self, group => "osh-realmDelete"); if (!$fnret) { HEXIT('ERR_SECURITY_VIOLATION', msg => "You're not allowed to run this, dear $self"); } } else { HEXIT('ERR_INTERNAL'); } } #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 account $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 osh_info("Backing up home directory..."); 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; # from now on, as we're starting to move/remove/delete things, errors will be non fatal # because we're trying to cleanup as much as we can, the idea is to log errors to the # syslog, and then report to the user (and in the final formatted log) that we # had issues and somebody might want to have a look my $nbErrors = 0; # File::Copy::move() sometimes craps itself when it gets -EXDEV from the OS, and doesn't # compensate for it, while /bin/mv does... $fnret = OVH::Bastion::execute_simple(cmd => ['mv', "/home/$account", "$fulldir/$account-home"], must_succeed => 1); if (!$fnret) { my $msg = substr($fnret->value->{'output'}, 0, 128); $msg =~ s=[^a-zA-Z0-9./_-]=_=g; warn_syslog("Error while backing up to-be-deleted '/home/$account' to '$fulldir/$account-home' ($msg)" . ", continuing anyway..."); $nbErrors++; } $fnret = OVH::Bastion::execute_simple( cmd => ['mv', "/home/allowkeeper/$account", "$fulldir/$account-allowkeeper"], must_succeed => 1 ); if (!$fnret) { my $msg = substr($fnret->value->{'output'}, 0, 128); $msg =~ s=[^a-zA-Z0-9./_-]=_=g; warn_syslog("Error while backing up to-be-deleted '/home/allowkeeper/$account' to " . "'$fulldir/$account-allowkeeper' ($msg), continuing anyway..."); $nbErrors++; } # 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); $nbErrors++; } # 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; $fnret = OVH::Bastion::execute(cmd => \@tarcmd, must_succeed => 1); if (!$fnret) { warn_syslog("Couldn't tar the backup homedir of this account (" . $fnret->msg . "), proceeding anyway."); my $i = 0; foreach (@{$fnret->value->{'stderr'} || []}) { warn_syslog("tar: $_"); if (++$i >= 10) { warn_syslog("more tar errors, suppressing"); last; } } $nbErrors++; } if (-e "$fulldir.tar.gz") { chmod 0000, "$fulldir.tar.gz"; } # if the folder still exists, tar failed in some way, warn the admins if (-d $fulldir) { chmod 0000, $fulldir; warn_syslog("While archiving the account '$account', $fulldir still exists, manual cleanup might be needed"); $nbErrors++; } osh_info("Backup done"); osh_info "Removing '$account' group membership from 'keyreader' user"; $fnret = OVH::Bastion::sys_delmemberfromgroup(user => "keyreader", group => $account); if (!$fnret) { warn_syslog("Couldn't delete '$account' group membership from 'keyreader' user ($fnret), proceeding anyway"); $nbErrors++; } osh_info "Deleting system user '$account'..."; $fnret = OVH::Bastion::sys_userdel(user => $account); if (!$fnret) { warn_syslog("Couldn't delete system user '$account' ($fnret), proceeding anyway"); $nbErrors++; } # some systems don't delete the primary group with userdel (suse at least) $fnret = OVH::Bastion::is_group_existing(group => $account); if ($fnret) { osh_info "Deleting account main group '$account'..."; $fnret = OVH::Bastion::sys_groupdel(group => $account); if (!$fnret) { warn_syslog("Couldn't delete account main group '$account' ($fnret), proceeding anyway"); $nbErrors++; } } if (defined $ttygroup) { osh_info "Deleting group $ttygroup..."; $fnret = OVH::Bastion::sys_groupdel(group => $ttygroup); if (!$fnret) { warn_syslog("Couldn't delete group '$ttygroup' ($fnret), proceeding anyway"); $nbErrors++; } } if ($nbErrors) { osh_warn("Encountered $nbErrors error(s) while deleting account, " . "please refer to the system log for more details (uniqid: " . $ENV{'UNIQID'} . ")"); } OVH::Bastion::syslogFormatted( severity => 'info', type => 'account', fields => [['action', 'delete'], ['account', $account], ['errors', $nbErrors]] ); HEXIT( 'OK', value => {account => $account, ttygroup => $ttygroup, operation => 'deleted', errors => $nbErrors + 0}, msg => "Account $account has been deleted" );