mirror of
https://github.com/ovh/the-bastion.git
synced 2025-01-08 00:12:10 +08:00
1207 lines
46 KiB
Perl
1207 lines
46 KiB
Perl
package OVH::Bastion;
|
|
|
|
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
|
|
|
|
use common::sense;
|
|
|
|
use Time::Piece; # $t->strftime
|
|
|
|
# Check if a system user belongs to a specific system group
|
|
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'}; # allow cache use of sys_getgr_name()
|
|
|
|
# mandatory keys
|
|
if (!$user || !$group) {
|
|
return R('ERR_MISSING_PARAMETER', msg => "Missing parameter 'user' or '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");
|
|
}
|
|
}
|
|
|
|
# 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'}; # 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'");
|
|
}
|
|
|
|
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 => $fnret->value->{'gid'},
|
|
keyhome => "/home/keykeeper/$group",
|
|
members => $fnret->value->{'members'},
|
|
}
|
|
);
|
|
}
|
|
|
|
# build a user-compatible error message if asked to, as it can make its way through osh_exit() # pragma:hookignore
|
|
if ($user_friendly_error) {
|
|
$group =~ s/^key//;
|
|
return R('KO_GROUP_NOT_FOUND',
|
|
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");
|
|
}
|
|
|
|
# validate uid/gid
|
|
sub is_valid_uid {
|
|
my %params = @_;
|
|
my $uid = $params{'uid'};
|
|
my $type = $params{'type'};
|
|
|
|
# Basic input validation
|
|
if ($uid !~ m/^\d+$/) {
|
|
return R('ERR_INVALID_PARAMETER', msg => "Parameter 'uid' should be numeric");
|
|
}
|
|
|
|
if ($type ne 'user' and $type ne 'group') {
|
|
return R('ERR_INVALID_PARAMETER', msg => "Parameter 'type' is invalid");
|
|
}
|
|
|
|
# Input validation against configuration
|
|
my $fnret = OVH::Bastion::load_configuration();
|
|
$fnret or return $fnret;
|
|
|
|
my ($accountUidMin, $accountUidMax, $ttyrecGroupIdOffset) =
|
|
@{$fnret->value}{qw{ accountUidMin accountUidMax ttyrecGroupIdOffset }};
|
|
|
|
if (not $accountUidMin or not $accountUidMax or not $ttyrecGroupIdOffset) {
|
|
return R('ERR_CANNOT_LOAD_CONFIGURATION');
|
|
}
|
|
|
|
my ($low, $high) = ($accountUidMin, $accountUidMax);
|
|
|
|
if ($type eq 'group') {
|
|
$high += $ttyrecGroupIdOffset;
|
|
}
|
|
|
|
if ($uid < $low or $uid > $high) {
|
|
return R('KO_BAD_RANGE', msg => "Parameter 'uid' should be between $low and $high");
|
|
}
|
|
|
|
# untaint
|
|
if ($uid =~ m/^(\d+)$/) {
|
|
return R('OK', value => $1);
|
|
}
|
|
warn_syslog("Got an invalid uid ('$uid')");
|
|
return R('ERR_INVALID_UID', msg => "Got an invalid uid ('$uid')");
|
|
}
|
|
|
|
sub get_next_available_uid {
|
|
my %params = @_;
|
|
|
|
# if true, also check for the availability of the corresponding GID:
|
|
my $available_gid = $params{'available_gid'};
|
|
|
|
# if true, also check for the availability of the corresponding GID + the ttyrec offset:
|
|
my $available_gid_ttyrec = $params{'available_gid_ttyrec'};
|
|
|
|
my $higher = OVH::Bastion::config('accountUidMax')->value();
|
|
my $lower = OVH::Bastion::config('accountUidMin')->value();
|
|
my $next = $higher;
|
|
my $found = 0;
|
|
while (1) {
|
|
|
|
# find the first available UID, starting from the upper ID allowed and decrementing
|
|
while ($next >= $lower) {
|
|
last if not scalar(getpwuid($next));
|
|
$next--;
|
|
}
|
|
|
|
# did we get out of the loop because we found a candidate, or because we're out of bounds?
|
|
last if $next < $lower;
|
|
|
|
# if $available_gid, also check if the corresponding GID is available
|
|
# if $available_gid_ttyrec, also check if the corresponding GID + the ttyrec offset is available
|
|
if ( (!$available_gid || !scalar(getgrgid($next)))
|
|
&& (!$available_gid_ttyrec || !scalar(getgrgid($next + OVH::Bastion::config('ttyrecGroupIdOffset')->value)))
|
|
)
|
|
{
|
|
$found = 1;
|
|
last;
|
|
}
|
|
|
|
# if we're here, at least one of the $available_gid* check failed, so continue looking
|
|
$next--;
|
|
}
|
|
return R('OK', value => $next) if $found;
|
|
return R('ERR_UID_COLLISION', msg => "No available UID in the allowed range");
|
|
}
|
|
|
|
sub is_bastion_account_valid_and_existing {
|
|
my %params = @_;
|
|
my $fnret = OVH::Bastion::is_account_valid(%params);
|
|
$fnret or return $fnret;
|
|
my %values = %{$fnret->value()};
|
|
my ($account, $realm, $sysaccount, $remoteaccount) = @values{qw{ account realm sysaccount remoteaccount}};
|
|
$fnret =
|
|
OVH::Bastion::is_account_existing(account => $sysaccount, checkBastionShell => 1, cache => $params{'cache'});
|
|
$fnret or return $fnret;
|
|
$fnret->value->{'account'} = $account;
|
|
$fnret->value->{'sysaccount'} = $sysaccount;
|
|
$fnret->value->{'realm'} = $realm;
|
|
$fnret->value->{'remoteaccount'} = $remoteaccount;
|
|
return $fnret;
|
|
}
|
|
|
|
# check if account name is valid, i.e. non-weird chars and non reserved parts
|
|
sub is_account_valid {
|
|
my %params = @_;
|
|
my $account = $params{'account'};
|
|
my $accountType = $params{'accountType'} || 'normal'; # normal (local account or $realm/$remoteself formatted account) | group (must start with key*) | realm (must start with realm_*)
|
|
my $localOnly = $params{'localOnly'}; # for accountType == normal, disallow realm-formatted accounts ($realm/$remoteself)
|
|
my $realmOnly = $params{'realmOnly'}; # for accountType == normal, allow only realm-formatted accounts ($realm/$remoteself)
|
|
|
|
if (!$account) {
|
|
return R('ERR_MISSING_PARAMETER', msg => "Missing parameter 'account'");
|
|
}
|
|
|
|
my $whatis = ($accountType eq 'realm' ? "Realm" : "Account");
|
|
|
|
if ($localOnly && $account =~ m{/}) {
|
|
return R('KO_REALM_FORBIDDEN', msg => "$whatis name must not contain any '/'");
|
|
}
|
|
elsif ($realmOnly && $account !~ m{/}) {
|
|
return R('KO_LOCAL_FORBIDDEN', msg => "$whatis name must contain a '/'");
|
|
}
|
|
elsif ($account =~ m/^[-.]/) {
|
|
return R('KO_FORBIDDEN_PREFIX', msg => "$whatis name must not start with a '-' nor a '.'");
|
|
}
|
|
elsif ($account =~ m/-(?:tty|aclkeeper|gatekeeper|owner)$/i) {
|
|
return R('KO_FORBIDDEN_SUFFIX', msg => "$whatis name contains an unauthorized suffix");
|
|
}
|
|
elsif ($account =~ m/^key/i && $accountType ne 'group') {
|
|
return R('KO_FORBIDDEN_PREFIX', msg => "$whatis name contains an unauthorized key prefix");
|
|
}
|
|
elsif ($account !~ m/^key/i && $accountType eq 'group') {
|
|
return R('KO_BAD_PREFIX', msg => "$whatis should start with the group prefix");
|
|
}
|
|
elsif ($account =~ m/^realm_/ && $accountType ne 'realm') {
|
|
return R('KO_FORBIDDEN_PREFIX', msg => "$whatis name contains an unauthorized realm prefix");
|
|
}
|
|
elsif ($account !~ m/^realm_/ && $accountType eq 'realm') {
|
|
return R('KO_BAD_PREFIX', msg => "$whatis should start with the realm prefix");
|
|
}
|
|
elsif (grep { $account eq $_ } qw{ root proxyhttp keykeeper passkeeper logkeeper realm realm_realm }) {
|
|
return R('KO_FORBIDDEN_NAME', msg => "$whatis name is reserved");
|
|
}
|
|
elsif ($account =~ m{^([a-zA-Z0-9-]+)/([a-zA-Z0-9._-]+)$} && $accountType eq 'normal') {
|
|
|
|
# 32 is the max Linux user length
|
|
if (length("realm_$1") > 32) {
|
|
return R('KO_TOO_LONG', msg => "$whatis name is too long, length(realm_$1) > 32");
|
|
}
|
|
elsif (length($1) < 2) {
|
|
return R('KO_TOO_SMALL', msg => "$whatis name is too long, length($1) < 2");
|
|
}
|
|
|
|
# 28 because all accounts have a corresponding "-tty" group, and 32 - length(-tty) == 28
|
|
elsif (length($2) > 28) {
|
|
return R('KO_TOO_LONG', msg => "Remote account name is too long, length($2) > 28");
|
|
}
|
|
elsif (length($2) < 2) {
|
|
return R('KO_TOO_SMALL', msg => "Remote account name is too short, length($2) < 2");
|
|
}
|
|
return R('OK', value => {sysaccount => "realm_$1", realm => $1, remoteaccount => $2, account => "$1/$2"}); # untainted
|
|
}
|
|
elsif ($account =~ m/^([a-zA-Z0-9._-]+)$/) {
|
|
if (length($1) < 2) {
|
|
return R('KO_TOO_SMALL', msg => "$whatis name is too small, length($1) < 2");
|
|
}
|
|
|
|
# 28 because all accounts have a corresponding "-tty" group, and 32 - length(-tty) == 28
|
|
elsif (length($1) > 28) {
|
|
return R('KO_TOO_LONG', msg => "$whatis name is too long, length($1) > 28");
|
|
}
|
|
return R('OK', value => {sysaccount => $1, realm => undef, remoteaccount => undef, account => $1}); # untainted
|
|
}
|
|
else {
|
|
return R('KO_FORBIDDEN_CHARS', msg => "$whatis name contains forbidden characters $account");
|
|
}
|
|
return R('ERR_IMPOSSIBLE_CASE');
|
|
}
|
|
|
|
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 sys_getpw_name()
|
|
|
|
if (!$account) {
|
|
return R('ERR_MISSING_PARAMETER', msg => "Missing parameter 'account'");
|
|
}
|
|
|
|
my %entry;
|
|
if (OVH::Bastion::is_mocking()) {
|
|
my @fields = OVH::Bastion::mock_get_account_entry(account => $account);
|
|
%entry = (
|
|
name => $fields[0],
|
|
passwd => $fields[1],
|
|
uid => $fields[2],
|
|
gid => $fields[3],
|
|
gcos => $fields[4],
|
|
dir => $fields[5],
|
|
shell => $fields[6],
|
|
);
|
|
}
|
|
else {
|
|
my $fnret = OVH::Bastion::sys_getpw_name(name => $account, cache => $cache);
|
|
if ($fnret) {
|
|
%entry = %{$fnret->value};
|
|
}
|
|
}
|
|
|
|
if (%entry) {
|
|
my ($newname) = $entry{'name'} =~ m{([a-zA-Z0-9._-]+)};
|
|
return R('ERR_SECURITY_VIOLATION', msg => "Forbidden characters in account name")
|
|
if ($newname ne $entry{'name'});
|
|
$entry{'name'} = $newname; # untaint
|
|
|
|
if ($checkBastionShell && $entry{'shell'} ne $OVH::Bastion::BASEPATH . "/bin/shell/osh.pl") {
|
|
return R('KO_NOT_FOUND', msg => "Account '$account' doesn't exist"); # msg is the same as below, voluntarily
|
|
}
|
|
|
|
my ($newdir) = $entry{'dir'} =~ m{([/a-zA-Z0-9._-]+)}; # untaint
|
|
return R('ERR_SECURITY_VIOLATION', msg => "Forbidden characters in account home directory")
|
|
if ($newdir ne $entry{'dir'});
|
|
$entry{'dir'} = $newdir; # untaint
|
|
return R('OK',
|
|
value => {uid => $entry{'uid'}, gid => $entry{'gid'}, dir => $entry{'dir'}, account => $entry{'name'}});
|
|
}
|
|
return R('KO_NOT_FOUND', msg => "Account '$account' doesn't exist");
|
|
}
|
|
|
|
# all ACL modifications (on groups, on accounts, including group-guests) are handled here
|
|
sub access_modify {
|
|
my %params = @_;
|
|
|
|
my $action = $params{'action'}; # add or del
|
|
|
|
my $user = $params{'user'}; # if undef, means a user-wildcard access
|
|
my $ip = $params{'ip'}; # can be a single ip or prefix
|
|
my $port = $params{'port'}; # if undef, means a port-wildcard access
|
|
|
|
my $ttl = $params{'ttl'};
|
|
my $comment = $params{'comment'};
|
|
|
|
my $way = $params{'way'}; # group, groupguest, personal
|
|
my $group = $params{'group'}; # only for way=group or way=groupguest
|
|
my $account = $params{'account'}; # only for way=personal
|
|
|
|
my $forceKey = $params{'forceKey'};
|
|
my $forcePassword = $params{'forcePassword'};
|
|
|
|
my $dryrun = $params{'dryrun'}; # don't do anything, just check params and prereqs
|
|
my $sudo = $params{'sudo'}; # passed as-is to subs we use
|
|
|
|
# deny accesses wider than these prefixes
|
|
my %widestVxPrefix = (
|
|
4 => $params{'widestV4Prefix'},
|
|
6 => $params{'widestV6Prefix'},
|
|
);
|
|
|
|
my $fnret;
|
|
|
|
foreach my $mandatoryParam (qw/action ip way/) {
|
|
if (!$params{$mandatoryParam}) {
|
|
return R('ERR_MISSING_PARAMETER', msg => "Missing parameter '$mandatoryParam'");
|
|
}
|
|
}
|
|
|
|
# if undef, default to sudo==1
|
|
$sudo //= 1;
|
|
|
|
# due to how plugins work, sometimes user and port are just '', make them undef in those cases
|
|
undef $user if (defined $user && $user eq '');
|
|
undef $port if (defined $port && $port eq '');
|
|
|
|
# check way
|
|
if ($way eq 'personal') {
|
|
return R('ERR_INVALID_PARAMETER', msg => "Group parameter specified with way=personal") if defined $group;
|
|
return R('ERR_MISSING_PARAMETER', msg => "Account parameter mandatory with way=personal")
|
|
if not defined $account;
|
|
}
|
|
elsif ($way eq 'group') {
|
|
return R('ERR_MISSING_PARAMETER', msg => "Group parameter mandatory with way=group") if not defined $group;
|
|
return R('ERR_INVALID_PARAMETER', msg => "Account parameter specified with way=group") if defined $account;
|
|
}
|
|
elsif ($way eq 'groupguest') {
|
|
if (not defined $account or not defined $group) {
|
|
return R('ERR_MISSING_PARAMETER', msg => "Account or group parameter missing with way=groupguest");
|
|
}
|
|
}
|
|
else {
|
|
return R('ERR_INVALID_PARAMETER', msg => "Parameter 'way' must be either personal, group or groupguest");
|
|
}
|
|
|
|
if ($action ne 'add' and $action ne 'del') {
|
|
return R('ERR_INVALID_PARAMETER', msg => "Action should be either 'del' or 'add'");
|
|
}
|
|
|
|
# check ip
|
|
$fnret = OVH::Bastion::is_valid_ip(ip => $ip, allowPrefixes => 1);
|
|
return $fnret unless $fnret;
|
|
$ip = $fnret->value->{'ip'};
|
|
|
|
if ($fnret->value->{'type'} eq 'prefix') {
|
|
my $ipVersion = $fnret->value->{'version'};
|
|
if (defined $widestVxPrefix{$ipVersion} && $fnret->value->{'prefixlen'} < $widestVxPrefix{$ipVersion}) {
|
|
return R(
|
|
'ERR_INVALID_PARAMETER',
|
|
msg => sprintf(
|
|
"Specified prefix (/%d) is too wide, maximum allowed for IPv%d is /%d by this bastion policy",
|
|
$fnret->value->{'prefixlen'},
|
|
$ipVersion, $widestVxPrefix{$ipVersion}
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
# check port
|
|
if (defined $port) {
|
|
$fnret = OVH::Bastion::is_valid_port(port => $port);
|
|
return $fnret unless $fnret;
|
|
$port = $fnret->value;
|
|
}
|
|
|
|
# check remote user
|
|
if (defined $user) {
|
|
$fnret = OVH::Bastion::is_valid_remote_user(user => $user);
|
|
return $fnret unless $fnret;
|
|
$user = $fnret->value;
|
|
}
|
|
|
|
# check account
|
|
my ($remoteaccount, $sysaccount);
|
|
if (defined $account) {
|
|
|
|
# accountType==normal : account must NOT be a realm_* account (but can be a realm/jdoe account)
|
|
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account, accountType => 'normal');
|
|
$fnret or return $fnret;
|
|
$sysaccount = $fnret->value->{'sysaccount'};
|
|
$account = $fnret->value->{'account'};
|
|
$remoteaccount = $fnret->value->{'remoteaccount'};
|
|
}
|
|
|
|
# check group
|
|
my $shortGroup;
|
|
if (defined $group) {
|
|
$fnret = OVH::Bastion::is_valid_group_and_existing(group => $group, groupType => 'key');
|
|
$fnret or return $fnret;
|
|
$group = $fnret->value->{'group'}; # untainted
|
|
$shortGroup = $fnret->value->{'shortGroup'}; # untainted
|
|
}
|
|
|
|
# check key fingerprint
|
|
if ($forceKey) {
|
|
$fnret = OVH::Bastion::is_valid_fingerprint(fingerprint => $forceKey);
|
|
$fnret or return $fnret;
|
|
$forceKey = $fnret->value->{'fingerprint'};
|
|
}
|
|
|
|
# check password hash
|
|
if ($forcePassword) {
|
|
$fnret = OVH::Bastion::is_valid_hash(hash => $forcePassword);
|
|
$fnret or return $fnret;
|
|
$forcePassword = $fnret->value->{'hash'};
|
|
}
|
|
|
|
if ($ttl) {
|
|
if ($ttl =~ /^(\d+)$/) {
|
|
$ttl = $1;
|
|
}
|
|
else {
|
|
return R('ERR_INVALID_PARAMETER', msg => "The TTL must be numeric");
|
|
}
|
|
}
|
|
|
|
# check if the caller has the right to make the change they're asking
|
|
# ... 1. either $> is allowkeeper and $ENV{'SUDO_USER'} is the requesting account
|
|
# ... 2. or $> is $grouptomodify and $ENV{'SUDO_USER'} is the requesting account
|
|
|
|
my ($running_as) = (getpwuid($>))[0] =~ /([0-9a-zA-Z_.-]+)/;
|
|
my $requester;
|
|
if ($sudo) {
|
|
($requester) = $ENV{'SUDO_USER'} =~ /([0-9a-zA-Z_.-]+)/;
|
|
}
|
|
else {
|
|
$requester = $running_as;
|
|
}
|
|
|
|
# requester can never be a realm_* account, because it's shared and should not be able to add access to anything
|
|
return R('ERR_SECURITY_VIOLATION', msg => "Requester can't be a realm user") if $requester =~ /^realm_/;
|
|
|
|
my @one_should_succeed;
|
|
my $expected_running_as = 'allowkeeper';
|
|
|
|
if ($way eq 'personal') {
|
|
if ($requester eq $account) {
|
|
push @one_should_succeed,
|
|
OVH::Bastion::is_user_in_group(
|
|
user => $requester,
|
|
group => 'osh-self' . ucfirst($action) . 'PersonalAccess',
|
|
sudo => $sudo,
|
|
);
|
|
}
|
|
|
|
# this is not a else here: somebody who has the account* right doesn't need the self* right
|
|
push @one_should_succeed,
|
|
OVH::Bastion::is_user_in_group(
|
|
user => $requester,
|
|
group => 'osh-account' . ucfirst($action) . 'PersonalAccess',
|
|
sudo => $sudo
|
|
);
|
|
}
|
|
elsif ($way eq 'group') {
|
|
$expected_running_as = $group;
|
|
push @one_should_succeed,
|
|
OVH::Bastion::is_group_aclkeeper(account => $requester, group => $shortGroup, superowner => 1, sudo => $sudo);
|
|
}
|
|
elsif ($way eq 'groupguest') {
|
|
push @one_should_succeed,
|
|
OVH::Bastion::is_group_gatekeeper(
|
|
account => $requester,
|
|
group => $shortGroup,
|
|
superowner => 1,
|
|
sudo => $sudo
|
|
);
|
|
}
|
|
|
|
if ($running_as ne $expected_running_as && !$dryrun) {
|
|
warn_syslog("Security violation: current running user ($running_as) unexpected (wanted $expected_running_as)");
|
|
return R('ERR_SECURITY_VIOLATION', msg => "Current running user unexpected");
|
|
}
|
|
|
|
if (grep({ $_ } @one_should_succeed) == 0 && $requester ne 'root' && !$dryrun) {
|
|
warn_syslog(
|
|
"Security violation: requesting user '$requester' doesn't have the right to do that (way=$way, group="
|
|
. ($shortGroup ? '<u>' : $shortGroup)
|
|
. ")");
|
|
return R('ERR_SECURITY_VIOLATION', msg => "You're not allowed to do that");
|
|
}
|
|
|
|
# end of dryrun
|
|
return R('OK', msg => "Would have added the access but we've been called with dryrun") if $dryrun;
|
|
|
|
# now, check if the access we're being asked to change is already in place or not
|
|
osh_debug(
|
|
"for action $action of $user\@$ip:$port of way $way with account=$account and group=$group, checking if already granted"
|
|
);
|
|
$fnret = OVH::Bastion::is_access_way_granted(
|
|
user => $user,
|
|
ip => $ip,
|
|
port => $port,
|
|
way => $way,
|
|
group => $shortGroup,
|
|
account => $account,
|
|
exactMatch => 1, # we're checking if the exact right we're asked to modify exists or not
|
|
);
|
|
osh_debug("... result is $fnret");
|
|
|
|
if ($action eq 'add' and $fnret) {
|
|
return R('OK_NO_CHANGE', msg => "The requested access to add was already granted");
|
|
}
|
|
elsif ($action eq 'del' and not $fnret) {
|
|
return R('OK_NO_CHANGE', msg => "The requested access to delete was not found, no change made");
|
|
}
|
|
|
|
# ok, now do the change, first define this sub
|
|
|
|
my $_access_modify_file = sub {
|
|
my %sub_params = @_;
|
|
my $file = $sub_params{'file'};
|
|
|
|
# we don't check our params or the rights because our caller already did, guaranteed by the scoping of this sub
|
|
|
|
# check if we can access the file
|
|
if (!(-e $file)) {
|
|
|
|
# it doesn't exist yet, create it
|
|
OVH::Bastion::touch_file($file, oct(644));
|
|
if (!(-e $file)) {
|
|
return R('ERR_CANNOT_CREATE_FILE', msg => "File '$file' is missing and couldn't be created");
|
|
}
|
|
}
|
|
|
|
# can we write to it ?
|
|
if (!(-w $file)) {
|
|
return R('ERR_CANNOT_OPEN_FILE', msg => "File '$file' cannot be written to");
|
|
}
|
|
|
|
# build the line we're either adding or looking for (to delete it)
|
|
my $entry = $ip;
|
|
$entry = $user . "@" . $entry if defined $user;
|
|
$entry = $entry . ":" . $port if defined $port;
|
|
my $machine = $entry;
|
|
|
|
my $t = localtime(time);
|
|
my $fmt = "%Y-%m-%d %H:%M:%S";
|
|
my $date = $t->strftime($fmt);
|
|
my $entryComment = "# $action by $requester on $date";
|
|
|
|
# if we're adding it, append other parameters as comments
|
|
if ($action eq 'add') {
|
|
|
|
$entry .= " $entryComment";
|
|
|
|
if ($forceKey) {
|
|
|
|
# hash is case-sensitive only for new SHA256 format
|
|
$forceKey = lc($forceKey) if ($forceKey !~ /^sha256:/i);
|
|
$entry .= " # FORCEKEY=" . $forceKey;
|
|
}
|
|
if ($forcePassword) {
|
|
$entry .= " # FORCEPASSWORD=" . $forcePassword;
|
|
}
|
|
if ($ttl) {
|
|
$entry .= " # EXPIRY=" . (time() + $ttl);
|
|
}
|
|
if ($comment) {
|
|
$comment =~ s{[#<>\\"']}{_}g;
|
|
$entry .= " # COMMENT=<" . $comment . ">";
|
|
}
|
|
}
|
|
|
|
# to be extra sure, remove any \n in $entry, which is impossible because we vetted all the params,
|
|
# but if somehow we failed, we'll be sure it doesn't permit to add multiple rights at once
|
|
$entry =~ s/[\r\n]*//gm;
|
|
|
|
# now, do the change
|
|
my $returnmsg;
|
|
if ($action eq 'add') {
|
|
osh_debug("going to add entry '$entry'");
|
|
if (open(my $fh_file, '>>', $file)) {
|
|
print $fh_file $entry . "\n";
|
|
close($fh_file);
|
|
}
|
|
else {
|
|
return R('ERR_CANNOT_OPEN_FILE', msg => "Error opening $file: $!");
|
|
}
|
|
my $ttlmsg =
|
|
$ttl ? (' (expires in ' . OVH::Bastion::duration2human(seconds => $ttl)->value->{'human'} . ')') : '';
|
|
$returnmsg = "Access to $machine successfully added$ttlmsg";
|
|
}
|
|
elsif ($action eq 'del') {
|
|
if (open(my $fh_file, '<', $file)) {
|
|
my $newFile;
|
|
my $found = 0;
|
|
while (my $line = <$fh_file>) {
|
|
if ($line =~ m{^\Q$entry\E(\s|$)}) {
|
|
chomp $line;
|
|
$line = "# $line # $comment\n";
|
|
$found++;
|
|
}
|
|
$newFile .= $line;
|
|
}
|
|
close($fh_file);
|
|
|
|
if ($found) {
|
|
|
|
# now rewrite
|
|
if (open(my $fh_file, '>', $file)) {
|
|
print $fh_file $newFile;
|
|
close($fh_file);
|
|
$returnmsg = "Access to $machine successfully removed";
|
|
}
|
|
else {
|
|
return R('ERR_CANNOT_OPEN_FILE', msg => "Unable to write open $file");
|
|
}
|
|
}
|
|
else {
|
|
return R('OK_NO_CHANGE', msg => "Entry $entry was not present in file $file");
|
|
}
|
|
}
|
|
}
|
|
OVH::Bastion::syslogFormatted(
|
|
severity => 'info',
|
|
type => 'acl',
|
|
fields => [
|
|
['action', $params{'action'}],
|
|
['type', $params{'way'}],
|
|
['group', $shortGroup],
|
|
['account', $params{'account'}],
|
|
['user', $params{'user'}],
|
|
['ip', $params{'ip'}],
|
|
['port', $params{'port'}],
|
|
['ttl', $params{'ttl'}],
|
|
['force_key', $params{'forceKey'}],
|
|
['force_password', $params{'forcePassword'}],
|
|
['comment', $params{'comment'}],
|
|
]
|
|
);
|
|
return R('OK', msg => $returnmsg) if $returnmsg;
|
|
return R('ERR_INTERNAL');
|
|
}; # end of sub definition
|
|
|
|
# then call the sub we just defined
|
|
delete $params{'file'};
|
|
my $ret;
|
|
my $prefix = $remoteaccount ? "allowed_$remoteaccount" : "allowed";
|
|
if ($way eq 'personal') {
|
|
$ret = $_access_modify_file->(%params, file => "/home/allowkeeper/$sysaccount/$prefix.private");
|
|
}
|
|
elsif ($way eq 'group') {
|
|
$ret = $_access_modify_file->(%params, file => "/home/$group/allowed.ip");
|
|
}
|
|
elsif ($way eq 'groupguest') {
|
|
$ret = $_access_modify_file->(%params, file => "/home/allowkeeper/$sysaccount/$prefix.partial.$shortGroup");
|
|
}
|
|
osh_debug("_access_modify_file() said $ret");
|
|
return $ret if defined $ret;
|
|
|
|
return R('ERR_INTERNAL'); # unreachable
|
|
}
|
|
|
|
# Check that a group is valid or not (syntax)
|
|
sub is_valid_group {
|
|
my %params = @_;
|
|
my $group = $params{'group'};
|
|
my $groupType = $params{'groupType'};
|
|
|
|
# possible groupTypes:
|
|
# osh: osh-accountList
|
|
# tty: login8-tty
|
|
# key: keymygroup
|
|
# gatekeeper: keymygroup-gatekeeper
|
|
# aclkeeper: keymygroup-aclkeeper
|
|
# owner: keymygroup-owner
|
|
# regular: no check appart from the length and forbidden prefixes/suffixes
|
|
|
|
if (!$group) {
|
|
return R('ERR_MISSING_PARAMETER', msg => "Missing parameter 'group'");
|
|
}
|
|
|
|
# autodetect if my caller prefixed the group name with 'key' or not, and adjust accordingly.
|
|
# we'll return normalized group and shortGroup values to our caller
|
|
if ($group !~ /^key/ && defined $groupType && grep { $groupType eq $_ } qw{ key gatekeeper aclkeeper owner }) {
|
|
$group = "key$group";
|
|
}
|
|
|
|
if ($group =~ m/keeper$/i and not grep { $groupType eq $_ } qw{ gatekeeper aclkeeper }) {
|
|
return R('KO_FORBIDDEN_SUFFIX', msg => 'Forbidden suffix in group name');
|
|
}
|
|
elsif ($group =~ m/owner$/i and $groupType ne 'owner') {
|
|
return R('KO_FORBIDDEN_SUFFIX', msg => 'Forbidden suffix in group name');
|
|
}
|
|
elsif ($group =~ m/-tty$/i and $groupType ne 'tty') {
|
|
return R('KO_FORBIDDEN_SUFFIX', msg => 'Forbidden suffix in group name');
|
|
}
|
|
elsif ($group =~ m/^key/i and not grep { $groupType eq $_ } qw{ key gatekeeper owner }) {
|
|
return R('KO_FORBIDDEN_PREFIX', msg => 'Forbidden prefix in group name');
|
|
}
|
|
elsif ($group =~ m/^[-.]/) {
|
|
return R('KO_FORBIDDEN_PREFIX', msg => "Group name can't start with a '-' nor a '.'");
|
|
}
|
|
elsif ($group =~ /^(key)?(private|root|user|self|legacy|osh)(-(gatekeeper|aclkeeper|owner))?$/) {
|
|
return R('KO_FORBIDDEN_NAME', msg => 'Forbidden group name');
|
|
}
|
|
elsif ($group =~ m/^([a-zA-Z0-9._-]+)$/) {
|
|
$group = $1; # untainted
|
|
if ($groupType eq 'key' and $group !~ m/^key/) {
|
|
return R('KO_MISSING_PREFIX', msg => "The group $group should have a prefix (group type $groupType)");
|
|
}
|
|
elsif ($groupType eq 'gatekeeper' and $group !~ m/^key.+-gatekeeper$/) {
|
|
return R('KO_MISSING_PREFIX', msg => "The group $group should have a prefix (group type $groupType)");
|
|
}
|
|
elsif ($groupType eq 'owner' and $group !~ m/^key.+-owner$/) {
|
|
return R('KO_MISSING_PREFIX', msg => "The group $group should have a prefix (group type $groupType)");
|
|
}
|
|
elsif ($groupType and $groupType eq 'tty' and $group !~ m/-tty$/) {
|
|
return R('KO_MISSING_SUFFIX', msg => "The group $group should have a suffix (group type $groupType)");
|
|
}
|
|
my $shortGroup = $group;
|
|
$shortGroup =~ s/^key|^osh-|-(gatekeeper|aclkeeper|owner|tty)$//g;
|
|
|
|
if (length($group) > 32) {
|
|
|
|
# 32 max for the whole group (system limit)
|
|
return R('KO_NAME_TOO_LONG', msg => 'Group name is too long (system limit)');
|
|
}
|
|
|
|
# 18 max for the short group name, because 32 - length(key) - length(-gatekeeper) == 18
|
|
if ((grep { $groupType eq $_ } qw{ key gatekeeper aclkeeper owner }) && (length($shortGroup) > 18)) {
|
|
return R('KO_NAME_TOO_LONG', msg => "Group name is too long (limit is 18 chars)");
|
|
}
|
|
|
|
return R('OK', value => {group => $group, shortGroup => $shortGroup});
|
|
}
|
|
return R('KO_FORBIDDEN_NAME', msg => 'Group name contains invalid characters');
|
|
}
|
|
|
|
sub is_valid_group_and_existing {
|
|
my %params = @_;
|
|
|
|
my $fnret = OVH::Bastion::is_valid_group(%params);
|
|
$fnret or return $fnret;
|
|
$params{'group'} = $fnret->value->{'group'};
|
|
return OVH::Bastion::is_group_existing(%params, user_friendly_error => 1);
|
|
}
|
|
|
|
# Add a user to a group
|
|
sub add_user_to_group {
|
|
my %params = @_;
|
|
my $group = $params{'group'};
|
|
my $user = $params{'user'};
|
|
my $accountType = $params{'accountType'};
|
|
my $groupType = $params{'groupType'};
|
|
my $fnret;
|
|
|
|
osh_debug('validating user');
|
|
$fnret = OVH::Bastion::is_account_valid(account => $user, accountType => $accountType);
|
|
$fnret or return $fnret;
|
|
osh_debug('user is ok');
|
|
$user = $fnret->value->{'account'} || $fnret->value->{'realm'};
|
|
|
|
osh_debug('validating group name');
|
|
if ($groupType) {
|
|
$fnret = OVH::Bastion::is_valid_group(group => $group, groupType => $groupType);
|
|
}
|
|
else {
|
|
$fnret = OVH::Bastion::is_valid_group(group => $group);
|
|
}
|
|
$fnret or return $fnret;
|
|
osh_debug('group name is ok');
|
|
$group = $fnret->value->{'group'};
|
|
|
|
$fnret = OVH::Bastion::sys_addmembertogroup(group => $group, user => $user);
|
|
$fnret or return R('ERR_USERMOD_FAILED', msg => "Error while adding $user to group $group (" . $fnret->msg . ")");
|
|
return R('OK');
|
|
}
|
|
|
|
# return the list of the bastion groups (i.e. not the system group list)
|
|
sub get_group_list {
|
|
my %params = @_;
|
|
my $cache = $params{'cache'}; # allow cache use of sys_getgr_all()
|
|
|
|
# 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)
|
|
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 })
|
|
{
|
|
my $entry = $fnret->value->{$name};
|
|
$name =~ s/^key//;
|
|
$groups{$name} = {gid => $entry->{'gid'}, members => $entry->{'members'}} if ($name ne '');
|
|
}
|
|
}
|
|
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'}; # allow cache use of sys_getpw_all()
|
|
# note that is_bastion_account_valid_and_existing() passthroughs its
|
|
# $cache param to sys_getpw_name() too
|
|
|
|
# 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;
|
|
foreach my $name (keys %{$fnret->value}) {
|
|
|
|
# if $accounts has been specified, only consider those
|
|
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);
|
|
|
|
my $entry = $fnret->value->{$name};
|
|
|
|
# add proper accounts, only include a subset of the fields we got
|
|
$users{$name} = {
|
|
name => $entry->{'name'},
|
|
gid => $entry->{'gid'},
|
|
home => $entry->{'dir'},
|
|
shell => $entry->{'shell'},
|
|
uid => $entry->{'uid'}
|
|
};
|
|
}
|
|
|
|
return R('OK', value => \%users);
|
|
}
|
|
|
|
sub get_realm_list {
|
|
my %params = @_;
|
|
my $realms = $params{'realms'} || [];
|
|
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
|
|
|
|
# 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;
|
|
foreach my $name (keys %{$fnret->value}) {
|
|
|
|
# if $realms has been specified, only consider those
|
|
next if (@$realms && !grep { $name eq "realm_$_" } @$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 !OVH::Bastion::is_bastion_account_valid_and_existing(
|
|
account => $name,
|
|
accountType => "realm",
|
|
cache => 1
|
|
);
|
|
|
|
# add proper realms
|
|
$name =~ s{^realm_}{};
|
|
$users{$name} = {name => $name};
|
|
}
|
|
|
|
return R('OK', value => \%users);
|
|
}
|
|
|
|
# check if account is a bastion admin (gives access to adminXyz commands)
|
|
# hint: an admin is also always a superowner
|
|
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;
|
|
}
|
|
|
|
if (not $account) {
|
|
return R('ERR_INTERNAL_ERROR');
|
|
}
|
|
if (not $sudo and exists $ENV{'SUDO_USER'}) {
|
|
|
|
# only legit case is if we have osh.pl under sudo because of an admin (adminSudo / ssh-as), check this
|
|
if (not OVH::Bastion::is_admin(account => $ENV{'SUDO_USER'}, sudo => 1)) {
|
|
warn_syslog("is_admin(): wasn't expected to be called under sudo, but was, with user "
|
|
. $ENV{'SUDO_USER'}
|
|
. " from account "
|
|
. $params{'account'});
|
|
return R('ERR_SECURITY_VIOLATION',
|
|
msg => "Wasn't expected to be called under sudo, but was, with user " . $ENV{'SUDO_USER'});
|
|
}
|
|
}
|
|
|
|
my $adminList = OVH::Bastion::config('adminAccounts')->value();
|
|
if (grep { $account eq $_ } @$adminList) {
|
|
return OVH::Bastion::is_user_in_group(group => "osh-admin", user => $account, cache => $cache);
|
|
}
|
|
return R('KO_ACCESS_DENIED');
|
|
}
|
|
|
|
# check if account is a superowner
|
|
# hint: an admin is also always a superowner
|
|
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;
|
|
}
|
|
|
|
if (not $account) {
|
|
return R('ERR_INTERNAL_ERROR');
|
|
}
|
|
if (not $sudo and exists $ENV{'SUDO_USER'}) {
|
|
|
|
# only legit case is if we have osh.pl under sudo because of an admin (adminSudo / ssh-as), check this
|
|
if (not OVH::Bastion::is_admin(account => $ENV{'SUDO_USER'}, sudo => 1)) {
|
|
warn_syslog("is_super_owner(): wasn't expected to be called under sudo, but was, with user "
|
|
. $ENV{'SUDO_USER'}
|
|
. " from account "
|
|
. $params{'account'});
|
|
return R('ERR_SECURITY_VIOLATION',
|
|
msg => "Wasn't expected to be called under sudo, but was, with user " . $ENV{'SUDO_USER'});
|
|
}
|
|
}
|
|
|
|
my $superownerList = OVH::Bastion::config('superOwnerAccounts')->value();
|
|
if (grep { $account eq $_ } @$superownerList) {
|
|
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, cache => $cache);
|
|
}
|
|
|
|
# check if account is an auditor
|
|
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;
|
|
}
|
|
|
|
if (not $account) {
|
|
return R('ERR_INTERNAL_ERROR');
|
|
}
|
|
if (not $sudo and exists $ENV{'SUDO_USER'}) {
|
|
|
|
# only legit case is if we have osh.pl under sudo because of an admin (adminSudo / ssh-as), check this
|
|
if (not OVH::Bastion::is_admin(account => $ENV{'SUDO_USER'}, sudo => 1)) {
|
|
warn_syslog("is_auditor(): wasn't expected to be called under sudo, but was, with user "
|
|
. $ENV{'SUDO_USER'}
|
|
. " from account "
|
|
. $params{'account'});
|
|
return R('ERR_SECURITY_VIOLATION',
|
|
msg => "Wasn't expected to be called under sudo, but was, with user " . $ENV{'SUDO_USER'});
|
|
}
|
|
}
|
|
|
|
return OVH::Bastion::is_user_in_group(group => "osh-auditor", user => $account);
|
|
}
|
|
|
|
# used by funcs below
|
|
sub _has_group_role {
|
|
my %params = @_;
|
|
my $account = $params{'account'};
|
|
my $shortGroup = $params{'group'};
|
|
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 of sys_getgr_name() through is_user_in_group() and
|
|
# is_bastion_account_valid_and_existing()
|
|
my $fnret;
|
|
|
|
if (not $account) {
|
|
$account = $sudo ? $ENV{'SUDO_USER'} : OVH::Bastion::get_user_from_env()->value;
|
|
}
|
|
if (not $account) {
|
|
return R('ERR_MISSING_PARAMETER', msg => 'Expected parameter account');
|
|
}
|
|
if (not $sudo and exists $ENV{'SUDO_USER'}) {
|
|
|
|
# only legit case is if we have osh.pl under sudo because of an admin (adminSudo / ssh-as), check this
|
|
if (not OVH::Bastion::is_admin(account => $ENV{'SUDO_USER'}, sudo => 1)) {
|
|
warn_syslog("_has_group_role(): wasn't expected to be called under sudo, but was, with user "
|
|
. $ENV{'SUDO_USER'}
|
|
. " from account "
|
|
. $params{'account'});
|
|
return R('ERR_SECURITY_VIOLATION',
|
|
msg => "Wasn't expected to be called under sudo, but was, with user " . $ENV{'SUDO_USER'});
|
|
}
|
|
}
|
|
|
|
my $group = "key$shortGroup";
|
|
|
|
# "regular" means "member or guest", i.e. user is in group key$GROUPNAME
|
|
if ($role ne 'regular') {
|
|
$group .= "-$role";
|
|
}
|
|
|
|
# for the realm case, we need to test sysaccount and not just account
|
|
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account, cache => $cache);
|
|
$fnret or return $fnret;
|
|
|
|
my $sysaccount = $fnret->value->{'sysaccount'};
|
|
|
|
$fnret = OVH::Bastion::is_user_in_group(user => $sysaccount, group => $group, cache => $cache);
|
|
osh_debug("is <$sysaccount> in <$group> ? => " . ($fnret ? 'yes' : 'no'));
|
|
if ($fnret) {
|
|
$fnret->{'value'} = {account => $account, sysaccount => $sysaccount};
|
|
return $fnret;
|
|
}
|
|
|
|
# if superowner allowed, try it
|
|
if ($superowner) {
|
|
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});
|
|
}
|
|
}
|
|
|
|
# not admin or no superowner allowed... return is_user_in_group status but fixup the value if true
|
|
$fnret->{'value'} = {account => $account, sysaccount => $sysaccount} if $fnret;
|
|
return $fnret;
|
|
}
|
|
|
|
sub is_group_aclkeeper {
|
|
my %params = @_;
|
|
$params{'role'} = 'aclkeeper';
|
|
return _has_group_role(%params);
|
|
}
|
|
|
|
sub is_group_gatekeeper {
|
|
my %params = @_;
|
|
$params{'role'} = 'gatekeeper';
|
|
return _has_group_role(%params);
|
|
}
|
|
|
|
sub is_group_owner {
|
|
my %params = @_;
|
|
$params{'role'} = 'owner';
|
|
return _has_group_role(%params);
|
|
}
|
|
|
|
sub _is_group_member_or_guest {
|
|
my %params = @_;
|
|
my $shortGroup = $params{'group'};
|
|
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 => $cache);
|
|
$fnret or return $fnret;
|
|
|
|
$account = $fnret->value->{'account'};
|
|
my $remoteaccount = $fnret->value->{'remoteaccount'};
|
|
my $sysaccount = $fnret->value->{'sysaccount'};
|
|
|
|
my $group = "key$shortGroup";
|
|
$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
|
|
|
|
my $weare = 'guest';
|
|
|
|
# to be a member (old name: "full member"); one also need to have the symlink
|
|
my $prefix = $remoteaccount ? "allowed_$remoteaccount" : "allowed";
|
|
if (-l "/home/allowkeeper/$sysaccount/$prefix.ip.$shortGroup") {
|
|
|
|
# -l => test that file exists and is a symlink
|
|
# -r => test that the symlink dest still exists => REMOVED, because we (the caller) might not have the right to read the file if we're not member or guest ourselves
|
|
$weare = 'member';
|
|
}
|
|
|
|
return R('OK') if ($weare eq $want);
|
|
return R('KO');
|
|
}
|
|
|
|
# test if account is strictly a guest (i.e. if a member, then answer is no)
|
|
sub is_group_guest {
|
|
my %params = @_;
|
|
$params{'want'} = 'guest';
|
|
return _is_group_member_or_guest(%params);
|
|
}
|
|
|
|
# test if account is strictly a member (i.e. if a guest, then answer is no)
|
|
sub is_group_member {
|
|
my %params = @_;
|
|
$params{'want'} = 'member';
|
|
return _is_group_member_or_guest(%params);
|
|
}
|
|
|
|
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", cache => $cache);
|
|
$fnret or return $fnret;
|
|
|
|
my $sysaccount = $fnret->value->{'sysaccount'};
|
|
my $allowkeeperdir = "/home/allowkeeper/$sysaccount/";
|
|
|
|
my %accounts;
|
|
if (opendir(my $dh, "/home/allowkeeper/$sysaccount")) {
|
|
while (my $filename = readdir($dh)) {
|
|
if ($filename =~ /allowed_([a-zA-Z0-9._-]+)\.(ip|partial|private)/) {
|
|
$accounts{$1} = 1;
|
|
}
|
|
}
|
|
closedir($dh);
|
|
}
|
|
return R('OK', value => [sort keys %accounts]);
|
|
}
|
|
|
|
sub is_valid_ttl {
|
|
my %params = @_;
|
|
my $ttl = $params{'ttl'};
|
|
my $seconds;
|
|
|
|
if ($ttl =~ /^\d+$/) {
|
|
return R('OK', value => {seconds => $ttl + 0});
|
|
}
|
|
elsif ($ttl =~ m{^(\d+[smhdwy]*)+$}i) {
|
|
while ($ttl =~ m{(\d+)([smhdwy])?}gi) {
|
|
if ($2 eq 'y') { $seconds += $1 * 86400 * 365 }
|
|
elsif ($2 eq 'w') { $seconds += $1 * 86400 * 7 }
|
|
elsif ($2 eq 'd') { $seconds += $1 * 86400 }
|
|
elsif ($2 eq 'h') { $seconds += $1 * 3600 }
|
|
elsif ($2 eq 'm') { $seconds += $1 * 60 }
|
|
else { $seconds += $1 }
|
|
}
|
|
return R('OK', value => {seconds => $seconds + 0});
|
|
}
|
|
|
|
return R('KO_INVALID_PARAMETER',
|
|
msg => "Invalid TTL ($ttl), expected an amount of seconds, or a duration string such as '2d8h15m'");
|
|
}
|
|
|
|
# used by groupList and accountList
|
|
sub build_re_from_wildcards {
|
|
my %params = @_;
|
|
my $wildcards = $params{'wildcards'};
|
|
my $implicit_contains = $params{'implicit_contains'};
|
|
|
|
# to avoid modifying the caller's array
|
|
my @relist = @$wildcards;
|
|
|
|
# qr// is true, so return undef if there's nothing to build
|
|
return R('OK', value => undef) if !@relist;
|
|
|
|
for (@relist) {
|
|
if ($implicit_contains) {
|
|
|
|
# if we have a word without any ? or *, guess that the user expects a "contains" behavior, i.e. *item*
|
|
$_ = '*' . $_ . '*' if not /[\*\?]/;
|
|
}
|
|
$_ = quotemeta;
|
|
s/\\\*/.*/g;
|
|
s/\\\?/./g;
|
|
$_ = '^' . $_ . '$';
|
|
}
|
|
my $stringified = join("|", @relist);
|
|
return R('OK', value => qr/$stringified/);
|
|
}
|
|
|
|
1;
|