#! /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; } $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 if ($ret{'guests'}) { 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')); } # 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; }