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;
|
2022-12-17 02:08:30 +08:00
|
|
|
use JSON;
|
2020-10-16 00:32:37 +08:00
|
|
|
|
|
|
|
use File::Basename;
|
|
|
|
use lib dirname(__FILE__) . '/../../../lib/perl';
|
|
|
|
use OVH::Result;
|
|
|
|
use OVH::Bastion;
|
|
|
|
use OVH::Bastion::Plugin qw( :DEFAULT help );
|
|
|
|
|
2022-07-07 19:23:55 +08:00
|
|
|
# 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 => {
|
2022-12-17 02:07:49 +08:00
|
|
|
"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
|
|
|
|
|
2021-03-31 14:54:15 +08:00
|
|
|
Usage: --osh SCRIPT_NAME [OPTIONS]
|
2020-10-16 00:32:37 +08:00
|
|
|
|
2022-12-17 02:08:30 +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
|
2021-03-31 14:54:15 +08:00
|
|
|
|
|
|
|
**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;
|
2022-12-17 02:07:49 +08:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2021-01-20 17:55:58 +08:00
|
|
|
# if we have excludes and/or includes, transform those into regexes
|
2021-03-31 14:54:15 +08:00
|
|
|
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;
|
2021-01-20 17:55:58 +08:00
|
|
|
|
2020-10-16 00:32:37 +08:00
|
|
|
my $result_hash = {};
|
|
|
|
foreach my $account (sort keys %$accounts) {
|
|
|
|
|
2021-01-20 17:55:58 +08:00
|
|
|
# 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) {
|
2022-12-17 02:08:30 +08:00
|
|
|
$fnret = OVH::Bastion::is_account_nonfrozen(account => $account);
|
|
|
|
$states{'is_frozen'} = undef;
|
|
|
|
if ($fnret->is_ok) {
|
|
|
|
$states{'is_frozen'} = 0;
|
|
|
|
}
|
|
|
|
elsif ($fnret->is_ko) {
|
|
|
|
$states{'is_frozen'} = 1;
|
|
|
|
$states{'freeze_info'} = $fnret->value;
|
|
|
|
}
|
|
|
|
|
2022-07-15 19:22:48 +08:00
|
|
|
$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;
|
2023-06-12 16:11:44 +08:00
|
|
|
my $seconds = ($fnret->value ? $fnret->value->{'seconds'} : undef);
|
2020-10-16 00:32:37 +08:00
|
|
|
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'}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-15 19:22:48 +08:00
|
|
|
$states{'can_connect'} = 1;
|
|
|
|
$states{'can_connect'} = 0 if (!defined $states{'is_active'} || $states{'is_active'} == 0);
|
2022-12-17 02:08:30 +08:00
|
|
|
$states{'can_connect'} = 0 if (!defined $states{'is_frozen'} || $states{'is_frozen'} == 0);
|
2022-07-15 19:22:48 +08:00
|
|
|
$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
|
|
|
|
2022-07-07 19:23:55 +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'} =
|
2022-07-07 19:23:55 +08:00
|
|
|
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;
|
2022-07-07 19:23:55 +08:00
|
|
|
$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'} =
|
2022-07-07 19:23:55 +08:00
|
|
|
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'} =
|
2022-07-07 19:23:55 +08:00
|
|
|
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'} =
|
2022-07-07 19:23:55 +08:00
|
|
|
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'} =
|
2022-07-07 19:23:55 +08:00
|
|
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-17 02:07:49 +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'};
|
|
|
|
|
2022-12-17 02:07:49 +08:00
|
|
|
# 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(
|
2022-12-17 02:08:30 +08:00
|
|
|
"%-18s %6d active:%-12s expired:%-12s frozen:%-12s ttl_expired:%-12s"
|
2022-12-17 02:07:49 +08:00
|
|
|
. "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'}),
|
2022-07-15 19:22:48 +08:00
|
|
|
tristate2str($states{'is_expired'}, 1),
|
2022-12-17 02:08:30 +08:00
|
|
|
tristate2str($states{'is_frozen'}, 1),
|
2022-07-15 19:22:48 +08:00
|
|
|
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'),
|
2021-09-29 22:16:07 +08:00
|
|
|
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'},
|
2022-12-17 02:07:49 +08:00
|
|
|
$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'});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-17 02:07:49 +08:00
|
|
|
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;
|