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 # globally allow sys_getpw* and sys_getgr* cache use
$ENV{'PW_GR_CACHE'} = 1; $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( my $remainingOptions = OVH::Bastion::Plugin::begin(
argv => \@ARGV,
header => "group info", argv => \@ARGV,
options => {'group=s' => \$group}, 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', helptext => <<'EOF',
Print some basic information about a group 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 EOF
); );
#
# code
#
my $fnret; my $fnret;
# if (!$group && !$all) {
# params check
#
if (!$group) {
help(); 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"); my $isAuditor = OVH::Bastion::is_auditor(account => $self);
$fnret or osh_exit($fnret); if (!$isAuditor && $all) {
osh_exit 'ERR_ACCESS_DENIED', "Only bastion auditors can use --all";
# 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 $result_hash = {group => $shortGroup}; my @groupsToCheck;
$result_hash->{'owners'} = $roles{'owner'}; if ($all) {
$result_hash->{'aclkeepers'} = $roles{'aclkeeper'}; $fnret = OVH::Bastion::get_group_list();
$result_hash->{'gatekeepers'} = $roles{'gatekeeper'}; $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 " # check all groups and get the untainted data
. $shortGroup my @groups;
. "'s Owners are: " foreach my $groupName (@groupsToCheck) {
. colored(@{$roles{'owner'}} ? join(" ", sort @{$roles{'owner'}}) : '-', "red"); $fnret = OVH::Bastion::is_valid_group_and_existing(group => $groupName, groupType => "key");
osh_info "Group " $fnret or osh_exit($fnret);
. $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))
{
osh_info "Group " # get returned untainted value
. $shortGroup push @groups, {group => $fnret->value->{'group'}, shortGroup => $fnret->value->{'shortGroup'}};
. "'s ACLKeepers (managing the group servers list) are: " }
. colored(@{$roles{'aclkeeper'}} ? join(" ", sort @{$roles{'aclkeeper'}}) : '-', "red");
# now, who is member / guest ? # gather this only once
my (@members, @guests); $fnret = OVH::Bastion::get_bastion_ips();
foreach my $account (@{$roles{'member'}}) { my $from;
osh_debug("what is $account?"); if ($fnret) {
if ($account =~ /^realm_(.+)/) { my @ips = @{$fnret->value};
my $pRealm = $1; $from = 'from="' . join(',', @ips) . '"';
$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 # big hash containing all the data we want to return
push @members, $user; my %return;
}
else { 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}) { foreach my $pRemoteaccount (@{$fnret->value}) {
if (OVH::Bastion::is_group_guest(group => $shortGroup, account => "$pRealm/$pRemoteaccount")) { if (OVH::Bastion::is_group_guest(group => $shortGroup, account => "$pRealm/$pRemoteaccount")) {
push @guests, "$pRealm/$pRemoteaccount"; push @guests, "$pRealm/$pRemoteaccount";
@ -106,141 +147,192 @@ if ( OVH::Bastion::is_group_owner(group => $shortGroup, account => $self, supe
} }
} }
} }
} # normal case (non-realm accounts)
else {
if (OVH::Bastion::is_group_guest(account => $account, group => $shortGroup)) {
push @guests, $account;
}
else { 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 @guest_text;
my @filtered_guests; foreach my $guest (@{$ret{'guests'}}) {
foreach my $guest (sort @guests) { my $nb = $ret{'guest_accesse'}{$guest};
$fnret = OVH::Bastion::get_acl_way(way => 'groupguest', group => $shortGroup, account => $guest); push @guest_text, sprintf("%s[%s]", $guest, $nb // '?');
# 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;
} }
osh_info "Group " osh_info("Group ${groupName}'s Guests (with access to SOME of the group servers) are: "
. $shortGroup . colored(@{$ret{'guests'}} ? join(" ", sort @guest_text) : '-', 'red'))
. "'s Guests (with access to SOME of the group servers) are: " if $ret{'guests'};
. colored(@filtered_guests ? join(" ", @guest_text) : '-', "red");
# deprecated in v2.18.00+ # current user doesn't have enough rights to get this info, tell them that
$result_hash->{'full_members'} = \@members; if (!$ret{'members'}) {
$result_hash->{'partial_members'} = \@filtered_guests; 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; if ($ret{'mfa_required'}) {
$result_hash->{'guests'} = \@filtered_guests; my %mfa2text = (
$result_hash->{'guests_accesses'} = \%guest_details; "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; if ($ret{'guest_ttl_limit'}) {
foreach my $account (@members) { osh_warn("Guest TTL enforced: guest accesses must have a TTL with a maximum duration of "
if (OVH::Bastion::is_account_active(account => $account)->is_ko) { . OVH::Bastion::duration2human(seconds => $ret{'guest_ttl_limit'})->value->{'duration'});
push @inactive, $account; }
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 (@inactive) {
osh_info "For your information, the following accounts are inactive: " . colored(join(" ", @inactive), "blue");
$result_hash->{'inactive'} = \@inactive;
} }
# policies if ($ret{'idle_lock_timeout'}) {
$fnret = OVH::Bastion::group_config(group => $group, key => 'mfa_required'); my $action = "NOT be locked";
if ($fnret && $fnret->value eq 'password') { if ($ret{'idle_lock_timeout'} > 0) {
osh_warn $action = "be locked after "
"MFA Required: when connecting to servers of this group, users will be asked for an additional authentication factor (password)"; . OVH::Bastion::duration2human(seconds => $ret{'idle_lock_timeout'})->value->{'duration'};
} }
elsif ($fnret && $fnret->value eq 'totp') { osh_warn "Specific idle kill timeout: idle sessions on servers of this group will $action";
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;
} }
$fnret = OVH::Bastion::group_config(group => $group, key => 'guest_ttl_limit'); if ($withKeys) {
if ($fnret && defined $fnret->value && $fnret->value =~ /^\d+$/) { osh_info ' ';
osh_warn "Guest TTL enforced: guest accesses must have a TTL with a maximum duration of " if (!%{$ret{'keys'}}) {
. OVH::Bastion::duration2human(seconds => $fnret->value)->value->{'duration'}; osh_info "This group has no SSH egress key, the owner may use groupGenerateEgressKey to generate one.";
$result_hash->{'guest_ttl_limit'} = $fnret->value; }
} elsif (keys %{$ret{'keys'}} == 1) {
osh_info "The public key of this group is:";
$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";
} }
else { else {
osh_warn "Specific idle kill timeout: idle sessions on servers of this group will be cut after " osh_info "The public key of this group are:";
. OVH::Bastion::duration2human(seconds => $fnret->value)->value->{'duration'}; }
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()}); return;
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"; if (!$all) {
} # only one group, don't return a hash of hash to keep backward compat
else { my @keys = keys %return;
osh_warn "Specific idle lock timeout: idle sessions on servers of this group will be locked after " osh_ok $return{$keys[0]};
. OVH::Bastion::duration2human(seconds => $fnret->value)->value->{'duration'};
}
$result_hash->{'idle_lock_timeout'} = $fnret->value;
}
} }
else { 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 # globally allow sys_getpw* and sys_getgr* cache use
$ENV{'PW_GR_CACHE'} = 1; $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( OVH::Bastion::Plugin::begin(
argv => \@ARGV, argv => \@ARGV,
header => "account information", header => "account information",
options => {'account=s' => \my $account, "list-groups" => \my $listGroups}, 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', helptext => <<'EOF',
Display some information about an account 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 --account ACCOUNT The account name to work on
--list-groups Show which groups the account has a role 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 EOF
); );
my $fnret; my $fnret;
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account); # check params
$fnret or osh_exit $fnret;
$account = $fnret->value->{'account'}; if ($account && $all) {
my $sysaccount = $fnret->value->{'sysaccount'}; osh_exit('ERR_INCOMPATIBLE_PARAMETERS', msg => "Can't use both --account and --all");
my $remoteaccount = $fnret->value->{'remoteaccount'}; }
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 = OVH::Bastion::get_plugin_list(restrictedOnly => 1);
$fnret or osh_exit $fnret; $fnret or osh_exit $fnret;
my @commands = sort keys %{$fnret->value};
my %ret; my @groups;
if (OVH::Bastion::is_admin(account => $account)) { if ($withGroups) {
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) {
$fnret = OVH::Bastion::get_group_list(); $fnret = OVH::Bastion::get_group_list();
$fnret or osh_exit $fnret; $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 %return;
my @flags; foreach my $accHash (@accounts) {
push @flags, 'owner' if OVH::Bastion::is_group_owner(group => $name, account => $account); my %ret;
push @flags, 'gatekeeper' if OVH::Bastion::is_group_gatekeeper(group => $name, account => $account); my ($account, $sysaccount, $remoteaccount) =
push @flags, 'aclkeeper' if OVH::Bastion::is_group_aclkeeper(group => $name, account => $account); ($accHash->{'account'}, $accHash->{'sysaccount'}, $accHash->{'remoteaccount'});
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); $ret{'account'} = $account;
if (@flags) { if (OVH::Bastion::is_admin(account => $account)) {
my $line = sprintf "%18s", $name; $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 " %14s", colored(grep({ $_ eq 'owner' } @flags) ? 'Owner' : '-', 'red');
$line .= sprintf " %19s", colored(grep({ $_ eq 'gatekeeper' } @flags) ? 'GateKeeper' : '-', 'yellow'); $line .= sprintf " %19s", colored(grep({ $_ eq 'gatekeeper' } @flags) ? 'GateKeeper' : '-', 'yellow');
$line .= sprintf " %18s", colored(grep({ $_ eq 'aclkeeper' } @flags) ? 'ACLKeeper' : '-', 'magenta'); $line .= sprintf " %18s", colored(grep({ $_ eq 'aclkeeper' } @flags) ? 'ACLKeeper' : '-', 'magenta');
$line .= sprintf " %15s", colored(grep({ $_ eq 'member' } @flags) ? 'Member' : '-', 'green'); $line .= sprintf " %15s", colored(grep({ $_ eq 'member' } @flags) ? 'Member' : '-', 'green');
$line .= sprintf " %14s", colored(grep({ $_ eq 'guest' } @flags) ? 'Guest' : '-', 'cyan'); $line .= sprintf " %14s", colored(grep({ $_ eq 'guest' } @flags) ? 'Guest' : '-', 'cyan');
osh_info $line; osh_info($line);
$result_hash->{$name} = {flags => \@flags, name => $name};
} }
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; if ($ret{'always_active'}) {
osh_info("This account is " . colored('always', 'green') . " active");
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;
} }
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'); 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 if (defined $ret{'is_ttl_set'}) {
my $human = '?';
$fnret = OVH::Bastion::is_account_nonexpired(sysaccount => $sysaccount, remoteaccount => $remoteaccount); if (!$ret{'is_ttl_set'}) {
if ($fnret->is_ok) { osh_info("This account has " . colored('no TTL set', 'green'));
osh_info "This account has seen recent-enough activity to " . colored('not be activity-expired', 'green'); }
$ret{'is_expired'} = 0; elsif (!$ret{'is_ttl_expired'}) {
} $fnret = OVH::Bastion::duration2human(seconds => $ret{'ttl_timestamp'} - time());
elsif ($fnret->is_ko) { $human = $fnret->value->{'human'};
osh_info "This account is " osh_info "This account " . colored('TTL is still valid', 'green') . " (for $human)";
. colored('EXPIRED', 'red') }
. " activity-wise (it hasn't been seen for a long time)"; else {
$canConnect = 0; $fnret = OVH::Bastion::duration2human(seconds => time() - $ret{'ttl_timestamp'}, tense => "past");
$ret{'is_expired'} = 1; $human = $fnret->value->{'human'};
} osh_info "This account " . colored('TTL is EXPIRED', 'red') . " (since $human)";
else { }
osh_warn "Error getting account expiration info (" . $fnret->msg . ")";
} }
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 " 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"; . " connect to this bastion\n\n";
$ret{'can_connect'} = $canConnect; }
if ($fnret->value->{'already_seen_before'}) { if (defined $ret{'already_seen_before'}) {
$ret{'already_seen_before'} = 1; if ($ret{'already_seen_before'}) {
if (defined $fnret->value->{'seconds'}) { if ($ret{'last_activity'}) {
$fnret = OVH::Bastion::duration2human(seconds => $fnret->value->{'seconds'}, tense => "past"); my $seenBeforeStr = $ret{'last_activity'}{'datetime_utc'};
if ($fnret) { if ( $ret{'last_activity'}{'datetime_local'}
my $seenBeforeStr = $fnret->value->{'datetime_utc'}; && $ret{'last_activity'}{'datetime_utc'} ne $ret{'last_activity'}{'datetime_local'})
if ( $fnret->value->{'datetime_local'} {
&& $fnret->value->{'datetime_utc'} ne $fnret->value->{'datetime_local'}) $seenBeforeStr .= " / " . $ret{'last_activity'}{'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'};
} }
$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 { else {
osh_info "This account has " . colored('NEVER', 'red') . " been used (yet)"; osh_info("This account has " . colored('NEVER', 'red') . " been used (yet)");
$ret{'already_seen_before'} = 0;
} }
} }
$fnret = OVH::Bastion::account_config(account => $account, key => "creation_info"); if ($ret{'creation_information'}) {
if ($fnret) { if ($ret{'creation_information'}{'datetime_utc'}) {
my $creation_info; my $createdOnStr = $ret{'creation_information'}{'datetime_utc'};
eval { $creation_info = decode_json($fnret->value); }; if ( $ret{'creation_information'}{'datetime_local'}
if ($@) { && $ret{'creation_information'}{'datetime_utc'} ne $ret{'creation_information'}{'datetime_local'})
osh_warn("While reading creation metadata information for account '$account', couldn't decode JSON: $@"); {
$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 { if ($ret{'creation_information'}{'by'}) {
$ret{'creation_information'} = $creation_info; osh_info("Created by " . colored($ret{'creation_information'}{'by'}, 'magenta'));
if ($creation_info->{'datetime_utc'}) { }
my $createdOnStr = $creation_info->{'datetime_utc'}; if ($ret{'creation_information'}{'bastion_version'}) {
if ( $creation_info->{'datetime_local'} osh_info("Created using The Bastion "
&& $creation_info->{'datetime_utc'} ne $creation_info->{'datetime_local'}) . colored('v' . $ret{'creation_information'}{'bastion_version'}, 'magenta'));
{ }
$createdOnStr .= " / " . $creation_info->{'datetime_local'}; if ($ret{'creation_information'}{'comment'}) {
} osh_info(
$createdOnStr = sprintf( "Creation with the following comment: " . colored($ret{'creation_information'}{'comment'}, 'magenta'));
"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');
}
} }
} }
osh_info "\nAccount egress SSH config:"; osh_info "\nAccount egress SSH config:";
$fnret = OVH::Bastion::account_ssh_config_get(account => $account); if ($ret{'account_egress_ssh_config'}{'type'} eq 'default') {
if ($fnret->err eq 'OK_EMPTY') {
osh_info "- (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!)"; osh_info "- (locally modified!)";
$ret{'account_egress_ssh_config'}{'type'} = 'locally_modified';
} }
elsif ($fnret) { elsif ($ret{'account_egress_ssh_config'}{'type'} eq 'custom') {
$ret{'account_egress_ssh_config'}{'type'} = 'custom'; foreach my $key (sort keys %{$ret{'account_egress_ssh_config'}{'items'} || {}}) {
foreach my $key (sort keys %{$fnret->value}) { osh_info "- $key " . $ret{'account_egress_ssh_config'}{'items'}{$key};
osh_info "- $key " . $fnret->value->{$key};
$ret{'account_egress_ssh_config'}{'items'}{$key} = $fnret->value->{$key};
} }
} }
else { else {
$ret{'account_egress_ssh_config'}{'type'} = 'unknown'; osh_info "- (unknown)";
osh_info "- (unknown: " . $fnret . ")";
} }
osh_info "\nAccount PIV-only policy status:"; 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'; my $ingress_piv_policy_print = $ret{'ingress_piv_policy'} || 'default';
osh_info "- PIV policy for ingress keys on this account is set to " 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'); . colored($ingress_piv_policy_print, $ingress_piv_policy_print eq 'default' ? 'blue' : 'green');
$fnret = OVH::Bastion::account_config( if ($ret{'ingress_piv_grace'} && $ret{'ingress_piv_grace'}{'seconds_remaining'}) {
account => $account, $fnret = OVH::Bastion::duration2human(seconds => $ret{'ingress_piv_grace'}{'seconds_remaining'})->value;
public => 1, osh_info("- PIV grace period for this account is "
key => OVH::Bastion::OPT_ACCOUNT_INGRESS_PIV_GRACE . colored('set', 'green')
); . " and expires in "
if ($fnret && $fnret->value > time()) { . $fnret->value->{'human'});
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'},
};
} }
else { else {
osh_info "- PIV grace period for this account is " . colored('inactive', 'blue'); 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 " osh_info "- Global PIV policy status is "
. ($ret{'global_ingress_policy'} ? colored('enabled', 'red') : colored('disabled', 'blue')); . ($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 " osh_info "- As a consequence, PIV policy is "
. ($ret{'effective_ingress_piv_policy'} ? colored('enforced', 'red') : colored('inactive', 'blue')) . ($ret{'effective_ingress_piv_policy'} ? colored('enforced', 'red') : colored('inactive', 'blue'))
. " for this account"; . " for this account";
osh_info "\nAccount Multi-Factor Authentication status:"; 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 " osh_info "- Additional password authentication is "
. ($ret{'mfa_password_required'} ? colored('required', 'green') : colored('not required', 'blue')) . ($ret{'mfa_password_required'} ? colored('required', 'green') : colored('not required', 'blue'))
. " for this account"; . " for this account";
@ -347,12 +541,6 @@ if (OVH::Bastion::is_auditor(account => $self)) {
osh_info "- Additional password authentication is " osh_info "- Additional password authentication is "
. ($ret{'mfa_password_configured'} ? colored('enabled and active', 'green') : colored('disabled', 'blue')); . ($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 " osh_info "- Additional TOTP authentication is "
. ($ret{'mfa_totp_required'} ? colored('required', 'green') : colored('not required', 'blue')) . ($ret{'mfa_totp_required'} ? colored('required', 'green') : colored('not required', 'blue'))
. " for this account"; . " for this account";
@ -362,31 +550,18 @@ if (OVH::Bastion::is_auditor(account => $self)) {
osh_info "- Additional TOTP authentication is " osh_info "- Additional TOTP authentication is "
. ($ret{'mfa_totp_configured'} ? colored('enabled and active', 'green') : colored('disabled', 'blue')); . ($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 " osh_info "- PAM authentication bypass is "
. ($ret{'pam_auth_bypass'} ? colored('enabled', 'green') : colored('disabled', 'blue')); . ($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 " osh_info "- Optional public key authentication is "
. ($ret{'pubkey_auth_optional'} ? colored('enabled', 'green') : colored('disabled', 'blue')); . ($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: " osh_info "- MFA policy on personal accesses (using personal keys) on egress side is: "
. $ret{'personal_egress_mfa_required'}; . $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: " osh_info "\n- Account is immune to idle counter-measures: "
. ($ret{'idle_ignore'} ? colored('yes', 'green') : colored('no', 'blue')); . ($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'}) { if (!defined $ret{'max_inactive_days'}) {
osh_info "- Maximum number of days of inactivity before account is disabled: (default)"; 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'}; 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 }; if ($ret{'password'}) {
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});
osh_info "Account PAM UNIX password information (used for password MFA):"; osh_info "Account PAM UNIX password information (used for password MFA):";
if ($ret{'password'}{'password'} eq 'locked') { if ($ret{'password'}{'password'} eq 'locked') {
osh_info "- No valid password is set"; 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 Output example
============== ==============
:: ::
~ user1 is a bastion admin │ user1 is a bastion admin
~ user1 is a bastion superowner │ user1 is a bastion superowner
~ user1 is a bastion auditor │ user1 is a bastion auditor
~ user1 has access to the following restricted commands:
~ - accountCreate │ user1 has access to the following restricted commands:
~ - accountDelete │ - accountCreate
~ - groupCreate │ - accountDelete
~ - groupDelete │ - groupCreate
~ │ - groupDelete
~ This account is part of the following groups:
~ testgroup1 Owner GateKeeper ACLKeeper Member - │ This account is part of the following groups:
~ gatekeeper-grp2 Owner GateKeeper - - - │ testgroup1 Owner GateKeeper ACLKeeper Member -
~ │ gatekeeper-grp2 Owner GateKeeper - - -
~ This account is active
~ This account is not expired │ This account is active
~ As a consequence, this account can connect to this bastion │ This account has no TTL set
~ │ This account is not frozen
~ This account has already been used at least once │ This account has seen recent-enough activity to not be activity-expired
~ Last seen on Wed 2020-07-15 12:06:27 UTC (00:00:00 ago) │ As a consequence, this account can connect to this bastion
~
~ Account egress SSH config: │ Last seen on Thu 2023-03-16 07:51:49 UTC (00:00:00 ago)
~ - (default) │ Created on Fri 2022-06-17 09:52:50 UTC (271d+21:58:59 ago)
~ │ Created by jdoe
~ PIV-enforced policy for ingress keys on this account is enabled │ Created using The Bastion v3.08.01
~
~ Account Multi-Factor Authentication status: │ Account egress SSH config:
~ - Additional password authentication is not required for this account │ - (default)
~ - Additional password authentication bypass is disabled for this account
~ - Additional password authentication is enabled and active │ PIV-enforced policy for ingress keys on this account is enabled
~ - Additional TOTP authentication is not required for this account
~ - Additional TOTP authentication bypass is disabled for this account │ Account Multi-Factor Authentication status:
~ - Additional TOTP authentication is disabled │ - Additional password authentication is not required for this account
~ - PAM authentication bypass is disabled │ - Additional password authentication bypass is disabled for this account
~ - Optional public key authentication is disabled │ - Additional password authentication is enabled and active
~ - MFA policy on personal accesses (using personal keys) on egress side is: password │ - 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 Output example
============== ==============
:: ::
~ Group mygroup's Owners are: user1 | Group mygroup's Owners are: user1
~ Group mygroup's GateKeepers (managing the members/guests list) are: user2 | 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 ACLKeepers (managing the group servers list) are: user3
~ Group mygroup's Members (with access to ALL the group servers) are: user4 | 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 | Group mygroup's Guests (with access to SOME of the group servers) are: user5
~ |
~ The public key of this group is: | The public key of this group is:
~ |
~ fingerprint: SHA256:r/PQS4wLdSWqjYsDca8ReKjhq0l9EX+zQgiUR5qKdlc (ED25519-256) [2018/04/16] | fingerprint: SHA256:r/PQS4wLdSWqjYsDca8ReKjhq0l9EX+zQgiUR5qKdlc (ED25519-256) [2018/04/16]
~ keyline follows, please copy the *whole* line: | keyline follows, please copy the *whole* line:
from="203.0.113.4/32,192.0.2.0/26" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILdD60bA3NgaOpRLgcACWfKcAMRQQRyFMppwp5GpHLTB mygroup@testbastion:1523886640 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. 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 else
perl "$pluginfile" '' '' '' '' | perl -e 'undef $/; $_=<>; s/\n+$/\n/; print $_' | perl -ne ' 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"; } 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"; } elsif ($l++ == 0) { chomp; print "$_\n"."="x(length($_))."\n\n"; }
else { print "$_"; } else { print "$_"; }
' '

View file

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

View file

@ -9,30 +9,58 @@ Print some basic information about a group
.. admonition:: usage .. admonition:: usage
:class: cmdusage :class: cmdusage
--osh groupInfo --group GROUP --osh groupInfo <--group GROUP|--all> [OPTIONS]
.. program:: groupInfo .. program:: groupInfo
.. option:: --group GROUP .. 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 Output example
============== ==============
:: ::
~ Group mygroup's Owners are: user1 | Group mygroup's Owners are: user1
~ Group mygroup's GateKeepers (managing the members/guests list) are: user2 | 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 ACLKeepers (managing the group servers list) are: user3
~ Group mygroup's Members (with access to ALL the group servers) are: user4 | 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 | Group mygroup's Guests (with access to SOME of the group servers) are: user5
~ |
~ The public key of this group is: | The public key of this group is:
~ |
~ fingerprint: SHA256:r/PQS4wLdSWqjYsDca8ReKjhq0l9EX+zQgiUR5qKdlc (ED25519-256) [2018/04/16] | fingerprint: SHA256:r/PQS4wLdSWqjYsDca8ReKjhq0l9EX+zQgiUR5qKdlc (ED25519-256) [2018/04/16]
~ keyline follows, please copy the *whole* line: | keyline follows, please copy the *whole* line:
from="203.0.113.4/32,192.0.2.0/26" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILdD60bA3NgaOpRLgcACWfKcAMRQQRyFMppwp5GpHLTB mygroup@testbastion:1523886640 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. 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 .. admonition:: usage
:class: cmdusage :class: cmdusage
--osh accountInfo --account ACCOUNT [--list-groups] --osh accountInfo <--account ACCOUNT|--all> [OPTIONS]
.. program:: accountInfo .. program:: accountInfo
@ -18,54 +18,95 @@ Display some information about an account
The account name to work on 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 Output example
============== ==============
:: ::
~ user1 is a bastion admin │ user1 is a bastion admin
~ user1 is a bastion superowner │ user1 is a bastion superowner
~ user1 is a bastion auditor │ user1 is a bastion auditor
~ user1 has access to the following restricted commands:
~ - accountCreate │ user1 has access to the following restricted commands:
~ - accountDelete │ - accountCreate
~ - groupCreate │ - accountDelete
~ - groupDelete │ - groupCreate
~ │ - groupDelete
~ This account is part of the following groups:
~ testgroup1 Owner GateKeeper ACLKeeper Member - │ This account is part of the following groups:
~ gatekeeper-grp2 Owner GateKeeper - - - │ testgroup1 Owner GateKeeper ACLKeeper Member -
~ │ gatekeeper-grp2 Owner GateKeeper - - -
~ This account is active
~ This account is not expired │ This account is active
~ As a consequence, this account can connect to this bastion │ This account has no TTL set
~ │ This account is not frozen
~ This account has already been used at least once │ This account has seen recent-enough activity to not be activity-expired
~ Last seen on Wed 2020-07-15 12:06:27 UTC (00:00:00 ago) │ As a consequence, this account can connect to this bastion
~
~ Account egress SSH config: │ Last seen on Thu 2023-03-16 07:51:49 UTC (00:00:00 ago)
~ - (default) │ Created on Fri 2022-06-17 09:52:50 UTC (271d+21:58:59 ago)
~ │ Created by jdoe
~ PIV-enforced policy for ingress keys on this account is enabled │ Created using The Bastion v3.08.01
~
~ Account Multi-Factor Authentication status: │ Account egress SSH config:
~ - Additional password authentication is not required for this account │ - (default)
~ - Additional password authentication bypass is disabled for this account
~ - Additional password authentication is enabled and active │ PIV-enforced policy for ingress keys on this account is enabled
~ - Additional TOTP authentication is not required for this account
~ - Additional TOTP authentication bypass is disabled for this account │ Account Multi-Factor Authentication status:
~ - Additional TOTP authentication is disabled │ - Additional password authentication is not required for this account
~ - PAM authentication bypass is disabled │ - Additional password authentication bypass is disabled for this account
~ - Optional public key authentication is disabled │ - Additional password authentication is enabled and active
~ - MFA policy on personal accesses (using personal keys) on egress side is: password │ - 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 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. This policy can still be overridden per-account if needed, see above.
Temporary grace period Temporary grace period

View file

@ -45,10 +45,10 @@ testsuite_accountinfo()
# a0 should see basic info about a2 # a0 should see basic info about a2
success a0_accountinfo_a2_basic $a0 --osh accountInfo --account $account2 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 # 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 .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.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 json .value.password.min_days 0 .value.password.user "$account2" .value.password.password locked
@ -114,10 +114,29 @@ testsuite_accountinfo()
revoke accountCreate revoke accountCreate
grant auditor grant auditor
success a0_accountinfo_a4_max_inactive_days $a0 --osh accountInfo --account $account4 success a0_accountinfo_a4_max_inactive_days $a0 --osh accountInfo --account $account4
json .value.max_inactive_days 42 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 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 revoke accountModify
# sleep to ensure TTL has expired. add 2 seconds to be extra-sure and avoid int-rounding errors # 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)] # 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 # 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 plgfail a2_fail_add_a3_as_g1_owner $a2 --osh groupAddOwner --group $group1 --account $account3