the-bastion/bin/plugin/open/groupInfo
2023-04-17 14:18:51 +02:00

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;
}