the-bastion/bin/plugin/restricted/accountList

276 lines
10 KiB
Text
Raw Normal View History

2020-10-16 00:32:37 +08:00
#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;
use Term::ANSIColor;
use File::Basename;
use lib dirname(__FILE__) . '/../../../lib/perl';
use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT help );
# globally allow sys_getpw* and sys_getgr* cache use
$ENV{'PW_GR_CACHE'} = 1;
2020-10-16 00:32:37 +08:00
my $remainingOptions = OVH::Bastion::Plugin::begin(
argv => \@ARGV,
header => "list bastion accounts",
options => {
"inactive-only" => \my $inactiveOnly,
"realm-only" => \my $realmOnly,
"account=s" => \my $account,
"audit" => \my $audit,
"no-password-info" => \my $noPasswordInfo,
"no-output" => \my $noOutput,
'exclude=s' => \my @excludes,
'include=s' => \my @includes,
2020-10-16 00:32:37 +08:00
},
helptext => <<'EOF',
List the bastion accounts
Usage: --osh SCRIPT_NAME [OPTIONS]
2020-10-16 00:32:37 +08:00
--account ACCOUNT Only list the specified account. This is an easy way to check whether the account exists
--inactive-only Only list inactive accounts
--audit Show more verbose information (SLOW!), you need to be a bastion auditor
--no-password-info Don't gather password info in audit mode (makes --audit way faster)
--no-output Don't print human-readable output (faster, use with --json)
--include PATTERN Only show accounts whose name match the given PATTERN (see below)
This option can be used multiple times to refine results
--exclude PATTERN Omit accounts whose name match the given PATTERN (see below)
This option can be used multiple times.
Note that --exclude takes precedence over --include
**Note:** PATTERN supports the ``*`` and ``?`` wildcards.
If PATTERN is a simple string without wildcards, then names containing this string will be considered.
2020-10-16 00:32:37 +08:00
EOF
);
sub tristate2str {
my $v = shift;
my $r = shift;
2022-06-30 21:00:29 +08:00
return (
defined $v
? ($v ? colored('yes', $r ? 'red' : 'green') : colored('no', $r ? 'green' : 'red'))
: colored('-', 'blue')
);
2020-10-16 00:32:37 +08:00
}
if ($realmOnly) {
osh_exit(R('ERR_INVALID_PARAMETER'), "Option --realm-only is no longer supported, use realmList instead");
}
my $fnret;
if ($account) {
$fnret = OVH::Bastion::get_account_list(accounts => [$account]);
}
else {
$fnret = OVH::Bastion::get_account_list();
}
$fnret or osh_exit $fnret;
my $accounts = $fnret->value;
if ($audit && !OVH::Bastion::is_auditor(account => $self)) {
osh_exit(R('ERR_PERMISSION_DENIED', msg => "You need to be a bastion auditor to use --audit"));
}
my $fnretPassword;
if ($audit && !$noPasswordInfo) {
2020-10-16 00:32:37 +08:00
# get UNIX password info for all accounts
my @command = qw{ sudo -n -u root -- /usr/bin/env perl -T };
push @command, $OVH::Bastion::BASEPATH . '/bin/helper/osh-accountGetPasswordInfo', '--all';
$fnretPassword = OVH::Bastion::helper(cmd => \@command);
}
# if we have excludes and/or includes, transform those into regexes
my $includere = OVH::Bastion::build_re_from_wildcards(wildcards => \@includes, implicit_contains => 1)->value;
my $excludere = OVH::Bastion::build_re_from_wildcards(wildcards => \@excludes, implicit_contains => 1)->value;
2020-10-16 00:32:37 +08:00
my $result_hash = {};
foreach my $account (sort keys %$accounts) {
# if we have excludes, match name against the built regex
next if ($excludere && $account =~ $excludere);
# same for includes
next if ($includere && $account !~ $includere);
my %states;
2020-10-16 00:32:37 +08:00
$states{'is_active'} = undef;
$fnret = OVH::Bastion::is_account_active(account => $account);
if ($fnret->is_ok) {
next if $inactiveOnly;
$states{'is_active'} = 1;
}
elsif ($fnret->is_ko) {
$states{'is_active'} = 0;
}
if ($audit) {
$fnret = OVH::Bastion::is_account_ttl_nonexpired(sysaccount => $account, account => $account);
$states{'is_ttl_expired'} = undef;
if ($fnret->is_ok) {
$states{'is_ttl_expired'} = 0;
}
elsif ($fnret->is_ko) {
$states{'is_ttl_expired'} = 1;
$states{'ttl_expired_details'} = $fnret->value->{'details'};
}
2020-10-16 00:32:37 +08:00
$fnret = OVH::Bastion::is_account_nonexpired(sysaccount => $account);
$states{'is_expired'} = undef;
if ($fnret->is_ok) {
$states{'is_expired'} = 0;
}
elsif ($fnret->is_ko) {
$states{'is_expired'} = 1;
$states{'expired_days'} = $fnret->value->{'days'};
}
$states{'already_seen_before'} = undef;
if ($fnret->value && defined $fnret->value->{'already_seen_before'}) {
$states{'already_seen_before'} = $fnret->value->{'already_seen_before'} ? 1 : 0;
}
$states{'last_activity'} = undef;
$states{'last_activity_timestamp'} = undef;
my $seconds = $fnret->value->{'seconds'};
if (defined $seconds) {
$fnret = OVH::Bastion::duration2human(seconds => $seconds, tense => "past");
if ($fnret) {
$states{'last_activity_timestamp'} = time() - $seconds;
$states{'last_activity'} = sprintf(
"%s on %s (%s ago)",
2022-06-30 21:00:29 +08:00
(
defined $states{'already_seen_before'}
? ($states{'already_seen_before'} ? "Last seen" : "Created")
: "Last activity"
),
2020-10-16 00:32:37 +08:00
$fnret->value->{'date'},
$fnret->value->{'duration'}
);
}
}
$states{'can_connect'} = 1;
$states{'can_connect'} = 0 if (!defined $states{'is_active'} || $states{'is_active'} == 0);
$states{'can_connect'} = 0 if (!defined $states{'is_expired'} || $states{'is_expired'} == 0);
$states{'can_connect'} = 0 if (!defined $states{'is_ttl_expired'} || $states{'is_ttl_expired'} == 0);
2020-10-16 00:32:37 +08:00
$states{'mfa_password_required'} = OVH::Bastion::is_user_in_group(
user => $account,
group => OVH::Bastion::MFA_PASSWORD_REQUIRED_GROUP,
) ? 1 : 0;
2022-06-30 21:00:29 +08:00
$states{'mfa_password_configured'} =
OVH::Bastion::is_user_in_group(
user => $account,
group => OVH::Bastion::MFA_PASSWORD_CONFIGURED_GROUP,
)
2022-06-30 21:00:29 +08:00
? 1
: 0;
$states{'mfa_password_bypass'} = OVH::Bastion::is_user_in_group(
user => $account,
group => OVH::Bastion::MFA_PASSWORD_BYPASS_GROUP,
) ? 1 : 0;
2022-06-30 21:00:29 +08:00
$states{'mfa_totp_required'} =
OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::MFA_TOTP_REQUIRED_GROUP)
? 1
: 0;
$states{'mfa_totp_configured'} = OVH::Bastion::is_user_in_group(
user => $account,
group => OVH::Bastion::MFA_TOTP_CONFIGURED_GROUP,
) ? 1 : 0;
2022-06-30 21:00:29 +08:00
$states{'mfa_totp_bypass'} =
OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::MFA_TOTP_BYPASS_GROUP)
? 1
: 0;
2022-06-30 21:00:29 +08:00
$states{'pam_auth_bypass'} =
OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::PAM_AUTH_BYPASS_GROUP)
? 1
: 0;
2022-06-30 21:00:29 +08:00
$states{'pubkey_auth_optional'} =
OVH::Bastion::is_user_in_group(
user => $account,
group => OVH::Bastion::OSH_PUBKEY_AUTH_OPTIONAL_GROUP,
)
2022-06-30 21:00:29 +08:00
? 1
: 0;
2020-10-16 00:32:37 +08:00
if ($fnretPassword) {
2022-06-30 21:00:29 +08:00
$states{"password_$_"} = $fnretPassword->value->{$account}{$_}
for (keys %{$fnretPassword->value->{$account}});
2020-10-16 00:32:37 +08:00
}
}
$fnret = OVH::Bastion::account_config(account => $account, key => "creation_info");
if ($fnret && $fnret->value) {
eval {
my $data = decode_json($fnret->value);
$states{'created_by'} = $data->{'by'};
};
if ($@) {
osh_warn("Error decoding creation_info of account '$account' ($@)");
}
}
2020-10-16 00:32:37 +08:00
$result_hash->{$account} = \%states;
$result_hash->{$account}{'name'} = $account;
$result_hash->{$account}{'uid'} = $accounts->{$account}{'uid'};
# don't print human-readable version (usually used with --json)
next if $noOutput;
2020-10-16 00:32:37 +08:00
if ($audit) {
my @mfaPassword;
push @mfaPassword, 'required' if $states{'mfa_password_required'};
push @mfaPassword, 'enabled' if $states{'mfa_password_configured'};
push @mfaPassword, 'bypass' if $states{'mfa_password_bypass'};
my @mfaTOTP;
push @mfaTOTP, 'required' if $states{'mfa_totp_required'};
push @mfaTOTP, 'enabled' if $states{'mfa_totp_configured'};
push @mfaTOTP, 'bypass' if $states{'mfa_totp_bypass'};
osh_info sprintf(
"%-18s %6d active:%-12s expired:%-12s ttl_expired:%-12s"
. "can_connect:%-12s already_seen:%-12s mfa_password:%-25s "
. "mfa_totp:%-25s pam_bypass:%-12s pubkey_auth_optional:%-12s "
. "pass_status:%-15s pass_changed:%-10s pass_min_days:%-3d "
. "pass_max_days:%-3d pass_warn_days:%-3d created_by:%-12s " . " %s\n",
2020-10-16 00:32:37 +08:00
$account,
$accounts->{$account}{'uid'},
tristate2str($states{'is_active'}),
tristate2str($states{'is_expired'}, 1),
tristate2str($states{'is_ttl_expired'}, 1),
2020-10-16 00:32:37 +08:00
tristate2str($states{'can_connect'}),
tristate2str($states{'already_seen_before'}),
@mfaPassword ? colored(join(',', @mfaPassword), 'green') : colored('-', 'blue'),
@mfaTOTP ? colored(join(',', @mfaTOTP), 'green') : colored('-', 'blue'),
tristate2str($states{'pam_auth_bypass'}, 1),
tristate2str($states{'pubkey_auth_optional'}, 1),
2020-10-16 00:32:37 +08:00
(
2022-06-30 21:00:29 +08:00
$states{'password_password'} eq 'locked' ? colored('locked', 'blue')
: (
$states{'password_password'} eq 'set' ? colored('set', 'green')
: colored($states{'password_password'}, 'red')
)
2020-10-16 00:32:37 +08:00
),
$states{'password_date_changed'},
$states{'password_min_days'},
$states{'password_max_days'},
$states{'password_warn_days'},
$states{'created_by'},
2020-10-16 00:32:37 +08:00
$states{'last_activity'},
);
}
else {
osh_info sprintf("%-18s %6d\n", $account, $accounts->{$account}{'uid'});
}
}
if ($noOutput) {
osh_info "No-output requested, if you see only this message, you might have omitted --json";
}
2020-10-16 00:32:37 +08:00
osh_ok $result_hash;