Commit 3f7ae26d authored by Xavier Guimard's avatar Xavier Guimard

Merge branch 'v2.0'

parents aa2fa220 ef5f8522
Pipeline #6792 failed with stages
in 18 minutes and 20 seconds
......@@ -115,6 +115,12 @@ License: CC-3
Comment: This work, "star1.png", is a derivative of
"Golden star with red border.png" by ANGELUS, under CC-BYSA-3.0.
Files: lemonldap-ng-portal/site/htdocs/static/common/icons/decryptValue.png
Copyright: Christophe Maudoux <chrmdx@gmail.com>
License: CC-3
Comment: This work, "decryptValue.png", is a derivative of
"secure.png" by Austin Condiff, under CC-BY-3.0.
Files: lemonldap-ng-portal/site/htdocs/static/common/icons/switchcontext_OFF.png
Copyright: Christophe Maudoux <chrmdx@gmail.com>
License: CC-4
......
......@@ -62,12 +62,12 @@ sub retrieveSession {
}
## @rmethod protected boolean createSession(id)
# Ask portal to create it through a SOAP request
# Send a create session request to the Portal
# @return true if the session is created, else false
sub createSession {
my ( $class, $req, $id ) = @_;
# Add client IP as X-Forwarded-For IP in SOAP request
# Add client IP as X-Forwarded-For IP in request
my $xheader = $req->env->{'HTTP_X_FORWARDED_FOR'};
$xheader .= ", " if ($xheader);
$xheader .= $req->{env}->{REMOTE_ADDR};
......@@ -149,7 +149,7 @@ sub hideCookie {
# else redirect him to the portal to display some message defined by $arg
# @param $url Url requested
# @param $arg optionnal GET parameters
# @return Apache2::Const::REDIRECT or Apache2::Const::AUTH_REQUIRED
# @return AUTH_REDIRECT or AUTH_REQUIRED constant
sub goToPortal {
my ( $class, $req, $url, $arg ) = @_;
if ($arg) {
......@@ -171,7 +171,6 @@ sub ua {
lwpSslOpts => $class->tsv->{lwpSslOpts}
}
);
return $_ua;
}
......
......@@ -18,7 +18,7 @@ sub fetchId {
my ( $t, $_session_id, @vhosts ) = split /:/, $s;
# Looking for service headers
my $vh = $class->resolveAlias($req);
my $vhost = $class->resolveAlias($req);
my %serviceHeaders;
@vhosts = grep {
if (/^([\w\-]+)=(.+)$/) {
......@@ -32,27 +32,31 @@ sub fetchId {
# $_session_id and at least one vhost
unless ( @vhosts and $_session_id ) {
$class->userLogger->error('Bad service token');
$class->logger->debug(
@vhosts ? 'No _session_id found' : 'No VH found' );
return 0;
}
# Is vhost listed in token ?
unless ( grep { $_ eq $vh } @vhosts ) {
unless ( grep { $_ eq $vhost } @vhosts ) {
$class->userLogger->error(
"$vh not authorized in token (" . join( ', ', @vhosts ) . ')' );
"$vhost not authorized in token (" . join( ', ', @vhosts ) . ')' );
return 0;
}
# Is token in good interval ?
my $localConfig = $class->localConfig;
my $ttl =
$localConfig->{vhostOptions}->{$vh}->{vhostServiceTokenTTL} <= 0
? $class->tsv->{handlerServiceTokenTTL}
: $localConfig->{vhostOptions}->{$vh}->{vhostServiceTokenTTL};
unless ( $t <= time and $t > time - $ttl ) {
my $ttl = $class->localConfig->{vhostOptions}->{$vhost}->{vhostServiceTokenTTL}
|| $class->tsv->{serviceTokenTTL}->{$vhost};
$ttl = $class->tsv->{handlerServiceTokenTTL} unless ( $ttl and $ttl > 0 );
my $now = time;
unless ( $t <= $now and $t > $now - $ttl ) {
$class->userLogger->warn('Expired service token');
$class->logger->debug("VH: $vhost with ServiceTokenTTL: $ttl");
$class->logger->debug("TokenTime: $t / Time: $now");
return 0;
}
# Send service headers if exist
if (%serviceHeaders) {
$class->logger->debug("Append service header(s)...");
$class->set_header_out( $req, %serviceHeaders );
......
......@@ -231,6 +231,8 @@ sub defaultValuesInit {
$conf->{vhostOptions}->{$vhost}->{vhostType};
$class->tsv->{authnLevel}->{$vhost} =
$conf->{vhostOptions}->{$vhost}->{vhostAuthnLevel};
$class->tsv->{serviceTokenTTL}->{$vhost} =
$conf->{vhostOptions}->{$vhost}->{vhostServiceTokenTTL};
}
}
return 1;
......
......@@ -97,10 +97,10 @@ sub checkType {
## @rmethod int run
# Check configuration and launch Lemonldap::NG::Handler::Main::run().
# Each $checkTime, the Apache child verify if its configuration is the same
# Each $checkTime, server child verifies if its configuration is the same
# as the configuration stored in the local storage.
# @param $rule optional Perl expression to grant access
# @return Apache constant
# @return constant
sub run {
my ( $class, $req, $rule, $protection ) = @_;
......@@ -121,11 +121,10 @@ sub run {
}
}
# Cross domain authentication
# Authentication process
my $uri = $req->{env}->{REQUEST_URI};
$uri = $req->{env}->{REQUEST_URI};
my ($cond);
( $cond, $protection ) = $class->conditionSub($rule) if ($rule);
$protection = $class->isUnprotected( $req, $uri ) || 0
unless ( defined $protection );
......@@ -273,6 +272,12 @@ sub grant {
$vhost ||= $class->resolveAlias($req);
if ( my $level = $class->tsv->{authnLevel}->{$vhost} ) {
if ( $session->{authenticationLevel} < $level ) {
$class->logger->debug(
"User authentication level = $session->{authenticationLevel}");
$class->logger->debug("Required authentication level = $level");
$class->logger->warn(
"User rejected due to insufficient authentication level -> Session upgrade enabled"
);
$session->{_upgrade} = 1;
return 0;
}
......@@ -415,7 +420,7 @@ sub fetchId {
my $value =
$lookForHttpCookie
? ( $t =~ /${cn}http=([^,; ]+)/o ? $1 : 0 )
: ( $t =~ /$cn=([^,; ]+)/o ? $1 : 0 );
: ( $t =~ /$cn=([^,; ]+)/o ? $1 : 0 );
if ( $value && $lookForHttpCookie && $class->tsv->{securedCookie} == 3 ) {
$value = $class->tsv->{cipher}->decryptHex( $value, "http" );
......
......@@ -166,7 +166,7 @@ ok( @headers == 0, 'NONE service header found' )
or print STDERR Data::Dumper::Dumper( $res->[1] );
count(1);
$token = $crypt->encrypt( join ':', time, $sessionId, '' );
$token = $crypt->encrypt( join ':', time, $sessionId );
ok(
$res = $client->_get(
'/', undef, 'test2.example.com', undef,
......
......@@ -1139,6 +1139,11 @@ qr/(?:(?:https?):\/\/(?:(?:(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.]
'dbiUserUser' => {
'type' => 'text'
},
'decryptValueFunctions' => {
'msgFail' => '__badCustomFuncName__',
'test' => qr/^(?:\w+(?:::\w+)*(?:\s+\w+(?:::\w+)*)*)?$/,
'type' => 'text'
},
'decryptValueRule' => {
'default' => 0,
'type' => 'boolOrExpr'
......
......@@ -540,6 +540,13 @@ sub attributes {
documentation => 'Decrypt value activation rule',
flags => 'p',
},
decryptValueFunctions => {
type => 'text',
test => qr/^(?:\w+(?:::\w+)*(?:\s+\w+(?:::\w+)*)*)?$/,
msgFail => "__badCustomFuncName__",
documentation => 'Custom function used for decrypting values',
flags => 'p',
},
skipRenewConfirmation => {
type => 'bool',
default => 0,
......
......@@ -755,10 +755,11 @@ sub tree {
},
{
title => 'decryptValue',
help => 'decryptValueRule.html',
help => 'decryptvalue.html',
form => 'simpleInputContainer',
nodes => [
'decryptValueRule',
'decryptValueFunctions',
]
},
]
......
......@@ -246,6 +246,7 @@
"dbiSchema":"مخطط",
"dbiUserTable":"جدول المستخدم",
"decryptValue":"Decrypt value",
"decryptValueFunctions":"Decrypt functions",
"decryptValueRule":"Use rule",
"default":"الاعْتيادي",
"defaultRule":"القاعدة الاعتيادية ",
......
......@@ -245,6 +245,7 @@
"dbiSchema":"Schema",
"dbiUserTable":"User table",
"decryptValue":"Decrypt value",
"decryptValueFunctions":"Decrypt functions",
"decryptValueRule":"Use rule",
"default":"Default",
"defaultRule":"Default rule",
......
......@@ -245,6 +245,7 @@
"dbiSchema":"Schema",
"dbiUserTable":"User table",
"decryptValue":"Decrypt value",
"decryptValueFunctions":"Decrypt functions",
"decryptValueRule":"Use rule",
"default":"Default",
"defaultRule":"Default rule",
......
......@@ -245,6 +245,7 @@
"dbiSchema":"Schéma",
"dbiUserTable":"Table des utilisateurs",
"decryptValue":"Déchiffrement",
"decryptValueFunctions":"Fonctions de déchiffrement",
"decryptValueRule":"Règle d'utilisation",
"default":"Défaut",
"defaultRule":"Règle par défaut",
......
......@@ -245,6 +245,7 @@
"dbiSchema":"Schema",
"dbiUserTable":"Tabella utente",
"decryptValue":"Decrypt value",
"decryptValueFunctions":"Decrypt functions",
"decryptValueRule":"Use rule",
"default":"Predefinito",
"defaultRule":"Regola predefinita",
......
......@@ -245,6 +245,7 @@
"dbiSchema":"Giản đồ",
"dbiUserTable":"Bảng người dùng",
"decryptValue":"Decrypt value",
"decryptValueFunctions":"Decrypt functions",
"decryptValueRule":"Use rule",
"default":"Mặc định",
"defaultRule":"Quy tắc mặc định",
......
......@@ -245,6 +245,7 @@
"dbiSchema":"Schema",
"dbiUserTable":"用户表",
"decryptValue":"Decrypt value",
"decryptValueFunctions":"Decrypt functions",
"decryptValueRule":"Use rule",
"default":"默认",
"defaultRule":"默认规则",
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -266,6 +266,7 @@ site/htdocs/static/common/fr.png
site/htdocs/static/common/icons/application_cascade.png
site/htdocs/static/common/icons/arrow_refresh.png
site/htdocs/static/common/icons/calendar.png
site/htdocs/static/common/icons/decryptValue.png
site/htdocs/static/common/icons/door_out.png
site/htdocs/static/common/icons/key.png
site/htdocs/static/common/icons/oidc.png
......@@ -574,6 +575,8 @@ t/43-MailPasswordReset-with-token.t
t/43-MailPasswordReset.t
t/44-CertificateResetByMail-LDAP.t
t/50-IssuerGet.t
t/58-DecryptValue-with-custom-function.t
t/58-DecryptValue-with-internal-function.t
t/59-Double-cookies-for-a-Single-session.t
t/59-Double-cookies-for-Double-sessions.t
t/59-Double-cookies-Refresh-and-Logout.t
......@@ -645,6 +648,7 @@ t/lib/Apache/Session/Timeout.pm
t/lib/Lemonldap/NG/Common/Conf/Backends/Timeout.pm
t/lib/Lemonldap/NG/Handler/Test.pm
t/lib/Lemonldap/NG/Portal/Auth/LDAPPolicy.pm
t/lib/Lemonldap/NG/Portal/Custom.pm
t/lmConf-1.json
t/pdata.pm
t/README.md
......
......@@ -356,7 +356,11 @@ sub reloadConf {
# Clean $req->pdata after authentication
push @{ $self->endAuth }, sub {
my $tmp = $_[0]->pdata->{keepPdata} //= [];
my $tmp =
( ref( $_[0]->pdata->{keepPdata} ) eq 'ARRAY' )
? $_[0]->pdata->{keepPdata}
: [];
foreach my $k ( keys %{ $_[0]->pdata } ) {
unless ( grep { $_ eq $k } @$tmp ) {
$self->logger->debug("Removing $k from pdata");
......
......@@ -130,6 +130,15 @@ sub params {
$self->logger->debug("Display SwitchContext link -> $res{contextSwitching}")
if $res{contextSwitching};
# Display DecryptValue link if allowed
my $dvPlugin =
$self->p->loadedModules->{'Lemonldap::NG::Portal::Plugins::DecryptValue'};
$res{decryptValue} =
$dvPlugin
? $dvPlugin->displayLink( $req, $req->userData )
: '';
$self->logger->debug("Display DecryptValue link") if $res{decryptValue};
return %res;
}
......
......@@ -160,7 +160,10 @@ sub authLogout {
my ( $self, $req ) = @_;
my $res = $self->_authentication->authLogout($req);
$self->logger->debug('Cleaning pdata');
my $tmp = $req->pdata->{keepPdata} //= [];
my $tmp =
( ref( $req->pdata->{keepPdata} ) eq 'ARRAY' )
? $req->pdata->{keepPdata}
: [];
foreach my $k ( keys %{ $req->pdata } ) {
delete $req->pdata->{$k} unless ( grep { $_ eq $k } @$tmp );
}
......
......@@ -81,7 +81,7 @@ sub display {
sub run {
my ( $self, $req ) = @_;
my $msg = '';
my ( $msg, $decryptedValue ) = ( '', '' );
# Check access rules
unless ( $self->rule->( $req, $req->userData ) ) {
......@@ -121,12 +121,43 @@ sub run {
}
my $cipheredValue = $req->param('cipheredValue') || '';
my $decryptedValue =
$self->p->HANDLER->tsv->{cipher}->decrypt($cipheredValue)
if $cipheredValue;
$self->logger->debug("decryptValue try with : $cipheredValue");
$self->logger->debug("Decrypted value = $decryptedValue") if $decryptedValue;
$self->logger->debug("decryptValue tried with value: $cipheredValue");
if ($cipheredValue) {
if ( $self->{conf}->{decryptValueFunctions}
and $self->{conf}->{decryptValueFunctions} =~
qr/^(?:\w+(?:::\w+)*(?:\s+\w+(?:::\w+)*)*)?$/ )
{
foreach ( split( /\s+/, $self->{conf}->{decryptValueFunctions} ) ) {
$self->userLogger->notice(
"Try to decrypt value with function: $_");
/^([\w:{2}]*?)(?:::)?(?:\w+)$/;
eval "require Lemonldap::NG::Portal::$1";
$self->logger->debug("Unable to load decrypt module: $@")
if ($@);
$decryptedValue = eval "$_" . '($cipheredValue)' unless ($@);
$self->logger->debug(
$@
? "Unable to eval decrypt function: $@"
: "Decrypted value = $decryptedValue"
);
last if $decryptedValue;
}
}
else {
$self->userLogger->notice("Malformed decrypt functions")
if $self->{conf}->{decryptValueFunctions};
$self->userLogger->notice(
"Try to decrypt value with internal LL::NG decrypt function");
$decryptedValue =
$self->p->HANDLER->tsv->{cipher}->decrypt($cipheredValue);
$self->logger->debug(
$@
? "Unable to decrypt value: $@"
: "Decrypted value = $decryptedValue"
);
}
}
# Display form
my $params = {
......@@ -155,4 +186,9 @@ sub run {
return $self->p->sendHtml( $req, 'decryptvalue', params => $params );
}
sub displayLink {
my ( $self, $req ) = @_;
return $self->rule->( $req, $req->userData );
}
1;
......@@ -74,10 +74,16 @@
</TMPL_IF>
<TMPL_IF NAME="contextSwitching">
<li class="dropdown-item"><a href="/switchcontext" class="nav-link">
<img src="<TMPL_VAR NAME="STATIC_PREFIX">common/icons/switchcontext_<TMPL_VAR NAME="contextSwitching">.png" width="20" height="20" alt="refresh" />
<img src="<TMPL_VAR NAME="STATIC_PREFIX">common/icons/switchcontext_<TMPL_VAR NAME="contextSwitching">.png" width="20" height="20" alt="switchContext" />
<span trspan="contextSwitching_<TMPL_VAR NAME="contextSwitching">">contextSwitching_<TMPL_VAR NAME="contextSwitching"></span>
</a></li>
</TMPL_IF>
<TMPL_IF NAME="decryptValue">
<li class="dropdown-item"><a href="/decryptvalue" class="nav-link">
<img src="<TMPL_VAR NAME="STATIC_PREFIX">common/icons/decryptValue.png" width="20" height="20" alt="decryptValue" />
<span trspan="decryptCipheredValue">decryptCipheredValue</span>
</a></li>
</TMPL_IF>
<li class="dropdown-item"><a href="/refresh" class="nav-link">
<img src="<TMPL_VAR NAME="STATIC_PREFIX">common/icons/arrow_refresh.png" width="16" height="16" alt="refresh" />
<span trspan="refreshrights">Refresh</span>
......
use Test::More;
use strict;
use IO::String;
use lib 't/lib';
BEGIN {
require 't/test-lib.pm';
}
my $res;
my $client = LLNG::Manager::Test->new( {
ini => {
logLevel => 'error',
authentication => 'Demo',
userDB => 'Same',
loginHistoryEnabled => 0,
brutForceProtection => 0,
requireToken => 0,
decryptValueRule => 1,
decryptValueFunctions =>
'Custom::empty Custom::test_uc Custom::undefined',
}
}
);
## Try to authenticate
ok( $res = $client->_get( '/', accept => 'text/html' ), 'Get Menu', );
count(1);
my ( $host, $url, $query ) = expectForm( $res, '#', undef, 'user', 'password' );
$query = 'user=dwho&password=dwho';
ok(
$res = $client->_post(
'/',
IO::String->new($query),
length => length($query),
accept => 'text/html',
),
'Auth query'
);
my $id = expectCookie($res);
expectRedirection( $res, 'http://auth.example.com/' );
ok(
$res = $client->_get(
'/',
cookie => "lemonldap=$id",
accept => 'text/html'
),
'CheckUser form',
);
ok( $res->[2]->[0] =~ m%<img src="/static/common/icons/decryptValue\.png"%,
'Found decryptValue.png' )
or explain( $res->[2]->[0], 'decryptValue.png' );
count(3);
# DecryptValue form
# ------------------------
ok(
$res = $client->_get(
'/decryptvalue',
cookie => "lemonldap=$id",
accept => 'text/html'
),
'DecryptValue form',
);
( $host, $url, $query ) =
expectForm( $res, undef, '/decryptvalue', 'cipheredValue' );
ok( $res->[2]->[0] =~ m%<span trspan="decryptCipheredValue">%,
'Found trspan="decryptCipheredValue"' )
or explain( $res->[2]->[0], 'trspan="decryptCipheredValue"' );
count(2);
# Decrypt ciphered value
$query =~
s%cipheredValue=%cipheredValue=lowercase%;
ok(
$res = $client->_post(
'/decryptvalue',
IO::String->new($query),
cookie => "lemonldap=$id",
length => length($query),
accept => 'text/html',
),
'POST decryptvalue with valid value'
);
ok( $res->[2]->[0] =~ m%<span trspan="LOWERCASE"></span>%, 'Found decryted value' )
or explain( $res->[2]->[0], 'Decryted value NOT found' );
count(2);
( $host, $url, $query ) =
expectForm( $res, undef, '/decryptvalue', 'cipheredValue' );
$client->logout($id);
clean_sessions();
done_testing( count() );
use Test::More;
use strict;
use IO::String;
BEGIN {
require 't/test-lib.pm';
}
my $res;
my $client = LLNG::Manager::Test->new( {
ini => {
logLevel => 'error',
authentication => 'Demo',
userDB => 'Same',
loginHistoryEnabled => 0,
brutForceProtection => 0,
decryptValueRule => '$uid eq "dwho"',
requireToken => 1,
}
}
);
## Try to authenticate
ok( $res = $client->_get( '/', accept => 'text/html' ), 'Get Menu', );
count(1);
my ( $host, $url, $query ) =
expectForm( $res, '#', undef, 'user', 'password', 'token' );
$query =~ s/user=/user=rtyler/;
$query =~ s/password=/password=rtyler/;
ok(
$res = $client->_post(
'/',
IO::String->new($query),
length => length($query),
accept => 'text/html',
),
'Auth query'
);
count(1);
my $id = expectCookie($res);
expectRedirection( $res, 'http://auth.example.com/' );
# DecryptValue form for a foridden user
# ------------------------
ok(
$res = $client->_get(
'/decryptvalue',
cookie => "lemonldap=$id",
accept => 'text/html'
),
'Try DecryptValue form for a forbidden user',
);
count(1);
ok( $res->[2]->[0] =~ m%<span trmsg="95">%, 'Found trmsg="95"' )
or explain( $res->[2]->[0], 'trmsg="95"' );
count(1);
$client->logout($id);
## Try to authenticate
ok( $res = $client->_get( '/', accept => 'text/html' ), 'Get Menu', );
count(1);
( $host, $url, $query ) =
expectForm( $res, '#', undef, 'user', 'password', 'token' );
$query =~ s/user=/user=dwho/;
$query =~ s/password=/password=dwho/;
ok(
$res = $client->_post(
'/',
IO::String->new($query),
length => length($query),
accept => 'text/html',
),
'Auth query'
);
$id = expectCookie($res);
expectRedirection( $res, 'http://auth.example.com/' );
ok(
$res = $client->_get(
'/',
cookie => "lemonldap=$id",
accept => 'text/html'
),
'CheckUser form',
);
ok( $res->[2]->[0] =~ m%<img src="/static/common/icons/decryptValue\.png"%,
'Found decryptValue.png' )
or explain( $res->[2]->[0], 'decryptValue.png' );
count(3);
# DecryptValue form
# ------------------------
ok(
$res = $client->_get(
'/decryptvalue',
cookie => "lemonldap=$id",
accept => 'text/html'
),
'DecryptValue form',
);
( $host, $url, $query ) =
expectForm( $res, undef, '/decryptvalue', 'cipheredValue', 'token' );
ok( $res->[2]->[0] =~ m%<span trspan="decryptCipheredValue">%,
'Found trspan="decryptCipheredValue"' )
or explain( $res->[2]->[0], 'trspan="decryptCipheredValue"' );
count(2);
# Valid ciphered value
$query =~
s%cipheredValue=%cipheredValue=CNCERR4E3BPrrEY0BZGnl3ISfUZARKXNhnDj3x7/xO5kxodXbeLzTk2VSHh1rq/C4TU78wzyWiove81YseYj/g==%;
ok(
$res = $client->_post(
'/decryptvalue',
IO::String->new($query),
cookie => "lemonldap=$id",
length => length($query),
accept => 'text/html',
),
'POST decryptvalue with valid value'
);
ok( $res->[2]->[0] =~ m%<span trspan="dwho"></span>%, 'Found decryted value' )
or explain( $res->[2]->[0], 'Decryted value NOT found' );
count(2);
( $host, $url, $query ) =
expectForm( $res, undef, '/decryptvalue', 'cipheredValue', 'token' );
# Unvalid ciphered value
$query =~ s%cipheredValue=%cipheredValue=test%;
ok(
$res = $client->_post(
'/decryptvalue',
IO::String->new($query),
cookie => "lemonldap=$id",
length => length($query),
accept => 'text/html',
),
'POST decryptvalue with unvalid value'
);
ok( $res->[2]->[0] =~ m%<span trspan="notAnEncryptedValue">%,
'Found trspan="notAnEncryptedValue"' )
or explain( $res->[2]->[0], 'trspan="notAnEncryptedValue"' );
count(2);
( $host, $url, $query ) =
expectForm( $res, undef, '/decryptvalue', 'cipheredValue', 'token' );
$client->logout($id);
clean_sessions();
done_testing( count() );