the-bastion/bin/helper/osh-accountDelete

222 lines
7.3 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 *
# 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'");
}
#<HEADER
#>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");
}
}
#<RIGHTSCHECK
#>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
#>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'});