#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;

use File::Basename;
use lib dirname(__FILE__) . '/../../../lib/perl';
use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT help );
use OVH::Bastion::Plugin::ACL;

my $remainingOptions = OVH::Bastion::Plugin::begin(
    loadConfig         => 1,
    argv               => \@ARGV,
    header             => "adding personal access to a server on an account",
    userAllowWildcards => 1,
    options            => {
        "account=s"        => \my $account,
        "protocol=s"       => \my $protocol,
        "force-key=s"      => \my $forceKey,
        "force-password=s" => \my $forcePassword,
        "ttl=s"            => \my $ttl,
        "comment=s"        => \my $comment,
        # undocumented/compatibility:
        "user-any" => \my $userAny,
        "port-any" => \my $portAny,
        "scpup"    => \my $scpUp,
        "scpdown"  => \my $scpDown,
        "sftp"     => \my $sftp,
    },
    helptext => <<'EOF',
Add a personal server access to an account

Usage: --osh SCRIPT_NAME --account ACCOUNT --host HOST --user USER --port PORT [OPTIONS]

  --account                Bastion account to add the access to
  --host HOST|IP|SUBNET    Host(s) to add access to, either a HOST which will be resolved to an IP immediately,
                             or an IP, or a whole subnet using the PREFIX/SIZE notation
  --user USER|PATTERN|*    Specify which remote user should be allowed to connect as.
                             Globbing characters '*' and '?' are supported, so you can specify a pattern
                             that will be matched against the actual remote user name.
                             To allow any user, use '--user *' (you might need to escape '*' from your shell)
  --port PORT|*            Remote port allowed to connect to
                             To allow any port, use '--port *' (you might need to escape '*' from your shell)
  --protocol PROTO         Specify that a special protocol should be allowed for this HOST:PORT tuple, note that you
                              must not specify --user in that case. However, for this protocol to be usable under a given
                              remote user, access to the USER@HOST:PORT tuple must also be allowed.
                              PROTO must be one of:
                              scpupload    allow SCP upload, you--bastion-->server
                              scpdownload  allow SCP download, you<--bastion--server
                              sftp         allow usage of the SFTP subsystem, through the bastion
                              rsync        allow usage of rsync, through the bastion
  --force-key FINGERPRINT  Only use the key with the specified fingerprint to connect to the server (cf accountListEgressKeys)
  --force-password HASH    Only use the password with the specified hash to connect to the server (cf accountListPasswords)
  --ttl SECONDS|DURATION   Specify a number of seconds (or a duration string, such as "1d7h8m") after which the access will automatically expire
  --comment "'ANY TEXT'"   Add a comment alongside this server. Quote it twice as shown if you're under a shell.

The access will work only if one of the account's personal egress public key has been copied to the remote server.
To get the list of an account's personal egress public keys, see ``accountListEgressKeyss`` and ``selfListEgressKeys``.
EOF
);

=pod CONFIGURATION

=head1 items

=head2 widest_v4_prefix (optional, integer, between 0 and 32)

When specified, this limits the size of subnets that can be added to an ACL, e.g. 24 would not allow
prefix lengths wider than /24 (such as /20 or /16).

Note that this doesn't prevent users from adding thousands of ACLs to cover a wide range of networks,
but this helps ensuring ACLs such as 0.0.0.0/0 can't be added in a single command.

=head2 self_remote_user_only (optional, boolean)

When true, this only allows to add ACLs with the remote user being the same than the account name,
i.e. adding an access to a bastion account named "johndoe" can only be done specifying this very account
name as the remote user name, with ``accountAddPersonalAccess --user johndoe``.

=head1 example

{
    "widest_v4_prefix": 24,
    "self_remote_user_only": true
}

=cut

my $fnret;

if (!$ip) {
    help();
    osh_exit 'ERR_MISSING_PARAMETER', "Missing parameter 'host' or didn't resolve correctly";
}

$fnret = OVH::Bastion::Plugin::ACL::check(
    user     => $user,
    userAny  => $userAny,
    port     => $port,
    portAny  => $portAny,
    scpUp    => $scpUp,
    scpDown  => $scpDown,
    sftp     => $sftp,
    protocol => $protocol,
);
if (!$fnret) {
    help();
    osh_exit($fnret);
}
$user = $fnret->value->{'user'};
$port = $fnret->value->{'port'};

if (defined $ttl) {
    $fnret = OVH::Bastion::is_valid_ttl(ttl => $ttl);
    $fnret or osh_exit $fnret;
    $ttl = $fnret->value->{'seconds'};
}

if ($forceKey && $forcePassword) {
    osh_exit 'ERR_INCOMPATIBLE_PARAMETERS', "Can't use --force-key and --force-password at the same time";
}

if (not $account) {
    help();
    osh_exit 'ERR_MISSING_PARAMETER', "Missing mandatory parameter 'account'";
}

$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account);
$fnret or osh_exit $fnret;
$account = $fnret->value->{'account'};

if ($forceKey) {
    $fnret = OVH::Bastion::is_valid_fingerprint(fingerprint => $forceKey);
    $fnret or osh_exit $fnret;
    $forceKey = $fnret->value->{'fingerprint'};
}

if ($forcePassword) {
    $fnret = OVH::Bastion::is_valid_hash(hash => $forcePassword);
    $fnret or osh_exit $fnret;
    $forcePassword = $fnret->value->{'hash'};
}

#
# Now do it
#

# check plugin config
if ($pluginConfig && $pluginConfig->{'self_remote_user_only'}) {
    if (!$user || $user ne $account) {
        osh_exit('ERR_INVALID_PARAMETER',
            msg => "This bastion policy forces the remote user of personal accesses to match\n"
              . "the account name: you may retry with --user $account");
    }
}

# if no comment is specified, but we're adding the server by hostname,
# use it to craft a comment
if (!$comment && $host ne $ip) {
    $comment = "hostname=$host";
}

# use dryrun to validate all parameters
$fnret = OVH::Bastion::access_modify(
    dryrun         => 1,
    sudo           => 0,
    way            => 'personal',
    account        => $account,
    action         => 'add',
    user           => $user,
    ip             => $ip,
    port           => $port,
    ttl            => $ttl,
    forceKey       => $forceKey,
    forcePassword  => $forcePassword,
    comment        => $comment,
    widestV4Prefix => ($pluginConfig ? $pluginConfig->{'widest_v4_prefix'} : undef),
);
$fnret or osh_exit($fnret);

osh_info "Can't verify whether $account\'s personal key has been installed to the remote server, "
  . "as you don't have access to their private keys, adding the access blindly";

my @command = qw{ sudo -n -u allowkeeper -- /usr/bin/env perl -T };
push @command, $OVH::Bastion::BASEPATH . '/bin/helper/osh-accountModifyPersonalAccess';
push @command, '--target',         'any';
push @command, '--action',         'add';
push @command, '--account',        $account;
push @command, '--ip',             $ip;
push @command, '--user',           $user          if $user;
push @command, '--port',           $port          if $port;
push @command, '--force-key',      $forceKey      if $forceKey;
push @command, '--force-password', $forcePassword if $forcePassword;
push @command, '--ttl',            $ttl           if $ttl;
push @command, '--comment',        $comment       if $comment;

osh_exit OVH::Bastion::helper(cmd => \@command);
