mirror of
https://github.com/ovh/the-bastion.git
synced 2025-09-13 00:14:18 +08:00
338 lines
12 KiB
Perl
Executable file
338 lines
12 KiB
Perl
Executable file
#! /usr/bin/env perl
|
|
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
|
|
use common::sense;
|
|
use Term::ANSIColor;
|
|
use POSIX qw{ strftime };
|
|
|
|
use File::Basename;
|
|
use lib dirname(__FILE__) . '/../../../lib/perl';
|
|
use OVH::Result;
|
|
use OVH::Bastion;
|
|
use OVH::Bastion::Plugin qw( :DEFAULT help );
|
|
|
|
# globally allow sys_getpw* and sys_getgr* cache use
|
|
$ENV{'PW_GR_CACHE'} = 1;
|
|
|
|
my $withKeys = 1;
|
|
|
|
sub toggle_all {
|
|
my $v = shift;
|
|
$withKeys = $v;
|
|
return;
|
|
}
|
|
|
|
my $remainingOptions = OVH::Bastion::Plugin::begin(
|
|
|
|
argv => \@ARGV,
|
|
header => "group info",
|
|
options => {
|
|
'group=s' => \my $group,
|
|
'all' => \my $all,
|
|
'with-keys' => sub { $withKeys = 1 },
|
|
'without-keys' => sub { $withKeys = 0 },
|
|
'with-everything' => sub { toggle_all(1) },
|
|
'without-everything' => sub { toggle_all(0) },
|
|
},
|
|
helptext => <<'EOF',
|
|
Print some basic information about a group
|
|
|
|
Usage: --osh SCRIPT_NAME <--group GROUP|--all> [OPTIONS]
|
|
|
|
--group GROUP Specify the group to display the info of
|
|
--all Dump info for all groups (auditors only), use with ``--json``
|
|
|
|
--with[out]-everything Include or exclude all below options, including future ones
|
|
--with[out]-keys Whether to include the group keys list (slow-ish, default: yes)
|
|
EOF
|
|
);
|
|
|
|
my $fnret;
|
|
|
|
if (!$group && !$all) {
|
|
help();
|
|
osh_exit 'ERR_MISSING_PARAMETER', "Missing '--group' or '--all' parameter";
|
|
}
|
|
|
|
my $isAuditor = OVH::Bastion::is_auditor(account => $self);
|
|
if (!$isAuditor && $all) {
|
|
osh_exit 'ERR_ACCESS_DENIED', "Only bastion auditors can use --all";
|
|
}
|
|
|
|
my @groupsToCheck;
|
|
if ($all) {
|
|
$fnret = OVH::Bastion::get_group_list();
|
|
$fnret or osh_exit($fnret);
|
|
@groupsToCheck = sort keys %{$fnret->value};
|
|
osh_info("Gathering data, this may take a few seconds...");
|
|
}
|
|
else {
|
|
@groupsToCheck = ($group);
|
|
}
|
|
|
|
# check all groups and get the untainted data
|
|
my @groups;
|
|
foreach my $groupName (@groupsToCheck) {
|
|
$fnret = OVH::Bastion::is_valid_group_and_existing(group => $groupName, groupType => "key");
|
|
$fnret or osh_exit($fnret);
|
|
|
|
# get returned untainted value
|
|
push @groups, {group => $fnret->value->{'group'}, shortGroup => $fnret->value->{'shortGroup'}};
|
|
}
|
|
|
|
# gather this only once
|
|
$fnret = OVH::Bastion::get_bastion_ips();
|
|
my $from;
|
|
if ($fnret) {
|
|
my @ips = @{$fnret->value};
|
|
$from = 'from="' . join(',', @ips) . '"';
|
|
}
|
|
|
|
# big hash containing all the data we want to return
|
|
my %return;
|
|
|
|
foreach my $groupData (@groups) {
|
|
$group = $groupData->{'group'};
|
|
my $shortGroup = $groupData->{'shortGroup'};
|
|
|
|
# get the member list of each system group mapped to one of the roles of the group
|
|
my %roles;
|
|
foreach my $role (qw{ member aclkeeper gatekeeper owner }) {
|
|
$fnret = OVH::Bastion::is_group_existing(group => $group . ($role eq 'member' ? '' : "-$role"));
|
|
if (!$fnret) {
|
|
osh_exit($fnret) if $role eq 'member'; # if this happens, we really have a problem here
|
|
$roles{$role} = [];
|
|
}
|
|
else {
|
|
$roles{$role} = [grep { $_ ne 'allowkeeper' } @{$fnret->value->{'members'} || []}];
|
|
}
|
|
}
|
|
|
|
# data that anybody can view:
|
|
my %ret = (
|
|
group => $shortGroup,
|
|
owners => $roles{'owner'},
|
|
gatekeepers => $roles{'gatekeeper'}
|
|
);
|
|
|
|
if ( $isAuditor
|
|
|| OVH::Bastion::is_group_owner(group => $shortGroup, account => $self, superowner => 1)
|
|
|| OVH::Bastion::is_group_gatekeeper(group => $shortGroup, account => $self)
|
|
|| OVH::Bastion::is_group_aclkeeper(group => $shortGroup, account => $self)
|
|
|| OVH::Bastion::is_group_member(group => $shortGroup, account => $self))
|
|
{
|
|
# members, aclkeepers, gatekeepers, owners and auditors can get the aclkeepers list
|
|
$ret{'aclkeepers'} = $roles{'aclkeeper'};
|
|
|
|
# being a member of the system group corresponding to the bastion group
|
|
# can mean either member or guest, so check this here, taking into account the
|
|
# case of the realm accounts
|
|
my (@members, @guests);
|
|
foreach my $account (@{$roles{'member'}}) {
|
|
# realm accounts
|
|
if ($account =~ /^realm_(.+)/) {
|
|
my $pRealm = $1;
|
|
$fnret = OVH::Bastion::get_remote_accounts_from_realm(realm => $pRealm);
|
|
if (!$fnret || !@{$fnret->value}) {
|
|
# we couldn't get the list, or the list is empty: at least show that the realm shared account is there
|
|
push @members, $user;
|
|
next;
|
|
}
|
|
# show remote realm accounts names, either as guests or members
|
|
foreach my $pRemoteaccount (@{$fnret->value}) {
|
|
if (OVH::Bastion::is_group_guest(group => $shortGroup, account => "$pRealm/$pRemoteaccount")) {
|
|
push @guests, "$pRealm/$pRemoteaccount";
|
|
}
|
|
else {
|
|
push @members, "$pRealm/$pRemoteaccount";
|
|
}
|
|
}
|
|
}
|
|
# normal case (non-realm accounts)
|
|
else {
|
|
if (OVH::Bastion::is_group_guest(account => $account, group => $shortGroup)) {
|
|
push @guests, $account;
|
|
}
|
|
else {
|
|
push @members, $account;
|
|
}
|
|
}
|
|
}
|
|
|
|
# for each guest, get the number of accesses they have on the group,
|
|
# so we can show it nicely
|
|
my %guest_nb_accesses;
|
|
my @filtered_guests;
|
|
foreach my $guest (sort @guests) {
|
|
$fnret = OVH::Bastion::get_acl_way(way => 'groupguest', group => $shortGroup, account => $guest);
|
|
|
|
# for realms, don't show remote accounts with zero accesses, this could be confusing
|
|
next if ($guest =~ m{/} && $fnret && @{$fnret->value} == 0);
|
|
|
|
$guest_nb_accesses{$guest} = $fnret ? scalar(@{$fnret->value}) : undef;
|
|
push @filtered_guests, $guest;
|
|
}
|
|
|
|
# deprecated in v2.18.00+
|
|
$ret{'full_members'} = \@members;
|
|
$ret{'partial_members'} = \@filtered_guests;
|
|
# /deprecated
|
|
|
|
$ret{'members'} = \@members;
|
|
$ret{'guests'} = \@filtered_guests;
|
|
$ret{'guests_accesses'} = \%guest_nb_accesses;
|
|
|
|
# add a hint about possibly inactive members
|
|
my @inactive;
|
|
foreach my $account (@members) {
|
|
if (OVH::Bastion::is_account_active(account => $account)->is_ko) {
|
|
push @inactive, $account;
|
|
}
|
|
}
|
|
$ret{'inactive'} = \@inactive;
|
|
|
|
# policies
|
|
$fnret = OVH::Bastion::group_config(group => $group, key => 'mfa_required');
|
|
if ($fnret && $fnret->value ne 'none') {
|
|
$ret{'mfa_required'} = $fnret->value;
|
|
}
|
|
|
|
$fnret = OVH::Bastion::group_config(group => $group, key => 'guest_ttl_limit');
|
|
if ($fnret && defined $fnret->value && $fnret->value =~ /^\d+$/) {
|
|
$ret{'guest_ttl_limit'} = $fnret->value;
|
|
}
|
|
|
|
$fnret = OVH::Bastion::group_config(group => $group, %{OVH::Bastion::OPT_GROUP_IDLE_KILL_TIMEOUT()});
|
|
if ($fnret && defined $fnret->value && $fnret->value =~ /^-?\d+$/) {
|
|
$ret{'idle_kill_timeout'} = $fnret->value;
|
|
}
|
|
|
|
$fnret = OVH::Bastion::group_config(group => $group, => %{OVH::Bastion::OPT_GROUP_IDLE_LOCK_TIMEOUT()});
|
|
if ($fnret && defined $fnret->value && $fnret->value =~ /^-?\d+$/) {
|
|
$ret{'idle_lock_timeout'} = $fnret->value;
|
|
}
|
|
}
|
|
|
|
# group egress keys if we've been asked those
|
|
if ($withKeys) {
|
|
$fnret = OVH::Bastion::get_group_keys(group => $group);
|
|
if ($fnret and $from) {
|
|
foreach my $keyfile (@{$fnret->value->{'sortedKeys'}}) {
|
|
my $key = $fnret->value->{'keys'}{$keyfile};
|
|
$key->{'prefix'} = $from;
|
|
$ret{'keys'}{$key->{'fingerprint'}} = $key;
|
|
}
|
|
}
|
|
}
|
|
|
|
$return{$shortGroup} = \%ret;
|
|
|
|
# print all this in a human-readable format, except if we've been asked
|
|
# to dump the data for all groups, in which case the caller will only use
|
|
# our JSON output
|
|
print_group_info(%ret) if !$all;
|
|
}
|
|
|
|
sub print_group_info {
|
|
my %ret = @_;
|
|
my $groupName = $ret{'group'};
|
|
|
|
osh_info("Group ${groupName}'s Owners are: "
|
|
. colored(@{$ret{'owners'}} ? join(" ", sort @{$ret{'owners'}}) : '-', 'red'))
|
|
if $ret{'owners'};
|
|
|
|
osh_info("Group ${groupName}'s GateKeepers (managing the members/guests list) are: "
|
|
. colored(@{$ret{'gatekeepers'}} ? join(" ", sort @{$ret{'gatekeepers'}}) : '-', 'red'))
|
|
if $ret{'gatekeepers'};
|
|
|
|
osh_info("Group ${groupName}'s ACLKeepers (managing the group servers list) are: "
|
|
. colored(@{$ret{'aclkeepers'}} ? join(" ", sort @{$ret{'aclkeepers'}}) : '-', 'red'))
|
|
if $ret{'aclkeepers'};
|
|
|
|
osh_info("Group ${groupName}'s Members (with access to ALL the group servers) are: "
|
|
. colored(@{$ret{'members'}} ? join(" ", sort @{$ret{'members'}}) : '-', 'red'))
|
|
if $ret{'members'};
|
|
|
|
# show guest info, along with the number of accesses each guest has
|
|
my @guest_text;
|
|
foreach my $guest (@{$ret{'guests'}}) {
|
|
my $nb = $ret{'guests_accesses'}{$guest};
|
|
push @guest_text, sprintf("%s[%s]", $guest, $nb // '?');
|
|
}
|
|
osh_info("Group ${groupName}'s Guests (with access to SOME of the group servers) are: "
|
|
. colored(@{$ret{'guests'}} ? join(" ", sort @guest_text) : '-', 'red'))
|
|
if $ret{'guests'};
|
|
|
|
# current user doesn't have enough rights to get this info, tell them that
|
|
if (!$ret{'members'}) {
|
|
osh_info "You should ask them if you think you need access for your work tasks.";
|
|
}
|
|
|
|
if (@{$ret{'inactive'}}) {
|
|
osh_info("For your information, the following accounts are inactive: "
|
|
. colored(join(" ", @{$ret{'inactive'}}), "blue"));
|
|
}
|
|
|
|
if ($ret{'mfa_required'}) {
|
|
my %mfa2text = (
|
|
"any" => "",
|
|
"totp" => " (TOTP)",
|
|
"password" => " (password)",
|
|
);
|
|
osh_warn("MFA Required: when connecting to servers of this group, users will be asked for an "
|
|
. "additional authentication factor"
|
|
. $mfa2text{$ret{'mfa_required'}});
|
|
}
|
|
|
|
if ($ret{'guest_ttl_limit'}) {
|
|
osh_warn("Guest TTL enforced: guest accesses must have a TTL with a maximum duration of "
|
|
. OVH::Bastion::duration2human(seconds => $ret{'guest_ttl_limit'})->value->{'duration'});
|
|
}
|
|
|
|
if ($ret{'idle_kill_timeout'}) {
|
|
my $action = "NOT be cut";
|
|
if ($ret{'idle_kill_timeout'} > 0) {
|
|
$action =
|
|
"be cut after " . OVH::Bastion::duration2human(seconds => $ret{'idle_kill_timeout'})->value->{'duration'};
|
|
}
|
|
osh_warn "Specific idle kill timeout: idle sessions on servers of this group will $action";
|
|
}
|
|
|
|
if ($ret{'idle_lock_timeout'}) {
|
|
my $action = "NOT be locked";
|
|
if ($ret{'idle_lock_timeout'} > 0) {
|
|
$action = "be locked after "
|
|
. OVH::Bastion::duration2human(seconds => $ret{'idle_lock_timeout'})->value->{'duration'};
|
|
}
|
|
osh_warn "Specific idle kill timeout: idle sessions on servers of this group will $action";
|
|
}
|
|
|
|
if ($withKeys) {
|
|
osh_info ' ';
|
|
if (!%{$ret{'keys'}}) {
|
|
osh_info "This group has no SSH egress key, the owner may use groupGenerateEgressKey to generate one.";
|
|
}
|
|
elsif (keys %{$ret{'keys'}} == 1) {
|
|
osh_info "The public key of this group is:";
|
|
}
|
|
else {
|
|
osh_info "The public key of this group are:";
|
|
}
|
|
osh_info ' ';
|
|
my @sorted = sort { $ret{'keys'}{$a}{'mtime'} <=> $ret{'keys'}{$b}{'mtime'} } keys %{$ret{'keys'}};
|
|
foreach my $fingerprint (@sorted) {
|
|
OVH::Bastion::print_public_key(key => $ret{'keys'}{$fingerprint});
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (!$all) {
|
|
# only one group, don't return a hash of hash to keep backward compat
|
|
my @keys = keys %return;
|
|
osh_ok $return{$keys[0]};
|
|
}
|
|
else {
|
|
osh_info "If you're only seeing this line, you might want to use --json";
|
|
osh_ok \%return;
|
|
}
|