the-bastion/bin/helper/osh-accountDelete

267 lines
9 KiB
Perl
Executable file

#! /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::Copy qw(move);
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'");
}
#<HEADER
#>PARAMS:TYPE
osh_debug("Checking type");
if (not grep { $type eq $_ } qw{ normal realm }) {
HEXIT('ERR_INVALID_PARAMETER', "Expected type 'normal' or 'realm'");
}
#<PARAMS:TYPE
#>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');
}
}
#<RIGHTSCHECK
#>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;
if (!move("/home/$account", "$fulldir/$account-home")) {
warn_syslog("Error while backing up to-be-deleted '/home/$account' to '$fulldir/$account-home': $!, continuing anyway...");
$nbErrors++;
}
if (!move("/home/allowkeeper/$account", "$fulldir/allowkeeper")) {
warn_syslog("Error while backing up to-be-deleted '/home/allowkeeper/$account' to '$fulldir/allowkeeper': $!, 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, 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', 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', 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', 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");