diff --git a/bin/admin/install b/bin/admin/install index 10dcb62..916d50b 100755 --- a/bin/admin/install +++ b/bin/admin/install @@ -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 diff --git a/bin/plugin/open/groupInfo b/bin/plugin/open/groupInfo index dcf61f8..523f2a0 100755 --- a/bin/plugin/open/groupInfo +++ b/bin/plugin/open/groupInfo @@ -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, diff --git a/bin/plugin/open/groupList b/bin/plugin/open/groupList index 63501af..17c9d5f 100755 --- a/bin/plugin/open/groupList +++ b/bin/plugin/open/groupList @@ -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; diff --git a/bin/plugin/open/groupListServers b/bin/plugin/open/groupListServers index a71aadc..e5a6748 100755 --- a/bin/plugin/open/groupListServers +++ b/bin/plugin/open/groupListServers @@ -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", diff --git a/bin/plugin/open/help b/bin/plugin/open/help index 7fb9e7a..7548e2c 100755 --- a/bin/plugin/open/help +++ b/bin/plugin/open/help @@ -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++; diff --git a/bin/plugin/open/info b/bin/plugin/open/info index fcc5522..378078e 100755 --- a/bin/plugin/open/info +++ b/bin/plugin/open/info @@ -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') diff --git a/bin/plugin/open/selfListAccesses b/bin/plugin/open/selfListAccesses index b9af0ac..b217912 100755 --- a/bin/plugin/open/selfListAccesses +++ b/bin/plugin/open/selfListAccesses @@ -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", diff --git a/bin/plugin/restricted/accountInfo b/bin/plugin/restricted/accountInfo index ee14a6d..00d08a6 100755 --- a/bin/plugin/restricted/accountInfo +++ b/bin/plugin/restricted/accountInfo @@ -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"; diff --git a/bin/plugin/restricted/accountList b/bin/plugin/restricted/accountList index 8e28aca..b5c82c8 100755 --- a/bin/plugin/restricted/accountList +++ b/bin/plugin/restricted/accountList @@ -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; diff --git a/bin/plugin/restricted/whoHasAccessTo b/bin/plugin/restricted/whoHasAccessTo index 3765e3e..7660664 100755 --- a/bin/plugin/restricted/whoHasAccessTo +++ b/bin/plugin/restricted/whoHasAccessTo @@ -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, diff --git a/lib/perl/OVH/Bastion.pm b/lib/perl/OVH/Bastion.pm index 69346f9..cbc2dbe 100644 --- a/lib/perl/OVH/Bastion.pm +++ b/lib/perl/OVH/Bastion.pm @@ -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}); } diff --git a/lib/perl/OVH/Bastion/allowdeny.inc b/lib/perl/OVH/Bastion/allowdeny.inc index b6ca1c9..497e1b3 100644 --- a/lib/perl/OVH/Bastion/allowdeny.inc +++ b/lib/perl/OVH/Bastion/allowdeny.inc @@ -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 { diff --git a/lib/perl/OVH/Bastion/allowkeeper.inc b/lib/perl/OVH/Bastion/allowkeeper.inc index 584e286..9b83bcd 100644 --- a/lib/perl/OVH/Bastion/allowkeeper.inc +++ b/lib/perl/OVH/Bastion/allowkeeper.inc @@ -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'}; diff --git a/lib/perl/OVH/Bastion/interactive.inc b/lib/perl/OVH/Bastion/interactive.inc index b803cac..e137257 100644 --- a/lib/perl/OVH/Bastion/interactive.inc +++ b/lib/perl/OVH/Bastion/interactive.inc @@ -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 '') { - $fnret = OVH::Bastion::get_account_list(cache => 1); - if ($fnret) { - push @autocomplete, sort keys %{$fnret->value()}; - next; - } + if ($_ eq '' && $accountList) { + push @autocomplete, sort keys %$accountList; + next; } - elsif ($_ eq '') { - $fnret = OVH::Bastion::get_group_list(cache => 1, groupType => 'key'); - if ($fnret) { - push @autocomplete, sort keys %{$fnret->value()}; - next; - } + elsif ($_ eq '' && $groupList) { + push @autocomplete, sort keys %$groupList; + next; } - elsif ($_ eq '') { - $fnret = OVH::Bastion::get_realm_list(); - if ($fnret) { - push @autocomplete, sort keys %{$fnret->value()}; - next; - } + elsif ($_ eq '' && $realmList) { + push @autocomplete, sort keys %$realmList; + next; } - elsif ($_ eq '') { - $fnret = OVH::Bastion::get_plugin_list(restrictedOnly => 1); - if ($fnret) { - push @autocomplete, 'auditor', sort keys %{$fnret->value()}; - next; - } + elsif ($_ eq '' && $pluginListRestricted) { + push @autocomplete, 'auditor', sort keys %$pluginListRestricted; + next; } - elsif ($_ eq '') { - $fnret = OVH::Bastion::get_plugin_list(); - if ($fnret) { - push @autocomplete, sort keys %{$fnret->value()}; - next; - } + elsif ($_ eq '' && $pluginList) { + push @autocomplete, sort keys %$pluginList; + next; } push @autocomplete, $_; } diff --git a/lib/perl/OVH/Bastion/os.inc b/lib/perl/OVH/Bastion/os.inc index 26b9e35..69f650c 100644 --- a/lib/perl/OVH/Bastion/os.inc +++ b/lib/perl/OVH/Bastion/os.inc @@ -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;