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

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

my $remainingOptions = OVH::Bastion::Plugin::begin(
    argv    => \@ARGV,
    header  => "add a new public key to your account",
    options => {
        "pubKey|public-key=s" => \my $pubKey,        # 'pubKey' is a deprecated name, keep it to not break scripts or people
        "piv"                 => \my $pivExplicit,
    },
    helptext => <<'EOF',
Add a new ingress public key to your account

Usage: --osh SCRIPT_NAME [--public-key '"ssh key text"'] [--piv]

  --public-key KEY  Your new ingress public SSH key to deposit on the bastion, use double-quoting if your're under a shell.
                      If this option is not specified, you'll be prompted interactively for your public SSH key. Note that you
                      can also pass it through STDIN directly. If the policy of this bastion allows it, you may prefix the key
                      with a 'from="IP1,IP2,..."' snippet, a la authorized_keys. However the policy might force a configured
                      'from' prefix that will override yours, or be used if you don't specify it yourself.
  --piv             Add a public SSH key from a PIV-compatible hardware token, along with its attestation certificate and key
                      certificate, both in PEM format. If you specified --public-key, then the attestation and key certificate are
                      expected on STDIN only, otherwise the public SSH key, the attestation and key certificate are expected on STDIN.
EOF
);

# ugly hack for space-enabled parameter
if (ref $remainingOptions eq 'ARRAY' and @$remainingOptions) {
    $pubKey .= " " . join(" ", @$remainingOptions);
}

#
# code
#
my $fnret;

my $pivEffectivePolicyEnabled = OVH::Bastion::is_effective_piv_account_policy_enabled(account => $self);

# before requesting the ssh pubkey, if we have to do PIV, check we have the piv helper
if (!OVH::Bastion::has_piv_helper()) {
    if ($pivExplicit) {
        osh_exit R('KO_PIV_NOT_AVAILABLE',
            msg =>
              "This bastion doesn't have PIV capabilities due to missing prerequisites. Please retry without --piv.");
    }
    elsif ($pivEffectivePolicyEnabled) {
        warn_syslog("selfAddIngressKey: $self is required to use PIV keys but we're missing the PIV helper");
        osh_exit R('KO_PIV_NOT_AVAILABLE',
            msg =>
              "You are required per the bastion policy to only use PIV keys but we're missing some prerequisites to validate PIV keys. "
              . "This is a configuration error, please contact your nearest sysadmin.");
    }
}

if (not defined $pubKey) {
    osh_info "Please paste the SSH key you want to add.";
    OVH::Bastion::print_accepted_key_algorithms(way => "ingress");
    osh_info "\nPlease ensure your private key is encrypted using a proper passphrase.";

    if (OVH::Bastion::config('ingressKeysFromAllowOverride')->value) {
        osh_info
          'You can prepend your key with a from="IP1,IP2,..." as this bastion policy allows ingress keys "from" override by users';
    }
    else {
        osh_info
          'Any from="IP1,IP2,..." you include will be ignored, as this bastion policy refuses ingress keys "from" override by users';
    }

    $pubKey = <STDIN>;

    # trim spaces
    $pubKey =~ s{^\s+|\s+$}{}g;
}

$fnret = OVH::Bastion::is_valid_public_key(pubKey => $pubKey, way => 'ingress');
if (!$fnret) {

    # maybe we decoded the key but for some reason we don't want/can't add it
    # in that case, return the data of the key in the same format as when this
    # call works (see last line with osh_ok)
    $fnret->{'value'} = {key => $fnret->value} if $fnret->value;
    osh_exit $fnret;
}
my $key = $fnret->value;

my $allowedKeyFile = $HOME . '/' . OVH::Bastion::AK_FILE;
if (checkExistKey($key->{'base64'})) {
    osh_exit R('KO_DUPLICATE_KEY', msg => "This public key already exists on your account!", value => {key => $key});
}

# we have a valid key, now handle PIV if needed

if ($pivEffectivePolicyEnabled) {
    osh_info "Your are required to add only SSH keys from PIV-compatible hardware tokens, by policy.";
}
elsif ($pivExplicit) {
    osh_info "You have requested to add a PIV-enabled SSH key.";
}

if ($pivExplicit || $pivEffectivePolicyEnabled) {
    osh_info "Please paste the PIV attestation certificate of your hardware key in PEM format.";
    osh_info "This snippet should start with '-----BEGIN CERTIFICATE-----' and end with '-----END CERTIFICATE-----':";
    osh_info " ";
    $fnret = readPEMFromSTDIN();
    $fnret or osh_exit $fnret;
    $key->{'pivAttestationCertificate'} = $fnret->value;

    osh_info " ";
    osh_info "Thanks, now please paste the PIV key certificate of your generated key in PEM format.";
    osh_info
      "This snippet should also start with '-----BEGIN CERTIFICATE-----' and end with '-----END CERTIFICATE-----':";
    osh_info " ";
    $fnret = readPEMFromSTDIN();
    $fnret or osh_exit $fnret;
    $key->{'pivKeyCertificate'} = $fnret->value;
    osh_info " ";

    $fnret = OVH::Bastion::verify_piv(
        key                    => $key->{'line'},
        attestationCertificate => $key->{'pivAttestationCertificate'},
        keyCertificate         => $key->{'pivKeyCertificate'}
    );
    $key->{'isPiv'}   = ($fnret ? 1 : 0);
    $key->{'pivInfo'} = $fnret->value if $fnret;

    if (!$key->{'isPiv'}) {
        osh_exit R('ERR_PIV_VALIDATION_FAILED',
            msg => "Those certificates didn't successfully validate the provided PIV key, aborting!");
    }
}

# end of PIV handling

$fnret = OVH::Bastion::get_from_for_user_key(userProvidedIpList => $key->{'fromList'}, key => $key);
$fnret or osh_exit $fnret;

$key->{'info'} = sprintf("ADDED_BY=%s USING=%s UNIQID=%s TIMESTAMP=%s DATETIME=%s VERSION=%s",
    $self, $scriptName, $ENV{'UNIQID'}, time(), DateTime->now(), $OVH::Bastion::VERSION);

$fnret = OVH::Bastion::add_key_to_authorized_keys_file(file => $allowedKeyFile, key => $key);
$fnret or osh_exit $fnret;

osh_info " ";
osh_info "Public key successfully added:";
OVH::Bastion::print_public_key(key => $key, nokeyline => 1);

if (ref $key->{'fromList'} eq 'ARRAY' && @{$key->{'fromList'}}) {
    osh_info "You will only be able to connect from: " . join(', ', @{$key->{'fromList'}});
}

sub checkExistKey {

    # only pass the base64 part of the key here (returned by get_ssh_pub_key_info->{'base64'})
    my $pubKeyB64 = shift;

    open(my $fh_keys, '<', $allowedKeyFile) || die("can't read the $allowedKeyFile file!\n");
    while (my $currentLine = <$fh_keys>) {
        chomp $currentLine;
        next if ($currentLine =~ /^\s*#/);
        my $parsedResult = OVH::Bastion::get_ssh_pub_key_info(pubKey => $currentLine, way => "ingress");
        if ($parsedResult && $parsedResult->value->{'base64'} eq $pubKeyB64) {
            close($fh_keys);
            return $currentLine;
        }
    }
    close($fh_keys);
    return 0;
}

sub readPEMFromSTDIN {
    my @pem;
    my $readingState = 0;
    while (my $line = <STDIN>) {
        chomp $line;

        # ignore empty lines, or lines with only space-like chars
        next if $line =~ m{^\s*$};

        # trim every space-like char before and after the line
        $line =~ s{^\s+|\s+$}{}g;
        if ($readingState == 0) {

            # we're waiting for the BEGIN line, ignore everything till we get there
            next if ($line ne '-----BEGIN CERTIFICATE-----');
            push @pem, $line;
            $readingState = 1;
        }
        elsif ($readingState == 1) {

            # we're after BEGIN and before END, read the PEM cert
            push @pem, $line;

            # if we're at the end, bail out and be happy
            if ($line eq '-----END CERTIFICATE-----') {
                $readingState = 2;
                last;
            }
        }
    }

    # here, if readingState != 2, then we don't have a complete PEM cert
    if ($readingState == 0) {
        return R('ERR_NO_PEM_START_MARKER', msg => "Couldn't find a valid '-----BEGIN CERTIFICATE-----' marker");
    }
    elsif ($readingState == 1) {
        return R('ER_NO_PEM_END_MARKER', msg => "Couldn't find a valid '-----END CERTIFICATE-----' marker");
    }
    elsif ($readingState == 2) {
        return R('OK', value => join("\n", @pem));
    }
    return R('ERR_INTERNAL');    # unreachable
}

$key->{'from_list'} = delete $key->{'fromList'};    # for json display
osh_ok {connect_only_from => $key->{'from_list'}, key => $key};
