#! /usr/bin/perl -T
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
# NEEDGROUP osh-accountModify
# SUDOERS # modify parameters/policy of an account
# SUDOERS %osh-accountModify ALL=(root) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountModify *
# 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;
use OVH::Result;

# Fetch command options
my $fnret;
my ($result,  @optwarns);
my ($account, @modify);
eval {
    local $SIG{__WARN__} = sub { push @optwarns, shift };
    $result = GetOptions(
        "account=s" => sub { $account //= $_[1] },
        "modify=s"  => \@modify,
    );
};
if ($@) { die $@ }

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

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

if (!$account || !@modify) {
    HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'account' or 'modify'");
}

#<HEADER

#>PARAMS:ACCOUNT
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account, localOnly => 1);
$fnret or HEXIT($fnret);

# get returned untainted value
$account = $fnret->value->{'account'};

#<PARAMS:ACCOUNT

#>RIGHTSCHECK
if ($self eq 'root') {
    osh_debug "Real root, skipping checks of permissions";
}
$fnret = OVH::Bastion::is_user_in_group(user => $self, group => "osh-accountModify");
if (!$fnret) {
    HEXIT('ERR_SECURITY_VIOLATION', msg => "You're not allowed to run this, dear $self");
}

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 modify the account of an admin without being admin yourself");
}

#<RIGHTSCHECK

#>CODE
my %result;

# the TOTP and UNIX Password toggle codes are extremely similar, factorize it here
sub _mfa_toggle {
    my ($key, $value, $mfaName, $mfaGroup, $mfaGroupBypass) = @_;
    my $jsonkey = $key;
    $jsonkey =~ s/-/_/g;

    # if the value is != bypass, remove the account from the bypass group
    if ($value ne 'bypass') {
        if (OVH::Bastion::is_user_in_group(user => $account, group => $mfaGroupBypass)) {
            $fnret =
              OVH::Bastion::sys_delmemberfromgroup(user => $account, group => $mfaGroupBypass, noisy_stderr => 1);
            if (!$fnret) {
                osh_warn "... error while removing the bypass option for this account";
                $result{$jsonkey} = R('ERR_REMOVING_FROM_GROUP');
                return;
            }
        }
    }

    # if the value is == bypass, remove the account from the required group
    elsif ($value eq 'bypass') {
        if (OVH::Bastion::is_user_in_group(user => $account, group => $mfaGroup)) {
            $fnret = OVH::Bastion::sys_delmemberfromgroup(user => $account, group => $mfaGroup, noisy_stderr => 1);
            if (!$fnret) {
                osh_warn "... error while removing the required option for this account";
                $result{$jsonkey} = R('ERR_REMOVING_FROM_GROUP');
                return;
            }
        }
    }

    $fnret = OVH::Bastion::is_user_in_group(user => $account, group => $mfaGroup);
    if ($value eq 'yes') {
        osh_info "Enforcing multi-factor authentication of type $mfaName for this account...";
        if ($fnret) {
            osh_info "... no change was required";
            $result{$jsonkey} = R('OK_NO_CHANGE');
            return;
        }

        $fnret = OVH::Bastion::sys_addmembertogroup(user => $account, group => $mfaGroup, noisy_stderr => 1);
        if (!$fnret) {
            osh_warn "... error while setting the enforce option";
            $result{$jsonkey} = R('ERR_ADDING_TO_GROUP');
            return;
        }

        osh_info("... done, this account is now required to setup a password with --osh selfMFASetup$mfaName ",
            "on the next connection, before being allowed to do anything else");
        $result{$jsonkey} = R('OK');
    }
    elsif ($value eq 'no') {
        osh_info "Removing multi-factor authentication of type $mfaName requirement for this account...";
        if (!$fnret) {
            osh_info "... no change was required";
            $result{$jsonkey} = R('OK_NO_CHANGE');
            return;
        }

        $fnret = OVH::Bastion::sys_delmemberfromgroup(user => $account, group => $mfaGroup, noisy_stderr => 1);
        if (!$fnret) {
            osh_warn "... error while setting the enforce option";
            $result{$jsonkey} = R('ERR_REMOVING_FROM_GROUP');
            return;
        }

        osh_info(
            "... done, this account is no longer required to setup a password, however if there's already ",
            "a password configured, it'll still be required (if this is not expected, the password can be reset ",
            "with --osh accountMFAResetPassword command)"
        );
        $result{$jsonkey} = R('OK');
    }
    elsif ($value eq 'bypass') {
        osh_info "Bypassing multi-factor authentication of type $mfaName requirement for this account...";
        $fnret = OVH::Bastion::is_user_in_group(user => $account, group => $mfaGroupBypass);
        if ($fnret) {
            osh_info "... no change was required";
            $result{$jsonkey} = R('OK_NO_CHANGE');
            return;
        }

        $fnret = OVH::Bastion::sys_addmembertogroup(user => $account, group => $mfaGroupBypass, noisy_stderr => 1);
        if (!$fnret) {
            osh_warn "... error while setting the enforce option";
            $result{$jsonkey} = R('ERR_ADDING_TO_GROUP');
            return;
        }

        osh_info(
            "... done, this account will no longer have to setup a password, even if this is enforced ",
            "by the default global policy.\n",
            "However if there's already a password configured, it'll still be required (if this is not expected, ",
            "the password can be reset with --osh accountMFAResetPassword command)"
        );
        $result{$jsonkey} = R('OK');
    }
    return;
}

sub _toggle_yes_no {
    my %params  = @_;
    my $keyname = $params{'keyname'};
    my $keyfile = $params{'keyfile'};
    my $value   = $params{'value'};
    my $public  = $params{'public'};

    # by default, if public is not specified, it's 1
    $public = 1 if !exists $params{'public'};

    $fnret = OVH::Bastion::account_config(account => $account, public => $public, key => $keyfile);
    if ($value eq 'yes') {
        osh_info "Setting this account as $keyname...";
        if ($fnret) {
            osh_info "... no change was required";
            return R('OK_NO_CHANGE');
        }

        $fnret = OVH::Bastion::account_config(account => $account, public => $public, key => $keyfile, value => 'yes');
        if (!$fnret) {
            osh_warn "... error while setting the option";
            return R('ERR_OPTION_CHANGE_FAILED');
        }

        osh_info "... done, this account is now $keyname";
        return R('OK');
    }
    elsif ($value eq 'no') {
        osh_info "Removing the $keyname flag from this account...";
        if (!$fnret) {
            osh_info "... no change was required";
            return R('OK_NO_CHANGE');
        }

        $fnret = OVH::Bastion::account_config(account => $account, public => $public, key => $keyfile, delete => 1);
        if (!$fnret) {
            osh_warn "... error while removing the option";
            return R('ERR_OPTION_CHANGE_FAILED');
        }

        osh_info "... done, this account has no longer the $keyname flag set";
        return R('OK');
    }
    else {
        return R('ERR_INVALID_PARAMETER', msg => "Invalid value passed to $keyfile");
    }
}

foreach my $tuple (@modify) {
    my ($key, $value) = $tuple =~ /^([a-zA-Z0-9-]+)=([a-zA-Z0-9-]+)$/;
    next if (!$key || !defined $value);
    my $jsonkey = $key;
    $jsonkey =~ s/-/_/g;

    osh_debug "working on tuple key=$key value=$value";
    if ($key eq 'always-active') {
        $result{$jsonkey} = _toggle_yes_no(
            value   => $value,
            keyfile => OVH::Bastion::OPT_ACCOUNT_ALWAYS_ACTIVE,
            keyname => 'always-active'
        );
    }
    elsif ($key eq 'idle-ignore') {
        $result{$jsonkey} =
          _toggle_yes_no(value => $value, keyfile => OVH::Bastion::OPT_ACCOUNT_IDLE_IGNORE, keyname => 'idle-ignore');
    }
    elsif ($key eq 'osh-only') {
        $result{$jsonkey} = _toggle_yes_no(
            value   => $value,
            public  => 0,
            keyfile => OVH::Bastion::OPT_ACCOUNT_OSH_ONLY,
            keyname => 'osh-only'
        );
    }
    elsif ($key eq 'pam-auth-bypass') {
        $fnret = OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::PAM_AUTH_BYPASS_GROUP);
        if ($value eq 'yes') {
            {
                osh_info "Bypassing sshd PAM auth usage for this account...";
                if ($fnret) {
                    osh_info "... no change was required";
                    $result{$jsonkey} = R('OK_NO_CHANGE');
                    last;
                }

                $fnret = OVH::Bastion::sys_addmembertogroup(
                    user         => $account,
                    group        => OVH::Bastion::PAM_AUTH_BYPASS_GROUP,
                    noisy_stderr => 1
                );
                if (!$fnret) {
                    osh_warn "... error while setting the bypass option";
                    $result{$jsonkey} = R('ERR_ADDING_TO_GROUP');
                    last;
                }

                osh_info "... done, this account will no longer use PAM for authentication";
                $result{$jsonkey} = R('OK');
            }
        }
        elsif ($value eq 'no') {
            {
                osh_info "Removing bypass of sshd PAM auth usage for this account...";
                if (!$fnret) {
                    osh_info "... no change was required";
                    $result{$jsonkey} = R('OK_NO_CHANGE');
                    last;
                }

                $fnret = OVH::Bastion::sys_delmemberfromgroup(
                    user         => $account,
                    group        => OVH::Bastion::PAM_AUTH_BYPASS_GROUP,
                    noisy_stderr => 1
                );
                if (!$fnret) {
                    osh_warn "... error while removing the bypass option";
                    $result{$jsonkey} = R('ERR_REMOVING_FROM_GROUP');
                    last;
                }

                osh_info "... done, this account will no longer bypass PAM for authentication";
                $result{$jsonkey} = R('OK');
            }
        }
    }
    elsif ($key eq 'pubkey-auth-optional') {
        $fnret =
          OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::OSH_PUBKEY_AUTH_OPTIONAL_GROUP);
        if ($value eq 'yes') {
            {
                osh_info "Making public key authentication optional for this account...";
                if ($fnret) {
                    osh_info "... no change was required";
                    $result{$jsonkey} = R('OK_NO_CHANGE');
                    last;
                }

                $fnret = OVH::Bastion::sys_addmembertogroup(
                    user         => $account,
                    group        => OVH::Bastion::OSH_PUBKEY_AUTH_OPTIONAL_GROUP,
                    noisy_stderr => 1
                );
                if (!$fnret) {
                    osh_warn "... error while setting the optional pubkey option";
                    $result{$jsonkey} = R('ERR_ADDING_TO_GROUP');
                    last;
                }

                osh_info("... done, this account can now authenticate with or without a pubkey ",
                    "if a password/TOTP is set");
                $result{$jsonkey} = R('OK');
            }
        }
        elsif ($value eq 'no') {
            {
                osh_info "Making pubkey authentication mandatory for this account...";
                if (!$fnret) {
                    osh_info "... no change was required";
                    $result{$jsonkey} = R('OK_NO_CHANGE');
                    last;
                }

                $fnret = OVH::Bastion::sys_delmemberfromgroup(
                    user         => $account,
                    group        => OVH::Bastion::OSH_PUBKEY_AUTH_OPTIONAL_GROUP,
                    noisy_stderr => 1
                );
                if (!$fnret) {
                    osh_warn "... error while removing the optional pubkey option";
                    $result{$jsonkey} = R('ERR_REMOVING_FROM_GROUP');
                    last;
                }

                osh_info "... done, this account now requires a pubkey to authenticate";
                $result{$jsonkey} = R('OK');
            }
        }
    }
    elsif ($key eq 'mfa-password-required') {
        _mfa_toggle(
            $key, $value, 'Password',
            OVH::Bastion::MFA_PASSWORD_REQUIRED_GROUP,
            OVH::Bastion::MFA_PASSWORD_BYPASS_GROUP
        );
    }
    elsif ($key eq 'mfa-totp-required') {
        _mfa_toggle($key, $value, 'TOTP', OVH::Bastion::MFA_TOTP_REQUIRED_GROUP, OVH::Bastion::MFA_TOTP_BYPASS_GROUP);
    }
    elsif ($key eq 'egress-strict-host-key-checking') {
        osh_info "Changing the egress StrictHostKeyChecking option for this account...";
        if (not grep { $value eq $_ } qw{ yes accept-new no ask default bypass }) {
            osh_warn "Invalid parameter '$value', skipping";
            $result{$jsonkey} = R('ERR_INVALID_PARAMETER');
        }
        else {
            my $hostsFile;    # undef, aka remove UserKnownHostsFile option
            if ($value eq 'bypass') {

                # special case: for 'bypass', we set Strict to no and UserKnownHostsFile to /dev/null
                $value     = 'no';
                $hostsFile = '/dev/null';
            }
            elsif ($value eq 'default') {

                # special case: for 'default', we actually remove the StrictHostKeyChecking option
                undef $value;
            }
            $fnret = OVH::Bastion::account_ssh_config_set(
                account => $account,
                key     => "StrictHostKeyChecking",
                value   => $value
            );
            $result{$jsonkey} = $fnret;
            if ($fnret) {
                $fnret = OVH::Bastion::account_ssh_config_set(
                    account => $account,
                    key     => "UserKnownHostsFile",
                    value   => $hostsFile
                );
                $result{$jsonkey} = $fnret;
            }
            if ($fnret) {
                osh_info "... modification done";
            }
            else {
                osh_warn "... error while setting StrictHostKeyChecking policy: " . $fnret->msg;
            }
        }
    }
    elsif ($key eq 'egress-session-multiplexing') {
        osh_info "Changing the egress ControlMaster/ControlPath options for this account...";
        my $controlPath;
        my $controlMaster;
        if ($value eq 'default') {
            # keep both vars undef, which will remove them from the account config file
            ;
        }
        elsif ($value eq 'yes') {
            $controlMaster = 'auto';
            # '~' is handled by ssh_config as the account's home directory
            # '%C' is a hash of local hostname, remote host, remote user, remote port
            $controlPath = "~/tmp/ssh_egress_%C";
        }
        elsif ($value eq 'no') {
            # never create a master connection
            $controlMaster = 'no';
            # 'none' is understood specifically for ssh_config as 'never try to use a master connection'
            $controlPath = 'none';
        }
        else {
            osh_warn "Invalid parameter '$value', skipping";
            $result{$jsonkey} = R('ERR_INVALID_PARAMETER');
        }
        $fnret = OVH::Bastion::account_ssh_config_set(
            account => $account,
            key     => "ControlMaster",
            value   => $controlMaster,
        );
        if ($fnret) {
            $fnret = OVH::Bastion::account_ssh_config_set(
                account => $account,
                key     => "ControlPath",
                value   => $controlPath,
            );
        }
        $result{$jsonkey} = $fnret;
        if ($fnret) {
            osh_info "... modification done";
        }
        else {
            osh_warn "... error while setting policy: $fnret";
        }
    }
    elsif ($key eq 'personal-egress-mfa-required') {
        osh_info
          "Changing the MFA policy for egress connections using the personal access (and keys) of the account...";
        if (not grep { $value eq $_ } qw{ password totp any none }) {
            osh_warn "Invalid parameter '$value', skipping";
            $result{$jsonkey} = R('ERR_INVALID_PARAMETER');
        }
        else {
            $fnret =
              OVH::Bastion::account_config(account => $account, key => "personal_egress_mfa_required", value => $value);
            $result{$jsonkey} = $fnret;
            if ($fnret) {
                osh_info "... modification done";
            }
            else {
                osh_warn "... error while setting MFA policy: " . $fnret->msg;
            }
        }
    }
    elsif ($key eq 'max-inactive-days') {
        osh_info "Changing the account expiration policy...";
        if ($value !~ /^(?:\d+|-1)$/) {
            osh_warn "Invalid parameter '$value', skipping";
            $result{$jsonkey} = R('ERR_INVALID_PARAMETER');
        }
        else {
            my %todo = ($value >= 0 ? (value => $value) : (delete => 1));
            $fnret = OVH::Bastion::account_config(
                account => $account,
                %todo, %{OVH::Bastion::OPT_ACCOUNT_MAX_INACTIVE_DAYS()}
            );
            $result{$jsonkey} = $fnret;
            if ($fnret) {
                osh_info "... modification done";
            }
            else {
                osh_warn "... error while setting the account expiration policy: " . $fnret->msg;
            }
        }
    }
}

HEXIT('OK', value => \%result);
