#! /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'");
}

#<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;

# 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");

# take a lock here, as we're going to remove system accounts and groups.
# additionally, the lock type "passwd" is used by all helpers
# that may modify /etc/passwd or /etc/group.
$fnret = OVH::Bastion::Helper::get_lock_fh(category => "passwd");
$fnret or HEXIT($fnret);
my $lock_fh = $fnret->value;
$fnret = OVH::Bastion::Helper::acquire_lock($lock_fh);
$fnret or HEXIT($fnret);

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"
);
