#! /usr/bin/perl -T
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
# NEEDGROUP osh-groupDelete
# SUDOERS %osh-groupDelete ALL=(root) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-groupDelete *
# KEYSUDOERS # as an owner, we can delete our own group
# KEYSUDOERS SUPEROWNERS, %%GROUP%-owner      ALL=(root)        NOPASSWD: /usr/bin/env perl -T %BASEPATH%/bin/helper/osh-groupDelete --group %GROUP%
# 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 ($group);
eval {
    local $SIG{__WARN__} = sub { push @optwarns, shift };
    $result = GetOptions("group=s" => sub { $group //= $_[1] },);
};
if ($@) { die $@ }

if (!$result) {
    local $" = ", ";
    HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns");
}

OVH::Bastion::Helper::check_spurious_args();

if (!$group) {
    HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'group'");
}

#<HEADER

#>PARAMS:GROUP
# test if start by key, append if necessary
osh_debug("Checking group");
$fnret = OVH::Bastion::is_valid_group_and_existing(group => $group, groupType => "key");
$fnret or HEXIT($fnret);

# get returned untainted value
$group = $fnret->value->{'group'};
my $shortGroup = $fnret->value->{'shortGroup'};

#<PARAMS:GROUP

#>RIGHTSCHECK
if ($self eq 'root') {
    osh_debug "Real root, skipping checks of permissions";
}
else {
    # either we can delete any group
    $fnret = OVH::Bastion::is_user_in_group(user => $self, group => "osh-groupDelete");
    if (!$fnret) {

        # or we can delete our own group as the owner of said group
        $fnret = OVH::Bastion::is_group_owner(account => $self, group => $shortGroup, sudo => 1, superowner => 1);
        if (!$fnret) {
            HEXIT('ERR_SECURITY_VIOLATION', msg => "You're not allowed to run this, dear $self");
        }
    }
}

#<RIGHTSCHECK

#>CODE
# last security check
if (not -e "/home/$group/allowed.ip" or not -e "/home/keykeeper/$group") {
    HEXIT('ERR_INVALID_GROUP', msg => "Sorry, but $shortGroup doesn't seem to be a legit bastion group");
}

# do the stuff
osh_info("Backing up group directory...");

if (!-d "/home/oldkeeper") {
    mkdir "/home/oldkeeper";
}
chown 0, 0, "/home/oldkeeper";
chmod 0700, "/home/oldkeeper";

if (!-d "/home/oldkeeper/groups") {
    mkdir "/home/oldkeeper/groups";
}
chown 0, 0, "/home/oldkeeper/groups";
chmod 0700, "/home/oldkeeper/groups";

my $suffix = 'at-' . time() . '.by-' . $self;

my $fulldir = "/home/oldkeeper/groups/$group.$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/$group", "$fulldir/$group-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/$group' to '$fulldir/$group-home' ($msg)"
          . ", continuing anyway...");
    $nbErrors++;
}
$fnret =
  OVH::Bastion::execute_simple(cmd => ['mv', "/home/keykeeper/$group", "$fulldir/$group-keykeeper"], 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/keykeeper/$group' to '$fulldir/$group-keykeeper' ($msg)"
          . ", continuing anyway...");
    $nbErrors++;
}

# now tar.gz the directory, this is important because inside we'll keep the
# old GID of the group, and we don't want GID-orphaned files 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 group (" . $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 group '$group', $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);

# remove dead symlinks in users homes
my $dh;
if (opendir($dh, "/home/allowkeeper")) {
    while (my $dir = readdir($dh)) {
        $dir =~ /^\./                 and next;
        $dir !~ /^([a-zA-Z0-9._-]+)$/ and next;
        $dir = "/home/allowkeeper/$1";    # and untaint
        -d $dir or next;
        foreach my $file ("$dir/allowed.ip.$shortGroup", "$dir/allowed.partial.$shortGroup") {
            if (-e $file || -l $file) {
                osh_info "Removing $file...";
                if (!unlink($file)) {
                    warn_syslog("Couldn't remove symlink '$file': $!");
                    $nbErrors++;
                }
            }
        }
    }
    close($dh);
}
else {
    warn_syslog("Couldn't open /home/allowkeeper to cleanup symlinks: $!");
    $nbErrors++;
}

# trying to remove main and gatekeeper and owner groups
foreach my $todelete ("$group-owner", "$group-aclkeeper", "$group-gatekeeper", $group) {
    $fnret = OVH::Bastion::is_group_existing(group => $todelete);
    if ($fnret) {
        $todelete = $fnret->value->{'group'};    # untaint
        my $members = $fnret->value->{'members'} || [];
        if (@$members) {
            osh_info "Found " . (scalar @$members) . " members, removing them from the group";
            foreach my $member (@$members) {
                osh_info "... removing $member from group $todelete";
                $fnret = OVH::Bastion::sys_delmemberfromgroup(user => $member, group => $todelete, noisy_stderr => 1);
                if ($fnret->err ne 'OK') {
                    warn_syslog("Error while attempting to remove member $member from group $todelete ("
                          . $fnret->msg
                          . "), proceeding anyway");
                    $nbErrors++;
                }
            }
        }

        if ($todelete eq $group) {
            osh_info "Deleting main user of group $todelete...",
              $fnret = OVH::Bastion::sys_userdel(user => $todelete, noisy_stderr => 1);
            if ($fnret->err ne 'OK') {
                warn_syslog("Error while attempting to delete main user of group $todelete ("
                      . $fnret->msg
                      . "), proceeding anyway");
                $nbErrors++;
            }
        }

        # some OSes delete the main group of user if it has the same name
        # and nobody else is a member of it, so check it still exists before
        # trying to delete it
        $fnret = OVH::Bastion::is_group_existing(group => $todelete);
        if ($fnret) {
            osh_info "Deleting group $todelete...";
            $fnret = OVH::Bastion::sys_groupdel(group => $todelete, noisy_stderr => 1);
            if ($fnret->err ne 'OK') {
                warn_syslog(
                    "Error while attempting to delete group $todelete (" . $fnret->msg . "), proceeding anyway");
                $nbErrors++;
            }
        }
    }
    else {
        osh_info "Group $todelete not found, ignoring...";
    }
}

# remove sudoers if it's there
$fnret = OVH::Bastion::execute(
    cmd          => [$OVH::Bastion::BASEPATH . '/bin/sudogen/generate-sudoers.sh', 'delete', 'group', $group],
    must_succeed => 1,
    noisy_stdout => 1
);
if (!$fnret) {
    warn_syslog("Error during group deletion of '$group', couldn't delete sudoers file: " . $fnret->msg);
    $nbErrors++;
}

OVH::Bastion::syslogFormatted(
    severity => 'info',
    type     => 'group',
    fields   => [['action', 'delete'], ['group', $shortGroup], ['errors', $nbErrors]]
);

HEXIT(
    'OK',
    value => {group => $group, operation => 'deleted', errors => $nbErrors + 0},
    msg   => "Group $group has been deleted"
);
