feat: add --all to groupInfo and accountInfo

This commit is contained in:
Stéphane Lesimple 2023-03-15 13:10:24 +00:00 committed by Stéphane Lesimple
parent a1812e34bb
commit 7a825aeec4
11 changed files with 993 additions and 565 deletions

View file

@ -13,90 +13,131 @@ use OVH::Bastion::Plugin qw( :DEFAULT help );
# globally allow sys_getpw* and sys_getgr* cache use
$ENV{'PW_GR_CACHE'} = 1;
my ($group);
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' => \$group},
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
Usage: --osh SCRIPT_NAME <--group GROUP|--all> [OPTIONS]
--group GROUP specify the group to display the infos of
--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
);
#
# code
#
my $fnret;
#
# params check
#
if (!$group) {
if (!$group && !$all) {
help();
osh_exit 'ERR_MISSING_PARAMETER', "Missing 'group' parameter";
osh_exit 'ERR_MISSING_PARAMETER', "Missing '--group' or '--all' parameter";
}
$fnret = OVH::Bastion::is_valid_group_and_existing(group => $group, groupType => "key");
$fnret or osh_exit($fnret);
# get returned untainted value
$group = $fnret->value->{'group'};
my $shortGroup = $fnret->value->{'shortGroup'};
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'; # critical
$roles{$role} = [];
}
else {
$roles{$role} = [grep { $_ ne 'allowkeeper' } @{$fnret->value->{'members'} || []}];
}
my $isAuditor = OVH::Bastion::is_auditor(account => $self);
if (!$isAuditor && $all) {
osh_exit 'ERR_ACCESS_DENIED', "Only bastion auditors can use --all";
}
my $result_hash = {group => $shortGroup};
$result_hash->{'owners'} = $roles{'owner'};
$result_hash->{'aclkeepers'} = $roles{'aclkeeper'};
$result_hash->{'gatekeepers'} = $roles{'gatekeeper'};
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);
}
osh_info "Group "
. $shortGroup
. "'s Owners are: "
. colored(@{$roles{'owner'}} ? join(" ", sort @{$roles{'owner'}}) : '-', "red");
osh_info "Group "
. $shortGroup
. "'s GateKeepers (managing the members/guests list) are: "
. colored(@{$roles{'gatekeeper'}} ? join(" ", sort @{$roles{'gatekeeper'}}) : '-', "red");
if ( 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)
|| OVH::Bastion::is_auditor(account => $self))
{
# 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);
osh_info "Group "
. $shortGroup
. "'s ACLKeepers (managing the group servers list) are: "
. colored(@{$roles{'aclkeeper'}} ? join(" ", sort @{$roles{'aclkeeper'}}) : '-', "red");
# get returned untainted value
push @groups, {group => $fnret->value->{'group'}, shortGroup => $fnret->value->{'shortGroup'}};
}
# now, who is member / guest ?
my (@members, @guests);
foreach my $account (@{$roles{'member'}}) {
osh_debug("what is $account?");
if ($account =~ /^realm_(.+)/) {
my $pRealm = $1;
$fnret = OVH::Bastion::get_remote_accounts_from_realm(realm => $pRealm);
if (!$fnret || !@{$fnret->value}) {
# gather this only once
$fnret = OVH::Bastion::get_bastion_ips();
my $from;
if ($fnret) {
my @ips = @{$fnret->value};
$from = 'from="' . join(',', @ips) . '"';
}
# we couldn't get the list, or the list is empty: at least show that the realm shared account is there
push @members, $user;
}
else {
# 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";
@ -106,141 +147,192 @@ if ( OVH::Bastion::is_group_owner(group => $shortGroup, account => $self, supe
}
}
}
}
else {
if (OVH::Bastion::is_group_guest(account => $account, group => $shortGroup)) {
push @guests, $account;
}
# normal case (non-realm accounts)
else {
push @members, $account;
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;
}
}
}
osh_info "Group "
. $shortGroup
. "'s Members (with access to ALL the group servers) are: "
. colored(@members ? join(" ", sort @members) : '-', "red");
my %guest_details;
$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{'shortGroup'};
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{'gatekeeper'}} ? 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;
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_details{$guest} = $fnret ? scalar(@{$fnret->value}) : '?';
push @guest_text, $guest . "[" . $guest_details{$guest} . "]";
push @filtered_guests, $guest;
foreach my $guest (@{$ret{'guests'}}) {
my $nb = $ret{'guest_accesse'}{$guest};
push @guest_text, sprintf("%s[%s]", $guest, $nb // '?');
}
osh_info "Group "
. $shortGroup
. "'s Guests (with access to SOME of the group servers) are: "
. colored(@filtered_guests ? join(" ", @guest_text) : '-', "red");
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'};
# deprecated in v2.18.00+
$result_hash->{'full_members'} = \@members;
$result_hash->{'partial_members'} = \@filtered_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.";
}
# /deprecated
if (@{$ret{'inactive'}}) {
osh_info("For your information, the following accounts are inactive: "
. colored(join(" ", @{$ret{'inactive'}}), "blue"));
}
$result_hash->{'members'} = \@members;
$result_hash->{'guests'} = \@filtered_guests;
$result_hash->{'guests_accesses'} = \%guest_details;
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'}});
}
my @inactive;
foreach my $account (@members) {
if (OVH::Bastion::is_account_active(account => $account)->is_ko) {
push @inactive, $account;
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'};
}
}
if (@inactive) {
osh_info "For your information, the following accounts are inactive: " . colored(join(" ", @inactive), "blue");
$result_hash->{'inactive'} = \@inactive;
osh_warn "Specific idle kill timeout: idle sessions on servers of this group will $action";
}
# policies
$fnret = OVH::Bastion::group_config(group => $group, key => 'mfa_required');
if ($fnret && $fnret->value eq 'password') {
osh_warn
"MFA Required: when connecting to servers of this group, users will be asked for an additional authentication factor (password)";
}
elsif ($fnret && $fnret->value eq 'totp') {
osh_warn
"MFA Required: when connecting to servers of this group, users will be asked for an additional authentication factor (TOTP)";
}
elsif ($fnret && $fnret->value eq 'any') {
osh_warn
"MFA Required: When connecting to servers of this group, users will be asked for an additional authentication factor";
}
if ($fnret && $fnret->value ne 'none') {
$result_hash->{'mfa_required'} = $fnret->value;
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";
}
$fnret = OVH::Bastion::group_config(group => $group, key => 'guest_ttl_limit');
if ($fnret && defined $fnret->value && $fnret->value =~ /^\d+$/) {
osh_warn "Guest TTL enforced: guest accesses must have a TTL with a maximum duration of "
. OVH::Bastion::duration2human(seconds => $fnret->value)->value->{'duration'};
$result_hash->{'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+$/) {
if ($fnret->value == 0) {
osh_warn "Specific idle kill timeout: idle sessions on servers of this group will NOT be cut";
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_warn "Specific idle kill timeout: idle sessions on servers of this group will be cut after "
. OVH::Bastion::duration2human(seconds => $fnret->value)->value->{'duration'};
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});
}
$result_hash->{'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+$/) {
if ($fnret->value == 0) {
osh_warn "Specific idle lock timeout: idle sessions on servers of this group will NOT be locked";
}
else {
osh_warn "Specific idle lock timeout: idle sessions on servers of this group will be locked after "
. OVH::Bastion::duration2human(seconds => $fnret->value)->value->{'duration'};
}
$result_hash->{'idle_lock_timeout'} = $fnret->value;
}
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 "You should ask him/her/them if you think you need access for your work tasks.";
osh_info "If you're only seeing this line, you might want to use --json";
osh_ok \%return;
}
# get pubkeys with the proper from='' and show them
$fnret = OVH::Bastion::get_bastion_ips();
my $from;
if ($fnret) {
my @ips = @{$fnret->value};
$from = 'from="' . join(',', @ips) . '"';
}
$fnret = OVH::Bastion::get_group_keys(group => $group);
if ($fnret and $from) {
osh_info ' ';
if ($fnret->value && !@{$fnret->value->{'sortedKeys'}}) {
osh_info "This group has no SSH egress key, the owner may use groupGenerateEgressKey to generate one.";
}
elsif (@{$fnret->value->{'sortedKeys'}} == 1) {
osh_info "The public key of this group is:";
}
else {
osh_info "The public keys of this group are:";
}
osh_info ' ';
foreach my $keyfile (@{$fnret->value->{'sortedKeys'}}) {
my $key = $fnret->value->{'keys'}{$keyfile};
$key->{'prefix'} = $from;
OVH::Bastion::print_public_key(key => $key);
$result_hash->{'keys'}{$key->{'fingerprint'}} = $key;
}
}
osh_ok $result_hash;

View file

@ -15,329 +15,523 @@ use OVH::Bastion::Plugin qw( :DEFAULT help );
# globally allow sys_getpw* and sys_getgr* cache use
$ENV{'PW_GR_CACHE'} = 1;
my $withGroups = 0;
my $withPasswordInfo = 0;
my $withEgressKeys = 0;
sub toggle_all {
my $v = shift;
$withGroups = $v;
$withPasswordInfo = $v;
$withEgressKeys = $v;
return;
}
OVH::Bastion::Plugin::begin(
argv => \@ARGV,
header => "account information",
options => {'account=s' => \my $account, "list-groups" => \my $listGroups},
argv => \@ARGV,
header => "account information",
options => {
'account=s' => \my $account,
"all" => \my $all,
"list-groups|with-groups" => sub { $withGroups = 1 },
"without-groups" => sub { $withGroups = 0 },
'with-mfa-password-info' => sub { $withPasswordInfo = 1 },
'without-mfa-password-info' => sub { $withPasswordInfo = 0 },
'with-egress-keys' => sub { $withEgressKeys = 1 },
'without-egress-keys' => sub { $withEgressKeys = 0 },
'with-everything' => sub { toggle_all(1) },
'without-everything' => sub { toggle_all(0) },
},
helptext => <<'EOF',
Display some information about an account
Usage: --osh SCRIPT_NAME --account ACCOUNT [--list-groups]
Usage: --osh SCRIPT_NAME <--account ACCOUNT|--all> [OPTIONS]
--account ACCOUNT The account name to work on
--list-groups Show which groups the account has a role on
--account ACCOUNT The account name to work on
--all Dump info for all accounts (auditors only), use with ``--json``
--with[out]-everything Include or exclude all below options, including future ones
--with[out]-groups Whether to include the groups the account has a role on (SLOW, default: no)
--with[out]-mfa-password-info Whether to include MFA password info of the account (SLOW, auditors only, default: no)
--with[out]-egress-keys Whether to include the account's egress keys (SLOW, auditors only, default: no)
EOF
);
my $fnret;
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account);
$fnret or osh_exit $fnret;
$account = $fnret->value->{'account'};
my $sysaccount = $fnret->value->{'sysaccount'};
my $remoteaccount = $fnret->value->{'remoteaccount'};
# check params
if ($account && $all) {
osh_exit('ERR_INCOMPATIBLE_PARAMETERS', msg => "Can't use both --account and --all");
}
if (($all || $withPasswordInfo || $withEgressKeys) && !OVH::Bastion::is_auditor(account => $self)) {
osh_exit('ERR_ACCESS_DENIED', msg => "This option can only be used by bastion auditors");
}
if (!$account && !$all) {
help();
osh_exit('ERR_MISSING_PARAMETER', msg => "Missing either --account or --all parameter");
}
# gather all accounts if $all, or only use the user-specified account if !$all
my @accountsToCheck;
if ($all) {
$fnret = OVH::Bastion::get_account_list();
$fnret or osh_exit $fnret;
@accountsToCheck = sort keys %{$fnret->value};
osh_info("Gathering data, this may take a few seconds...");
}
else {
@accountsToCheck = ($account);
}
# validate each account and get their corresponding sys/remote name, while also untainting it
my @accounts;
foreach my $anAccount (@accountsToCheck) {
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $anAccount);
$fnret or osh_exit $fnret;
$account = $fnret->value->{'account'};
my $sysaccount = $fnret->value->{'sysaccount'};
my $remoteaccount = $fnret->value->{'remoteaccount'};
push @accounts, {account => $account, sysaccount => $sysaccount, remoteaccount => $remoteaccount};
}
# load these only once
$fnret = OVH::Bastion::get_plugin_list(restrictedOnly => 1);
$fnret or osh_exit $fnret;
my @commands = sort keys %{$fnret->value};
my %ret;
if (OVH::Bastion::is_admin(account => $account)) {
osh_info "$account is a bastion " . colored('admin', 'green');
$ret{'is_admin'} = 1;
}
if (OVH::Bastion::is_super_owner(account => $account)) {
osh_info "$account is a bastion " . colored('superowner', 'green');
$ret{'is_superowner'} = 1;
}
if (OVH::Bastion::is_auditor(account => $account)) {
osh_info "$account is a bastion " . colored('auditor', 'green');
$ret{'is_auditor'} = 1;
}
osh_info "This account has access to the following restricted commands:";
my @granted;
foreach my $plugin (sort keys %{$fnret->value}) {
$fnret = OVH::Bastion::is_user_in_group(user => $account, group => "osh-$plugin");
if ($fnret) {
push @granted, $plugin;
osh_info "- $plugin";
}
}
if (!@granted) {
osh_info "(none)";
}
$ret{'allowed_commands'} = \@granted;
my $result_hash = {};
if ($listGroups) {
my @groups;
if ($withGroups) {
$fnret = OVH::Bastion::get_group_list();
$fnret or osh_exit $fnret;
@groups = sort keys %{$fnret->value};
}
osh_info "\nThis account is part of the following groups:";
# gather info from the account(s)
foreach my $name (sort keys %{$fnret->value}) {
my @flags;
push @flags, 'owner' if OVH::Bastion::is_group_owner(group => $name, account => $account);
push @flags, 'gatekeeper' if OVH::Bastion::is_group_gatekeeper(group => $name, account => $account);
push @flags, 'aclkeeper' if OVH::Bastion::is_group_aclkeeper(group => $name, account => $account);
push @flags, 'member' if OVH::Bastion::is_group_member(group => $name, account => $account);
push @flags, 'guest' if OVH::Bastion::is_group_guest(group => $name, account => $account);
if (@flags) {
my $line = sprintf "%18s", $name;
my %return;
foreach my $accHash (@accounts) {
my %ret;
my ($account, $sysaccount, $remoteaccount) =
($accHash->{'account'}, $accHash->{'sysaccount'}, $accHash->{'remoteaccount'});
$ret{'account'} = $account;
if (OVH::Bastion::is_admin(account => $account)) {
$ret{'is_admin'} = 1;
}
if (OVH::Bastion::is_super_owner(account => $account)) {
$ret{'is_superowner'} = 1;
}
if (OVH::Bastion::is_auditor(account => $account)) {
$ret{'is_auditor'} = 1;
}
my @granted;
foreach my $plugin (@commands) {
$fnret = OVH::Bastion::is_user_in_group(user => $account, group => "osh-$plugin");
push @granted, $plugin if $fnret;
}
$ret{'allowed_commands'} = \@granted;
my $groups_hash = {};
if ($withGroups) {
foreach my $name (@groups) {
my @flags;
push @flags, 'owner' if OVH::Bastion::is_group_owner(group => $name, account => $account);
push @flags, 'gatekeeper' if OVH::Bastion::is_group_gatekeeper(group => $name, account => $account);
push @flags, 'aclkeeper' if OVH::Bastion::is_group_aclkeeper(group => $name, account => $account);
push @flags, 'member' if OVH::Bastion::is_group_member(group => $name, account => $account);
push @flags, 'guest' if OVH::Bastion::is_group_guest(group => $name, account => $account);
$groups_hash->{$name} = {flags => \@flags, name => $name} if @flags;
}
}
$ret{'groups'} = $groups_hash;
my $canConnect = 1;
$ret{'always_active'} = OVH::Bastion::account_config(
account => $account,
key => OVH::Bastion::OPT_ACCOUNT_ALWAYS_ACTIVE,
public => 1
) ? 1 : 0;
if ($ret{'always_active'}) {
$ret{'is_active'} = 1;
}
else {
$fnret = OVH::Bastion::is_account_active(account => $account);
if ($fnret->is_ok) {
$ret{'is_active'} = 1;
}
elsif ($fnret->is_ko) {
$canConnect = 0;
$ret{'is_active'} = 0;
}
}
if (OVH::Bastion::is_auditor(account => $self)) {
# TTL check
$fnret = OVH::Bastion::is_account_ttl_nonexpired(sysaccount => $sysaccount, account => $account);
if ($fnret->is_ok && $fnret->err eq 'OK_NO_TTL') {
$ret{'is_ttl_set'} = 0;
$ret{'is_ttl_expired'} = 0;
}
elsif ($fnret->is_ok && $fnret->err eq 'OK_TTL_VALID') {
$ret{'is_ttl_set'} = 1;
$ret{'is_ttl_expired'} = 0;
}
elsif ($fnret->is_ko) {
$canConnect = 0;
$ret{'is_ttl_set'} = 1;
$ret{'is_ttl_expired'} = 1;
}
else {
osh_warn "Error getting account TTL expiration info (" . $fnret->msg . ")";
}
$ret{'ttl_timestamp'} =
($fnret->value && $fnret->value->{'expiry_time'}) ? $fnret->value->{'expiry_time'} : undef;
# freeze check
$fnret = OVH::Bastion::is_account_nonfrozen(account => $account);
$ret{'is_frozen'} = undef;
if ($fnret->is_ok) {
$ret{'is_frozen'} = 0;
}
elsif ($fnret->is_ko) {
$ret{'is_frozen'} = 1;
$ret{'freeze_info'} = $fnret->value;
$canConnect = 0;
}
# expi check
$fnret = OVH::Bastion::is_account_nonexpired(sysaccount => $sysaccount, remoteaccount => $remoteaccount);
if ($fnret->is_ok) {
$ret{'is_expired'} = 0;
}
elsif ($fnret->is_ko) {
$canConnect = 0;
$ret{'is_expired'} = 1;
}
else {
osh_warn "Error getting account expiration info (" . $fnret->msg . ")";
}
if (!$fnret->is_err) {
$ret{'can_connect'} = $canConnect;
if ($fnret->value->{'already_seen_before'}) {
$ret{'already_seen_before'} = 1;
if (defined $fnret->value->{'seconds'}) {
$fnret = OVH::Bastion::duration2human(seconds => $fnret->value->{'seconds'}, tense => "past");
if ($fnret) {
$ret{'last_activity'}{$_} = $fnret->value->{$_} for qw{ datetime_local datetime_utc };
$ret{'last_activity'}{'ago'} = $fnret->value->{'duration'};
$ret{'last_activity'}{'timestamp'} = time() - $fnret->value->{'seconds'};
}
}
}
else {
$ret{'already_seen_before'} = 0;
}
}
$fnret = OVH::Bastion::account_config(account => $account, key => "creation_info");
if ($fnret) {
my $creation_info;
eval { $creation_info = decode_json($fnret->value); };
if ($@) {
osh_warn(
"While reading creation metadata information for account '$account', couldn't decode JSON: $@");
}
else {
$ret{'creation_information'} = $creation_info;
}
}
$fnret = OVH::Bastion::account_ssh_config_get(account => $account);
if ($fnret->err eq 'OK_EMPTY') {
$ret{'account_egress_ssh_config'}{'type'} = 'default';
}
elsif ($fnret->err eq 'ERR_FILE_LOCALLY_MODIFIED') {
$ret{'account_egress_ssh_config'}{'type'} = 'locally_modified';
}
elsif ($fnret) {
$ret{'account_egress_ssh_config'}{'type'} = 'custom';
foreach my $key (sort keys %{$fnret->value}) {
$ret{'account_egress_ssh_config'}{'items'}{$key} = $fnret->value->{$key};
}
}
else {
$ret{'account_egress_ssh_config'}{'type'} = 'unknown';
}
$fnret = OVH::Bastion::account_config(
account => $account,
public => 1,
key => OVH::Bastion::OPT_ACCOUNT_INGRESS_PIV_POLICY
);
$ret{'ingress_piv_enforced'} = ($fnret && $fnret->value eq 'yes') ? 1 : 0; # keep for backwards compat
$ret{'ingress_piv_policy'} = $fnret->value || undef;
$fnret = OVH::Bastion::account_config(
account => $account,
public => 1,
key => OVH::Bastion::OPT_ACCOUNT_INGRESS_PIV_GRACE
);
if ($fnret && $fnret->value > time()) {
my $expiry = $fnret->value - time();
my $human = OVH::Bastion::duration2human(seconds => $expiry)->value;
$ret{'ingress_piv_grace'} = {
enabled => 1,
expiration_timestamp => $fnret->value,
seconds_remaining => $expiry,
expiration_date => $human->{'date'},
time_remaining => $human->{'duration'},
};
}
else {
$ret{'ingress_piv_grace'} = {enabled => 0};
}
$ret{'global_ingress_policy'} = !!OVH::Bastion::config('ingressRequirePIV')->value + 0;
$ret{'effective_ingress_piv_policy'} =
!!OVH::Bastion::is_effective_piv_account_policy_enabled(account => $account)->is_ok + 0;
$ret{'mfa_password_required'} =
!!OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::MFA_PASSWORD_REQUIRED_GROUP) + 0;
$ret{'mfa_password_bypass'} =
!!OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::MFA_PASSWORD_BYPASS_GROUP) + 0;
$ret{'mfa_password_configured'} =
!!OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::MFA_PASSWORD_CONFIGURED_GROUP) + 0;
$ret{'mfa_totp_required'} =
!!OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::MFA_TOTP_REQUIRED_GROUP) + 0;
$ret{'mfa_totp_bypass'} =
!!OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::MFA_TOTP_BYPASS_GROUP) + 0;
$ret{'mfa_totp_configured'} =
!!OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::MFA_TOTP_CONFIGURED_GROUP) + 0;
$ret{'pam_auth_bypass'} =
!!OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::PAM_AUTH_BYPASS_GROUP) + 0;
$ret{'pubkey_auth_optional'} =
!!OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::OSH_PUBKEY_AUTH_OPTIONAL_GROUP) + 0;
$ret{'personal_egress_mfa_required'} =
OVH::Bastion::account_config(account => $account, key => "personal_egress_mfa_required")->value;
$ret{'personal_egress_mfa_required'} ||= 'none'; # no config means no mfa
$ret{'idle_ignore'} = !!OVH::Bastion::account_config(
account => $account,
key => OVH::Bastion::OPT_ACCOUNT_IDLE_IGNORE,
public => 1
) + 0;
$ret{'max_inactive_days'} =
OVH::Bastion::account_config(account => $account, %{OVH::Bastion::OPT_ACCOUNT_MAX_INACTIVE_DAYS()})->value;
if ($withPasswordInfo) {
my @command = qw{ sudo -n -u root -- /usr/bin/env perl -T };
push @command, $OVH::Bastion::BASEPATH . '/bin/helper/osh-accountGetPasswordInfo';
push @command, '--account', $account;
$fnret = OVH::Bastion::helper(cmd => \@command);
if ($fnret) {
$ret{'password'}{$_} = $fnret->value->{$_} for (keys %{$fnret->value});
}
}
}
$return{$account} = \%ret;
# print all this in a human-readable format, except if we've been asked
# to dump the data for all accounts, in which case the caller will only use
# our JSON output
print_account_info(%ret) if !$all;
}
sub print_account_info {
my %ret = @_;
my $acc = $ret{'account'};
osh_info("$acc is a bastion " . colored('admin', 'green')) if $ret{'is_admin'};
osh_info("$account is a bastion " . colored('superowner', 'green')) if $ret{'is_superowner'};
osh_info("$account is a bastion " . colored('auditor', 'green')) if $ret{'is_auditor'};
osh_info "This account has access to the following restricted commands:";
osh_info("- $_") for @{$ret{'allowed_commands'}};
osh_info "(none)" if (!@{$ret{'allowed_commands'}});
if ($withGroups) {
osh_info("\nThis account is part of the following groups:");
foreach my $groupName (sort keys %{$ret{'groups'}}) {
my @flags = @{$ret{'groups'}{$groupName}{'flags'}};
my $line = sprintf "%18s", $groupName;
$line .= sprintf " %14s", colored(grep({ $_ eq 'owner' } @flags) ? 'Owner' : '-', 'red');
$line .= sprintf " %19s", colored(grep({ $_ eq 'gatekeeper' } @flags) ? 'GateKeeper' : '-', 'yellow');
$line .= sprintf " %18s", colored(grep({ $_ eq 'aclkeeper' } @flags) ? 'ACLKeeper' : '-', 'magenta');
$line .= sprintf " %15s", colored(grep({ $_ eq 'member' } @flags) ? 'Member' : '-', 'green');
$line .= sprintf " %14s", colored(grep({ $_ eq 'guest' } @flags) ? 'Guest' : '-', 'cyan');
osh_info $line;
$result_hash->{$name} = {flags => \@flags, name => $name};
osh_info($line);
}
osh_info("(none)") if not %{$ret{'groups'}};
osh_info("\n");
}
if (not keys %$result_hash) {
osh_info "(none)";
}
osh_info "\n";
}
$ret{'groups'} = $result_hash;
my $canConnect = 1;
$ret{'always_active'} = OVH::Bastion::account_config(
account => $account,
key => OVH::Bastion::OPT_ACCOUNT_ALWAYS_ACTIVE,
public => 1
) ? 1 : 0;
if ($ret{'always_active'}) {
$ret{'is_active'} = 1;
osh_info "This account is " . colored('always', 'green') . " active";
}
else {
$fnret = OVH::Bastion::is_account_active(account => $account);
if ($fnret->is_ok) {
osh_info "\nThis account is " . colored('active', 'green');
$ret{'is_active'} = 1;
if ($ret{'always_active'}) {
osh_info("This account is " . colored('always', 'green') . " active");
}
elsif ($fnret->is_ko) {
elsif ($ret{'is_active'}) {
osh_info("This account is " . colored('active', 'green'));
}
else {
osh_info "\nThis account is " . colored('INACTIVE', 'red');
$canConnect = 0;
$ret{'is_active'} = 0;
}
}
if (OVH::Bastion::is_auditor(account => $self)) {
# TTL check
$fnret = OVH::Bastion::is_account_ttl_nonexpired(sysaccount => $sysaccount, account => $account);
if ($fnret->is_ok && $fnret->err eq 'OK_NO_TTL') {
osh_info "This account has " . colored('no TTL set', 'green');
$ret{'is_ttl_expired'} = 0;
}
elsif ($fnret->is_ok && $fnret->err eq 'OK_TTL_VALID') {
osh_info "This account "
. colored('TTL is still valid', 'green')
. " (for "
. $fnret->value->{'details'}{'human'} . ")";
$ret{'is_ttl_expired'} = 0;
}
elsif ($fnret->is_ko) {
osh_info "This account "
. colored('TTL is EXPIRED', 'red')
. " (since "
. $fnret->value->{'details'}{'human'} . ")";
$canConnect = 0;
$ret{'is_ttl_expired'} = 1;
}
else {
osh_warn "Error getting account TTL expiration info (" . $fnret->msg . ")";
}
$ret{'ttl_timestamp'} = ($fnret && $fnret->value) ? $fnret->value->{'expiry_time'} : undef;
# freeze check
$fnret = OVH::Bastion::is_account_nonfrozen(account => $account);
$ret{'is_frozen'} = undef;
if ($fnret->is_ok) {
osh_info "This account is " . colored('not frozen', 'green');
$ret{'is_frozen'} = 0;
}
elsif ($fnret->is_ko) {
my $freezeReason = $fnret->value->{'reason'} || 'no reason given';
osh_info "This account has been "
. colored('FROZEN', 'red') . " by "
. $fnret->value->{'by'}
. " ($freezeReason)";
$ret{'is_frozen'} = 1;
$ret{'freeze_info'} = $fnret->value;
$canConnect = 0;
}
# expi check
$fnret = OVH::Bastion::is_account_nonexpired(sysaccount => $sysaccount, remoteaccount => $remoteaccount);
if ($fnret->is_ok) {
osh_info "This account has seen recent-enough activity to " . colored('not be activity-expired', 'green');
$ret{'is_expired'} = 0;
}
elsif ($fnret->is_ko) {
osh_info "This account is "
. colored('EXPIRED', 'red')
. " activity-wise (it hasn't been seen for a long time)";
$canConnect = 0;
$ret{'is_expired'} = 1;
}
else {
osh_warn "Error getting account expiration info (" . $fnret->msg . ")";
if (defined $ret{'is_ttl_set'}) {
my $human = '?';
if (!$ret{'is_ttl_set'}) {
osh_info("This account has " . colored('no TTL set', 'green'));
}
elsif (!$ret{'is_ttl_expired'}) {
$fnret = OVH::Bastion::duration2human(seconds => $ret{'ttl_timestamp'} - time());
$human = $fnret->value->{'human'};
osh_info "This account " . colored('TTL is still valid', 'green') . " (for $human)";
}
else {
$fnret = OVH::Bastion::duration2human(seconds => time() - $ret{'ttl_timestamp'}, tense => "past");
$human = $fnret->value->{'human'};
osh_info "This account " . colored('TTL is EXPIRED', 'red') . " (since $human)";
}
}
if (!$fnret->is_err) {
if (defined $ret{'is_frozen'}) {
if (!$ret{'is_frozen'}) {
osh_info "This account is " . colored('not frozen', 'green');
}
else {
my $freezeReason = $ret{'freeze_info'}{'reason'} || 'no reason given';
my $freezeBy = $ret{'freeze_info'}{'by'} || '(unknown)';
osh_info "This account has been " . colored('FROZEN', 'red') . " by $freezeBy ($freezeReason)";
}
}
if (defined $ret{'is_expired'}) {
if ($ret{'is_expired'}) {
osh_info "This account is "
. colored('EXPIRED', 'red')
. " activity-wise (it hasn't been seen for a long time)";
}
else {
osh_info "This account has seen recent-enough activity to " . colored('not be activity-expired', 'green');
}
}
if (defined $ret{'can_connect'}) {
osh_info "As a consequence, this account "
. ($canConnect ? colored("can", 'green') : colored("CANNOT", 'red'))
. ($ret{'can_connect'} ? colored("can", 'green') : colored("CANNOT", 'red'))
. " connect to this bastion\n\n";
$ret{'can_connect'} = $canConnect;
}
if ($fnret->value->{'already_seen_before'}) {
$ret{'already_seen_before'} = 1;
if (defined $fnret->value->{'seconds'}) {
$fnret = OVH::Bastion::duration2human(seconds => $fnret->value->{'seconds'}, tense => "past");
if ($fnret) {
my $seenBeforeStr = $fnret->value->{'datetime_utc'};
if ( $fnret->value->{'datetime_local'}
&& $fnret->value->{'datetime_utc'} ne $fnret->value->{'datetime_local'})
{
$seenBeforeStr .= " / " . $fnret->value->{'datetime_local'};
}
$seenBeforeStr = sprintf(
"Last seen on %s (%s ago)",
colored($seenBeforeStr, 'magenta'),
$fnret->value->{'duration'},
);
osh_info $seenBeforeStr;
$ret{'last_activity'}{$_} = $fnret->value->{$_} for qw{ datetime_local datetime_utc };
$ret{'last_activity'}{'ago'} = $fnret->value->{'duration'};
$ret{'last_activity'}{'timestamp'} = time() - $fnret->value->{'seconds'};
if (defined $ret{'already_seen_before'}) {
if ($ret{'already_seen_before'}) {
if ($ret{'last_activity'}) {
my $seenBeforeStr = $ret{'last_activity'}{'datetime_utc'};
if ( $ret{'last_activity'}{'datetime_local'}
&& $ret{'last_activity'}{'datetime_utc'} ne $ret{'last_activity'}{'datetime_local'})
{
$seenBeforeStr .= " / " . $ret{'last_activity'}{'datetime_local'};
}
$seenBeforeStr = sprintf("Last seen on %s (%s ago)", colored($seenBeforeStr, 'magenta'),
$ret{'last_activity'}{'ago'},);
osh_info($seenBeforeStr);
}
else {
osh_info("This account has already been used at least once");
}
}
else {
osh_info "This account has " . colored('NEVER', 'red') . " been used (yet)";
$ret{'already_seen_before'} = 0;
osh_info("This account has " . colored('NEVER', 'red') . " been used (yet)");
}
}
$fnret = OVH::Bastion::account_config(account => $account, key => "creation_info");
if ($fnret) {
my $creation_info;
eval { $creation_info = decode_json($fnret->value); };
if ($@) {
osh_warn("While reading creation metadata information for account '$account', couldn't decode JSON: $@");
if ($ret{'creation_information'}) {
if ($ret{'creation_information'}{'datetime_utc'}) {
my $createdOnStr = $ret{'creation_information'}{'datetime_utc'};
if ( $ret{'creation_information'}{'datetime_local'}
&& $ret{'creation_information'}{'datetime_utc'} ne $ret{'creation_information'}{'datetime_local'})
{
$createdOnStr .= " / " . $ret{'creation_information'}{'datetime_local'};
}
$createdOnStr = sprintf(
"Created on %s (%s ago)",
colored($createdOnStr, 'magenta'),
OVH::Bastion::duration2human(seconds => time() - $ret{'creation_information'}{'timestamp'})
->value->{'duration'}
);
osh_info($createdOnStr);
}
else {
$ret{'creation_information'} = $creation_info;
if ($creation_info->{'datetime_utc'}) {
my $createdOnStr = $creation_info->{'datetime_utc'};
if ( $creation_info->{'datetime_local'}
&& $creation_info->{'datetime_utc'} ne $creation_info->{'datetime_local'})
{
$createdOnStr .= " / " . $creation_info->{'datetime_local'};
}
$createdOnStr = sprintf(
"Created on %s (%s ago)",
colored($createdOnStr, 'magenta'),
OVH::Bastion::duration2human(seconds => time() - $creation_info->{'timestamp'})->value->{'duration'}
);
osh_info $createdOnStr;
}
if ($creation_info->{'by'}) {
osh_info "Created by " . colored($creation_info->{'by'}, 'magenta');
}
if ($creation_info->{'bastion_version'}) {
osh_info "Created using The Bastion " . colored('v' . $creation_info->{'bastion_version'}, 'magenta');
}
if ($creation_info->{'comment'}) {
osh_info "Creation with the following comment: " . colored($creation_info->{'comment'}, 'magenta');
}
if ($ret{'creation_information'}{'by'}) {
osh_info("Created by " . colored($ret{'creation_information'}{'by'}, 'magenta'));
}
if ($ret{'creation_information'}{'bastion_version'}) {
osh_info("Created using The Bastion "
. colored('v' . $ret{'creation_information'}{'bastion_version'}, 'magenta'));
}
if ($ret{'creation_information'}{'comment'}) {
osh_info(
"Creation with the following comment: " . colored($ret{'creation_information'}{'comment'}, 'magenta'));
}
}
osh_info "\nAccount egress SSH config:";
$fnret = OVH::Bastion::account_ssh_config_get(account => $account);
if ($fnret->err eq 'OK_EMPTY') {
if ($ret{'account_egress_ssh_config'}{'type'} eq 'default') {
osh_info "- (default)";
$ret{'account_egress_ssh_config'}{'type'} = 'default';
}
elsif ($fnret->err eq 'ERR_FILE_LOCALLY_MODIFIED') {
elsif ($ret{'account_egress_ssh_config'}{'type'} eq 'locally_modified') {
osh_info "- (locally modified!)";
$ret{'account_egress_ssh_config'}{'type'} = 'locally_modified';
}
elsif ($fnret) {
$ret{'account_egress_ssh_config'}{'type'} = 'custom';
foreach my $key (sort keys %{$fnret->value}) {
osh_info "- $key " . $fnret->value->{$key};
$ret{'account_egress_ssh_config'}{'items'}{$key} = $fnret->value->{$key};
elsif ($ret{'account_egress_ssh_config'}{'type'} eq 'custom') {
foreach my $key (sort keys %{$ret{'account_egress_ssh_config'}{'items'} || {}}) {
osh_info "- $key " . $ret{'account_egress_ssh_config'}{'items'}{$key};
}
}
else {
$ret{'account_egress_ssh_config'}{'type'} = 'unknown';
osh_info "- (unknown: " . $fnret . ")";
osh_info "- (unknown)";
}
osh_info "\nAccount PIV-only policy status:";
$fnret = OVH::Bastion::account_config(
account => $account,
public => 1,
key => OVH::Bastion::OPT_ACCOUNT_INGRESS_PIV_POLICY
);
$ret{'ingress_piv_enforced'} = ($fnret && $fnret->value eq 'yes') ? 1 : 0; # keep for backwards compat
$ret{'ingress_piv_policy'} = $fnret->value || undef;
my $ingress_piv_policy_print = $ret{'ingress_piv_policy'} || 'default';
osh_info "- PIV policy for ingress keys on this account is set to "
. colored($ingress_piv_policy_print, $ingress_piv_policy_print eq 'default' ? 'blue' : 'green');
$fnret = OVH::Bastion::account_config(
account => $account,
public => 1,
key => OVH::Bastion::OPT_ACCOUNT_INGRESS_PIV_GRACE
);
if ($fnret && $fnret->value > time()) {
my $expiry = $fnret->value - time();
my $human = OVH::Bastion::duration2human(seconds => $expiry)->value;
osh_info "- PIV grace period for this account is "
. colored('set', 'green')
. " and expires in "
. $human->{'human'};
$ret{'ingress_piv_grace'} = {
enabled => 1,
expiration_timestamp => $fnret->value,
seconds_remaining => $expiry,
expiration_date => $human->{'date'},
time_remaining => $human->{'duration'},
};
if ($ret{'ingress_piv_grace'} && $ret{'ingress_piv_grace'}{'seconds_remaining'}) {
$fnret = OVH::Bastion::duration2human(seconds => $ret{'ingress_piv_grace'}{'seconds_remaining'})->value;
osh_info("- PIV grace period for this account is "
. colored('set', 'green')
. " and expires in "
. $fnret->value->{'human'});
}
else {
osh_info "- PIV grace period for this account is " . colored('inactive', 'blue');
$ret{'ingress_piv_grace'} = {enabled => 0};
}
$fnret = OVH::Bastion::config('ingressRequirePIV')->value;
$ret{'global_ingress_policy'} = $fnret = OVH::Bastion::config('ingressRequirePIV')->value ? 1 : 0;
osh_info "- Global PIV policy status is "
. ($ret{'global_ingress_policy'} ? colored('enabled', 'red') : colored('disabled', 'blue'));
$fnret = OVH::Bastion::is_effective_piv_account_policy_enabled(account => $account);
$ret{'effective_ingress_piv_policy'} = $fnret->is_ok ? 1 : 0;
osh_info "- As a consequence, PIV policy is "
. ($ret{'effective_ingress_piv_policy'} ? colored('enforced', 'red') : colored('inactive', 'blue'))
. " for this account";
osh_info "\nAccount Multi-Factor Authentication status:";
$ret{'mfa_password_required'} =
OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::MFA_PASSWORD_REQUIRED_GROUP) ? 1 : 0;
$ret{'mfa_password_bypass'} =
OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::MFA_PASSWORD_BYPASS_GROUP) ? 1 : 0;
$ret{'mfa_password_configured'} =
OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::MFA_PASSWORD_CONFIGURED_GROUP) ? 1 : 0;
osh_info "- Additional password authentication is "
. ($ret{'mfa_password_required'} ? colored('required', 'green') : colored('not required', 'blue'))
. " for this account";
@ -347,12 +541,6 @@ if (OVH::Bastion::is_auditor(account => $self)) {
osh_info "- Additional password authentication is "
. ($ret{'mfa_password_configured'} ? colored('enabled and active', 'green') : colored('disabled', 'blue'));
$ret{'mfa_totp_required'} =
OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::MFA_TOTP_REQUIRED_GROUP) ? 1 : 0;
$ret{'mfa_totp_bypass'} =
OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::MFA_TOTP_BYPASS_GROUP) ? 1 : 0;
$ret{'mfa_totp_configured'} =
OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::MFA_TOTP_CONFIGURED_GROUP) ? 1 : 0;
osh_info "- Additional TOTP authentication is "
. ($ret{'mfa_totp_required'} ? colored('required', 'green') : colored('not required', 'blue'))
. " for this account";
@ -362,31 +550,18 @@ if (OVH::Bastion::is_auditor(account => $self)) {
osh_info "- Additional TOTP authentication is "
. ($ret{'mfa_totp_configured'} ? colored('enabled and active', 'green') : colored('disabled', 'blue'));
$ret{'pam_auth_bypass'} =
OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::PAM_AUTH_BYPASS_GROUP) ? 1 : 0;
osh_info "- PAM authentication bypass is "
. ($ret{'pam_auth_bypass'} ? colored('enabled', 'green') : colored('disabled', 'blue'));
$ret{'pubkey_auth_optional'} =
OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::OSH_PUBKEY_AUTH_OPTIONAL_GROUP) ? 1 : 0;
osh_info "- Optional public key authentication is "
. ($ret{'pubkey_auth_optional'} ? colored('enabled', 'green') : colored('disabled', 'blue'));
$ret{'personal_egress_mfa_required'} =
OVH::Bastion::account_config(account => $account, key => "personal_egress_mfa_required")->value;
$ret{'personal_egress_mfa_required'} ||= 'none'; # no config means no mfa
osh_info "- MFA policy on personal accesses (using personal keys) on egress side is: "
. $ret{'personal_egress_mfa_required'};
$ret{'idle_ignore'} =
OVH::Bastion::account_config(account => $account, key => OVH::Bastion::OPT_ACCOUNT_IDLE_IGNORE, public => 1)
? 1
: 0;
osh_info "\n- Account is immune to idle counter-measures: "
. ($ret{'idle_ignore'} ? colored('yes', 'green') : colored('no', 'blue'));
$ret{'max_inactive_days'} =
OVH::Bastion::account_config(account => $account, %{OVH::Bastion::OPT_ACCOUNT_MAX_INACTIVE_DAYS()})->value;
if (!defined $ret{'max_inactive_days'}) {
osh_info "- Maximum number of days of inactivity before account is disabled: (default)";
}
@ -397,12 +572,7 @@ if (OVH::Bastion::is_auditor(account => $self)) {
osh_info "- Maximum number of days of inactivity before account is disabled: " . $ret{'max_inactive_days'};
}
my @command = qw{ sudo -n -u root -- /usr/bin/env perl -T };
push @command, $OVH::Bastion::BASEPATH . '/bin/helper/osh-accountGetPasswordInfo';
push @command, '--account', $account;
$fnret = OVH::Bastion::helper(cmd => \@command);
if ($fnret) {
$ret{'password'}{$_} = $fnret->value->{$_} for (keys %{$fnret->value});
if ($ret{'password'}) {
osh_info "Account PAM UNIX password information (used for password MFA):";
if ($ret{'password'}{'password'} eq 'locked') {
osh_info "- No valid password is set";
@ -432,6 +602,16 @@ if (OVH::Bastion::is_auditor(account => $self)) {
}
}
}
return;
}
osh_ok(\%ret);
if (!$all) {
# only one account, 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;
}

View file

@ -1,47 +1,71 @@
Usage examples
==============
Show info about a specific account::
--osh accountInfo --account jdoe12
Gather info about all accounts, with no extra data except their egress keys::
--osh accountInfo --all --without-everything --with-egress-keys --json
Gather info about all accounts, including all extra data (and possibly future options)::
--osh accountInfo --all --with-everything --json
Output example
==============
::
~ user1 is a bastion admin
~ user1 is a bastion superowner
~ user1 is a bastion auditor
~ user1 has access to the following restricted commands:
~ - accountCreate
~ - accountDelete
~ - groupCreate
~ - groupDelete
~
~ This account is part of the following groups:
~ testgroup1 Owner GateKeeper ACLKeeper Member -
~ gatekeeper-grp2 Owner GateKeeper - - -
~
~ This account is active
~ This account is not expired
~ As a consequence, this account can connect to this bastion
~
~ This account has already been used at least once
~ Last seen on Wed 2020-07-15 12:06:27 UTC (00:00:00 ago)
~
~ Account egress SSH config:
~ - (default)
~
~ PIV-enforced policy for ingress keys on this account is enabled
~
~ Account Multi-Factor Authentication status:
~ - Additional password authentication is not required for this account
~ - Additional password authentication bypass is disabled for this account
~ - Additional password authentication is enabled and active
~ - Additional TOTP authentication is not required for this account
~ - Additional TOTP authentication bypass is disabled for this account
~ - Additional TOTP authentication is disabled
~ - PAM authentication bypass is disabled
~ - Optional public key authentication is disabled
~ - MFA policy on personal accesses (using personal keys) on egress side is: password
│ user1 is a bastion admin
│ user1 is a bastion superowner
│ user1 is a bastion auditor
│ user1 has access to the following restricted commands:
│ - accountCreate
│ - accountDelete
│ - groupCreate
│ - groupDelete
│ This account is part of the following groups:
│ testgroup1 Owner GateKeeper ACLKeeper Member -
│ gatekeeper-grp2 Owner GateKeeper - - -
│ This account is active
│ This account has no TTL set
│ This account is not frozen
│ This account has seen recent-enough activity to not be activity-expired
│ As a consequence, this account can connect to this bastion
│ Last seen on Thu 2023-03-16 07:51:49 UTC (00:00:00 ago)
│ Created on Fri 2022-06-17 09:52:50 UTC (271d+21:58:59 ago)
│ Created by jdoe
│ Created using The Bastion v3.08.01
│ Account egress SSH config:
│ - (default)
│ PIV-enforced policy for ingress keys on this account is enabled
│ Account Multi-Factor Authentication status:
│ - Additional password authentication is not required for this account
│ - Additional password authentication bypass is disabled for this account
│ - Additional password authentication is enabled and active
│ - Additional TOTP authentication is not required for this account
│ - Additional TOTP authentication bypass is disabled for this account
│ - Additional TOTP authentication is disabled
│ - PAM authentication bypass is disabled
│ - Optional public key authentication is disabled
│ - MFA policy on personal accesses (using personal keys) on egress side is: password
│ - Account is immune to idle counter-measures: no
│ - Maximum number of days of inactivity before account is disabled: (default)
│ Account PAM UNIX password information (used for password MFA):
│ - Password is set
│ - Password was last changed on 2023-01-27
│ - Password must be changed every 90 days at least
│ - A warning is displayed 75 days before expiration
│ - Account will not be disabled after password expiration
~ Account PAM UNIX password information (used for password MFA):
~ - Password is set
~ - Password was last changed on 2020-04-27
~ - Password must be changed every 90 days at least
~ - A warning is displayed 75 days before expiration
~ - Account will not be disabled after password expiration

View file

@ -1,18 +1,33 @@
Usage examples
==============
Show info about a specific group::
--osh groupInfo --group mygroup2
Gather info about all groups, with no extra data except their keys::
--osh groupInfo --all --without-everything --with-keys --json
Gather info about all groups, including all extra data (and possibly future options)::
--osh groupInfo --all --with-everything --json
Output example
==============
::
~ Group mygroup's Owners are: user1
~ Group mygroup's GateKeepers (managing the members/guests list) are: user2
~ Group mygroup's ACLKeepers (managing the group servers list) are: user3
~ Group mygroup's Members (with access to ALL the group servers) are: user4
~ Group mygroup's Guests (with access to SOME of the group servers) are: user5
~
~ The public key of this group is:
~
~ fingerprint: SHA256:r/PQS4wLdSWqjYsDca8ReKjhq0l9EX+zQgiUR5qKdlc (ED25519-256) [2018/04/16]
~ keyline follows, please copy the *whole* line:
| Group mygroup's Owners are: user1
| Group mygroup's GateKeepers (managing the members/guests list) are: user2
| Group mygroup's ACLKeepers (managing the group servers list) are: user3
| Group mygroup's Members (with access to ALL the group servers) are: user4
| Group mygroup's Guests (with access to SOME of the group servers) are: user5
|
| The public key of this group is:
|
| fingerprint: SHA256:r/PQS4wLdSWqjYsDca8ReKjhq0l9EX+zQgiUR5qKdlc (ED25519-256) [2018/04/16]
| keyline follows, please copy the *whole* line:
from="203.0.113.4/32,192.0.2.0/26" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILdD60bA3NgaOpRLgcACWfKcAMRQQRyFMppwp5GpHLTB mygroup@testbastion:1523886640
The first paragraph of the output lists the different roles along with the people having these roles.

View file

@ -44,7 +44,7 @@ do
else
perl "$pluginfile" '' '' '' '' | perl -e 'undef $/; $_=<>; s/\n+$/\n/; print $_' | perl -ne '
if (m{^Usage: (.+)}) { print ".. admonition:: usage\n :class: cmdusage\n\n $1\n\n.. program:: '"$name"'\n\n"; }
elsif (m{^ (-[- ,a-z|/A-Z"'"'"']+) (.+)}) { ($c,$t)=($1,$2); $c=~s/ +$//; print ".. option:: $c\n\n $t\n\n"; }
elsif (m{^ (-[- ,a-z|/A-Z"'"'"'\[\]]+) (.+)}) { ($c,$t)=($1,$2); $c=~s/ +$//; print ".. option:: $c\n\n $t\n\n"; }
elsif ($l++ == 0) { chomp; print "$_\n"."="x(length($_))."\n\n"; }
else { print "$_"; }
'

View file

@ -62,6 +62,9 @@ source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# Default syntax highlighting language.
highlight_language = 'shell'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#

View file

@ -9,30 +9,58 @@ Print some basic information about a group
.. admonition:: usage
:class: cmdusage
--osh groupInfo --group GROUP
--osh groupInfo <--group GROUP|--all> [OPTIONS]
.. program:: groupInfo
.. option:: --group GROUP
specify the group to display the infos of
Specify the group to display the info of
.. option:: --all
Dump info for all groups (auditors only), use with ``--json``
.. option:: --with[out]-everything
Include or exclude all below options, including future ones
.. option:: --with[out]-keys
Whether to include the group keys list (slow-ish, default: yes)
Usage examples
==============
Show info about a specific group::
--osh groupInfo --group mygroup2
Gather info about all groups, with no extra data except their keys::
--osh groupInfo --all --without-everything --with-keys --json
Gather info about all groups, including all extra data (and possibly future options)::
--osh groupInfo --all --with-everything --json
Output example
==============
::
~ Group mygroup's Owners are: user1
~ Group mygroup's GateKeepers (managing the members/guests list) are: user2
~ Group mygroup's ACLKeepers (managing the group servers list) are: user3
~ Group mygroup's Members (with access to ALL the group servers) are: user4
~ Group mygroup's Guests (with access to SOME of the group servers) are: user5
~
~ The public key of this group is:
~
~ fingerprint: SHA256:r/PQS4wLdSWqjYsDca8ReKjhq0l9EX+zQgiUR5qKdlc (ED25519-256) [2018/04/16]
~ keyline follows, please copy the *whole* line:
| Group mygroup's Owners are: user1
| Group mygroup's GateKeepers (managing the members/guests list) are: user2
| Group mygroup's ACLKeepers (managing the group servers list) are: user3
| Group mygroup's Members (with access to ALL the group servers) are: user4
| Group mygroup's Guests (with access to SOME of the group servers) are: user5
|
| The public key of this group is:
|
| fingerprint: SHA256:r/PQS4wLdSWqjYsDca8ReKjhq0l9EX+zQgiUR5qKdlc (ED25519-256) [2018/04/16]
| keyline follows, please copy the *whole* line:
from="203.0.113.4/32,192.0.2.0/26" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILdD60bA3NgaOpRLgcACWfKcAMRQQRyFMppwp5GpHLTB mygroup@testbastion:1523886640
The first paragraph of the output lists the different roles along with the people having these roles.

View file

@ -9,7 +9,7 @@ Display some information about an account
.. admonition:: usage
:class: cmdusage
--osh accountInfo --account ACCOUNT [--list-groups]
--osh accountInfo <--account ACCOUNT|--all> [OPTIONS]
.. program:: accountInfo
@ -18,54 +18,95 @@ Display some information about an account
The account name to work on
.. option:: --list-groups
.. option:: --all
Show which groups the account has a role on
Dump info for all accounts (auditors only), use with ``--json``
.. option:: --with[out]-everything
Include or exclude all below options, including future ones
.. option:: --with[out]-groups
Whether to include the groups the account has a role on (SLOW, default: no)
.. option:: --with[out]-mfa-password-info
Whether to include MFA password info of the account (SLOW, auditors only, default: no)
.. option:: --with[out]-egress-keys
Whether to include the account's egress keys (SLOW, auditors only, default: no)
Usage examples
==============
Show info about a specific account::
--osh accountInfo --account jdoe12
Gather info about all accounts, with no extra data except their egress keys::
--osh accountInfo --all --without-everything --with-egress-keys --json
Gather info about all accounts, including all extra data (and possibly future options)::
--osh accountInfo --all --with-everything --json
Output example
==============
::
~ user1 is a bastion admin
~ user1 is a bastion superowner
~ user1 is a bastion auditor
~ user1 has access to the following restricted commands:
~ - accountCreate
~ - accountDelete
~ - groupCreate
~ - groupDelete
~
~ This account is part of the following groups:
~ testgroup1 Owner GateKeeper ACLKeeper Member -
~ gatekeeper-grp2 Owner GateKeeper - - -
~
~ This account is active
~ This account is not expired
~ As a consequence, this account can connect to this bastion
~
~ This account has already been used at least once
~ Last seen on Wed 2020-07-15 12:06:27 UTC (00:00:00 ago)
~
~ Account egress SSH config:
~ - (default)
~
~ PIV-enforced policy for ingress keys on this account is enabled
~
~ Account Multi-Factor Authentication status:
~ - Additional password authentication is not required for this account
~ - Additional password authentication bypass is disabled for this account
~ - Additional password authentication is enabled and active
~ - Additional TOTP authentication is not required for this account
~ - Additional TOTP authentication bypass is disabled for this account
~ - Additional TOTP authentication is disabled
~ - PAM authentication bypass is disabled
~ - Optional public key authentication is disabled
~ - MFA policy on personal accesses (using personal keys) on egress side is: password
│ user1 is a bastion admin
│ user1 is a bastion superowner
│ user1 is a bastion auditor
│ user1 has access to the following restricted commands:
│ - accountCreate
│ - accountDelete
│ - groupCreate
│ - groupDelete
│ This account is part of the following groups:
│ testgroup1 Owner GateKeeper ACLKeeper Member -
│ gatekeeper-grp2 Owner GateKeeper - - -
│ This account is active
│ This account has no TTL set
│ This account is not frozen
│ This account has seen recent-enough activity to not be activity-expired
│ As a consequence, this account can connect to this bastion
│ Last seen on Thu 2023-03-16 07:51:49 UTC (00:00:00 ago)
│ Created on Fri 2022-06-17 09:52:50 UTC (271d+21:58:59 ago)
│ Created by jdoe
│ Created using The Bastion v3.08.01
│ Account egress SSH config:
│ - (default)
│ PIV-enforced policy for ingress keys on this account is enabled
│ Account Multi-Factor Authentication status:
│ - Additional password authentication is not required for this account
│ - Additional password authentication bypass is disabled for this account
│ - Additional password authentication is enabled and active
│ - Additional TOTP authentication is not required for this account
│ - Additional TOTP authentication bypass is disabled for this account
│ - Additional TOTP authentication is disabled
│ - PAM authentication bypass is disabled
│ - Optional public key authentication is disabled
│ - MFA policy on personal accesses (using personal keys) on egress side is: password
│ - Account is immune to idle counter-measures: no
│ - Maximum number of days of inactivity before account is disabled: (default)
│ Account PAM UNIX password information (used for password MFA):
│ - Password is set
│ - Password was last changed on 2023-01-27
│ - Password must be changed every 90 days at least
│ - A warning is displayed 75 days before expiration
│ - Account will not be disabled after password expiration
~ Account PAM UNIX password information (used for password MFA):
~ - Password is set
~ - Password was last changed on 2020-04-27
~ - Password must be changed every 90 days at least
~ - A warning is displayed 75 days before expiration
~ - Account will not be disabled after password expiration

View file

@ -148,7 +148,7 @@ The main takeaways are:
Global policy
=============
If you want to apply a policy bastion-wide, please refer to the :ref:`ingressRequiresPIV` option.
If you want to apply a policy bastion-wide, please refer to the :ref:`ingressRequirePIV` option.
This policy can still be overridden per-account if needed, see above.
Temporary grace period

View file

@ -45,10 +45,10 @@ testsuite_accountinfo()
# a0 should see basic info about a2
success a0_accountinfo_a2_basic $a0 --osh accountInfo --account $account2
json_document '{"error_message":"OK","command":"accountInfo","error_code":"OK","value":{"always_active":1,"is_active":1,"allowed_commands":[],"groups":{}}}'
json_document '{"error_message":"OK","command":"accountInfo","error_code":"OK","value":{"account":"'"$account2"'","always_active":1,"is_active":1,"allowed_commands":[],"groups":{}}}'
# a1 should see detailed info about a2
success a1_accountinfo_a2_detailed $a1 --osh accountInfo --account $account2
success a1_accountinfo_a2_detailed $a1 --osh accountInfo --account $account2 --with-mfa-password-info
json .error_code OK .command accountInfo .value.always_active 1 .value.is_active 1 .value.allowed_commands "[]"
json .value.ingress_piv_policy null .value.personal_egress_mfa_required none .value.pam_auth_bypass 0
json .value.password.min_days 0 .value.password.user "$account2" .value.password.password locked
@ -114,10 +114,29 @@ testsuite_accountinfo()
revoke accountCreate
grant auditor
success a0_accountinfo_a4_max_inactive_days $a0 --osh accountInfo --account $account4
json .value.max_inactive_days 42
# take the opportunity to test --all
success a0_accountinfo_all $a0 --osh accountInfo --all
json $(cat <<EOS
.command accountInfo
.error_code OK
.value|length 6
.value["$account4"].creation_information.by $account0
.value["$account4"].personal_egress_mfa_required none
.value["healthcheck"].allowed_commands|length 0
.value["$account0"].max_inactive_days null
EOS
)
revoke auditor
# --all should no longer work
plgfail a0_accountinfo_all_no_auditor $a0 --osh accountInfo --all
json .command accountInfo .error_code ERR_ACCESS_DENIED .value null
revoke accountModify
# sleep to ensure TTL has expired. add 2 seconds to be extra-sure and avoid int-rounding errors

View file

@ -500,6 +500,32 @@ EOS
# new state: g1[a1(ow,gk,acl,member) a2(acl)] g3[a0,a2,a3(ow,gk,acl,member)]
# --all requires auditor rights
plgfail a0_groupInfo_all_not_auditor $a0 --osh groupInfo --all
json .command groupInfo .error_code ERR_ACCESS_DENIED .value null
grant auditor
success a0_groupInfo_all $a0 --osh groupInfo --all
json $(cat <<EOS
.command groupInfo
.error_code OK
.value|length 2
.value["$group1"].aclkeepers[0] $account1
.value["$group1"].aclkeepers[1] $account2
.value["$group1"].gatekeepers[0] $account1
.value["$group1"].members[0] $account1
.value["$group1"].owners[0] $account1
.value["$group1"].guests|length 0
.value["$group1"].keys|.[]|.family RSA
.value["$group3"].owners[0] $account0
.value["$group3"].owners[1] $account3
.value["$group3"].owners[2] $account2
EOS
)
revoke auditor
# then check that owner/gatekeeper commands still don't work
plgfail a2_fail_add_a3_as_g1_owner $a2 --osh groupAddOwner --group $group1 --account $account3