mirror of
https://github.com/ovh/the-bastion.git
synced 2025-01-11 01:41:39 +08:00
1181 lines
48 KiB
Perl
1181 lines
48 KiB
Perl
package OVH::Bastion;
|
|
|
|
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
|
|
|
|
use common::sense;
|
|
|
|
use Socket qw{ :all };
|
|
|
|
sub get_personal_account_keys {
|
|
my %params = @_;
|
|
my $account = $params{'account'};
|
|
my $listOnly = $params{'listOnly'} ? 1 : 0;
|
|
my $forceKey = $params{'forceKey'};
|
|
my $fnret;
|
|
|
|
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account, accountType => ($account =~ /^realm_/ ? "realm" : "normal"));
|
|
$fnret or return $fnret;
|
|
$account = $fnret->value->{'account'}; # untainted version
|
|
|
|
return _get_pub_keys_from_directory(
|
|
dir => "/home/$account/.ssh",
|
|
pattern => qr/^private\.pub$|^id_[a-z0-9]+[_.]private\.\d+\.pub$/,
|
|
listOnly => $listOnly, # don't be slow and don't parse the keys (by calling ssh-keygen -lf)
|
|
forceKey => $forceKey,
|
|
wantPrivate => 1,
|
|
);
|
|
}
|
|
|
|
my %_cache_get_group_keys;
|
|
|
|
sub get_group_keys {
|
|
my %params = @_;
|
|
my $group = $params{'group'};
|
|
my $cache = $params{'cache'}; # allow cache use (useful for multicall)
|
|
my $listOnly = $params{'listOnly'} ? 1 : 0;
|
|
my $forceKey = $params{'forceKey'};
|
|
my $fnret;
|
|
|
|
my $cacheKey = "$group:$listOnly";
|
|
|
|
if ($cache and exists $_cache_get_group_keys{$cacheKey}) {
|
|
return $_cache_get_group_keys{$cacheKey};
|
|
}
|
|
|
|
$fnret = OVH::Bastion::is_valid_group_and_existing(group => $group, groupType => 'key');
|
|
$fnret or return $fnret;
|
|
$group = $fnret->value->{'group'}; # untainted version
|
|
my $shortGroup = $fnret->value->{'shortGroup'};
|
|
|
|
$fnret = _get_pub_keys_from_directory(
|
|
dir => '/home/keykeeper/' . $group,
|
|
pattern => qr/^id_([a-z0-9]+)_\Q$shortGroup\E/,
|
|
listOnly => $listOnly,
|
|
forceKey => $forceKey,
|
|
wantPrivate => 1,
|
|
);
|
|
$_cache_get_group_keys{$cacheKey} = $fnret;
|
|
return $fnret;
|
|
}
|
|
|
|
# this function simply checks if the user@ip:port is allowed in the way given,
|
|
# i.e. personal access, group access, groupguest access, or legacy access.
|
|
# it calls is_access_granted_in_file with the proper file location depending
|
|
# on the access way that is tested. note that for e.g. group accesses, we don't
|
|
# check if a given account has access to the group or not, we just check if the
|
|
# group itself has access. this check must be done by our caller.
|
|
# returns: { match, size, forceKey } for best match, if any
|
|
sub is_access_way_granted {
|
|
my %params = @_;
|
|
|
|
my $exactIpMatch = $params{'exactIpMatch'}; # $ip must be explicitely allowed (not given through a wider slash or a 0.0.0.0/0 in grantfile)
|
|
my $exactPortMatch = $params{'exactPortMatch'}; # $port must be explicitely allowed (port wildcards in grantfile will be ignored)
|
|
my $exactUserMatch = $params{'exactUserMatch'}; # $user must be explicitely allowed (user wildcards in grantfile will be ignored)
|
|
my $exactMatch = $params{'exactMatch'}; # sets exactIpMatch exactPortMatch and exactUserMatch
|
|
|
|
my $ignoreUser = $params{'ignoreUser'}; # ignore remote user COMPLETELY (plop@, or root@, or <nil>@ will all match)
|
|
my $ignorePort = $params{'ignorePort'}; # ignore port COMPLETELY (port 22, 2345, or port-wildcard will all match)
|
|
|
|
my $wantedUser = $params{'user'}; # if undef, means we look for a user wildcard allow
|
|
my $wantedIp = $params{'ip'}; # can be a single IP or a prefix
|
|
my $wantedPort = $params{'port'}; # if undef, means we look for a port wildcard allow
|
|
|
|
my $way = $params{'way'}; # personal|group|groupguest|legacy
|
|
my $group = $params{'group'}; # only meaningful and needed if type=group or type=groupguest
|
|
my $account = $params{'account'}; # only meaningful and needed if type=personal or type=groupguest
|
|
|
|
my $fnret;
|
|
|
|
$exactIpMatch = $exactPortMatch = $exactUserMatch = 1 if $exactMatch;
|
|
|
|
# 'group', 'account', and 'way' parameters are only useful to, and checked by, get_acl_way()
|
|
$fnret = OVH::Bastion::get_acl_way(way => $way, account => $account, group => $group);
|
|
$fnret or return $fnret;
|
|
my @acl = @{$fnret->value || []};
|
|
|
|
osh_debug(
|
|
"checking way $way/$account/$group with ignorePort=$ignorePort ignoreUser=$ignoreUser exactIpMatch=$exactIpMatch exactPortMatch=$exactPortMatch exactUserMatch=$exactUserMatch"
|
|
);
|
|
|
|
my ($bestMatch, $bestMatchSize, $forceKey);
|
|
foreach my $entry (@acl) {
|
|
my $allowedIp = $entry->{'ip'}; # can be a prefix
|
|
my $allowedUser = $entry->{'user'}; # can be undef (if any-user)
|
|
my $allowedPort = $entry->{'port'}; # can be undef (if any-port)
|
|
my $localForceKey = $entry->{'forceKey'};
|
|
|
|
osh_debug("checking wanted "
|
|
. (defined $wantedUser ? $wantedUser : '<u>') . '@'
|
|
. (defined $wantedIp ? $wantedIp : '<u>') . ':'
|
|
. (defined $wantedPort ? $wantedPort : '<u>')
|
|
. ' against '
|
|
. (defined $allowedUser ? $allowedUser : '<u>') . '@'
|
|
. (defined $allowedIp ? $allowedIp : '<u>') . ':'
|
|
. (defined $allowedPort ? $allowedPort : '<u>'));
|
|
|
|
$allowedIp or next; # can't be empty
|
|
|
|
# first, check port stuff
|
|
# if we get ignorePort, we skip the checks entirely
|
|
if (not $ignorePort) {
|
|
if ($exactPortMatch) {
|
|
|
|
# we want an exact match
|
|
if (not defined $allowedPort) {
|
|
if (not defined $wantedPort) {
|
|
; # both undefined ? ok
|
|
}
|
|
else {
|
|
next; # if only one of two is undef, it's not an exact match
|
|
}
|
|
}
|
|
else {
|
|
if (not defined $wantedPort) {
|
|
next; # if only one of two is undef, it's not an exact match
|
|
}
|
|
else {
|
|
next if ($wantedPort ne $allowedPort); # both defined but unequal, not a match
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
# we don't want an exact match (aka wildcards allowed)
|
|
if (not defined $allowedPort) {
|
|
; # it's a wildcard, will always match
|
|
}
|
|
else {
|
|
if (not defined $wantedPort) {
|
|
next; # we want a wildcard, but we don't have it
|
|
}
|
|
else {
|
|
next if ($wantedPort ne $allowedPort); # both defined but unequal, not a match
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# second, check user stuff
|
|
# if we get ignoreUser, we skip the checks entirely
|
|
if (not $ignoreUser) {
|
|
if ($exactUserMatch) {
|
|
|
|
# we want an exact match
|
|
if (not defined $allowedUser) {
|
|
if (not defined $wantedUser) {
|
|
; # both undefined ? ok
|
|
}
|
|
else {
|
|
next; # if only one of two is undef, it's not an exact match
|
|
}
|
|
}
|
|
else {
|
|
if (not defined $wantedUser) {
|
|
next; # if only one of two is undef, it's not an exact match
|
|
}
|
|
else {
|
|
next if ($wantedUser ne $allowedUser); # both defined but unequal, not a match
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
# we don't want an exact match (aka wildcards allowed)
|
|
if (not defined $allowedUser) {
|
|
; # it's a wildcard, will always match
|
|
}
|
|
else {
|
|
if (not defined $wantedUser) {
|
|
next; # we want a wildcard, but we don't have it
|
|
}
|
|
else {
|
|
next if ($wantedUser ne $allowedUser); # both defined but unequal, not a match
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# then, check IP
|
|
# if we want an exact match, it's a stupid strcmp()
|
|
if ($exactIpMatch) {
|
|
next if ($allowedIp ne $wantedIp);
|
|
|
|
# here, we got a perfect match
|
|
$forceKey = $localForceKey;
|
|
$bestMatch = $allowedIp;
|
|
$bestMatchSize = undef; # not needed
|
|
last; # perfect match, don't search further
|
|
}
|
|
|
|
# check IP in not-exactIpMatch case. if it contains / then it's a prefix
|
|
if ($allowedIp =~ m{/}) {
|
|
|
|
# build slash and test
|
|
require Net::Netmask;
|
|
my $ipCheck = Net::Netmask->new2($allowedIp);
|
|
if ($ipCheck && $ipCheck->match($wantedIp)) {
|
|
osh_debug("... we got a slash match !");
|
|
if (not defined $bestMatchSize or $ipCheck->size() < $bestMatchSize) {
|
|
$forceKey = $localForceKey;
|
|
$bestMatch = $allowedIp;
|
|
$bestMatchSize = $ipCheck->size();
|
|
$bestMatchSize == 1 and last; # we won't get better than this
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
# it's a single ip, so a stupid strcmp() does the trick
|
|
if ($allowedIp eq $wantedIp) {
|
|
osh_debug("... we got a singleip match !");
|
|
$forceKey = $localForceKey;
|
|
$bestMatch = $allowedIp;
|
|
$bestMatchSize = 1;
|
|
last;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (defined $bestMatch) {
|
|
return R('OK', value => {match => $bestMatch, size => $bestMatchSize, forceKey => $forceKey});
|
|
}
|
|
return R('KO_ACCESS_DENIED');
|
|
}
|
|
|
|
# from a given hostname, check if we have an ip or a range of ip or try to resolve
|
|
sub get_ip {
|
|
my %params = @_;
|
|
my $host = $params{'host'};
|
|
my $v4 = $params{'v4'}; # allow ipv4 ?
|
|
my $v6 = $params{'v6'}; # allow ipv6 ?
|
|
|
|
if (!$host) {
|
|
return R('ERR_MISSING_PARAMETER', msg => "Missing parameter 'host'");
|
|
}
|
|
|
|
# by default, only v4 unless specified otherwise
|
|
$v4 = 1 if not defined $v4;
|
|
$v6 = 0 if not defined $v6;
|
|
|
|
# try to see if it's already an IP
|
|
osh_debug("checking if '$host' is already an IP");
|
|
my $fnret = OVH::Bastion::is_valid_ip(ip => $host, allowPrefixes => 0);
|
|
if ($fnret) {
|
|
osh_debug("Host $host is already an IP");
|
|
if ( ($fnret->value->{'version'} == 4 && $v4)
|
|
|| ($fnret->value->{'version'} == 6 && $v6))
|
|
{
|
|
return R('OK', value => {ip => $fnret->value->{'ip'}, iplist => [$fnret->value->{'ip'}]});
|
|
}
|
|
return R('ERR_INVALID_IP', msg => "IP $host version is not allowed");
|
|
}
|
|
|
|
osh_debug("Trying to resolve '$host' because is_valid_ip() says it's not an IP");
|
|
my ($err, @res);
|
|
eval {
|
|
# dns resolving, v4/v6 compatible
|
|
# can croak
|
|
($err, @res) = getaddrinfo($host, undef, {socktype => SOCK_STREAM});
|
|
};
|
|
return R('ERR_HOST_NOT_FOUND', msg => $@) if $@;
|
|
return R('ERR_HOST_NOT_FOUND', msg => $err) if $err;
|
|
|
|
my %iplist;
|
|
my $lastip;
|
|
foreach my $item (@res) {
|
|
if ($item->{'family'} == AF_INET) {
|
|
next if not $v4;
|
|
}
|
|
elsif ($item->{'family'} == AF_INET6) {
|
|
next if not $v6;
|
|
}
|
|
else {
|
|
# unknown weird family ?
|
|
next;
|
|
}
|
|
my $as_text;
|
|
undef $err;
|
|
eval {
|
|
($err, $as_text) = getnameinfo($item->{'addr'}, NI_NUMERICHOST); # NI flag: don't use dns, just unpack the binary 'addr'
|
|
};
|
|
if (not $@ and not $err) {
|
|
$iplist{$as_text} = 1;
|
|
$lastip = $as_text;
|
|
}
|
|
}
|
|
|
|
if (%iplist) {
|
|
return R('OK', value => {ip => $lastip, iplist => [keys %iplist]});
|
|
}
|
|
|
|
# %iplist empty, not resolved (?)
|
|
return R('ERR_HOST_NOT_FOUND', msg => "Unable to resolve '$host'");
|
|
}
|
|
|
|
# reverse-dns of an IPv4 or IPv6
|
|
sub ip2host {
|
|
my $ip = shift;
|
|
my ($err, @sockaddr, $host);
|
|
|
|
eval {
|
|
# ip => packedip. AI_PASSIVE: don't use dns, just build sockaddr
|
|
# can croak
|
|
($err, @sockaddr) = getaddrinfo($ip, 0, {flags => AI_PASSIVE, socktype => SOCK_STREAM});
|
|
};
|
|
return R('ERR_INVALID_IP', msg => $@) if $@;
|
|
return R('ERR_INVALID_IP', msg => $err) if $err;
|
|
|
|
eval {
|
|
# can croak
|
|
($err, $host, undef) = getnameinfo($sockaddr[0]->{'addr'}, NI_NAMEREQD);
|
|
};
|
|
return R('ERR_HOST_NOT_FOUND', msg => $@) if $@;
|
|
return R('ERR_HOST_NOT_FOUND', msg => $err) if $err;
|
|
|
|
return R('OK', value => $host);
|
|
}
|
|
|
|
# Return an array containings the groups for which user is a member of
|
|
my %_cache_get_user_groups;
|
|
|
|
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)
|
|
|
|
if (not $user) {
|
|
return R('ERR_MISSING_PARAMETER', msg => "Missing parameter 'account'");
|
|
}
|
|
|
|
if (not %_cache_get_user_groups) {
|
|
|
|
# build cache, it'll be faster than even one exec `id -nG` anyway
|
|
setgrent();
|
|
my %users;
|
|
while (my ($name, $passwd, $gid, $members) = getgrent()) {
|
|
foreach my $member (split / /, $members) {
|
|
push @{$_cache_get_user_groups{$member}}, $name;
|
|
}
|
|
}
|
|
setgrent();
|
|
}
|
|
|
|
my @groups = @{$_cache_get_user_groups{$user} || []};
|
|
my @availableGroups;
|
|
foreach my $group (@groups) {
|
|
if ($group =~ /^key.+-(gatekeeper|aclkeeper|owner)$/) {
|
|
push @availableGroups, $group if $extra;
|
|
}
|
|
else {
|
|
push @availableGroups, $group if $group =~ /^key/;
|
|
}
|
|
}
|
|
|
|
if (scalar(@availableGroups)) {
|
|
return R('OK', value => \@availableGroups);
|
|
}
|
|
else {
|
|
return R('ERR_NO_GROUP', msg => 'Unable to find any group');
|
|
}
|
|
}
|
|
|
|
sub _get_pub_keys_from_directory {
|
|
my %params = @_;
|
|
my $dir = $params{'dir'};
|
|
my $pattern = $params{'pattern'};
|
|
my $listOnly = $params{'listOnly'}; # don't open the files, just return file names
|
|
my $noexec = $params{'noexec'}; # passed to is_valid_public_key
|
|
my $forceKey = $params{'forceKey'};
|
|
my $wantPrivate = $params{'wantPrivate'}; # if set, will return the fullpath of the private key, not the public one
|
|
my $fnret;
|
|
|
|
osh_debug("looking for pub keys in dir $dir as user $ENV{'USER'}");
|
|
if (!-d $dir) {
|
|
return R('ERR_DIRECTORY_NOT_FOUND', msg => "directory $dir doesn't exist");
|
|
}
|
|
my $dh;
|
|
if (!opendir($dh, $dir)) {
|
|
return R('ERR_CANNOT_OPEN_DIRECTORY', msg => "can't open directory $dir: $!");
|
|
}
|
|
|
|
if (defined $pattern and ref $pattern ne 'Regexp') {
|
|
return R('ERR_INVALID_PARAMETER', msg => 'pattern is not a Regexp reference');
|
|
}
|
|
|
|
my %return;
|
|
while (my $file = readdir($dh)) {
|
|
$file =~ /^([a-zA-Z0-9._-]+\.pub)$/ or next;
|
|
$file = $1; # untaint
|
|
if (defined $pattern) {
|
|
$file =~ /$pattern/ or next;
|
|
}
|
|
my $filename = $file;
|
|
$file = "$dir/$file";
|
|
-f -r $file or next;
|
|
|
|
# ok file exists, is readable and matches the pattern
|
|
osh_debug("file $file matches the pattern in $dir");
|
|
|
|
my $mtime = (stat(_))[9];
|
|
if ($listOnly) {
|
|
$return{$file} = {fullpath => $file, filename => $filename, mtime => $mtime};
|
|
if ($wantPrivate) {
|
|
$return{$file}{'fullpath'} =~ s/\.pub$//;
|
|
$return{$file}{'filename'} =~ s/\.pub$//;
|
|
}
|
|
}
|
|
else {
|
|
# open the file and read the key
|
|
my $fh_key;
|
|
if (!open($fh_key, '<', $file)) {
|
|
osh_debug("can't open file $file ($!), skipping");
|
|
next;
|
|
}
|
|
|
|
while (my $line = <$fh_key>) {
|
|
|
|
# stop when we find a key or at EOF
|
|
chomp $line;
|
|
$fnret = OVH::Bastion::is_valid_public_key(way => 'egress', pubKey => $line, noexec => ($noexec && !$forceKey));
|
|
if (!$fnret) {
|
|
osh_debug("key in $file is not valid: " . $fnret->err);
|
|
osh_debug($fnret->msg);
|
|
}
|
|
else {
|
|
if ((not defined $forceKey) || ($forceKey eq $fnret->value->{'fingerprint'})) {
|
|
$return{$file} = $fnret->value;
|
|
$return{$file}{'fullpath'} = $file;
|
|
$return{$file}{'mtime'} = $mtime;
|
|
$return{$file}{'filename'} = $filename;
|
|
if ($wantPrivate) {
|
|
$return{$file}{'fullpath'} =~ s/\.pub$//;
|
|
$return{$file}{'filename'} =~ s/\.pub$//;
|
|
}
|
|
}
|
|
last;
|
|
}
|
|
}
|
|
close($fh_key);
|
|
}
|
|
}
|
|
close($dh);
|
|
|
|
# return a sorted keys list too f(mtime) desc
|
|
my @sortedKeys = sort { $return{$b}{'mtime'} <=> $return{$a}{'mtime'} } keys %return;
|
|
return R('OK', value => {keys => \%return, sortedKeys => \@sortedKeys});
|
|
}
|
|
|
|
sub duration2human {
|
|
my %params = @_;
|
|
my $s = $params{'seconds'};
|
|
my $tense = $params{'tense'};
|
|
|
|
require POSIX;
|
|
my $date = POSIX::strftime("%a %Y-%m-%d %H:%M:%S %Z", localtime(time() + ($tense eq 'past' ? -$s : $s)));
|
|
|
|
my $d = int($s / 86400);
|
|
$s -= $d * 86400;
|
|
my $h = int($s / 3600);
|
|
$s -= $h * 3600;
|
|
my $m = int($s / 60);
|
|
$s -= $m * 60;
|
|
|
|
my $duration = $d ? sprintf('%dd+%02d:%02d:%02d', $d, $h, $m, $s) : sprintf('%02d:%02d:%02d', $h, $m, $s);
|
|
return R('OK', value => {duration => $duration, date => $date, human => "$duration ($date)"});
|
|
}
|
|
|
|
sub print_acls {
|
|
my %params = @_;
|
|
my $acls = $params{'acls'} || [];
|
|
my $reverse = $params{'reverse'};
|
|
my $hideGroups = $params{'hideGroups'};
|
|
|
|
my $printIpLen = $reverse ? 30 : 15;
|
|
osh_info(
|
|
sprintf("%-" . $printIpLen . "s %5s %20s %30s %10s %13s %45s %40s %s", "IP", "PORT", "USER", "ACCESS-BY", "ADDED-BY", "ADDED-AT", "EXPIRY?", "COMMENT", "FORCED-KEY?"));
|
|
|
|
my @flatArray;
|
|
foreach my $contextAcl (@$acls) {
|
|
my $type = $contextAcl->{'type'};
|
|
my $group = $contextAcl->{'group'};
|
|
my $acl = $contextAcl->{'acl'};
|
|
|
|
next if ($hideGroups and $type =~ /^group/);
|
|
my $accessType = ($group ? "$group($type)" : $type);
|
|
|
|
foreach my $entry (@$acl) {
|
|
my $addedBy = $entry->{'addedBy'} || '(unknown)';
|
|
my $addedDate = $entry->{'addedDate'} || '(unknown)';
|
|
$addedDate = substr($addedDate, 0, 10);
|
|
my $forceKey = $entry->{'forceKey'} || '-';
|
|
my $expiry = $entry->{'expiry'} ? (duration2human(seconds => ($entry->{'expiry'} - time()))->value->{'human'}) : '-';
|
|
|
|
# type => member ('full'), guest ('partial'), personal or legacy
|
|
my $ipReverse = OVH::Bastion::ip2host($entry->{'ip'})->value if $reverse;
|
|
$entry->{'reverseDns'} = $ipReverse;
|
|
|
|
push @flatArray, $entry;
|
|
osh_info(
|
|
sprintf(
|
|
"%-" . $printIpLen . "s %5s %20s %30s %10s %13s %45s %40s %s",
|
|
$ipReverse ? $ipReverse : $entry->{'ip'},
|
|
$entry->{'port'} ? $entry->{'port'} : '(any)',
|
|
$entry->{'user'} ? $entry->{'user'} : '(any)',
|
|
$accessType, $addedBy, $addedDate, $expiry, $entry->{'userComment'} || '-', $forceKey
|
|
)
|
|
);
|
|
}
|
|
}
|
|
osh_info(scalar(@flatArray) . " accesses listed");
|
|
return R('OK', value => \@flatArray);
|
|
}
|
|
|
|
# checks if ip matches any given array of prefixes/networks
|
|
sub _is_in_any_net {
|
|
my %params = @_;
|
|
my $ip = $params{'ip'};
|
|
my $networks = $params{'networks'};
|
|
|
|
if (!$ip) {
|
|
return R('ERR_MISSING_PARAMETER', msg => "Missing parameter 'ip'");
|
|
}
|
|
if (ref $networks ne 'ARRAY') {
|
|
return R('ERR_INVALID_PARAMETER', msg => "Parameter 'networks' must be an array");
|
|
}
|
|
|
|
foreach my $net (@$networks) {
|
|
if ($net =~ m{/}) {
|
|
|
|
# build slash and test
|
|
require Net::Netmask;
|
|
my $ipCheck = Net::Netmask->new2($net);
|
|
return R('OK', value => {matched => $net}) if ($ipCheck && $ipCheck->match($ip));
|
|
}
|
|
else {
|
|
# it's a single ip, so it's a stupid strcmp() does the trick
|
|
return R('OK', value => {matched => $net}) if ($net eq $ip);
|
|
}
|
|
}
|
|
return R('KO', msg => "No match found");
|
|
}
|
|
|
|
# this function checks if the given account has access to user@ip:port
|
|
# through any of the supported ways (personal/group/guest/legacy accesses),
|
|
# by calling is_access_way_granted() multiple times with the proper params.
|
|
# it can also add the fullpath of the keys to try for allowed accesses if asked to
|
|
# returns: arrayref of contextualized grants, contextualized-grant: { type, group, $granthashref }
|
|
# granthashref: returned by is_access_way_granted, i.e. { match, size, forceKey }
|
|
sub is_access_granted {
|
|
my %params = @_;
|
|
|
|
# we'll use delete for params that we won't pass through is_access_way_granted()
|
|
my $account = delete $params{'account'}; # account to check the access grants of.
|
|
# can also be of the format "realm/remoteself"
|
|
|
|
my $ipfrom = $params{'ipfrom'}; # must be an IP (client IP)
|
|
my $ip = $params{'ip'}; # can be a single IP or a slash
|
|
my $port = $params{'port'}; # if undef, means we look for a port wildcard allow
|
|
my $user = $params{'user'}; # if undef, means we look for a user wildcard allow
|
|
|
|
my $listOnly = $params{'listOnly'}; # don't open the files, just return file names
|
|
my $noexec = $params{'noexec'}; # passed to is_valid_public_key
|
|
|
|
my $wantKeys = delete $params{'wantKeys'}; # if set, look for and return ssh keys along with allowed accesses
|
|
|
|
delete $params{'way'}; # WE specify this parameter, not our caller
|
|
delete $params{'group'}; # WE specify this parameter, not our caller
|
|
|
|
my @grants;
|
|
my $fnret;
|
|
require Data::Dumper;
|
|
|
|
# 0a/3 check if we're in a forbidden network. if we are, just bail out
|
|
my $forbiddenNetworks = OVH::Bastion::config('forbiddenNetworks')->value;
|
|
$fnret = _is_in_any_net(ip => $ip, networks => $forbiddenNetworks);
|
|
return R('KO_ACCESS_DENIED', msg => "Can't connect you to $ip as it's part of the forbidden networks of this bastion (see --osh info)") if $fnret->is_ok;
|
|
|
|
# 0b/3 check if we're not outside of the bastion allowed networks, if we are, just bail out
|
|
my $allowedNetworks = OVH::Bastion::config('allowedNetworks')->value;
|
|
if (@$allowedNetworks) {
|
|
$fnret = _is_in_any_net(ip => $ip, networks => $allowedNetworks);
|
|
return R('KO_ACCESS_DENIED', msg => "Can't connect you to $ip as it's not part of the allowed networks of this bastion (see --osh info)") if $fnret->is_ko;
|
|
}
|
|
|
|
# 0c/3 check if there are more complex "ingressToEgressRules" defined, and potentially bail out whether needed
|
|
$fnret = OVH::Bastion::config('ingressToEgressRules');
|
|
my @rules = @{$fnret->value || []};
|
|
for (my $ruleNb = 0 ; $ruleNb < @rules ; $ruleNb++) {
|
|
my ($inNets, $outNets, $policy) = @{$rules[$ruleNb]};
|
|
|
|
$fnret = _is_in_any_net(ip => $ipfrom, networks => $inNets);
|
|
if ($fnret->is_err) {
|
|
warn("Denied access due to potential configuration error in ingressToEgressRules (rule #$ruleNb, ingress");
|
|
return R('KO_ACCESS_DENIED', msg => "Error checking ingressToEgressRules, warn your bastion admin!");
|
|
}
|
|
|
|
# ingress IP doesn't match for this rule, go to next:
|
|
next if $fnret->is_ko;
|
|
|
|
# ingress IP matches, check whether egress IP matches
|
|
$fnret = _is_in_any_net(ip => $ip, networks => $outNets);
|
|
if ($fnret->is_err) {
|
|
warn("Denied access due to potential configuration error in ingressToEgressRules (rule #$ruleNb, egress");
|
|
return R('KO_ACCESS_DENIED', msg => "Error checking ingressToEgressRules, warn your bastion admin!");
|
|
}
|
|
if ($policy eq 'ALLOW-EXCLUSIVE') {
|
|
if ($fnret->is_ok) {
|
|
|
|
# egress matches: allowed, stop checking more rules
|
|
last;
|
|
}
|
|
|
|
# is_ko: we're in exclusive mode, stop checking and deny
|
|
return R('KO_ACCESS_DENIED', msg => "Can't connect you to $ip, as it's not part of the allowed networks given where you're connecting from ($ipfrom)");
|
|
}
|
|
elsif ($policy eq 'DENY') {
|
|
if ($fnret->is_ok) {
|
|
|
|
# egress matches: we have been asked to deny
|
|
return R('KO_ACCESS_DENIED', msg => "Can't connect you to $ip, as it's not part of the allowed networks given where you're connecting from ($ipfrom)");
|
|
}
|
|
|
|
# is_ko: egress doesn't match, check next rule
|
|
}
|
|
elsif ($policy eq 'ALLOW') {
|
|
if ($fnret->is_ok) {
|
|
|
|
# egress matches: we have been asked to allow, stop checking more rules
|
|
last;
|
|
}
|
|
|
|
# is_ko: egress doesn't match, check next rule
|
|
}
|
|
else {
|
|
# invalid policy
|
|
warn("Denied access due to potential configuration error in ingressToEgressRules (rule #$ruleNb, policy");
|
|
return R('KO_ACCESS_DENIED', msg => "Error checking ingressToEgressRules, warn your bastion admin!");
|
|
}
|
|
}
|
|
|
|
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account);
|
|
$fnret or return $fnret;
|
|
|
|
$account = $fnret->value->{'account'};
|
|
my $sysaccount = $fnret->value->{'sysaccount'};
|
|
|
|
# 1/3 check for personal accesses
|
|
# ... normal way
|
|
my $grantedPersonal = is_access_way_granted(%params, way => 'personal', account => $account);
|
|
osh_debug("is_access_granted: grantedPersonal=" . Data::Dumper::Dumper($grantedPersonal));
|
|
push @grants, {type => 'personal', %{$grantedPersonal->value}} if $grantedPersonal;
|
|
|
|
# ... legacy way
|
|
my $grantedLegacy = is_access_way_granted(%params, way => 'legacy', account => $account);
|
|
osh_debug("is_access_granted: grantedLegacy=" . Data::Dumper::Dumper($grantedLegacy));
|
|
push @grants, {type => 'personal-legacy', %{$grantedLegacy->value}} if $grantedLegacy;
|
|
|
|
# 2/3 check groups
|
|
$fnret = OVH::Bastion::get_user_groups(account => $sysaccount);
|
|
osh_debug("is_access_granted: get_user_groups of $sysaccount says " . $fnret->msg . " with grouplist " . Data::Dumper::Dumper($fnret->value));
|
|
|
|
foreach my $group (@{$fnret->value || []}) {
|
|
|
|
# sanitize the group name
|
|
$fnret = OVH::Bastion::is_valid_group(group => $group, groupType => "key");
|
|
$fnret or next;
|
|
$group = $fnret->value->{'group'}; # untaint
|
|
my $shortGroup = $fnret->value->{'shortGroup'};
|
|
|
|
# then check for group access
|
|
my $grantedGroup = is_access_way_granted(%params, way => "group", group => $shortGroup);
|
|
osh_debug("is_access_granted: grantedGroup=" . Data::Dumper::Dumper($grantedGroup));
|
|
next if not $grantedGroup; # if group doesn't have access, don't even check legacy either
|
|
|
|
# now we have to cases, if the group has access: either the account is member or guest
|
|
if (OVH::Bastion::is_group_member(group => $shortGroup, account => $account, sudo => $params{'sudo'})) {
|
|
|
|
# normal member case, just reuse $grantedGroup
|
|
osh_debug("is_access_granted: adding grantedGroup to grants because is member");
|
|
push @grants, {type => 'group-member', group => $shortGroup, %{$grantedGroup->value}};
|
|
}
|
|
elsif (OVH::Bastion::is_group_guest(group => $shortGroup, account => $account, sudo => $params{'sudo'})) {
|
|
|
|
# normal guest case
|
|
my $grantedGuest = is_access_way_granted(%params, way => "groupguest", group => $shortGroup, account => $account);
|
|
osh_debug("is_access_granted: grantedGuest=" . Data::Dumper::Dumper($grantedGuest));
|
|
|
|
# the guy must have a guest access but the group itself must also still have access
|
|
if ($grantedGuest && $grantedGroup) {
|
|
push @grants, {type => 'group-guest', group => $shortGroup, %{$grantedGuest->value}};
|
|
osh_debug("is_access_granted: adding grantedGuest to grants because is guest and group has access");
|
|
}
|
|
|
|
# special legacy case; we also check if account has a legacy access for ip AND that the group ALSO has access to this ip
|
|
if ($grantedLegacy && $grantedGroup) {
|
|
osh_debug("is_access_granted: adding grantedLegacy to grants because legacy not null and group has access");
|
|
push @grants, {type => 'group-guest-legacy', group => $shortGroup, %{$grantedLegacy->value}};
|
|
}
|
|
}
|
|
else {
|
|
# should not happen
|
|
osh_debug("is_access_granted: $account is in group $shortGroup but is neither member or guest !!?");
|
|
}
|
|
}
|
|
|
|
# 3/3 fill up keys if asked to
|
|
if ($wantKeys) {
|
|
foreach my $access (@grants) {
|
|
undef $fnret;
|
|
my $mfaFnret;
|
|
if ($access->{'type'} =~ /^group/ and $access->{'group'}) {
|
|
$fnret = OVH::Bastion::get_group_keys(group => $access->{'group'}, listOnly => $listOnly, noexec => $noexec, forceKey => $access->{'forceKey'});
|
|
$mfaFnret = OVH::Bastion::group_config(key => "mfa_required", group => $access->{'group'});
|
|
}
|
|
elsif ($access->{'type'} =~ /^personal/) {
|
|
$fnret = OVH::Bastion::get_personal_account_keys(account => $sysaccount, listOnly => $listOnly, noexec => $noexec, forceKey => $access->{'forceKey'});
|
|
$mfaFnret = OVH::Bastion::account_config(key => "personal_egress_mfa_required", account => $sysaccount);
|
|
}
|
|
else {
|
|
; # unknown access type? no key!
|
|
}
|
|
if ($fnret) {
|
|
|
|
# TODO implement $access->{forceKey} check to include only the proper key
|
|
$access->{'keys'} = $fnret->value->{'keys'};
|
|
$access->{'sortedKeys'} = $fnret->value->{'sortedKeys'};
|
|
$access->{'mfaRequired'} = $mfaFnret->value if $mfaFnret;
|
|
}
|
|
}
|
|
}
|
|
|
|
return R('OK', value => \@grants) if @grants;
|
|
|
|
my $machine = $ip;
|
|
$machine .= ":$port" if $port;
|
|
$machine = $user . '@' . $machine if $user;
|
|
return R('KO_ACCESS_DENIED', msg => "Access denied for $account to $machine");
|
|
}
|
|
|
|
sub ssh_test_access_way {
|
|
my %params = @_;
|
|
my $account = $params{'account'};
|
|
my $group = $params{'group'};
|
|
|
|
my $port = $params{'port'};
|
|
my $ip = $params{'ip'};
|
|
my $user = $params{'user'};
|
|
my $fnret;
|
|
|
|
if (defined $account and defined $group) {
|
|
return R('ERR_INCOMPATIBLE_PARAMETERS');
|
|
}
|
|
|
|
$fnret = OVH::Bastion::is_valid_ip(ip => $ip, allowPrefixes => 1);
|
|
$fnret or return $fnret;
|
|
if ($fnret->value->{'type'} eq 'prefix') {
|
|
return R('OK_PREFIX', msg => "Can't test a connection to a prefix, assuming it's OK");
|
|
}
|
|
$ip = $fnret->value->{'ip'};
|
|
|
|
if ($port) {
|
|
$fnret = OVH::Bastion::is_valid_port(port => $port);
|
|
$fnret or return $fnret;
|
|
$port = $fnret->value;
|
|
}
|
|
|
|
$user = OVH::Bastion::config("defaultLogin")->value if not $user;
|
|
$user = $account if not $user; # defaultLogin empty means the user himself
|
|
$user = OVH::Bastion::get_user_from_env()->value if not $user; # no user or account ? get from env then
|
|
$fnret = OVH::Bastion::is_valid_remote_user(user => $user);
|
|
$fnret or return $fnret;
|
|
$user = $fnret->value;
|
|
|
|
if ($group) {
|
|
$fnret = OVH::Bastion::is_valid_group_and_existing(group => $group, groupType => "key");
|
|
$fnret or return $fnret;
|
|
my $shortGroup = $fnret->value->{'shortGroup'};
|
|
$group = $fnret->value->{'group'};
|
|
|
|
$fnret = OVH::Bastion::get_group_keys(group => $shortGroup);
|
|
}
|
|
elsif ($account) {
|
|
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account);
|
|
$fnret or return $fnret;
|
|
$account = $fnret->value->{'account'};
|
|
|
|
$fnret = OVH::Bastion::get_personal_account_keys(account => $account);
|
|
}
|
|
else {
|
|
return R('ERR_MISSING_PARAMETER', msg => "Missing 'group' or 'account' for ssh_test_access_way");
|
|
}
|
|
$fnret or return $fnret;
|
|
|
|
my @keyList;
|
|
foreach my $keyfile (@{$fnret->value->{'sortedKeys'}}) {
|
|
my $key = $fnret->value->{'keys'}{$keyfile};
|
|
my $privkey = $key->{'fullpath'};
|
|
$privkey =~ s/\.pub$//;
|
|
push @keyList, $privkey if -r $privkey;
|
|
}
|
|
|
|
if (not @keyList) {
|
|
return R('OK_NO_KEYS_TO_TEST',
|
|
msg =>
|
|
"Couldn't find any accessible SSH key to test connection with, you're probably adding access to an account or a group you don't have access to yourself, nevermind, will continue"
|
|
);
|
|
}
|
|
|
|
if ($user eq '!scpupload' || $user eq '!scpdownload') {
|
|
return R('OK_MAGIC_USER', msg => "Didn't really test the connection, as the specified user is special");
|
|
}
|
|
|
|
my $preferredAuthentications = 'publickey';
|
|
$preferredAuthentications .= ',keyboard-interactive' if $ENV{'OSH_KBD_INTERACTIVE'};
|
|
|
|
# ssh -i with the correct keys
|
|
# UserKnownHostsFile/StrictHostKeyChecking: avoid problem when opening /dev/tty under sudo
|
|
my @command = qw{ ssh -o ConnectTimeout=5 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no };
|
|
push @command, '-o', 'PreferredAuthentications=' . $preferredAuthentications;
|
|
foreach (@keyList) {
|
|
push @command, "-i", $_;
|
|
}
|
|
if (!OVH::Bastion::is_openbsd()) {
|
|
unshift @command, qw{ timeout -k 1 6 };
|
|
}
|
|
|
|
# add port when specified
|
|
push @command, ("-p", $port) if $port;
|
|
|
|
push @command, "-l", $user, $ip, '-T', '--', 'true';
|
|
|
|
osh_info("Testing connection to $user\@$ip, please wait...");
|
|
$fnret = OVH::Bastion::execute(cmd => \@command, noisy_stderr => 1);
|
|
$fnret or return $fnret;
|
|
|
|
no strict 'subs';
|
|
if (grep { $fnret->value->{'sysret'} eq $_ } (0, OVH::Bastion::EXIT_ACCOUNT_INVALID, OVH::Bastion::EXIT_HOST_NOT_FOUND)) {
|
|
return R('OK');
|
|
}
|
|
use strict 'subs';
|
|
|
|
my $hint;
|
|
|
|
# 124 is the return code from the timeout system command when it times out
|
|
# tested on Linux, NetBSD
|
|
if ($fnret->value->{'sysret'} == 124 || grep { $_ =~ /timed out/i } @{$fnret->value->{'stderr'} || []}) {
|
|
$hint = "Hint: did you remotely allow this bastion to access the SSH port?";
|
|
}
|
|
elsif (grep { $_ =~ /Permission denied/i } @{$fnret->value->{'stderr'} || []}) {
|
|
$hint = "Hint: did you add the proper public key to the remote's authorized_keys?";
|
|
}
|
|
my $msg = "Couldn't connect to $user\@$ip (ssh returned error " . $fnret->value->{'sysret'} . ")";
|
|
$msg .= ". $hint" if defined $hint;
|
|
|
|
return R('ERR_CONNECTION_FAILED', msg => $msg);
|
|
}
|
|
|
|
# get all accesses from an account, by any way possible
|
|
# returns: arrayref of contextualized acls, contextualized-acl: { type, group, \@aclentries }
|
|
sub get_acls {
|
|
my %params = @_;
|
|
my $account = $params{'account'};
|
|
|
|
my @acls;
|
|
my $fnret;
|
|
require Data::Dumper;
|
|
|
|
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account);
|
|
$fnret or return $fnret;
|
|
|
|
$account = $fnret->value->{'account'};
|
|
my $sysaccount = $fnret->value->{'sysaccount'};
|
|
|
|
# 1/3 check for personal accesses
|
|
# ... normal way
|
|
my $grantedPersonal = OVH::Bastion::get_acl_way(way => 'personal', account => $account);
|
|
osh_debug("get_acls: grantedPersonal=" . Data::Dumper::Dumper($grantedPersonal));
|
|
push @acls, {type => 'personal', acl => $grantedPersonal->value} if ($grantedPersonal && @{$grantedPersonal->value});
|
|
|
|
# ... legacy way
|
|
my $grantedLegacy = OVH::Bastion::get_acl_way(way => 'legacy', account => $account);
|
|
osh_debug("get_acls: grantedLegacy=" . Data::Dumper::Dumper($grantedLegacy));
|
|
push @acls, {type => 'personal-legacy', acl => $grantedLegacy->value} if ($grantedLegacy && @{$grantedLegacy->value});
|
|
|
|
# 2/3 check groups
|
|
$fnret = OVH::Bastion::get_user_groups(account => $sysaccount);
|
|
osh_debug("get_acls: get_user_groups of $sysaccount says " . $fnret->msg . " with grouplist " . Data::Dumper::Dumper($fnret->value));
|
|
|
|
foreach my $group (@{$fnret->value || []}) {
|
|
|
|
# sanitize the group name
|
|
$fnret = OVH::Bastion::is_valid_group(group => $group, groupType => "key");
|
|
$fnret or next;
|
|
$group = $fnret->value->{'group'}; # untaint
|
|
my $shortGroup = $fnret->value->{'shortGroup'};
|
|
|
|
# then check for group access
|
|
my $grantedGroup = OVH::Bastion::get_acl_way(way => "group", group => $shortGroup);
|
|
osh_debug("get_acls: grantedGroup=" . Data::Dumper::Dumper($grantedGroup));
|
|
next if not $grantedGroup; # if group doesn't have access, don't even check legacy either
|
|
|
|
# now we have to cases, if the group has access: either the account is member or guest
|
|
if (OVH::Bastion::is_group_member(group => $shortGroup, account => $account)) {
|
|
|
|
# normal member case, just reuse $grantedGroup
|
|
osh_debug("get_acls: adding grantedGroup to grants because is member");
|
|
push @acls, {type => 'group-member', group => $shortGroup, acl => $grantedGroup->value} if ($grantedGroup && @{$grantedGroup->value});
|
|
}
|
|
elsif (OVH::Bastion::is_group_guest(group => $shortGroup, account => $account)) {
|
|
|
|
# normal guest case
|
|
my $grantedGuest = OVH::Bastion::get_acl_way(way => "groupguest", group => $shortGroup, account => $account);
|
|
osh_debug("get_acls: grantedGuest=" . Data::Dumper::Dumper($grantedGuest));
|
|
|
|
# the guy must have a guest access but the group itself must also still have access
|
|
if ($grantedGuest && $grantedGroup) {
|
|
osh_debug("get_acls: adding grantedGuest to grants because is guest and group has access");
|
|
push @acls, {type => 'group-guest', group => $shortGroup, acl => $grantedGuest->value} if @{$grantedGuest->value};
|
|
}
|
|
|
|
# special legacy case; we also check if account has a legacy access for ip AND that the group ALSO has access to this ip
|
|
if ($grantedLegacy && $grantedGroup) {
|
|
osh_debug("get_acls: adding grantedLegacy to grants because legacy not null and group has access");
|
|
push @acls, {type => 'group-guest-legacy', group => $shortGroup, acl => $grantedLegacy->value} if @{$grantedLegacy->value};
|
|
}
|
|
}
|
|
else {
|
|
# should not happen
|
|
osh_debug("get_acls: $account is in group $shortGroup but is neither member or guest !!?");
|
|
}
|
|
}
|
|
return R('OK', value => \@acls);
|
|
}
|
|
|
|
# this function simply returns the requested acl
|
|
# i.e. personal or legacy access of an account, group access, or groupguest access.
|
|
# it just calls get_acl_from_file() with the proper file location
|
|
# returns: arrayref of entries, entry: { ip,user,port,forceKey,addedBy,addedDate,comment }
|
|
my %_cache_get_acl_way;
|
|
|
|
sub get_acl_way {
|
|
my %params = @_;
|
|
my $way = delete $params{'way'}; # personal|group|groupguest|legacy
|
|
my $group = delete $params{'group'}; # only meaningful and needed if type=group or type=groupguest
|
|
my $account = delete $params{'account'}; # only meaningful and needed if type=personal or type=groupguest
|
|
|
|
my $fnret;
|
|
my ($sysaccount, $remoteaccount);
|
|
my $key = $way;
|
|
my $prefix = 'allowed';
|
|
|
|
return R('ERR_MISSING_PARAMETER', msg => "Missing argument 'way'") if not defined $way;
|
|
|
|
if ($account) {
|
|
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account);
|
|
$fnret or return $fnret;
|
|
$account = $fnret->value->{'account'};
|
|
$sysaccount = $fnret->value->{'sysaccount'};
|
|
$remoteaccount = $fnret->value->{'remoteaccount'};
|
|
$prefix = "allowed_$remoteaccount" if $remoteaccount;
|
|
$key .= ":$account";
|
|
}
|
|
|
|
my $shortGroup;
|
|
if ($group) {
|
|
$fnret = OVH::Bastion::is_valid_group_and_existing(group => $group, groupType => 'key');
|
|
$fnret or return $fnret;
|
|
$group = $fnret->value->{'group'}; # untainted version
|
|
$shortGroup = $fnret->value->{'shortGroup'};
|
|
$key .= ":$group";
|
|
}
|
|
|
|
return $_cache_get_acl_way{$key} if exists $_cache_get_acl_way{$key};
|
|
|
|
if ($way eq 'personal') {
|
|
return R('ERR_MISSING_PARAMETER', msg => "Missing parameter 'account' for $way way") if not $account;
|
|
if (OVH::Bastion::is_mocking()) {
|
|
return _get_acl_from_file(mock_data => OVH::Bastion::mock_get_account_personal_accesses(account => $account));
|
|
}
|
|
if (!(-f -r "/home/allowkeeper/$sysaccount/$prefix.private")) {
|
|
return R('ERR_PERMISSION_DENIED', msg => "Couldn't open permission file with your current rights or it doesn't exist");
|
|
}
|
|
$_cache_get_acl_way{$key} = _get_acl_from_file(file => "/home/allowkeeper/$sysaccount/$prefix.private");
|
|
}
|
|
elsif ($way eq 'legacy') {
|
|
return R('ERR_MISSING_PARAMETER', msg => "Missing parameter 'account' for $way way") if not $account;
|
|
if (OVH::Bastion::is_mocking()) {
|
|
return _get_acl_from_file(mock_data => OVH::Bastion::mock_get_account_legacy_accesses(account => $account));
|
|
}
|
|
if (-f "/home/allowkeeper/$sysaccount/$prefix.private" && !-e "/home/allowkeeper/$sysaccount/$prefix.ip") {
|
|
|
|
# legacy file doesn't exist: no legacy rights
|
|
$_cache_get_acl_way{$key} = R('OK_EMPTY', value => []);
|
|
}
|
|
elsif (!(-f -r "/home/allowkeeper/$sysaccount/$prefix.ip")) {
|
|
return R('ERR_PERMISSION_DENIED', msg => "Couldn't open permission file with your current rights or it doesn't exist");
|
|
}
|
|
else {
|
|
$_cache_get_acl_way{$key} = _get_acl_from_file(file => "/home/allowkeeper/$sysaccount/$prefix.ip");
|
|
}
|
|
}
|
|
elsif ($way eq 'group') {
|
|
return R('ERR_MISSING_PARAMETER', msg => "Missing parameter 'group' for $way way") if not $group;
|
|
if (OVH::Bastion::is_mocking()) {
|
|
return _get_acl_from_file(mock_data => OVH::Bastion::mock_get_group_accesses(group => $group));
|
|
}
|
|
if (!(-f -r "/home/$group/$prefix.ip")) {
|
|
return R('ERR_PERMISSION_DENIED', msg => "Couldn't open permission file with your current rights or it doesn't exist");
|
|
}
|
|
$_cache_get_acl_way{$key} = _get_acl_from_file(file => "/home/$group/$prefix.ip");
|
|
}
|
|
elsif ($way eq 'groupguest') {
|
|
return R('ERR_MISSING_PARAMETER', msg => "Missing parameter 'account' or 'group' for $way way") if (not $group or not $account);
|
|
if (OVH::Bastion::is_mocking()) {
|
|
return _get_acl_from_file(mock_data => OVH::Bastion::mock_get_account_guest_accesses(group => $group, account => $account));
|
|
}
|
|
if (-f "/home/allowkeeper/$sysaccount/$prefix.private" && !-e "/home/allowkeeper/$sysaccount/$prefix.partial.$shortGroup") {
|
|
|
|
# guest file doesn't exist: no guest rights
|
|
$_cache_get_acl_way{$key} = R('OK_EMPTY', value => []);
|
|
}
|
|
elsif (!(-f -r "/home/allowkeeper/$sysaccount/$prefix.partial.$shortGroup")) {
|
|
return R('ERR_PERMISSION_DENIED', msg => "Couldn't open permission file with your current rights or it doesn't exist");
|
|
}
|
|
else {
|
|
$_cache_get_acl_way{$key} = _get_acl_from_file(file => "/home/allowkeeper/$sysaccount/$prefix.partial.$shortGroup");
|
|
}
|
|
}
|
|
|
|
return $_cache_get_acl_way{$key} if exists $_cache_get_acl_way{$key};
|
|
return R('ERR_INVALID_PARAMETER', msg => "Expected a parameter way with allowed values [personal,legacy,group,groupguest]");
|
|
}
|
|
|
|
# returns the parsed contents of an allowkeeper-style file
|
|
sub _get_acl_from_file {
|
|
my %params = @_;
|
|
my $file = $params{'file'};
|
|
my $mock_data = $params{'mock_data'};
|
|
|
|
my $fnret;
|
|
my @lines;
|
|
|
|
if ($mock_data) {
|
|
die "attempted to mock_data outside of mocking" if !OVH::Bastion::is_mocking();
|
|
@lines = @$mock_data;
|
|
}
|
|
else {
|
|
osh_debug("Reading ACL from '$file'");
|
|
|
|
if (not $file) {
|
|
return R('ERR_MISSING_PARAMETER', msg => "Missing parameter 'file'");
|
|
}
|
|
if (!(-e $file)) {
|
|
return R('ERR_CANNOT_OPEN_FILE', msg => "File '$file' doesn't exist");
|
|
}
|
|
if (!(-r _)) {
|
|
return R('ERR_CANNOT_OPEN_FILE', msg => "File '$file' is not readable");
|
|
}
|
|
|
|
if (open(my $fh_file, '<', $file)) {
|
|
@lines = <$fh_file>;
|
|
close($fh_file);
|
|
chomp @lines;
|
|
}
|
|
else {
|
|
return R('ERR_CANNOT_OPEN_FILE', msg => "Can't open '$file' for read ($!)");
|
|
}
|
|
}
|
|
|
|
my @entries;
|
|
foreach my $line (@lines) {
|
|
my ($ip, $user, $port, $comment, $forceKey, $expiry, $addedBy, $addedDate, $extra, $comment, $userComment);
|
|
|
|
# extract comment if any
|
|
$line =~ s/(#.*)// and $comment = $1;
|
|
|
|
# remove white spaces
|
|
$line =~ s/\s//g;
|
|
|
|
# empty line ?
|
|
$line or next;
|
|
|
|
# extract custom port if present
|
|
if ($line =~ s/:(\d+)$//) {
|
|
$fnret = OVH::Bastion::is_valid_port(port => $1);
|
|
if (!$fnret) {
|
|
osh_debug("skipping line <$line> because port ($1) is invalid");
|
|
next;
|
|
}
|
|
$port = $fnret->value;
|
|
}
|
|
|
|
# extract custom user if present
|
|
if ($line =~ s/^(\S+)\@//) {
|
|
$fnret = OVH::Bastion::is_valid_remote_user(user => $1);
|
|
if (!$fnret) {
|
|
osh_debug("skipping line <$line> because user ($1) is invalid");
|
|
next;
|
|
}
|
|
$user = $fnret->value;
|
|
}
|
|
|
|
# extract ip (v4 or v6)
|
|
if ($line =~ m{([0-9a-f./:]+)}i) {
|
|
$fnret = OVH::Bastion::is_valid_ip(ip => $1, allowPrefixes => 1, fast => 1);
|
|
if (!$fnret) {
|
|
osh_debug("skipping line <$line> because IP ($1) is invalid");
|
|
next;
|
|
}
|
|
$ip = $fnret->value->{'ip'};
|
|
}
|
|
else {
|
|
osh_debug("skipping line <$line> because no valid IP found");
|
|
next;
|
|
}
|
|
|
|
# if we have a comment, there might be stuff to extract from it
|
|
if (defined $comment) {
|
|
osh_debug("Parsing comment ($comment)");
|
|
if ($comment =~ s/# EXPIRY=(\d+)//) {
|
|
if ($1 < time()) {
|
|
osh_debug("found an expired line <$line>, skipping it");
|
|
next;
|
|
}
|
|
$expiry = $1 + 0;
|
|
}
|
|
if ($comment =~ s/# FORCEKEY=(\S+)//) {
|
|
$fnret = OVH::Bastion::is_valid_fingerprint(fingerprint => $1);
|
|
if (!$fnret) {
|
|
osh_debug("skipping line <$line> because invalid forcekey fingerprint ($1) found");
|
|
next;
|
|
}
|
|
$forceKey = $fnret->value->{'fingerprint'};
|
|
osh_debug("found a valid forced key <$forceKey>");
|
|
}
|
|
if ($comment =~ s/# COMMENT=<([^>]+)>//) {
|
|
$userComment = $1;
|
|
}
|
|
if ($comment =~ s/# add(ed)? by (\S+) on (\S+ \S+)//) {
|
|
$addedBy = $2;
|
|
$addedDate = $3;
|
|
}
|
|
$comment !~ /^\s*$/ and $extra = $comment;
|
|
}
|
|
|
|
push @entries,
|
|
{
|
|
ip => $ip,
|
|
user => $user,
|
|
port => $port,
|
|
forceKey => $forceKey,
|
|
expiry => $expiry,
|
|
addedBy => $addedBy,
|
|
addedDate => $addedDate,
|
|
userComment => $userComment,
|
|
comment => $extra,
|
|
};
|
|
}
|
|
|
|
osh_debug("found " . (scalar @entries) . " valid entries");
|
|
return R(@entries ? 'OK' : 'OK_EMPTY', value => \@entries);
|
|
}
|
|
|
|
1;
|