mirror of
https://github.com/ovh/the-bastion.git
synced 2025-01-10 09:23:52 +08:00
231 lines
8.3 KiB
Perl
231 lines
8.3 KiB
Perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
|
|
package OVH::Bastion;
|
|
|
|
use common::sense;
|
|
use Term::ReadLine;
|
|
use JSON;
|
|
use POSIX ();
|
|
|
|
# autocompletion rules
|
|
my @rules;
|
|
|
|
sub interactive {
|
|
my %params = @_;
|
|
my $realOptions = $params{'realOptions'};
|
|
my $timeoutHandler = $params{'timeoutHandler'};
|
|
my $self = $params{'self'};
|
|
my $fnret;
|
|
|
|
my $bastionName = OVH::Bastion::config('bastionName')->value();
|
|
my $interactiveModeTimeout = OVH::Bastion::config('interactiveModeTimeout')->value() || 0;
|
|
my $slaveOrMaster = (OVH::Bastion::config('readOnlySlaveMode')->value() ? 'slave' : 'master');
|
|
|
|
my $term = Term::ReadLine->new('Bastion Interactive');
|
|
my $prompt =
|
|
"\001\033[0m\033[33m$self\033[1;35m@\033[32m\002$bastionName\001\033[1;35m\002(\001\033[0m\033[36m\002$slaveOrMaster\001\033[1;35m\002)\001\033[0m\033[32m\002>\001\033[0m\002 ";
|
|
|
|
my $prompt_non_readline = $prompt;
|
|
$prompt_non_readline =~ s=\001|\002==g;
|
|
|
|
print <<EOM;
|
|
|
|
Welcome to $bastionName interactive mode, type `help' for available commands.
|
|
You can use <tab> and <tab><tab> for autocompletion.
|
|
You'll be disconnected after $interactiveModeTimeout seconds of inactivity.
|
|
EOM
|
|
|
|
# dynamically get the list of plugins we can use
|
|
|
|
print "Loading... ";
|
|
$fnret = OVH::Bastion::get_plugin_list();
|
|
$fnret or return ();
|
|
my $pluginList = $fnret->value;
|
|
|
|
my @cmdlist = ('exit', 'ssh');
|
|
foreach my $plugin (sort keys %$pluginList) {
|
|
$fnret = OVH::Bastion::can_account_execute_plugin(plugin => $plugin, account => $self);
|
|
next if !$fnret;
|
|
|
|
push @cmdlist, $plugin;
|
|
|
|
# also load autocompletion rules for this plugin
|
|
if (open(my $jsonFd, '<', $pluginList->{$plugin}->{'dir'} . '/' . $plugin . '.json')) {
|
|
local $/ = undef;
|
|
my $jsonPayload = <$jsonFd>;
|
|
close($jsonFd);
|
|
my $jsonData;
|
|
eval { $jsonData = decode_json($jsonPayload); };
|
|
if (ref $jsonData eq 'HASH' && ref $jsonData->{'interactive'} eq 'ARRAY') {
|
|
push @rules, @{$jsonData->{'interactive'}};
|
|
}
|
|
}
|
|
}
|
|
print scalar(@cmdlist) . " commands and " . (@rules / 2) . " autocompletion rules loaded.\n\n";
|
|
|
|
# setup readline
|
|
|
|
$term->ornaments(1);
|
|
my $attribs = $term->Attribs;
|
|
|
|
$attribs->{'completion_function'} = sub {
|
|
my ($word, $line, $start) = @_;
|
|
|
|
# word: current word being typed
|
|
# line: whole line so far
|
|
# start: cursor pos
|
|
|
|
# avoid disconnection because the user seems to be alive
|
|
alarm($interactiveModeTimeout);
|
|
|
|
if (!$line) {
|
|
|
|
# autocompletion asked without anything written yet
|
|
return @cmdlist;
|
|
}
|
|
|
|
# easter egg
|
|
if ($line eq $word and $word =~ /^con/) {
|
|
return ('configure');
|
|
}
|
|
if ($line =~ /^conf(igure)?(\s|$)/ and ('terminal' =~ /^\Q$word\E/)) {
|
|
return ('terminal');
|
|
}
|
|
|
|
# /easter egg
|
|
|
|
if ($line eq $word) {
|
|
|
|
# first word of line, user wants completion
|
|
my @validcmds;
|
|
foreach my $cmd (@cmdlist) {
|
|
push @validcmds, $cmd if ($cmd =~ /^\Q$word\E/);
|
|
}
|
|
return @validcmds;
|
|
}
|
|
|
|
for (my $i = 0 ; $i < @rules ; $i += 2) {
|
|
my $re = $rules[$i];
|
|
my $item = $rules[$i + 1];
|
|
|
|
next unless ($line =~ m{^$re\s*$} or $line =~ m{^$re\s\Q$word\E\s*$});
|
|
|
|
# but wait, even if it matches, user must have the right to use this plugin,
|
|
# check that here
|
|
|
|
my ($typedPlugin) = $line =~ m{^(\S+)};
|
|
next unless grep { $typedPlugin eq $_ } @cmdlist;
|
|
|
|
# if autocomplete specified, just return it
|
|
if ($item->{'ac'}) {
|
|
|
|
# but before, check there's no magic inside, i.e. replace ACCOUNT by @account_list and GROUP by @group_list
|
|
my @autocomplete;
|
|
foreach (@{$item->{'ac'}}) {
|
|
if ($_ eq '<ACCOUNT>') {
|
|
my $fnret = OVH::Bastion::get_account_list(cache => 1);
|
|
if ($fnret) {
|
|
push @autocomplete, sort keys %{$fnret->value()};
|
|
next;
|
|
}
|
|
}
|
|
elsif ($_ eq '<GROUP>') {
|
|
my $fnret = OVH::Bastion::get_group_list(cache => 1, groupType => 'key');
|
|
if ($fnret) {
|
|
push @autocomplete, sort keys %{$fnret->value()};
|
|
next;
|
|
}
|
|
}
|
|
elsif ($_ eq '<REALM>') {
|
|
my $fnret = OVH::Bastion::get_realm_list();
|
|
if ($fnret) {
|
|
push @autocomplete, sort keys %{$fnret->value()};
|
|
next;
|
|
}
|
|
}
|
|
elsif ($_ eq '<RESTRICTED_COMMAND>') {
|
|
my $fnret = OVH::Bastion::get_plugin_list(restrictedOnly => 1);
|
|
if ($fnret) {
|
|
push @autocomplete, 'auditor', sort keys %{$fnret->value()};
|
|
next;
|
|
}
|
|
}
|
|
push @autocomplete, $_;
|
|
}
|
|
return @autocomplete;
|
|
}
|
|
|
|
# else, we just print stuff ourselves
|
|
if ($item->{'pr'}) {
|
|
print "\n" . join("\n", @{$item->{'pr'}}) . "\n$prompt_non_readline$line";
|
|
return ();
|
|
}
|
|
}
|
|
|
|
# nothing matches, we have nothing to return
|
|
return ();
|
|
};
|
|
|
|
# for obscure reasons, perl signal handling code doesn't work well with readline
|
|
# unless we force them to "unsafe" mode before perl starts, which is ugly.
|
|
# so for this one, use direct sigaction() call and bypass perl signal mechanics
|
|
# cf http://stackoverflow.com/questions/13316232/perl-termreadlinegnu-signal-handling-difficulties
|
|
POSIX::sigaction POSIX::SIGALRM, POSIX::SigAction->new(
|
|
sub {
|
|
print "\n\nIdle timeout, goodbye!\n\n";
|
|
&$timeoutHandler('TIMEOUT') if ref $timeoutHandler eq 'CODE';
|
|
exit 1; # normally not reached
|
|
}
|
|
);
|
|
|
|
my $BASTION_USER = OVH::Bastion::get_user_from_env()->value;
|
|
alarm($interactiveModeTimeout);
|
|
while (defined(my $line = $term->readline($prompt))) {
|
|
alarm(0); # disable timeout
|
|
$line =~ s/^\s+|\s+$//g;
|
|
next if (length($line) == 0); # ignore empty lines
|
|
last if ($line eq 'exit' or $line eq 'quit' or $line eq 'q'); # break out of loop if asked
|
|
|
|
$term->addhistory($line);
|
|
|
|
if ($line =~ /^conf(i(g(u(r(e)?)?)?)?)? t(e(r(m(i(n(a(l)?)?)?)?)?)?)?$/) {
|
|
print "Nice try, but... no :)\n";
|
|
next;
|
|
}
|
|
|
|
{
|
|
local $ENV{'OSH_NO_INTERACTIVE'} = 1;
|
|
if ($line =~ /^ssh (.+)$/) {
|
|
system($0, '-c', "$realOptions $1");
|
|
}
|
|
else {
|
|
system($0, '-c', "$realOptions --osh $line");
|
|
}
|
|
}
|
|
|
|
my (%before, %after);
|
|
$fnret = OVH::Bastion::execute(cmd => [qw{ id -G -n }]);
|
|
if ($fnret->err eq 'OK' and $fnret->value and $fnret->value->{'stdout'}) {
|
|
chomp $fnret->value->{'stdout'}->[0];
|
|
%before = map { $_ => 1 } split(/ /, $fnret->value->{'stdout'}->[0]);
|
|
}
|
|
$fnret = OVH::Bastion::execute(cmd => ['id', '-G', '-n', $BASTION_USER]);
|
|
if ($fnret->err eq 'OK' and $fnret->value and $fnret->value->{'stdout'}) {
|
|
chomp $fnret->value->{'stdout'}->[0];
|
|
%after = map { $_ => 1 } split(/ /, $fnret->value->{'stdout'}->[0]);
|
|
}
|
|
my @newgroups = grep { !exists $before{$_} && $_ !~ /^mfa-/ } keys %after;
|
|
if (@newgroups) {
|
|
osh_warn("IMPORTANT: You have been added to new groups since the session started.");
|
|
osh_warn("You'll need to logout/login again from this interactive session to have");
|
|
osh_warn("your new rights applied, or you'll get sudo errors if you try to use them.");
|
|
}
|
|
}
|
|
continue {
|
|
alarm($interactiveModeTimeout);
|
|
}
|
|
alarm(0); # disable timeout
|
|
print "\n\nGoodbye!\n\n";
|
|
return 0;
|
|
}
|
|
|
|
1;
|