2020-10-16 00:32:37 +08:00
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' };
2021-02-15 19:20:15 +08:00
my $keyhome = $fnret -> value -> { 'keyhome' };
2020-10-16 00:32:37 +08:00
$fnret = _get_pub_keys_from_directory (
2021-02-15 19:20:15 +08:00
dir => $keyhome ,
2020-10-16 00:32:37 +08:00
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 = @ _ ;
2020-11-06 01:36:17 +08:00
my $exactIpMatch = $params { 'exactIpMatch' }; # $ip must be explicitly allowed (not given through a wider slash or a 0.0.0.0/0 in grantfile)
my $exactPortMatch = $params { 'exactPortMatch' }; # $port must be explicitly allowed (port wildcards in grantfile will be ignored)
my $exactUserMatch = $params { 'exactUserMatch' }; # $user must be explicitly allowed (user wildcards in grantfile will be ignored)
2020-10-16 00:32:37 +08:00
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 );
}
2020-11-06 01:36:17 +08:00
# Return an array containing the groups for which user is a member of
2020-10-16 00:32:37 +08:00
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 ();
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' } || '-' ;
2020-11-23 05:05:45 +08:00
my $expiry = $entry -> { 'expiry' } ? ( duration2human ( seconds => ( $entry -> { 'expiry' } - time ())) -> value -> { 'human' }) : '-' ;
2020-10-16 00:32:37 +08:00
# type => member ('full'), guest ('partial'), personal or legacy
2020-12-15 18:20:08 +08:00
my $ipReverse ;
$ipReverse = OVH :: Bastion :: ip2host ( $entry -> { 'ip' }) -> value if $reverse ;
2020-10-16 00:32:37 +08:00
$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 || []};
2020-12-15 18:20:08 +08:00
foreach my $ruleNb ( 0 .. $ #rules) {
2020-10-16 00:32:37 +08:00
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' }) {
2020-11-23 05:05:45 +08:00
$fnret = OVH :: Bastion :: get_group_keys ( group => $access -> { 'group' }, listOnly => $listOnly , noexec => $noexec , forceKey => $access -> { 'forceKey' });
2020-10-16 00:32:37 +08:00
$mfaFnret = OVH :: Bastion :: group_config ( key => " mfa_required " , group => $access -> { 'group' });
}
elsif ( $access -> { 'type' } =~ /^ personal / ) {
2020-11-23 05:05:45 +08:00
$fnret = OVH :: Bastion :: get_personal_account_keys ( account => $sysaccount , listOnly => $listOnly , noexec => $noexec , forceKey => $access -> { 'forceKey' });
2020-10-16 00:32:37 +08:00
$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 ;
2020-12-15 18:20:08 +08:00
if ( grep { $fnret -> value -> { 'sysret' } eq $_ } ( 0 , OVH :: Bastion :: EXIT_ACCOUNT_INVALID (), OVH :: Bastion :: EXIT_HOST_NOT_FOUND ())) {
2020-10-16 00:32:37 +08:00
return R ( 'OK' );
}
my $hint ;
# 124 is the return code from the timeout system command when it times out
# tested on Linux, NetBSD
2020-12-15 18:20:08 +08:00
if ( $fnret -> value -> { 'sysret' } == 124 || grep { / timed out / i } @ { $fnret -> value -> { 'stderr' } || []}) {
2020-10-16 00:32:37 +08:00
$hint = " Hint: did you remotely allow this bastion to access the SSH port? " ;
}
2020-12-15 18:20:08 +08:00
elsif ( grep { / Permission denied / i } @ { $fnret -> value -> { 'stderr' } || []}) {
2020-10-16 00:32:37 +08:00
$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 - 9 a - 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 ;