fix: performance issues introduced by effab4a

Commit that introduced the performance degradation is effab4a
(fix: workaround for undocumented caching in getpw/getgr funcs)

Rewrote caching at the getpwent/getpwnam/getgrent/getgrnam level,
which restores performance pre-effab4a and even enhances it in somes cases,
for example on a 2000-accounts and 2000-groups bastion, we are:

- 11% faster on --osh help
- 35% faster on --osh selfListAccesses (reduces syscalls by 87%)
pull/327/head
Stéphane Lesimple 4 years ago committed by Stéphane Lesimple
parent 7a3306a00d
commit 72cefa6417

@ -322,7 +322,7 @@ if [ "${opt[modify-motd]}" = 1 ] ; then
fi
if [ "${opt[regen-hostkeys]}" = 1 ] ; then
action_doing "Change sshd host keys (this can cake a while)"
action_doing "Change sshd host keys (this can take a while)"
rm -f $SSH_DIR/ssh_host_{dsa,rsa,ecdsa,ed25519}_key{,.pub}
ssh-keygen -q -t rsa -b 4096 -N '' -f $SSH_DIR/ssh_host_rsa_key >/dev/null
ssh-keygen -q -t ecdsa -b 521 -N '' -f $SSH_DIR/ssh_host_ecdsa_key >/dev/null || true

@ -10,6 +10,9 @@ use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT help );
# globally allow sys_getpw* and sys_getgr* cache use
$ENV{'PW_GR_CACHE'} = 1;
my ($group);
my $remainingOptions = OVH::Bastion::Plugin::begin(
argv => \@ARGV,

@ -9,6 +9,9 @@ use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT help );
# globally allow sys_getpw* and sys_getgr* cache use
$ENV{'PW_GR_CACHE'} = 1;
my $remainingOptions = OVH::Bastion::Plugin::begin(
argv => \@ARGV,
header => "group list",
@ -52,11 +55,11 @@ foreach my $name (sort keys %{$fnret->value}) {
next if ($includere && $name !~ $includere);
my @flags;
push @flags, 'owner' if OVH::Bastion::is_group_owner(group => $name, cache => 1);
push @flags, 'gatekeeper' if OVH::Bastion::is_group_gatekeeper(group => $name, cache => 1);
push @flags, 'aclkeeper' if OVH::Bastion::is_group_aclkeeper(group => $name, cache => 1);
push @flags, 'member' if OVH::Bastion::is_group_member(group => $name, cache => 1);
push @flags, 'guest' if OVH::Bastion::is_group_guest(group => $name, cache => 1);
push @flags, 'owner' if OVH::Bastion::is_group_owner(group => $name);
push @flags, 'gatekeeper' if OVH::Bastion::is_group_gatekeeper(group => $name);
push @flags, 'aclkeeper' if OVH::Bastion::is_group_aclkeeper(group => $name);
push @flags, 'member' if OVH::Bastion::is_group_member(group => $name);
push @flags, 'guest' if OVH::Bastion::is_group_guest(group => $name);
if (@flags or $all) {
push @flags, 'no-access' if not @flags;
my $line = sprintf "%18s", $name;

@ -8,6 +8,9 @@ use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT help );
# globally allow sys_getpw* and sys_getgr* cache use
$ENV{'PW_GR_CACHE'} = 1;
my $remainingOptions = OVH::Bastion::Plugin::begin(
argv => \@ARGV,
header => "list of servers pertaining to the group",

@ -9,6 +9,9 @@ use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT help );
# globally allow sys_getpw* and sys_getgr* cache use
$ENV{'PW_GR_CACHE'} = 1;
my $remainingOptions = OVH::Bastion::Plugin::begin(
argv => \@ARGV,
header => "OSH help",
@ -128,7 +131,7 @@ while ($i < scalar @knownPlugins) {
my $curLen;
my $curIndex;
foreach my $cmd (@plugins) {
$fnret = OVH::Bastion::can_account_execute_plugin(account => $self, plugin => $cmd);
$fnret = OVH::Bastion::can_account_execute_plugin(account => $self, plugin => $cmd, cache => 1);
next unless $fnret;
if (($curLen + length($fnret->value->{'plugin'})) > 80) {
$curIndex++;

@ -10,6 +10,9 @@ use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT help );
# globally allow sys_getpw* and sys_getgr* cache use
$ENV{'PW_GR_CACHE'} = 1;
my ($name);
OVH::Bastion::Plugin::begin(
argv => \@ARGV,
@ -135,9 +138,9 @@ osh_info
$ret{'hostname'} = Sys::Hostname::hostname();
$ret{'bastion_name'} = $config->{'bastionName'};
$fnret = OVH::Bastion::get_account_list(cache => 1);
$fnret = OVH::Bastion::get_account_list();
my $nbaccounts = $fnret ? keys %{$fnret->value} : '?';
$fnret = OVH::Bastion::get_group_list(cache => 1);
$fnret = OVH::Bastion::get_group_list();
my $nbgroups = $fnret ? keys %{$fnret->value} : '?';
osh_info "I have "
. colored($nbaccounts, 'green')

@ -8,6 +8,9 @@ use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT help );
# globally allow sys_getpw* and sys_getgr* cache use
$ENV{'PW_GR_CACHE'} = 1;
my $remainingOptions = OVH::Bastion::Plugin::begin(
argv => \@ARGV,
header => "your access list",

@ -12,6 +12,9 @@ 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;
OVH::Bastion::Plugin::begin(
argv => \@ARGV,
header => "account information",
@ -68,18 +71,18 @@ $ret{'allowed_commands'} = \@granted;
my $result_hash = {};
if ($listGroups) {
$fnret = OVH::Bastion::get_group_list(cache => 1);
$fnret = OVH::Bastion::get_group_list();
$fnret or osh_exit $fnret;
osh_info "\nThis account is part of the following groups:";
foreach my $name (sort keys %{$fnret->value}) {
my @flags;
push @flags, 'owner' if OVH::Bastion::is_group_owner(group => $name, account => $account, cache => 1);
push @flags, 'gatekeeper' if OVH::Bastion::is_group_gatekeeper(group => $name, account => $account, cache => 1);
push @flags, 'aclkeeper' if OVH::Bastion::is_group_aclkeeper(group => $name, account => $account, cache => 1);
push @flags, 'member' if OVH::Bastion::is_group_member(group => $name, account => $account, cache => 1);
push @flags, 'guest' if OVH::Bastion::is_group_guest(group => $name, account => $account, cache => 1);
push @flags, 'owner' if OVH::Bastion::is_group_owner(group => $name, account => $account);
push @flags, 'gatekeeper' if OVH::Bastion::is_group_gatekeeper(group => $name, account => $account);
push @flags, 'aclkeeper' if OVH::Bastion::is_group_aclkeeper(group => $name, account => $account);
push @flags, 'member' if OVH::Bastion::is_group_member(group => $name, account => $account);
push @flags, 'guest' if OVH::Bastion::is_group_guest(group => $name, account => $account);
if (@flags) {
my $line = sprintf "%18s", $name;
$line .= sprintf " %14s", colored(grep({ $_ eq 'owner' } @flags) ? 'Owner' : '-', 'red');
@ -100,10 +103,11 @@ if ($listGroups) {
$ret{'groups'} = $result_hash;
my $canConnect = 1;
$ret{'always_active'} =
OVH::Bastion::account_config(account => $account, key => OVH::Bastion::OPT_ACCOUNT_ALWAYS_ACTIVE, public => 1)
? 1
: 0;
$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";

@ -9,6 +9,9 @@ use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT help );
# globally allow sys_getpw* and sys_getgr* cache use
$ENV{'PW_GR_CACHE'} = 1;
my $remainingOptions = OVH::Bastion::Plugin::begin(
argv => \@ARGV,
header => "list bastion accounts",
@ -139,24 +142,42 @@ foreach my $account (sort keys %$accounts) {
$states{'can_connect'} = ($states{'is_active'} && !$states{'is_expired'}) ? 1 : 0;
$states{'mfa_password_required'} =
OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::MFA_PASSWORD_REQUIRED_GROUP) ? 1 : 0;
$states{'mfa_password_required'} = OVH::Bastion::is_user_in_group(
user => $account,
group => OVH::Bastion::MFA_PASSWORD_REQUIRED_GROUP,
) ? 1 : 0;
$states{'mfa_password_configured'} =
OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::MFA_PASSWORD_CONFIGURED_GROUP)
OVH::Bastion::is_user_in_group(
user => $account,
group => OVH::Bastion::MFA_PASSWORD_CONFIGURED_GROUP,
)
? 1
: 0;
$states{'mfa_password_bypass'} =
OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::MFA_PASSWORD_BYPASS_GROUP) ? 1 : 0;
$states{'mfa_password_bypass'} = OVH::Bastion::is_user_in_group(
user => $account,
group => OVH::Bastion::MFA_PASSWORD_BYPASS_GROUP,
) ? 1 : 0;
$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;
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;
$states{'mfa_totp_bypass'} =
OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::MFA_TOTP_BYPASS_GROUP) ? 1 : 0;
OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::MFA_TOTP_BYPASS_GROUP)
? 1
: 0;
$states{'pam_auth_bypass'} =
OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::PAM_AUTH_BYPASS_GROUP) ? 1 : 0;
OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::PAM_AUTH_BYPASS_GROUP)
? 1
: 0;
$states{'pubkey_auth_optional'} =
OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::OSH_PUBKEY_AUTH_OPTIONAL_GROUP)
OVH::Bastion::is_user_in_group(
user => $account,
group => OVH::Bastion::OSH_PUBKEY_AUTH_OPTIONAL_GROUP,
)
? 1
: 0;

@ -9,6 +9,9 @@ use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT help );
# globally allow sys_getpw* and sys_getgr* cache use
$ENV{'PW_GR_CACHE'} = 1;
my (@ignoreGroups, $ignorePersonal, $showWildcards);
my $remainingOptions = OVH::Bastion::Plugin::begin(
argv => \@ARGV,

@ -148,7 +148,7 @@ my %_autoload_files = (
qw{ enable_mocking is_mocking set_mock_data mock_get_account_entry mock_get_account_accesses mock_get_account_personal_accesses mock_get_account_legacy_accesses mock_get_group_accesses mock_get_account_guest_accesses }
],
os => [
qw{ sysinfo is_linux is_debian is_redhat is_bsd is_freebsd is_openbsd is_netbsd has_acls sys_useradd sys_groupadd sys_userdel sys_groupdel sys_addmembertogroup sys_delmemberfromgroup sys_changepassword sys_neutralizepassword sys_setpasswordpolicy sys_getpasswordinfo sys_getsudoersfolder sys_setfacl is_in_path sys_getent_pw sys_getent_gr }
qw{ sysinfo is_linux is_debian is_redhat is_bsd is_freebsd is_openbsd is_netbsd has_acls sys_useradd sys_groupadd sys_userdel sys_groupdel sys_addmembertogroup sys_delmemberfromgroup sys_changepassword sys_neutralizepassword sys_setpasswordpolicy sys_getpasswordinfo sys_getsudoersfolder sys_setfacl is_in_path sys_getpw_all sys_getpw_all_cached sys_getpw_name sys_getgr_all sys_getgr_all_cached sys_getgr_name }
],
password => [qw{ get_hashes_from_password get_password_file get_hashes_list is_valid_hash }],
ssh => [
@ -765,6 +765,7 @@ sub can_account_execute_plugin {
my %params = @_;
my $account = $params{'account'} || OVH::Bastion::get_user_from_env()->value;
my $plugin = $params{'plugin'};
my $cache = $params{'cache'}; # allow cache use in get_user_groups(), is_user_in_group() etc.
my $fnret;
if (not $plugin or not $account) {
@ -815,14 +816,14 @@ sub can_account_execute_plugin {
# need to parse group to see if maybe member of group-gatekeeper or group-owner (or super owner)
my %canDo = (gatekeeper => 0, aclkeeper => 0, owner => 0);
$fnret = OVH::Bastion::get_user_groups(extra => 1, account => $account);
$fnret = OVH::Bastion::get_user_groups(extra => 1, account => $account, cache => $cache);
my @userGroups = $fnret ? @{$fnret->value} : ();
foreach my $type (qw{ aclkeeper gatekeeper owner }) {
if (-f "$path_plugin/group-$type/$plugin") {
# we can always execute these commands if we are a super owner
my $canDo = OVH::Bastion::is_super_owner(account => $account) ? 1 : 0;
my $canDo = OVH::Bastion::is_super_owner(account => $account, cache => $cache) ? 1 : 0;
# or if we are $type on at least one group
$canDo += grep { /^key.*-\Q$type\E$/ } @userGroups;
@ -852,7 +853,7 @@ sub can_account_execute_plugin {
# restricted plugins (osh-* system groups based)
if (-f ($path_plugin . '/restricted/' . $plugin)) {
if (OVH::Bastion::is_user_in_group(user => $account, group => "osh-$plugin")) {
if (OVH::Bastion::is_user_in_group(user => $account, group => "osh-$plugin", cache => $cache)) {
return R('OK',
value => {fullpath => $path_plugin . '/restricted/' . $plugin, type => 'restricted', plugin => $plugin}
);
@ -868,7 +869,7 @@ sub can_account_execute_plugin {
# admin plugins
if (-f ($path_plugin . '/admin/' . $plugin)) {
if (OVH::Bastion::is_admin(account => $account)) {
if (OVH::Bastion::is_admin(account => $account, cache => $cache)) {
return R('OK',
value => {fullpath => $path_plugin . '/admin/' . $plugin, type => 'admin', plugin => $plugin});
}

@ -358,19 +358,29 @@ sub get_user_groups {
my %params = @_;
my $user = $params{'user'} || $params{'account'};
my $extra = $params{'extra'}; # Do we want to include gatekeeper/aclkeeper/owner groups ?
my $cache = $params{'cache'}; # allow cache use (multicall)
my $cache = $params{'cache'}; # allow cache use of sys_getgr_all(), and also our own
# cache if we've already been called with same params before
state %cached_response;
if (not $user) {
return R('ERR_MISSING_PARAMETER', msg => "Missing parameter 'account'");
}
# try to use sys_getent_gr()'s cache if available and allowed by caller.
my $cachekey = sprintf("user=%s,extra=%d", $user, $extra ? 1 : 0);
if ($cache && defined $cached_response{$cachekey}) {
return $cached_response{$cachekey};
}
# we loop through all the system groups to find the ones having user
# as a meember
# as a member
my $fnret = OVH::Bastion::sys_getgr_all(cache => $cache);
$fnret or return $fnret;
my @groups;
foreach my $gr (@{OVH::Bastion::sys_getent_gr(cache => $cache)->value}) {
if (grep { $user eq $_ } @{$gr->{'members'} || []}) {
push @groups, $gr->{'name'};
foreach my $name (keys %{$fnret->value}) {
my $entry = $fnret->value->{$name};
if (grep { $user eq $_ } @{$entry->{'members'} || []}) {
push @groups, $name;
}
}
@ -385,11 +395,12 @@ sub get_user_groups {
}
if (scalar(@availableGroups)) {
return R('OK', value => \@availableGroups);
$cached_response{$cachekey} = R('OK', value => \@availableGroups);
}
else {
return R('ERR_NO_GROUP', msg => 'Unable to find any group');
$cached_response{$cachekey} = R('ERR_NO_GROUP', msg => 'Unable to find any group');
}
return $cached_response{$cachekey};
}
sub _get_pub_keys_from_directory {

@ -11,53 +11,48 @@ sub is_user_in_group {
my %params = @_;
my $group = $params{'group'};
my $user = $params{'user'} || OVH::Bastion::get_user_from_env()->value;
my $cache = $params{'cache'}; # if true, allow cache use of sys_getent_gr()
my $cache = $params{'cache'}; # allow cache use of sys_getgr_name()
# mandatory keys
if (!$user || !$group) {
return R('ERR_MISSING_PARAMETER', msg => "Missing parameter 'user' or 'group'");
}
# try to use sys_getent_gr()'s cache if available and allowed by caller.
# we loop through all the system groups to find the proper one, then
# check whether $user appears in the member list
foreach my $gr (@{OVH::Bastion::sys_getent_gr(cache => $cache)->value}) {
next if $gr->{'name'} ne $group;
if (grep { $user eq $_ } @{$gr->{'members'} || []}) {
return R('OK', value => {account => $user});
}
else {
return R('KO_NOT_IN_GROUP', msg => "Account $user doesn't belong to the group $group");
}
my $fnret = OVH::Bastion::sys_getgr_name(name => $group, cache => $cache);
$fnret or return $fnret;
if (grep { $user eq $_ } @{$fnret->value->{'members'} || []}) {
return R('OK', value => {group => $group, account => $user});
}
else {
return R('KO_NOT_IN_GROUP', msg => "Account $user doesn't belong to the group $group");
}
return R('KO_GROUP_NOT_FOUND', msg => "The group $group doesn't exist");
}
# does this system group exist ? if it happens to be mapped to a bastion group,
# does this system group exist? if it happens to be mapped to a bastion group,
# also return the corresponding "shortGroup" (with the "key" prefix removed)
sub is_group_existing {
my %params = @_;
my $group = $params{'group'};
my $cache = $params{'cache'}; # if true, allow cache use from potential previous calls
my $cache = $params{'cache'}; # allow cache use of sys_getgr_name()
my $user_friendly_error = $params{'user_friendly_error'};
if (!$group) {
return R('ERR_MISSING_PARAMETER', msg => "Missing parameter 'group'");
}
# try to use sys_getent_gr()'s cache if available and allowed by caller.
# we loop through all the system groups to find the proper one
foreach my $gr (@{OVH::Bastion::sys_getent_gr(cache => $cache)->value}) {
next if $gr->{'name'} ne $group;
my $fnret = OVH::Bastion::sys_getgr_name(name => $group, cache => $cache);
if ($fnret) {
my (undef, $shortGroup) = $group =~ m{^(key)?(.+)};
return R(
'OK',
value => {
group => $group,
shortGroup => $shortGroup,
gid => $gr->{'gid'},
gid => $fnret->value->{'gid'},
keyhome => "/home/keykeeper/$group",
members => $gr->{'members'},
members => $fnret->value->{'members'},
}
);
}
@ -66,8 +61,8 @@ sub is_group_existing {
if ($user_friendly_error) {
$group =~ s/^key//;
return R('KO_GROUP_NOT_FOUND',
msg =>
"The bastion group '$group' doesn't exist.\nYou may use groupList --all to see all existing groups.");
msg => "The bastion group '$group' doesn't exist.\n"
. "You may use groupList --all to see all existing groups.");
}
return R('KO_GROUP_NOT_FOUND', msg => "Group '$group' doesn't exist");
}
@ -254,7 +249,7 @@ sub is_account_existing {
my %params = @_;
my $account = $params{'account'};
my $checkBastionShell = $params{'checkBastionShell'}; # check if this account is a bastion user
my $cache = $params{'cache'}; # allow cache use
my $cache = $params{'cache'}; # allow cache use sys_getpw_name()
if (!$account) {
return R('ERR_MISSING_PARAMETER', msg => "Missing parameter 'account'");
@ -274,12 +269,9 @@ sub is_account_existing {
);
}
else {
# try to use sys_getent_pw()'s cache if available and allowed by caller.
# we loop through all the system accounts to find the proper one
foreach my $pw (@{OVH::Bastion::sys_getent_pw(cache => $cache)->value}) {
next if $pw->{'name'} ne $account;
%entry = %$pw;
last;
my $fnret = OVH::Bastion::sys_getpw_name(name => $account, cache => $cache);
if ($fnret) {
%entry = %{$fnret->value};
}
}
@ -763,118 +755,99 @@ sub add_user_to_group {
# return the list of the bastion groups (i.e. not the system group list)
sub get_group_list {
my %params = @_;
my $cache = $params{'cache'}; # if true, allow cache use
state $cached_response;
# if we've been called before and can use the cache, just return it
if ($cache and $cached_response) {
return $cached_response;
}
my %groups;
my $cache = $params{'cache'}; # allow cache use of sys_getgr_all()
# sys_getent_gr() might have been called before, and has its own cache,
# so also try to use its cache if allowed and available.
# we loop through all the system groups and only retain those starting
# with "key", and not finishing in -owner, -gatekeeper or -aclkeeper.
# we also exclude special builtin groups (keykeeper and keyreader)
foreach my $gr (@{OVH::Bastion::sys_getent_gr(cache => $cache)->value}) {
if ( $gr->{'name'} =~ /^key/
&& $gr->{'name'} !~ /-(?:owner|gatekeeper|aclkeeper)$/
&& !grep { $gr->{'name'} eq $_ } qw{ keykeeper keyreader })
my $fnret = OVH::Bastion::sys_getgr_all(cache => $cache);
$fnret or return $fnret;
my %groups;
foreach my $name (keys %{$fnret->value}) {
if ( $name =~ /^key/
&& $name !~ /-(?:owner|gatekeeper|aclkeeper)$/
&& !grep { $name eq $_ } qw{ keykeeper keyreader })
{
$gr->{'name'} =~ s/^key//;
$groups{$gr->{'name'}} = {gid => $gr->{'gid'}, members => $gr->{'members'}} if ($gr->{'name'} ne '');
my $entry = $fnret->value->{$name};
$name =~ s/^key//;
$groups{$name} = {gid => $entry->{'gid'}, members => $entry->{'members'}} if ($name ne '');
}
}
$cached_response = R('OK', value => \%groups);
return $cached_response;
return R('OK', value => \%groups);
}
# return the list of bastion accounts (i.e. not the system user list)
sub get_account_list {
my %params = @_;
my $accounts = $params{'accounts'} || [];
my $cache = $params{'cache'}; # if true, allow cache use
state $cached_response;
my $cache = $params{'cache'}; # allow cache use of sys_getpw_all()
# note that is_bastion_account_valid_and_existing() passthroughs its
# $cache param to sys_getpw_name() too
# if we've been called before and can use the cache, just return it
# don't do it if we're asked to only return a subset of all the accounts
if ($cache && $cached_response && !@$accounts) {
return $cached_response;
}
# we loop through all the accounts known to the OS
my $fnret = OVH::Bastion::sys_getpw_all(cache => $cache);
$fnret or return $fnret;
my %users;
# sys_getent_pw() might have been called before, and has its own cache,
# so also try to use its cache if allowed and available.
# we loop through all the accounts known to the OS
foreach my $pw (@{OVH::Bastion::sys_getent_pw(cache => $cache)->value}) {
foreach my $name (keys %{$fnret->value}) {
# if $accounts has been specified, only consider those
next if (@$accounts && !grep { $pw->{'name'} eq $_ } @$accounts);
next if (@$accounts && !grep { $name eq $_ } @$accounts);
# skip invalid accounts.
# if !$cache, then we've filled the cache with sys_getpw_all() just above,
# so it's OK to actually use it in all cases
next if not OVH::Bastion::is_bastion_account_valid_and_existing(account => $name, cache => 1);
# skip invalid accounts
next if not OVH::Bastion::is_bastion_account_valid_and_existing(account => $pw->{'name'});
my $entry = $fnret->value->{$name};
# add proper accounts, only include a subset of the fields we got
$users{$pw->{'name'}} = {
name => $pw->{'name'},
gid => $pw->{'gid'},
home => $pw->{'dir'},
shell => $pw->{'shell'},
uid => $pw->{'uid'}
$users{$name} = {
name => $entry->{'name'},
gid => $entry->{'gid'},
home => $entry->{'dir'},
shell => $entry->{'shell'},
uid => $entry->{'uid'}
};
}
if (@$accounts) {
return R('OK', value => \%users);
}
else {
$cached_response = R('OK', value => \%users);
return $cached_response;
}
return R('OK', value => \%users);
}
sub get_realm_list {
my %params = @_;
my $realms = $params{'realms'} || [];
my $cache = $params{'cache'}; # if true, allow cache use
state $cached_response;
my $cache = $params{'cache'}; # allow cache use of sys_getent_pw()
# note that is_bastion_account_valid_and_existing() passthroughs its
# $cache param to sys_getent_pw() too
# if we've been called before and can use the cache, just return it
# don't do it if we're asked to only return a subset of all the accounts
if ($cache && $cached_response && !@$realms) {
return $cached_response;
}
# we loop through all the accounts known to the OS
my $fnret = OVH::Bastion::sys_getpw_all(cache => $cache);
$fnret or return $fnret;
my %users;
# sys_getent_pw() might have been called before, and has its own cache,
# so also try to use its cache if allowed and available.
# we loop through all the accounts known to the OS
foreach my $pw (@{OVH::Bastion::sys_getent_pw(cache => $cache)->value}) {
foreach my $name (keys %{$fnret->value}) {
# if $realms has been specified, only consider those
next if (@$realms && !grep { $pw->{'name'} eq "realm_$_" } @$realms);
next if (@$realms && !grep { $name eq "realm_$_" } @$realms);
# skip invalid realms
# skip invalid realms.
# if !$cache, then we've filled the cache with sys_getpw_all() just above,
# so it's OK to actually use it in all cases
next
if not OVH::Bastion::is_bastion_account_valid_and_existing(account => $pw->{'name'}, accountType => "realm");
if !OVH::Bastion::is_bastion_account_valid_and_existing(
account => $name,
accountType => "realm",
cache => 1
);
# add proper realms
my $name = $pw->{'name'};
$name =~ s{^realm_}{};
$users{$name} = {name => $name};
}
if (@$realms) {
return R('OK', value => \%users);
}
else {
$cached_response = R('OK', value => \%users);
return $cached_response;
}
return R('OK', value => \%users);
}
# check if account is a bastion admin (gives access to adminXyz commands)
@ -883,6 +856,7 @@ sub is_admin {
my %params = @_;
my $sudo = $params{'sudo'}; # we're run under sudo
my $account = $params{'account'};
my $cache = $params{'cache'}; # allow cache use of sys_getgr_name() through is_user_in_group()
if (not $account) {
$account = $sudo ? $ENV{'SUDO_USER'} : OVH::Bastion::get_user_from_env()->value;
@ -906,7 +880,7 @@ sub is_admin {
my $adminList = OVH::Bastion::config('adminAccounts')->value();
if (grep { $account eq $_ } @$adminList) {
return OVH::Bastion::is_user_in_group(group => "osh-admin", user => $account);
return OVH::Bastion::is_user_in_group(group => "osh-admin", user => $account, cache => $cache);
}
return R('KO_ACCESS_DENIED');
}
@ -917,6 +891,7 @@ sub is_super_owner {
my %params = @_;
my $sudo = $params{'sudo'}; # we're run under sudo
my $account = $params{'account'};
my $cache = $params{'cache'}; # allow cache use of sys_getgr_name() through is_user_in_group()
if (not $account) {
$account = $sudo ? $ENV{'SUDO_USER'} : OVH::Bastion::get_user_from_env()->value;
@ -940,11 +915,11 @@ sub is_super_owner {
my $superownerList = OVH::Bastion::config('superOwnerAccounts')->value();
if (grep { $account eq $_ } @$superownerList) {
return OVH::Bastion::is_user_in_group(group => "osh-superowner", user => $account);
return OVH::Bastion::is_user_in_group(group => "osh-superowner", user => $account, cache => $cache);
}
# if admin, then we're good too
return OVH::Bastion::is_admin(account => $account, sudo => $sudo);
return OVH::Bastion::is_admin(account => $account, sudo => $sudo, cache => $cache);
}
# check if account is an auditor
@ -952,6 +927,7 @@ sub is_auditor {
my %params = @_;
my $sudo = $params{'sudo'}; # we're run under sudo
my $account = $params{'account'};
my $cache = $params{'cache'}; # allow cache use of sys_getgr_name() through is_user_in_group()
if (not $account) {
$account = $sudo ? $ENV{'SUDO_USER'} : OVH::Bastion::get_user_from_env()->value;
@ -984,7 +960,8 @@ sub _has_group_role {
my $role = $params{'role'}; # regular or gatekeeper or owner
my $superowner = $params{'superowner'}; # allow superowner (will always return yes if so)
my $sudo = $params{'sudo'}; # are we run under sudo ?
my $cache = $params{'cache'}; # allow cache use (for commands that don't modify accounts or groups, such as groupList)
my $cache = $params{'cache'}; # allow cache use of sys_getgr_name() through is_user_in_group() and
# is_bastion_account_valid_and_existing()
my $fnret;
if (not $account) {
@ -1028,7 +1005,7 @@ sub _has_group_role {
# if superowner allowed, try it
if ($superowner) {
if (OVH::Bastion::is_super_owner(account => $sysaccount, sudo => $sudo)) {
if (OVH::Bastion::is_super_owner(account => $sysaccount, sudo => $sudo, cache => $cache)) {
osh_debug("is <$sysaccount> in <$group> ? => no but superowner so YES!");
return R('OK', value => {account => $account, sysaccount => $sysaccount, superowner => 1});
}
@ -1060,13 +1037,16 @@ sub is_group_owner {
sub _is_group_member_or_guest {
my %params = @_;
my $shortGroup = $params{'group'};
my $want = $params{'want'}; # guest or member
my $want = $params{'want'}; # guest or member
my $cache = $params{'cache'}; # allow cache use of sys_getpw_name() through
# is_bastion_account_valid_and_existing() and sys_getgr_name()
# through is_valid_group_and_existing()
my $fnret = _has_group_role(%params, role => "regular");
$fnret or return $fnret;
my $account = $fnret->value()->{'account'};
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account, cache => $params{'cache'});
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account, cache => $cache);
$fnret or return $fnret;
$account = $fnret->value->{'account'};
@ -1074,7 +1054,7 @@ sub _is_group_member_or_guest {
my $sysaccount = $fnret->value->{'sysaccount'};
my $group = "key$shortGroup";
$fnret = OVH::Bastion::is_valid_group_and_existing(group => $group, groupType => "key", cache => $params{'cache'});
$fnret = OVH::Bastion::is_valid_group_and_existing(group => $group, groupType => "key", cache => $cache);
$fnret or return $fnret;
$group = $fnret->value()->{'group'};
$shortGroup = $fnret->value()->{'shortGroup'}; # untainted
@ -1111,9 +1091,11 @@ sub is_group_member {
sub get_remote_accounts_from_realm {
my %params = @_;
my $realm = $params{'realm'};
my $cache = $params{'cache'}; # allow cache use of sys_getpw_name() through is_bastion_account_valid_and_existing()
$realm = "realm_$realm" if $realm !~ /^realm_/;
my $fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $realm, accountType => "realm");
my $fnret =
OVH::Bastion::is_bastion_account_valid_and_existing(account => $realm, accountType => "realm", cache => $cache);
$fnret or return $fnret;
my $sysaccount = $fnret->value->{'sysaccount'};

@ -68,7 +68,7 @@ EOM
my @cmdlist = qw{ exit ssh mfa enable nomfa end };
foreach my $plugin (sort keys %$pluginList) {
$fnret = OVH::Bastion::can_account_execute_plugin(plugin => $plugin, account => $self);
$fnret = OVH::Bastion::can_account_execute_plugin(plugin => $plugin, account => $self, cache => 1);
next if !$fnret;
push @cmdlist, $plugin;
@ -138,6 +138,10 @@ EOM
return @validcmds;
}
my $accountList = OVH::Bastion::get_account_list(cache => 1)->value;
my $groupList = OVH::Bastion::get_group_list(cache => 1, groupType => 'key')->value;
my $realmList = OVH::Bastion::get_realm_list(cache => 1)->value;
my $pluginListRestricted = OVH::Bastion::get_plugin_list(restrictedOnly => 1)->value;
foreach my $i (0 .. $#rules) {
my $re = $rules[$i++];
my $item = $rules[$i++];
@ -156,40 +160,25 @@ EOM
# but before, check there's no magic inside, i.e. replace ACCOUNT by @account_list and GROUP by @group_list
my @autocomplete;
foreach (@{$item->{'ac'}}) {
if ($_ eq '<ACCOUNT>') {
$fnret = OVH::Bastion::get_account_list(cache => 1);
if ($fnret) {
push @autocomplete, sort keys %{$fnret->value()};
next;
}
if ($_ eq '<ACCOUNT>' && $accountList) {
push @autocomplete, sort keys %$accountList;
next;
}
elsif ($_ eq '<GROUP>') {
$fnret = OVH::Bastion::get_group_list(cache => 1, groupType => 'key');
if ($fnret) {
push @autocomplete, sort keys %{$fnret->value()};
next;
}
elsif ($_ eq '<GROUP>' && $groupList) {
push @autocomplete, sort keys %$groupList;
next;
}
elsif ($_ eq '<REALM>') {
$fnret = OVH::Bastion::get_realm_list();
if ($fnret) {
push @autocomplete, sort keys %{$fnret->value()};
next;
}
elsif ($_ eq '<REALM>' && $realmList) {
push @autocomplete, sort keys %$realmList;
next;
}
elsif ($_ eq '<RESTRICTED_COMMAND>') {
$fnret = OVH::Bastion::get_plugin_list(restrictedOnly => 1);
if ($fnret) {
push @autocomplete, 'auditor', sort keys %{$fnret->value()};
next;
}
elsif ($_ eq '<RESTRICTED_COMMAND>' && $pluginListRestricted) {
push @autocomplete, 'auditor', sort keys %$pluginListRestricted;
next;
}
elsif ($_ eq '<COMMAND>') {
$fnret = OVH::Bastion::get_plugin_list();
if ($fnret) {
push @autocomplete, sort keys %{$fnret->value()};
next;
}
elsif ($_ eq '<COMMAND>' && $pluginList) {
push @autocomplete, sort keys %$pluginList;
next;
}
push @autocomplete, $_;
}

@ -616,81 +616,203 @@ sub is_in_path {
# as setpwent/endpwent can have side effects to any getpwent used between
# them, possibly in other parts of the program, encapsulate everything here
# to avoid side effects
sub sys_getent_pw {
my %_pw_cache;
my $_pw_all_cached = 0;
sub sys_getpw_all {
my %params = @_;
my $cache = $params{'cache'};
state $cached_response;
my $cache = $params{'cache'} || $ENV{'PW_GR_CACHE'};
if (!$cache || !$_pw_all_cached) {
# end/set: for some reason, if we don't end() before set(), there seem
# to be a cache somewhere, and we get entries that have been
# deleted (milli)seconds before. This behaviour is undocumented, as set()
# is supposed to reset the pointer, and a no mention of a cache is done
# anywhere. Luckily, Prepending the set() with an end() seems to
# reliably fix the issue on all tested OSes.
%_pw_cache = ();
endpwent();
setpwent();
while (my @line = getpwent()) {
$_pw_cache{$line[0]} = {
name => $line[0],
passwd => $line[1],
uid => $line[2],
gid => $line[3],
quota => $line[4],
comment => $line[5],
gcos => $line[6],
dir => $line[7],
shell => $line[8],
expire => $line[9],
};
}
endpwent();
$_pw_all_cached = 1;
}
if ($cache && $cached_response) {
return $cached_response;
}
my @all;
# end/set: for some reason, if we don't end() before set(), there seem
# to be a cache somewhere, and we get entries that have been
# deleted (milli)seconds before. This behaviour is undocumented, as set()
# is supposed to reset the pointer, and a no mention of a cache is done
# anywhere. Luckily, Prepending the set() with an end() seems to
# reliably fix the issue on all tested OSes.
endpwent();
setpwent();
while (my @line = getpwent()) {
push @all,
{
name => $line[0],
passwd => $line[1],
uid => $line[2],
gid => $line[3],
quota => $line[4],
comment => $line[5],
gcos => $line[6],
dir => $line[7],
shell => $line[8],
expire => $line[9],
};
}
endpwent();
$cached_response = R('OK', value => \@all);
return $cached_response;
return R('OK', value => \%_pw_cache);
}
sub sys_getpw_all_cached {
return R($_pw_all_cached ? 'OK' : 'KO');
}
# get a system account entry from name (getent passwd / getpwnam)
sub sys_getpw_name {
my %params = @_;
my $cache = $params{'cache'} || $ENV{'PW_GR_CACHE'};
my $name = $params{'name'};
if (!$name) {
return R('ERR_MISSING_PARAMETER', msg => "Missing 'name' parameter");
}
if ($cache) {
# if cache is allowed and our $name entry exists in cache, return it
if (exists $_pw_cache{$name}) {
if (defined $_pw_cache{$name}) {
return R('OK', value => $_pw_cache{$name});
}
else {
# we cached its non-existence
return R('KO_NOT_FOUND', msg => "Account '$name' doesn't exist");
}
}
# if cache is allowed, cache has been filled but our name entry is not
# there, it means the account doesn't exist
elsif ($_pw_all_cached) {
return R('KO_NOT_FOUND', msg => "Account '$name' doesn't exist");
}
# if cache is allowed, cache has not been filled and our entry doesn't
# exist in cache, either we never cached anything, or we partly
# cached some accounts due to previous calls to sys_getpw_name()
# without a single call to sys_getpw_all(), in which case we need
# to fetch the data from the system using getpwnam()
}
# if cache is not allowed, fetch data from system with getpwnam() and
# cache it. As we're just caching data for this account, don't set
# $_pw_all_cached so that other funcs know that the cache is partial
my @fields = getpwnam($name);
if (@fields) {
$_pw_cache{$fields[0]} = {
name => $fields[0],
passwd => $fields[1],
uid => $fields[2],
gid => $fields[3],
quota => $fields[4],
comment => $fields[5],
gcos => $fields[6],
dir => $fields[7],
shell => $fields[8],
expire => $fields[9],
};
return R('OK', value => $_pw_cache{$fields[0]});
}
else {
# cache the non-existence of this account
$_pw_cache{$name} = undef;
return R('KO_NOT_FOUND', msg => "Account '$name' doesn't exist");
}
# unreachable
return R('ERR_INTERNAL');
}
# as setgrent/endgrent can have side effects to any getgrent used between
# them, possibly in other parts of the program, encapsulate everything here
# to avoid side effects
sub sys_getent_gr {
my %params = @_;
my $cache = $params{'cache'};
state $cached_response;
my %_gr_cache;
my $_gr_all_cached = 0;
if ($cache && $cached_response) {
return $cached_response;
sub sys_getgr_all {
my %params = @_;
my $cache = $params{'cache'} || $ENV{'PW_GR_CACHE'};
if (!$cache || !$_gr_all_cached) {
# end/set: for some reason, if we don't end() before set(), there seem
# to be a cache somewhere, and we get entries that have been
# deleted (milli)seconds before. This behaviour is undocumented, as set()
# is supposed to reset the pointer, and a no mention of a cache is done
# anywhere. Luckily, pPrepending the set() with an end() seems to
# reliably fix the issue on all tested OSes.
%_gr_cache = ();
endgrent();
setgrent();
while (my @line = getgrent()) {
$_gr_cache{$line[0]} = {
name => $line[0],
passwd => $line[1],
gid => $line[2],
members => [split(/ /, $line[3])],
};
}
endgrent();
$_gr_all_cached = 1;
}
my @all;
return R('OK', value => \%_gr_cache);
}
sub sys_getgr_all_cached {
return R($_gr_all_cached ? 'OK' : 'KO');
}
# get a system group entry from name (getent group / getgrnam)
sub sys_getgr_name {
my %params = @_;
my $name = $params{'name'};
my $cache = $params{'cache'} || $ENV{'PW_GR_CACHE'};
if (!$name) {
return R('ERR_MISSING_PARAMETER', msg => "Missing 'name' parameter");
}
# end/set: for some reason, if we don't end() before set(), there seem
# to be a cache somewhere, and we get entries that have been
# deleted (milli)seconds before. This behaviour is undocumented, as set()
# is supposed to reset the pointer, and a no mention of a cache is done
# anywhere. Luckily, pPrepending the set() with an end() seems to
# reliably fix the issue on all tested OSes.
endgrent();
setgrent();
while (my @line = getgrent()) {
push @all,
{
name => $line[0],
passwd => $line[1],
gid => $line[2],
members => [split(/ /, $line[3])],
};
if ($cache) {
# if cache is allowed and our $name entry exists in cache, return it
if (exists $_gr_cache{$name}) {
if (defined $_gr_cache{$name}) {
return R('OK', value => $_gr_cache{$name});
}
else {
# we cached its non-existence
return R('KO_NOT_FOUND', msg => "Group '$name' doesn't exist");
}
}
# if cache is allowed, cache has been filled but our name entry is not
# there, it means the group doesn't exist
elsif ($_gr_all_cached) {
return R('KO_NOT_FOUND', msg => "Group '$name' doesn't exist");
}
# if cache is allowed, cache has not been filled and our entry doesn't
# exist in cache, either we never cached anything, or we partly
# cached some groups due to previous calls to sys_getgr_name()
# without a single call to sys_getgr_all(), in which case we need
# to fetch the data from the system using getgrnam()
}
# if cache is not allowed, fetch data from system with getgrnam() and
# cache it. As we're just caching data for this group, don't set
# $_gr_all_cached so that other funcs know that the cache is partial
my @fields = getgrnam($name);
if (@fields) {
$_gr_cache{$fields[0]} = {
name => $fields[0],
passwd => $fields[1],
gid => $fields[2],
members => [split(/ /, $fields[3])],
};
return R('OK', value => $_gr_cache{$fields[0]});
}
else {
# cache the non-existence of this group
$_gr_cache{$name} = undef;
return R('KO_NOT_FOUND', msg => "Group '$name' doesn't exist");
}
endgrent();
$cached_response = R('OK', value => \@all);
return $cached_response;
# unreachable
return R('ERR_INTERNAL');
}
1;

Loading…
Cancel
Save