Merge branch 'feature-cas-service-url-2321' into 'v2.0'
Feature cas service url 2321 See merge request lemonldap-ng/lemonldap-ng!175
This commit is contained in:
commit
127aa91a8f
|
@ -106,7 +106,7 @@ Options
|
|||
^^^^^^^
|
||||
|
||||
- **Service URL** : the service (user-facing) URL of the CAS-enabled
|
||||
application.
|
||||
application. See :ref:`idpcas-url-matching`
|
||||
- **User attribute** : session field that will be used as main
|
||||
identifier.
|
||||
- **Authentication Level** : required authentication level to access this
|
||||
|
@ -125,3 +125,21 @@ Macros
|
|||
|
||||
You can define here macros that will be only evaluated for this service,
|
||||
and not registered in the session of the user.
|
||||
|
||||
.. _idpcas-url-matching:
|
||||
|
||||
URL Matching
|
||||
^^^^^^^^^^^^
|
||||
|
||||
.. versionchanged:: 2.0.10
|
||||
|
||||
Before version 2.0.10, only the hostname was taken into account, which made it impossible to have two different CAS services behind the same hostname.
|
||||
|
||||
Since version 2.0.10, the entire service URL is compared to the Service URL defined in LemonLDAP::NG. The longest prefix wins.
|
||||
|
||||
For example, if you declared two applications in LemonLDAP::NG with the following service URLs:
|
||||
|
||||
* https://cas.example.com/applications/zone1
|
||||
* https://cas.example.com/applications/
|
||||
|
||||
An application located at https://cas.example.com/applications/zone1/myapp will match the first CAS service definition
|
||||
|
|
|
@ -861,14 +861,14 @@ sub tests {
|
|||
return ( $res, join( ', ', @msg ) );
|
||||
},
|
||||
|
||||
# CAS APP URL must be unique
|
||||
# CAS APP URL must be defined and unique
|
||||
casAppHostnameUniqueness => sub {
|
||||
return 1
|
||||
unless ( $conf->{casAppMetaDataOptions}
|
||||
and %{ $conf->{casAppMetaDataOptions} } );
|
||||
my @msg;
|
||||
my $res = 1;
|
||||
my %casHosts;
|
||||
my %casUrl;
|
||||
foreach my $casConfKey ( keys %{ $conf->{casAppMetaDataOptions} } )
|
||||
{
|
||||
my $appUrl =
|
||||
|
@ -883,13 +883,13 @@ sub tests {
|
|||
next;
|
||||
}
|
||||
|
||||
if ( defined $casHosts{$appHost} ) {
|
||||
if ( defined $casUrl{$appUrl} ) {
|
||||
push @msg,
|
||||
"$casConfKey and $casHosts{$appHost} have the same Service hostname";
|
||||
"$casConfKey and $casUrl{$appUrl} have the same Service URL";
|
||||
$res = 0;
|
||||
next;
|
||||
}
|
||||
$casHosts{$appHost} = $casConfKey;
|
||||
$casUrl{$appUrl} = $casConfKey;
|
||||
}
|
||||
return ( $res, join( ', ', @msg ) );
|
||||
},
|
||||
|
|
|
@ -59,10 +59,9 @@ sub init {
|
|||
);
|
||||
|
||||
# Add CAS Services, so we can check service= parameter on logout
|
||||
foreach my $casSrv ( keys %{ $self->conf->{casAppMetaDataOptions} } ) {
|
||||
foreach my $casSrv ( keys %{ $self->casAppList } ) {
|
||||
if ( my $serviceUrl =
|
||||
$self->conf->{casAppMetaDataOptions}->{$casSrv}
|
||||
->{casAppMetaDataOptionsService} )
|
||||
$self->casAppList->{$casSrv}->{casAppMetaDataOptionsService} )
|
||||
{
|
||||
push @{ $self->p->{additionalTrustedDomains} }, $serviceUrl;
|
||||
$self->logger->debug(
|
||||
|
@ -96,14 +95,14 @@ sub storeEnvAndCheckGateway {
|
|||
|
||||
if ( $service and $service =~ m#^(https?://[^/]+)(/.*)?$# ) {
|
||||
my ( $host, $uri ) = ( $1, $2 );
|
||||
my $app = $self->casAppList->{$host};
|
||||
my $app = $self->getCasApp($service);
|
||||
|
||||
if ($app) {
|
||||
$req->env->{llng_cas_app} = $app;
|
||||
|
||||
# Store target authentication level in pdata
|
||||
my $targetAuthnLevel = $self->conf->{casAppMetaDataOptions}->{$app}
|
||||
->{casAppMetaDataOptionsAuthnLevel};
|
||||
my $targetAuthnLevel =
|
||||
$self->casAppList->{$app}->{casAppMetaDataOptionsAuthnLevel};
|
||||
$req->pdata->{targetAuthnLevel} = $targetAuthnLevel
|
||||
if $targetAuthnLevel;
|
||||
|
||||
|
@ -168,12 +167,12 @@ sub run {
|
|||
return PE_ERROR;
|
||||
}
|
||||
my ( $host, $uri ) = ( $1, $2 );
|
||||
my $app = $self->casAppList->{$host};
|
||||
my $app = $self->getCasApp($service);
|
||||
|
||||
my $spAuthnLevel = 0;
|
||||
if ($app) {
|
||||
$spAuthnLevel = $self->conf->{casAppMetaDataOptions}->{$app}
|
||||
->{casAppMetaDataOptionsAuthnLevel} || 0;
|
||||
$spAuthnLevel =
|
||||
$self->casAppList->{$app}->{casAppMetaDataOptionsAuthnLevel} || 0;
|
||||
}
|
||||
|
||||
# Renew
|
||||
|
@ -851,10 +850,8 @@ sub getUsernameForApp {
|
|||
|
||||
my $username_attribute =
|
||||
( $app
|
||||
and $self->conf->{casAppMetaDataOptions}->{$app}
|
||||
->{casAppMetaDataOptionsUserAttribute} )
|
||||
? $self->conf->{casAppMetaDataOptions}->{$app}
|
||||
->{casAppMetaDataOptionsUserAttribute}
|
||||
and $self->casAppList->{$app}->{casAppMetaDataOptionsUserAttribute} )
|
||||
? $self->casAppList->{$app}->{casAppMetaDataOptionsUserAttribute}
|
||||
: ( $self->conf->{casAttr}
|
||||
|| $self->conf->{whatToTrace} );
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ use Mouse;
|
|||
use Lemonldap::NG::Common::FormEncode;
|
||||
use XML::Simple;
|
||||
use Lemonldap::NG::Common::UserAgent;
|
||||
use URI;
|
||||
|
||||
our $VERSION = '2.0.8';
|
||||
|
||||
|
@ -43,20 +44,21 @@ sub loadSrv {
|
|||
return 1;
|
||||
}
|
||||
|
||||
# Load CAS application list, key is the service URL
|
||||
# Load CAS application list
|
||||
sub loadApp {
|
||||
my ($self) = @_;
|
||||
unless ( $self->conf->{casAppMetaDataOptions}
|
||||
if ( $self->conf->{casAppMetaDataOptions}
|
||||
and %{ $self->conf->{casAppMetaDataOptions} } )
|
||||
{
|
||||
$self->casAppList( $self->conf->{casAppMetaDataOptions} );
|
||||
}
|
||||
else {
|
||||
$self->logger->info("No CAS apps found in configuration");
|
||||
}
|
||||
|
||||
foreach ( keys %{ $self->conf->{casAppMetaDataOptions} } ) {
|
||||
my $tmp =
|
||||
$self->conf->{casAppMetaDataOptions}->{$_}
|
||||
->{casAppMetaDataOptionsService};
|
||||
$tmp =~ s#^(https?://[^/]+).*$#$1#;
|
||||
$self->casAppList->{$tmp} = $_;
|
||||
|
||||
# Load access rule
|
||||
my $rule = $self->conf->{casAppMetaDataOptions}->{$_}
|
||||
->{casAppMetaDataOptionsRule};
|
||||
if ( length $rule ) {
|
||||
|
@ -497,6 +499,59 @@ sub retrievePT {
|
|||
return $pt;
|
||||
}
|
||||
|
||||
# Get CAS App from service URL
|
||||
sub getCasApp {
|
||||
my ( $self, $uri_param ) = @_;
|
||||
|
||||
my $uri = URI->new($uri_param);
|
||||
my $hostname = $uri->authority;
|
||||
my $uriCanon = $uri->canonical;
|
||||
return undef unless $hostname;
|
||||
|
||||
my $prefixConfKey;
|
||||
my $longestCandidate = "";
|
||||
my $hostnameConfKey;
|
||||
|
||||
for my $app ( keys %{ $self->casAppList } ) {
|
||||
|
||||
my $candidateUri =
|
||||
URI->new( $self->casAppList->{$app}->{casAppMetaDataOptionsService} );
|
||||
my $candidateHost = $candidateUri->authority;
|
||||
my $candidateCanon = $candidateUri->canonical;
|
||||
|
||||
# Try to match prefix, remembering the longest match found
|
||||
if ( index( $uriCanon, $candidateCanon ) == 0 ) {
|
||||
if ( length($longestCandidate) < length($candidateCanon) ) {
|
||||
$longestCandidate = $candidateCanon;
|
||||
$prefixConfKey = $app;
|
||||
}
|
||||
}
|
||||
|
||||
# Try to match host
|
||||
$hostnameConfKey = $app if ( $hostname eq $candidateHost );
|
||||
}
|
||||
|
||||
# Application found by prefix has priority
|
||||
return $prefixConfKey if $prefixConfKey;
|
||||
$self->logger->warn(
|
||||
"Matched CAS service $hostnameConfKey based on hostname only. "
|
||||
. "This will be deprecated in a future version" )
|
||||
if $hostnameConfKey;
|
||||
return $hostnameConfKey;
|
||||
}
|
||||
|
||||
# This method returns the host part of the given URL
|
||||
# If the URL has no scheme, return it completely
|
||||
# http://example.com/uri => example.com
|
||||
# foo.bar => foo.bar
|
||||
sub _getHostForService {
|
||||
my ( $self, $service ) = @_;
|
||||
return undef unless $service;
|
||||
|
||||
my $uri = URI->new($service);
|
||||
return $uri->scheme ? $uri->host : $uri->as_string;
|
||||
}
|
||||
|
||||
1;
|
||||
__END__
|
||||
|
||||
|
|
98
lemonldap-ng-portal/t/32-CAS-Prefix.t
Normal file
98
lemonldap-ng-portal/t/32-CAS-Prefix.t
Normal file
|
@ -0,0 +1,98 @@
|
|||
use lib 'inc';
|
||||
use Test::More;
|
||||
use strict;
|
||||
use IO::String;
|
||||
use LWP::UserAgent;
|
||||
use LWP::Protocol::PSGI;
|
||||
use MIME::Base64;
|
||||
|
||||
BEGIN {
|
||||
require 't/test-lib.pm';
|
||||
}
|
||||
|
||||
my $debug = 'error';
|
||||
my ( $issuer, $res );
|
||||
|
||||
eval { require XML::Simple };
|
||||
plan skip_all => "Missing dependencies: $@" if ($@);
|
||||
|
||||
# Login
|
||||
ok( $issuer = issuer(), 'Issuer portal' );
|
||||
count(1);
|
||||
my $s = "user=dwho&password=dwho";
|
||||
my $id = expectCookie(
|
||||
$issuer->_post(
|
||||
'/',
|
||||
IO::String->new($s),
|
||||
accept => 'text/html',
|
||||
length => length($s),
|
||||
)
|
||||
);
|
||||
|
||||
# Service 1, will be matched by URI
|
||||
ok(
|
||||
$res = $issuer->_get(
|
||||
'/cas/login',
|
||||
query => 'service=http://auth.sp.com/srv1/index.php',
|
||||
accept => 'text/html',
|
||||
cookie => "lemonldap=$id",
|
||||
),
|
||||
'Query CAS server'
|
||||
);
|
||||
count(1);
|
||||
expectRedirection( $res,
|
||||
qr#^http://auth.sp.com/srv1/index.php\?(ticket=[^&]+)$# );
|
||||
|
||||
# Service 2, will be matched by hostname
|
||||
ok(
|
||||
$res = $issuer->_get(
|
||||
'/cas/login',
|
||||
query => 'service=http://auth.other.com/srv2',
|
||||
accept => 'text/html',
|
||||
cookie => "lemonldap=$id",
|
||||
),
|
||||
'Query CAS server'
|
||||
);
|
||||
count(1);
|
||||
expectRedirection( $res, qr#^http://auth.other.com/srv2\?(ticket=[^&]+)$# );
|
||||
|
||||
clean_sessions();
|
||||
done_testing( count() );
|
||||
|
||||
sub issuer {
|
||||
return LLNG::Manager::Test->new( {
|
||||
ini => {
|
||||
logLevel => $debug,
|
||||
domain => 'idp.com',
|
||||
portal => 'http://auth.idp.com',
|
||||
authentication => 'Demo',
|
||||
userDB => 'Same',
|
||||
issuerDBCASActivation => 1,
|
||||
casAttr => 'uid',
|
||||
casAppMetaDataOptions => {
|
||||
sp1 => {
|
||||
casAppMetaDataOptionsService =>
|
||||
'https://auth.other.com/xxxyz',
|
||||
casAppMetaDataOptionsRule => "1",
|
||||
},
|
||||
sp2 => {
|
||||
casAppMetaDataOptionsService => 'http://auth.sp.com/',
|
||||
casAppMetaDataOptionsRule => "0",
|
||||
},
|
||||
sp3 => {
|
||||
casAppMetaDataOptionsService =>
|
||||
'http://auth.sp.com/srv1/',
|
||||
casAppMetaDataOptionsRule => "1",
|
||||
},
|
||||
sp4 => {
|
||||
casAppMetaDataOptionsService =>
|
||||
'http://auth.sp.com/srv2/',
|
||||
casAppMetaDataOptionsRule => "0",
|
||||
},
|
||||
},
|
||||
casAccessControlPolicy => 'error',
|
||||
multiValuesSeparator => ';',
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user