Merge branch 'v2.0'

This commit is contained in:
Christophe Maudoux 2020-01-10 23:01:12 +01:00
commit aab0dcca14
78 changed files with 3429 additions and 136 deletions

View File

@ -428,6 +428,7 @@ prepare_test_server:
ETCDEFAULTDIR=`pwd`/e2e-tests/conf/def
#@cp -f e2e-tests/index.* e2e-tests/conf/
@cp -f $(SRCMANAGERDIR)/site/htdocs/manager* e2e-tests/conf/manager
@cp -f $(SRCMANAGERDIR)/site/htdocs/api* e2e-tests/conf/manager
@cp -f $(SRCPORTALDIR)/site/htdocs/index* e2e-tests/conf/portal
@cp e2e-tests/persistent/5efe8af397fc3577e05b483aca964f1b e2e-tests/conf/persistents
@cp e2e-tests/saml-sp.xml e2e-tests/conf/site/saml-sp.xml

View File

@ -99,3 +99,76 @@
# Uncomment this if site if you use SSL only
#Header set Strict-Transport-Security "max-age=15768000"
</VirtualHost>
# API virtual host (manager.__DNSDOMAIN__)
<VirtualHost __VHOSTLISTEN__>
ServerName api.__DNSDOMAIN__
LogLevel notice
# See above to set LLNG user id in Apache logs
#CustomLog __APACHELOGDIR__/manager.log llng
#ErrorLog __APACHELOGDIR__/lm_err.log
# Uncomment this if you are running behind a reverse proxy and want
# LemonLDAP::NG to see the real IP address of the end user
# Adjust the settings to match the IP address of your reverse proxy
# and the header containing the original IP address
#
#RemoteIPHeader X-Forwarded-For
#RemoteIPInternalProxy 127.0.0.1
# FASTCGI CONFIGURATION
# ---------------------
# 1) URI management
RewriteEngine on
# For performances, you can delete the previous RewriteRule line after
# puttings html files: simply put the HTML results of differents modules
# (configuration, sessions, notifications) as manager.html, sessions.html,
# notifications.html and uncomment the 2 following lines:
# DirectoryIndex manager.html
# RewriteCond "%{REQUEST_URI}" "!\.html(?:/.*)?$"
# REST URLs
RewriteCond "%{REQUEST_URI}" "!^/(?:static|doc|lib|javascript|favicon).*"
RewriteRule "^/(.+)$" "/api.fcgi/$1" [PT]
# 2) FastCGI engine
# You can choose any FastCGI system. Here is an example using mod_fcgid
# mod_fcgid configuration
FcgidMaxRequestLen 2000000
<Files *.fcgi>
SetHandler fcgid-script
Options +ExecCGI
header unset Lm-Remote-User
</Files>
# If you want to use mod_fastcgi, replace lines below by:
#FastCgiServer __MANAGERSITEDIR__/manager.fcgi
# GLOBAL CONFIGURATION
# --------------------
DocumentRoot __MANAGERSITEDIR__
<Location />
Require all denied
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/javascript text/css
SetOutputFilter DEFLATE
BrowserMatch ^Mozilla/4 gzip-only-text/html
BrowserMatch ^Mozilla/4\.0[678] no-gzip
BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png)$ no-gzip dont-vary
</IfModule>
<IfModule mod_headers.c>
Header append Vary User-Agent env=!dont-vary
</IfModule>
</Location>
# Uncomment this if site if you use SSL only
#Header set Strict-Transport-Security "max-age=15768000"
</VirtualHost>

View File

@ -118,3 +118,83 @@
# Uncomment this if site if you use SSL only
#Header set Strict-Transport-Security "max-age=15768000"
</VirtualHost>
# API virtual host (manager.__DNSDOMAIN__)
<VirtualHost __VHOSTLISTEN__>
ServerName api.__DNSDOMAIN__
LogLevel notice
# See above to set LLNG user id in Apache logs
#CustomLog __APACHELOGDIR__/manager.log llng
#ErrorLog __APACHELOGDIR__/lm_err.log
# Uncomment this if you are running behind a reverse proxy and want
# LemonLDAP::NG to see the real IP address of the end user
# Adjust the settings to match the IP address of your reverse proxy
# and the header containing the original IP address
#
#RemoteIPHeader X-Forwarded-For
#RemoteIPInternalProxy 127.0.0.1
# FASTCGI CONFIGURATION
# ---------------------
# 1) URI management
RewriteEngine on
# For performances, you can delete the previous RewriteRule line after
# puttings html files: simply put the HTML results of differents modules
# (configuration, sessions, notifications) as manager.html, sessions.html,
# notifications.html and uncomment the 2 following lines:
# DirectoryIndex manager.html
# RewriteCond "%{REQUEST_URI}" "!\.html(?:/.*)?$"
# REST URLs
RewriteCond "%{REQUEST_URI}" "!^/(?:static|doc|lib|javascript|favicon).*"
RewriteRule "^/(.+)$" "/api.fcgi/$1" [PT]
# 2) FastCGI engine
# You can choose any FastCGI system. Here is an example using mod_fcgid
# mod_fcgid configuration
FcgidMaxRequestLen 2000000
<Files *.fcgi>
SetHandler fcgid-script
Options +ExecCGI
header unset Lm-Remote-User
</Files>
# If you want to use mod_fastcgi, replace lines below by:
#FastCgiServer __MANAGERSITEDIR__/manager.fcgi
# GLOBAL CONFIGURATION
# --------------------
DocumentRoot __MANAGERSITEDIR__
<Location />
<IfVersion >= 2.3>
Require all denied
</IfVersion>
<IfVersion < 2.3>
Order Deny,Allow
Deny from all
</IfVersion>
Options +FollowSymLinks
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/javascript text/css
SetOutputFilter DEFLATE
BrowserMatch ^Mozilla/4 gzip-only-text/html
BrowserMatch ^Mozilla/4\.0[678] no-gzip
BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png)$ no-gzip dont-vary
</IfModule>
<IfModule mod_headers.c>
Header append Vary User-Agent env=!dont-vary
</IfModule>
</Location>
# Uncomment this if site if you use SSL only
#Header set Strict-Transport-Security "max-age=15768000"
</VirtualHost>

View File

@ -102,3 +102,77 @@
# Uncomment this if site if you use SSL only
#Header set Strict-Transport-Security "max-age=15768000"
</VirtualHost>
# API virtual host (api.__DNSDOMAIN__)
<VirtualHost __VHOSTLISTEN__>
ServerName api.__DNSDOMAIN__
LogLevel notice
# See above to set LLNG user id in Apache logs
#CustomLog __APACHELOGDIR__/manager.log llng
#ErrorLog __APACHELOGDIR__/lm_err.log
# Uncomment this if you are running behind a reverse proxy and want
# LemonLDAP::NG to see the real IP address of the end user
# Adjust the settings to match the IP address of your reverse proxy
# and the header containing the original IP address
#
#RemoteIPHeader X-Forwarded-For
#RemoteIPInternalProxy 127.0.0.1
# FASTCGI CONFIGURATION
# ---------------------
# 1) URI management
RewriteEngine on
# For performances, you can delete the previous RewriteRule line after
# puttings html files: simply put the HTML results of differents modules
# (configuration, sessions, notifications) as manager.html, sessions.html,
# notifications.html and uncomment the 2 following lines:
# DirectoryIndex manager.html
# RewriteCond "%{REQUEST_URI}" "!\.html(?:/.*)?$"
# REST URLs
RewriteCond "%{REQUEST_URI}" "!^/(?:static|doc|lib|javascript|favicon).*"
RewriteRule "^/(.+)$" "/api.fcgi/$1" [PT]
# 2) FastCGI engine
# You can choose any FastCGI system. Here is an example using mod_fcgid
# mod_fcgid configuration
FcgidMaxRequestLen 2000000
<Files *.fcgi>
SetHandler fcgid-script
Options +ExecCGI
header unset Lm-Remote-User
</Files>
# If you want to use mod_fastcgi, replace lines below by:
#FastCgiServer __MANAGERSITEDIR__/manager.fcgi
# GLOBAL CONFIGURATION
# --------------------
DocumentRoot __MANAGERSITEDIR__
<Location />
Order Deny,Allow
Deny from all
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/javascript text/css
SetOutputFilter DEFLATE
BrowserMatch ^Mozilla/4 gzip-only-text/html
BrowserMatch ^Mozilla/4\.0[678] no-gzip
BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png)$ no-gzip dont-vary
</IfModule>
<IfModule mod_headers.c>
Header append Vary User-Agent env=!dont-vary
</IfModule>
</Location>
# Uncomment this if site if you use SSL only
#Header set Strict-Transport-Security "max-age=15768000"
</VirtualHost>

View File

@ -38,7 +38,7 @@ our $authParameters = {
casParams => [qw(casAuthnLevel)],
choiceParams => [qw(authChoiceParam authChoiceModules authChoiceAuthBasic)],
combinationParams => [qw(combination combModules combinationForms)],
customParams => [qw(customAuth customUserDB customPassword customRegister customAddParams)],
customParams => [qw(customAuth customUserDB customPassword customRegister customResetCertByMail customAddParams)],
dbiParams => [qw(dbiAuthnLevel dbiExportedVars dbiAuthChain dbiAuthUser dbiAuthPassword dbiUserChain dbiUserUser dbiUserPassword dbiAuthTable dbiUserTable dbiAuthLoginCol dbiAuthPasswordCol dbiPasswordMailCol userPivot dbiAuthPasswordHash dbiDynamicHashEnabled dbiDynamicHashValidSchemes dbiDynamicHashValidSaltedSchemes dbiDynamicHashNewPasswordScheme)],
demoParams => [qw(demoExportedVars)],
facebookParams => [qw(facebookAuthnLevel facebookExportedVars facebookAppId facebookAppSecret facebookUserField)],

View File

@ -92,6 +92,35 @@ sub _put {
);
}
sub _patch {
my ( $self, $path, $query, $body, $type, $len ) = @_;
die "$body must be a IO::Handle"
unless ( ref($body) and $body->can('read') );
return $self->app->( {
'HTTP_ACCEPT' => 'application/json, text/plain, */*',
'SCRIPT_NAME' => '',
'HTTP_ACCEPT_ENCODING' => 'gzip, deflate',
'SERVER_NAME' => '127.0.0.1',
'QUERY_STRING' => $query,
'HTTP_CACHE_CONTROL' => 'max-age=0',
'HTTP_ACCEPT_LANGUAGE' => 'fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3',
'PATH_INFO' => $path,
'REQUEST_METHOD' => 'PATCH',
'REQUEST_URI' => $path . ( $query ? "?$query" : '' ),
'SERVER_PORT' => '8002',
'SERVER_PROTOCOL' => 'HTTP/1.1',
'HTTP_USER_AGENT' =>
'Mozilla/5.0 (VAX-4000; rv:36.0) Gecko/20350101 Firefox',
'REMOTE_ADDR' => '127.0.0.1',
'HTTP_HOST' => '127.0.0.1:8002',
'psgix.input.buffered' => 1,
'psgi.input' => $body,
'CONTENT_LENGTH' => $len // scalar( ( stat $body )[7] ),
'CONTENT_TYPE' => $type,
}
);
}
sub _del {
my ( $self, $path, $query ) = @_;
return $self->app->( {

View File

@ -129,18 +129,18 @@ PSGIs
=head1 SYNOPSIS
package My::PSGI;
use base Lemonldap::NG::Common::PSGI;
# See Lemonldap::NG::Common::PSGI
...
sub handler {
my ( $self, $req ) = @_;
# Do something and return a PSGI response
# NB: $req is a Lemonldap::NG::Common::PSGI::Request object
if ( $req->accept eq 'text/plain' ) { ... }
return [ 200, [ 'Content-Type' => 'text/plain' ], [ 'Body lines' ] ];
}

View File

@ -12,7 +12,7 @@ extends 'Lemonldap::NG::Common::PSGI';
has 'routes' => (
is => 'rw',
isa => 'HashRef',
default => sub { { GET => {}, POST => {}, PUT => {}, DELETE => {} } }
default => sub { { GET => {}, POST => {}, PUT => {}, PATCH => {}, DELETE => {} } }
);
has 'defaultRoute' => ( is => 'rw', default => 'index.html' );
@ -20,7 +20,7 @@ has 'defaultRoute' => ( is => 'rw', default => 'index.html' );
sub addRoute {
my ( $self, $word, $dest, $methods, $transform ) = (@_);
$methods ||= [qw(GET POST PUT DELETE)];
$methods ||= [qw(GET POST PUT PATCH DELETE)];
foreach my $method (@$methods) {
$self->logger->debug("Add $method route:");
$self->genRoute( $self->routes->{$method}, $word, $dest, $transform );

View File

@ -214,7 +214,7 @@ sub defaultValuesInit {
# Override with vhost options
if ( $conf->{vhostOptions} ) {
my $name = 'vhost' . ucfirst($opt);
foreach my $vhost ( keys %{ $conf->{vhostOptions} } ) {
foreach my $vhost ( sort keys %{ $conf->{vhostOptions} } ) {
$conf->{vhostOptions}->{$vhost} ||= {};
my $val = $conf->{vhostOptions}->{$vhost}->{$name};
@ -228,7 +228,7 @@ sub defaultValuesInit {
}
}
if ( $conf->{vhostOptions} ) {
foreach my $vhost ( keys %{ $conf->{vhostOptions} } ) {
foreach my $vhost ( sort keys %{ $conf->{vhostOptions} } ) {
$class->tsv->{type}->{$vhost} =
$conf->{vhostOptions}->{$vhost}->{vhostType};
$class->tsv->{authnLevel}->{$vhost} =
@ -326,7 +326,7 @@ sub locationRulesInit {
## @imethod protected void sessionStorageInit(hashRef args)
# Initialize the Apache::Session::* module choosed to share user's variables
# and the Cache::Cache module choosed to cache sessions
# and the Cache::Cache module chosen to cache sessions
# @param $args reference to the configuration hash
sub sessionStorageInit {
my ( $class, $conf ) = @_;

View File

@ -7,6 +7,11 @@ eg/manager.psgi
KINEMATIC.md
lib/Lemonldap/NG/Manager.pm
lib/Lemonldap/NG/Manager/2ndFA.pm
lib/Lemonldap/NG/Manager/Api.pm
lib/Lemonldap/NG/Manager/Api/2F.pm
lib/Lemonldap/NG/Manager/Api/Common.pm
lib/Lemonldap/NG/Manager/Api/Providers/OidcRp.pm
lib/Lemonldap/NG/Manager/Api/Providers/SamlSp.pm
lib/Lemonldap/NG/Manager/Attributes.pm
lib/Lemonldap/NG/Manager/Build.pm
lib/Lemonldap/NG/Manager/Build/Attributes.pm
@ -40,6 +45,7 @@ site/coffee/notifications.coffee
site/coffee/sessions.coffee
site/coffee/viewDiff.coffee
site/coffee/viewer.coffee
site/htdocs/api.fcgi
site/htdocs/manager.fcgi
site/htdocs/manager.psgi
site/htdocs/static/bwr/angular-animate/angular-animate.js
@ -215,8 +221,10 @@ site/templates/viewDiff.tpl
site/templates/viewer.tpl
t/02-HTML-template.t
t/03-HTML-forms.t
t/04-2F-api.t
t/04-providers-api.t
t/05-rest-api.t
t/06-rest-api.t
t/06-rest-api-RSA.t
t/07-utf8.t
t/10-save-unchanged-conf.t
t/11-save-appCat-changed-conf.t

View File

@ -52,7 +52,7 @@ sub init {
return 0;
}
$self->{enabledModules} ||= "conf, sessions, notifications, 2ndFA";
$self->{enabledModules} ||= "conf, sessions, notifications, 2ndFA, api";
my @links;
my @enabledModules =
map { push @links, $_; "Lemonldap::NG::Manager::" . ucfirst($_) }

View File

@ -0,0 +1,153 @@
# This module implements all the methods that responds to '/api/*' requests
package Lemonldap::NG::Manager::Api;
use 5.10.0;
use utf8;
use Mouse;
extends 'Lemonldap::NG::Common::Conf::RESTServer',
'Lemonldap::NG::Common::Session::REST';
use Lemonldap::NG::Manager::Api::2F;
use Lemonldap::NG::Manager::Api::Providers::OidcRp;
use Lemonldap::NG::Manager::Api::Providers::SamlSp;
our $VERSION = '2.0.7';
#############################
# I. INITIALIZATION METHODS #
#############################
use constant defaultRoute => 'api.html';
sub addRoutes {
my ( $self, $conf ) = @_;
# HTML template
$self->addRoute( 'api.html', undef, ['GET'] )
->addRoute(
api => {
v1 => {
providers => {
oidc => {
rp => {
findByConfKey => {
':uPattern' => 'findOidcRpByConfKey'
},
findByClientId => {
':uClientId' => 'findOidcRpByClientId'
},
':confKey' => 'getOidcRpByConfKey'
},
},
saml => {
sp => {
findByConfKey => {
':uPattern' => 'findSamlSpByConfKey'
},
findByEntityId => {
':uEntityId' => 'findSamlSpByEntityId'
},
':confKey' => 'getSamlSpByConfKey'
},
},
},
secondFactor => {
':uid' => {
id => {
':id' => 'getSecondFactorsById'
},
type => {
':type' => 'getSecondFactorsByType'
},
'*' => 'getSecondFactors'
},
},
},
},
['GET']
)
->addRoute(
api => {
v1 => {
providers => {
oidc => {
rp => 'addOidcRp'
},
saml => {
sp => 'addSamlSp'
},
},
},
},
['POST']
)
->addRoute(
api => {
v1 => {
providers => {
oidc => {
rp => { ':confKey' => 'replaceOidcRp' }
},
saml => {
sp => { ':confKey' => 'replaceSamlSp' }
},
},
},
},
['PUT']
)
->addRoute(
api => {
v1 => {
providers => {
oidc => {
rp => { ':confKey' => 'updateOidcRp' }
},
saml => {
sp => { ':confKey' => 'updateSamlSp' }
},
},
},
},
['PATCH']
)
->addRoute(
api => {
v1 => {
providers => {
oidc => {
rp => { ':confKey' => 'deleteOidcRp' }
},
saml => {
sp => { ':confKey' => 'deleteSamlSp' }
},
},
secondFactor => {
':uid' => {
id => {
':id' => 'deleteSecondFactorsById'
},
type => {
':type' => 'deleteSecondFactorsByType'
},
'*' => 'deleteSecondFactors'
},
},
},
},
['DELETE']
);
$self->setTypes($conf);
$self->{multiValuesSeparator} ||= '; ';
$self->{hiddenAttributes} //= "_password";
$self->{TOTPCheck} = $self->{U2FCheck} = $self->{UBKCheck} = '1';
}
1;

View File

@ -0,0 +1,345 @@
package Lemonldap::NG::Manager::Api::2F;
our $VERSION = '2.0.8';
package Lemonldap::NG::Manager::Api;
use 5.10.0;
use utf8;
use Mouse;
use JSON;
use MIME::Base64;
use Lemonldap::NG::Common::Session;
sub getSecondFactors {
my ( $self, $req ) = @_;
my ( $uid, $res );
$uid = $req->params('uid')
or return $self->sendError( $req, 'uid is missing', 400 );
$self->logger->debug("[API] 2F for $uid requested");
$res = $self->_get2F($uid);
return $self->sendError( $req, $res->{msg}, $res->{code} )
unless ( $res->{res} eq 'ok' );
return $self->sendJSONresponse( $req, $res->{secondFactors} );
}
sub getSecondFactorsByType {
my ( $self, $req ) = @_;
my ( $uid, $type, $res );
$uid = $req->params('uid')
or return $self->sendError( $req, 'Uid is missing', 400 );
$type = $req->params('type')
or return $self->sendError( $req, 'Type is missing', 400 );
$self->logger->debug("[API] 2F for $uid with type $type requested");
$res = $self->_get2F( $uid, uc $type );
return $self->sendError( $req, $res->{msg}, $res->{code} )
unless ( $res->{res} eq 'ok' );
return $self->sendJSONresponse( $req, $res->{secondFactors} );
}
sub getSecondFactorsById {
my ( $self, $req ) = @_;
my ( $uid, $id, $res );
$uid = $req->params('uid')
or return $self->sendError( $req, 'uid is missing', 400 );
$id = $req->params('id')
or return $self->sendError( $req, 'id is missing', 400 );
$self->logger->debug("[API] 2F for $uid with id $id requested");
$res = $self->_get2F( $uid, undef, $id );
return $self->sendError( $req, $res->{msg}, $res->{code} )
unless ( $res->{res} eq 'ok' );
return $self->sendError( $req, "2F id '$id' not found for user '$uid'",
404 )
unless ( scalar @{ $res->{secondFactors} } > 0 );
return $self->sendJSONresponse( $req, @{ $res->{secondFactors} }[0] );
}
sub deleteSecondFactors {
my ( $self, $req ) = @_;
my ( $uid, $res );
$uid = $req->params('uid')
or return $self->sendError( $req, 'uid is missing', 400 );
$self->logger->debug("[API] Delete all 2F for $uid requested");
$res = $self->_delete2F($uid);
return $self->sendError( $req, $res->{msg}, $res->{code} )
unless ( $res->{res} eq 'ok' );
return $self->sendJSONresponse( $req, { message => $res->{msg} } );
}
sub deleteSecondFactorsById {
my ( $self, $req ) = @_;
my ( $uid, $id, $res );
$uid = $req->params('uid')
or return $self->sendError( $req, 'uid is missing', 400 );
$id = $req->params('id')
or return $self->sendError( $req, 'id is missing', 400 );
$self->logger->debug("[API] Delete 2F for $uid with id $id requested");
$res = $self->_delete2F( $uid, undef, $id );
return $self->sendError( $req, $res->{msg}, $res->{code} )
unless ( $res->{res} eq 'ok' );
return $self->sendError( $req, "2F id '$id' not found for user '$uid'",
404 )
unless ( $res->{removed} > 0 );
return $self->sendJSONresponse( $req, { message => $res->{msg} } );
}
sub deleteSecondFactorsByType {
my ( $self, $req ) = @_;
my ( $uid, $type, $res );
$uid = $req->params('uid')
or return $self->sendError( $req, 'uid is missing', 400 );
$type = $req->params('type')
or return $self->sendError( $req, 'type is missing', 400 );
$self->logger->debug(
"[API] Delete all 2F for $uid with type $type requested");
$res = $self->_delete2F( $uid, uc $type );
return $self->sendError( $req, $res->{msg}, $res->{code} )
unless ( $res->{res} eq 'ok' );
return $self->sendJSONresponse( $req, { message => $res->{msg} } );
}
sub _get2F {
my ( $self, $uid, $type, $id ) = @_;
my ( $res, $psessions, @secondFactors );
if ( defined $type ) {
$res = $self->_checkType($type);
return $res if ( $res->{res} ne 'ok' );
}
$psessions = $self->_getSessions2F( $self->_getPersistentMod, 'Persistent',
'_session_uid', $uid );
foreach ( keys %$psessions ) {
my $devices =
from_json( $psessions->{$_}->{_2fDevices}, { allow_nonref => 1 } );
foreach my $device ( @{$devices} ) {
$self->logger->debug(
"Check device [epoch=$device->{epoch}, type=$device->{type}, name=$device->{name}]"
);
push @secondFactors,
{
id => $self->_genId2F($device),
type => $device->{type},
name => $device->{name}
}
unless ( ( defined $type and $type ne $device->{type} )
or ( defined $id and $id ne $self->_genId2F($device) ) );
}
}
$self->logger->debug(
"Found " . scalar @secondFactors . " 2F devices for uid $uid." );
return { res => 'ok', secondFactors => [@secondFactors] };
}
sub _genId2F {
my ( $self, $device ) = @_;
return encode_base64( "$device->{epoch}::$device->{type}::$device->{name}",
"" );
}
sub _getPersistentMod {
my ($self) = @_;
my $mod = $self->sessionTypes->{persistent};
$mod->{options}->{backend} = $mod->{module};
return $mod;
}
sub _getSSOMod {
my ($self) = @_;
my $mod = $self->sessionTypes->{global};
$mod->{options}->{backend} = $mod->{module};
return $mod;
}
sub _getSessions2F {
my ( $self, $mod, $kind, $key, $uid ) = @_;
$self->logger->debug("Looking for sessions for uid $uid ...");
my $sessions =
Lemonldap::NG::Common::Apache::Session->searchOn( $mod->{options}, $key,
$uid,
( '_session_kind', '_session_uid', '_session_id', '_2fDevices' ) );
foreach ( keys %$sessions ) {
delete $sessions->{$_}
unless ( $sessions->{$_}->{_session_kind} eq $kind );
}
$self->logger->debug( "Found "
. scalar( keys %$sessions )
. " $kind sessions for uid $uid." );
return $sessions;
}
sub _getSession2F {
my ( $self, $sessionId, $mod ) = @_;
$self->logger->debug("Looking for session with sessionId $sessionId ...");
my $session = $self->getApacheSession( $mod, $sessionId );
$self->logger->debug(
defined $session
? "Session $sessionId found."
: " No session found for sessionId $sessionId"
);
return $session;
}
sub _delete2FFromSessions {
my ( $self, $uid, $type, $id, $mod, $kind, $key ) = @_;
my ( $sessions, $session, $devices, @keep, $removed,
$total, $module, $localStorage );
$sessions = $self->_getSessions2F( $mod, $kind, $key, $uid );
foreach ( keys %$sessions ) {
$session = $self->_getSession2F( $_, $mod )
or return { res => 'ko', code => 500, msg => $@ };
$self->logger->debug(
"Looking for 2F Device(s) attached to sessionId $_");
if ( $session->data->{_2fDevices} ) {
$devices =
from_json( $session->data->{_2fDevices}, { allow_nonref => 1 } );
$total = scalar @$devices;
$self->logger->debug(
"Found $total 2F devices attached to sessionId $_");
@keep = ();
while (@$devices) {
my $element = shift @$devices;
if (
( defined $type or defined $id )
and ( ( defined $type and $type ne $element->{type} )
or
( defined $id and $id ne $self->_genId2F($element) ) )
)
{
push @keep, $element;
}
else {
$removed->{ $self->_genId2F($element) } = "removed";
}
}
if ( ( $total - scalar @keep ) > 0 ) {
# Update session
$self->logger->debug( "Removing "
. ( $total - scalar @keep )
. " 2F device(s) attached to sessionId $_ ..." );
$session->data->{_2fDevices} = to_json( \@keep );
$session->update( $session->data );
# Delete from local cache
if ( $session->{options}->{localStorage} ) {
$module = $session->{options}->{localStorage};
eval "use $module;";
$localStorage =
$module->new(
$session->{options}->{localStorageOptions} );
if ( $localStorage->get($_) ) {
$self->logger->debug(
"Delete local cache for session $_");
$localStorage->remove($_);
}
}
}
else {
$self->logger->debug(
"No matching 2F devices attached to sessionId $_ were selected for removal."
);
}
}
else {
$self->logger->debug(
"No 2F devices attached to sessionId $_ were found.");
}
}
return { res => 'ok', removed => $removed };
}
sub _delete2F {
my ( $self, $uid, $type, $id ) = @_;
my ( $res, $removed, $count );
if ( defined $type ) {
$res = $self->_checkType($type);
return $res if ( $res->{res} ne 'ok' );
}
$res =
$self->_delete2FFromSessions( $uid, $type, $id, $self->_getPersistentMod,
'Persistent', '_session_uid' );
return $res if ( $res->{res} ne 'ok' );
$removed = $res->{removed} || {};
$res =
$self->_delete2FFromSessions( $uid, $type, $id, $self->_getSSOMod, 'SSO',
'uid' );
return $res if ( $res->{res} ne 'ok' );
$res->{removed} ||= {};
# merge results
$removed = { %$removed, %{ $res->{removed} } };
$count = scalar( keys %$removed );
return {
res => 'ok',
removed => $count,
msg => $count > 0
? "Successful operation: " . $count . " 2F were removed"
: "No operation performed"
};
}
sub _checkType {
my ( $self, $type ) = @_;
return {
res => "ko",
code => 405,
msg =>
"Invalid input: Type \"$type\" does not exist. Allowed values for type are: \"U2F\", \"TOTP\" or \"UBK\""
}
unless ( $type =~ /\b(?:U2F|TOTP|UBK)\b/ );
return { res => "ok" };
}
1;

View File

@ -0,0 +1,79 @@
package Lemonldap::NG::Manager::Api::Common;
our $VERSION = '2.0.8';
package Lemonldap::NG::Manager::Api;
use Lemonldap::NG::Manager::Build::Attributes;
use Lemonldap::NG::Manager::Build::CTrees;
# use Scalar::Util 'weaken'; ?
sub _isSimpleKeyValueHash {
my ( $self, $hash ) = @_;
return 0 if ( ref($hash) ne "HASH" );
foreach ( keys %$hash ) {
return 0 if ( ref( $hash->{$_} ) ne '' || ref($_) ne '' );
}
return 1;
}
sub _setDefaultValues {
my ( $self, $attrs, $rootNode ) = @_;
my @allAttrs = $self->_listAttributes($rootNode);
my $defaultAttrs = Lemonldap::NG::Manager::Build::Attributes::attributes();
foreach $attr (@allAttrs) {
unless ( defined $attrs->{$attr} ) {
$attrs->{$attr} = $defaultAttrs->{$attr}->{default}
if ( defined $defaultAttrs->{$attr}
&& defined $defaultAttrs->{$attr}->{default} );
}
}
return $attrs;
}
sub _hasAllowedAttributes {
my ( $self, $attributes, $rootNode ) = @_;
my @allowedAttributes = $self->_listAttributes($rootNode);
foreach $attribute ( keys %{$attributes} ) {
if ( length( ref($attribute) ) ) {
return {
res => "ko",
msg => "Invalid input: Attribute $attribute is not a string."
};
}
unless ( grep { /^$attribute$/ } @allowedAttributes ) {
return {
res => "ko",
msg => "Invalid input: Attribute $attribute does not exist."
};
}
}
return { res => "ok" };
}
sub _listAttributes {
my ( $self, $rootNode ) = @_;
my $mainTree = Lemonldap::NG::Manager::Build::CTrees::cTrees();
my $rootNodes = [ grep { ref($_) eq "HASH" } @{ $mainTree->{$rootNode} } ];
my @attributes = map { $self->_listNodeAttributes($_) } @$rootNodes;
return @attributes;
}
sub _listNodeAttributes {
my ( $self, $node ) = @_;
my @attributes =
map { ref($_) eq "HASH" ? $self->_listNodeAttributes($_) : $_ }
@{ $node->{nodes} };
return @attributes;
}
1;

View File

@ -0,0 +1,356 @@
package Lemonldap::NG::Manager::Api::Providers::OidcRp;
our $VERSION = '2.0.8';
package Lemonldap::NG::Manager::Api;
use 5.10.0;
use utf8;
use Mouse;
extends 'Lemonldap::NG::Manager::Api::Common';
sub getOidcRpByConfKey {
my ( $self, $req ) = @_;
my $confKey = $req->params('confKey')
or return $self->sendError( $req, 'confKey is missing', 400 );
$self->logger->debug("[API] OIDC RP $confKey configuration requested");
# Get latest configuration
my $conf = $self->_confAcc->getConf;
my $oidcRp = $self->_getOidcRpByConfKey( $conf, $confKey );
# Return 404 if not found
return $self->sendError( $req,
"OIDC relying party '$confKey' not found", 404 )
unless ( defined $oidcRp );
return $self->sendJSONresponse( $req, $oidcRp );
}
sub findOidcRpByConfKey {
my ( $self, $req ) = @_;
my $pattern = (
defined $req->params('uPattern')
? $req->params('uPattern')
: ( defined $req->params('pattern') ? $req->params('pattern') : undef )
);
return $self->sendError( $req, 'Invalid input: pattern is missing', 405 )
unless ( defined $pattern );
$self->logger->debug(
"[API] Find OIDC RPs by confKey regexp $pattern requested");
# Get latest configuration
my $conf = $self->_confAcc->getConf;
my @oidcRps =
map { $_ =~ $pattern ? $self->_getOidcRpByConfKey( $conf, $_ ) : () }
keys %{ $conf->{oidcRPMetaDataOptions} };
return $self->sendJSONresponse( $req, [@oidcRps] );
}
sub findOidcRpByClientId {
my ( $self, $req ) = @_;
my $clientId = (
defined $req->params('uClientId') ? $req->params('uClientId')
: (
defined $req->params('clientId') ? $req->params('clientId')
: undef
)
);
return $self->sendError( $req, 'Invalid input: clientId is missing', 405 )
unless ( defined $clientId );
$self->logger->debug("[API] Find OIDC RPs by clientId $clientId requested");
# Get latest configuration
my $conf = $self->_confAcc->getConf;
my $oidcRp = $self->_getOidcRpByClientId( $conf, $clientId );
return $self->sendError( $req,
"OIDC relying party with clientId '$clientId' not found", 404 )
unless ( defined $oidcRp );
return $self->sendJSONresponse( $req, $oidcRp );
}
sub addOidcRp {
my ( $self, $req ) = @_;
my $add = $req->jsonBodyToObj;
return $self->sendError( $req, "Invalid input: " . $req->error, 405 )
unless ($add);
return $self->sendError( $req, 'Invalid input: confKey is missing', 405 )
unless ( defined $add->{confKey} );
return $self->sendError( $req, 'Invalid input: clientId is missing', 405 )
unless ( defined $add->{clientId} );
$self->logger->debug(
"[API] Add OIDC RP with confKey $add->{confKey} and clientId $add->{clientId} requested"
);
# Get latest configuration
my $conf = $self->_confAcc->getConf( { noCache => 1 } );
return $self->sendError(
$req,
"Invalid input: An OIDC RP with confKey $add->{confKey} already exists",
405
) if ( defined $self->_getOidcRpByConfKey( $conf, $add->{confKey} ) );
return $self->sendError(
$req,
"Invalid input: An OIDC RP with clientId $add->{clientId} already exists",
405
) if ( defined $self->_getOidcRpByClientId( $conf, $add->{clientId} ) );
$add->{options} = {} unless ( defined $add->{options} );
$add->{options}->{oidcRPMetaDataOptionsClientID} = $add->{clientId};
my $res = $self->_pushOidcRp( $conf, $add->{confKey}, $add, 1 );
return $self->sendError( $req, $res->{msg}, 405 )
unless ( $res->{res} eq 'ok' );
return $self->sendJSONresponse( $req,
{ message => "Successful operation" } );
}
sub updateOidcRp {
my ( $self, $req ) = @_;
my $confKey = $req->params('confKey')
or return $self->sendError( $req, 'confKey is missing', 400 );
my $update = $req->jsonBodyToObj;
return $self->sendError( $req, "Invalid input: " . $req->error, 405 )
unless ($update);
$self->logger->debug(
"[API] OIDC RP $confKey configuration update requested");
# Get latest configuration
my $conf = $self->_confAcc->getConf( { noCache => 1 } );
my $current = $self->_getOidcRpByConfKey( $conf, $confKey );
# Return 404 if not found
return $self->sendError( $req,
"OIDC relying party '$confKey' not found", 404 )
unless ( defined $current );
# check if new clientID exists already
my $res = $self->_isNewOidcRpClientIdUnique( $conf, $confKey, $update );
return $self->sendError( $req, $res->{msg}, 405 )
unless ( $res->{res} eq 'ok' );
$res = $self->_pushOidcRp( $conf, $confKey, $update, 0 );
return $self->sendError( $req, $res->{msg}, 405 )
unless ( $res->{res} eq 'ok' );
return $self->sendJSONresponse( $req,
{ message => "Successful operation" } );
}
sub replaceOidcRp {
my ( $self, $req ) = @_;
my $confKey = $req->params('confKey')
or return $self->sendError( $req, 'confKey is missing', 400 );
my $replace = $req->jsonBodyToObj;
return $self->sendError( $req, "Invalid input: " . $req->error, 405 )
unless ($replace);
return $self->sendError( $req, 'Invalid input: clientId is missing', 405 )
unless ( defined $replace->{clientId} );
$self->logger->debug(
"[API] OIDC RP $confKey configuration replace requested");
# Get latest configuration
my $conf = $self->_confAcc->getConf( { noCache => 1 } );
# Return 404 if not found
return $self->sendError( $req,
"OIDC relying party '$confKey' not found", 404 )
unless ( defined $self->_getOidcRpByConfKey( $conf, $confKey ) );
# check if new clientID exists already
my $res = $self->_isNewOidcRpClientIdUnique( $conf, $confKey, $replace );
return $self->sendError( $req, $res->{msg}, 405 )
unless ( $res->{res} eq 'ok' );
$res = $self->_pushOidcRp( $conf, $confKey, $replace, 1 );
return $self->sendError( $req, $res->{msg}, 405 )
unless ( $res->{res} eq 'ok' );
return $self->sendJSONresponse( $req,
{ message => "Successful operation" } );
}
sub deleteOidcRp {
my ( $self, $req ) = @_;
my $confKey = $req->params('confKey')
or return $self->sendError( $req, 'confKey is missing', 400 );
# Get latest configuration
my $conf = $self->_confAcc->getConf( { noCache => 1 } );
my $delete = $self->_getOidcRpByConfKey( $conf, $confKey );
# Return 404 if not found
return $self->sendError( $req,
"OIDC relying party '$confKey' not found", 404 )
unless ( defined $delete );
delete $conf->{oidcRPMetaDataOptions}->{$confKey};
delete $conf->{oidcRPMetaDataExportedVars}->{$confKey};
delete $conf->{oidcRPMetaDataOptionsExtraClaims}->{$confKey};
# Save configuration
$self->_confAcc->saveConf($conf);
return $self->sendJSONresponse( $req,
{ message => "Successful operation" } );
}
sub _getOidcRpByConfKey {
my ( $self, $conf, $confKey ) = @_;
# Check if confKey is defined
return undef unless ( defined $conf->{oidcRPMetaDataOptions}->{$confKey} );
# Get Client ID
my $clientId = $conf->{oidcRPMetaDataOptions}->{$confKey}
->{oidcRPMetaDataOptionsClientID};
# Get exported vars
my $exportedVars = $conf->{oidcRPMetaDataExportedVars}->{$confKey};
# Get extra claim
my $extraClaim = $conf->{oidcRPMetaDataOptionsExtraClaims}->{$confKey};
# Get options
my $options = $conf->{oidcRPMetaDataOptions}->{$confKey};
return {
confKey => $confKey,
clientId => $clientId,
exportedVars => $exportedVars,
extraClaim => $extraClaim,
options => $options
};
}
sub _getOidcRpByClientId {
my ( $self, $conf, $clientId ) = @_;
foreach ( keys %{ $conf->{oidcRPMetaDataOptions} } ) {
return $self->_getOidcRpByConfKey( $conf, $_ )
if ( $conf->{oidcRPMetaDataOptions}->{$_}
->{oidcRPMetaDataOptionsClientID} eq $clientId );
}
return undef;
}
sub _isNewOidcRpClientIdUnique {
my ( $self, $conf, $confKey, $oidcRp ) = @_;
my $curClientId = $self->_getOidcRpByConfKey( $conf, $confKey )->{clientId};
my $newClientId =
$oidcRp->{clientId}
|| $oidcRp->{options}->{oidcRPMetaDataOptionsClientID}
|| "";
if ( $newClientId ne '' && $newClientId ne $curClientId ) {
return {
res => 'ko',
msg =>
"An OIDC relying party with clientId '$newClientId' already exists"
}
if ( defined $self->_getOidcRpByClientId( $conf, $newClientId ) );
}
return { res => 'ok' };
}
sub _pushOidcRp {
my ( $self, $conf, $confKey, $push, $replace ) = @_;
if ($replace) {
$conf->{oidcRPMetaDataOptions}->{$confKey} = {};
$conf->{oidcRPMetaDataExportedVars}->{$confKey} = {};
$conf->{oidcRPMetaDataOptionsExtraClaims}->{$confKey} = {};
$push->{options} =
$self->_setDefaultValues( $push->{options}, 'oidcRPMetaDataNode' );
}
if ( defined $push->{options} ) {
my $res = $self->_hasAllowedAttributes( $push->{options},
'oidcRPMetaDataNode' );
return $res unless ( $res->{res} eq 'ok' );
foreach ( keys %{ $push->{options} } ) {
$conf->{oidcRPMetaDataOptions}->{$confKey}->{$_} =
$push->{options}->{$_};
}
}
$conf->{oidcRPMetaDataOptions}->{$confKey}->{oidcRPMetaDataOptionsClientID}
= $push->{clientId}
if ( defined $push->{clientId} );
if ( defined $push->{exportedVars} ) {
if ( $self->_isSimpleKeyValueHash( $push->{exportedVars} ) ) {
foreach ( keys %{ $push->{exportedVars} } ) {
$conf->{oidcRPMetaDataExportedVars}->{$confKey}->{$_} =
$push->{exportedVars}->{$_};
}
}
else {
return {
res => 'ko',
msg =>
"Invalid input: exportedVars is not a hash object with \"key\":\"value\" attributes"
};
}
}
if ( defined $push->{extraClaim} ) {
if ( $self->_isSimpleKeyValueHash( $push->{extraClaim} ) ) {
foreach ( keys %{ $push->{extraClaim} } ) {
$conf->{oidcRPMetaDataOptionsExtraClaims}->{$confKey}->{$_} =
$push->{extraClaim}->{$_};
}
}
else {
return {
res => 'ko',
msg =>
"Invalid input: extraClaim is not a hash object with \"key\":\"value\" attributes"
};
}
}
# Save configuration
$self->_confAcc->saveConf($conf);
return { res => 'ok' };
}
1;

View File

@ -0,0 +1,403 @@
package Lemonldap::NG::Manager::Api::Providers::SamlSp;
our $VERSION = '2.0.8';
package Lemonldap::NG::Manager::Api;
use 5.10.0;
use utf8;
use Mouse;
extends 'Lemonldap::NG::Manager::Api::Common';
sub getSamlSpByConfKey {
my ( $self, $req ) = @_;
my $confKey = $req->params('confKey')
or return $self->sendError( $req, 'confKey is missing', 400 );
$self->logger->debug("[API] SAML SP $confKey configuration requested");
# Get latest configuration
my $conf = $self->_confAcc->getConf;
my $samlSp = $self->_getSamlSpByConfKey( $conf, $confKey );
# Check if confKey is defined
return $self->sendError( $req,
"SAML service Provider '$confKey' not found", 404 )
unless ( defined $samlSp );
return $self->sendJSONresponse( $req, $samlSp );
}
sub findSamlSpByConfKey {
my ( $self, $req ) = @_;
my $pattern = (
defined $req->params('uPattern')
? $req->params('uPattern')
: ( defined $req->params('pattern') ? $req->params('pattern') : undef )
);
return $self->sendError( $req, 'Invalid input: pattern is missing', 405 )
unless ( defined $pattern );
$self->logger->debug(
"[API] Find SAML SPs by confKey regexp $pattern requested");
# Get latest configuration
my $conf = $self->_confAcc->getConf;
my @samlSps =
map { $_ =~ $pattern ? $self->_getSamlSpByConfKey( $conf, $_ ) : () }
keys %{ $conf->{samlSPMetaDataXML} };
return $self->sendJSONresponse( $req, [@samlSps] );
}
sub findSamlSpByEntityId {
my ( $self, $req ) = @_;
my $entityId = (
defined $req->params('uEntityId') ? $req->params('uEntityId')
: (
defined $req->params('entityId') ? $req->params('entityId')
: undef
)
);
return $self->sendError( $req, 'entityId is missing', 405 )
unless ( defined $entityId );
$self->logger->debug("[API] Find SAML SPs by entityId $entityId requested");
# Get latest configuration
my $conf = $self->_confAcc->getConf;
my $samlSp = $self->_getSamlSpByEntityId( $conf, $entityId );
return $self->sendError( $req,
"SAML service Provider with entityID '$entityId' not found", 404 )
unless ( defined $samlSp );
return $self->sendJSONresponse( $req, $samlSp );
}
sub addSamlSp {
my ( $self, $req ) = @_;
my $add = $req->jsonBodyToObj;
return $self->sendError( $req, "Invalid input: " . $req->error, 405 )
unless ($add);
return $self->sendError( $req, 'Invalid input: confKey is missing', 405 )
unless ( defined $add->{confKey} );
return $self->sendError( $req, 'Invalid input: metadata is missing', 405 )
unless ( defined $add->{metadata} );
my $entityId = $self->_readSamlSpEntityId( $add->{metadata} );
return $self->sendError( $req,
'Invalid input: entityID is missing in metadata', 405 )
unless ( defined $entityId );
$self->logger->debug(
"[API] Add SAML SP with confKey $add->{confKey} and entityID $entityId requested"
);
# Get latest configuration
my $conf = $self->_confAcc->getConf( { noCache => 1 } );
return $self->sendError(
$req,
"Invalid input: A SAML SP with confKey $add->{confKey} already exists",
405
) if ( defined $self->_getSamlSpByConfKey( $conf, $add->{confKey} ) );
return $self->sendError( $req,
"Invalid input: A SAML SP with entityID $entityId already exists", 405 )
if ( defined $self->_getSamlSpByEntityId( $conf, $entityId ) );
my $res = $self->_pushSamlSp( $conf, $add->{confKey}, $add, 1 );
return $self->sendError( $req, $res->{msg}, 405 )
unless ( $res->{res} eq 'ok' );
return $self->sendJSONresponse( $req,
{ message => "Successful operation" } );
}
sub replaceSamlSp {
my ( $self, $req ) = @_;
my $confKey = $req->params('confKey')
or return $self->sendError( $req, 'confKey is missing', 400 );
my $replace = $req->jsonBodyToObj;
return $self->sendError( $req, "Invalid input: " . $req->error, 405 )
unless ($replace);
return $self->sendError( $req, 'Invalid input: metadata is missing', 405 )
unless ( defined $replace->{metadata} );
$self->logger->debug(
"[API] SAML SP $confKey configuration replace requested");
# Get latest configuration
my $conf = $self->_confAcc->getConf( { noCache => 1 } );
# Return 404 if not found
return $self->sendError( $req,
"SAML service provider '$confKey' not found", 404 )
unless ( defined $self->_getSamlSpByConfKey( $conf, $confKey ) );
# check if new entityId exists already
my $res = $self->_isNewSamlSpEntityIdUnique( $conf, $confKey, $replace );
return $self->sendError( $req, $res->{msg}, 405 )
unless ( $res->{res} eq 'ok' );
$res = $self->_pushSamlSp( $conf, $confKey, $replace, 1 );
return $self->sendError( $req, $res->{msg}, 405 )
unless ( $res->{res} eq 'ok' );
return $self->sendJSONresponse( $req,
{ message => "Successful operation" } );
}
sub updateSamlSp {
my ( $self, $req ) = @_;
my $res;
my $confKey = $req->params('confKey')
or return $self->sendError( $req, 'confKey is missing', 400 );
my $update = $req->jsonBodyToObj;
return $self->sendError( $req, "Invalid input: " . $req->error, 405 )
unless ($update);
$self->logger->debug(
"[API] SAML SP $confKey configuration update requested");
# Get latest configuration
my $conf = $self->_confAcc->getConf( { noCache => 1 } );
my $current = $self->_getSamlSpByConfKey( $conf, $confKey );
# Return 404 if not found
return $self->sendError( $req,
"SAML service provider '$confKey' not found", 404 )
unless ( defined $current );
if ( defined $update->{metadata} ) {
# check if new entityId exists already
$res = $self->_isNewSamlSpEntityIdUnique( $conf, $confKey, $update );
return $self->sendError( $req, $res->{msg}, 405 )
unless ( $res->{res} eq 'ok' );
}
$res = $self->_pushSamlSp( $conf, $confKey, $update, 0 );
return $self->sendError( $req, $res->{msg}, 405 )
unless ( $res->{res} eq 'ok' );
return $self->sendJSONresponse( $req,
{ message => "Successful operation" } );
}
sub deleteSamlSp {
my ( $self, $req ) = @_;
my $confKey = $req->params('confKey')
or return $self->sendError( $req, 'confKey is missing', 400 );
# Get latest configuration
my $conf = $self->_confAcc->getConf( { noCache => 1 } );
my $delete = $self->_getSamlSpByConfKey( $conf, $confKey );
# Return 404 if not found
return $self->sendError( $req,
"SAML service provider '$confKey' not found", 404 )
unless ( defined $delete );
delete $conf->{samlSPMetaDataXML}->{$confKey};
delete $conf->{samlSPMetaDataOptions}->{$confKey};
delete $conf->{samlSPMetaDataExportedAttributes}->{$confKey};
# Save configuration
$self->_confAcc->saveConf($conf);
return $self->sendJSONresponse( $req,
{ message => "Successful operation" } );
}
sub _getSamlSpByConfKey {
my ( $self, $conf, $confKey ) = @_;
# Check if confKey is defined
return undef unless ( defined $conf->{samlSPMetaDataXML}->{$confKey} );
# Get metadata
my $metadata = $conf->{samlSPMetaDataXML}->{$confKey}->{samlSPMetaDataXML};
# Get options
my $options = $conf->{samlSPMetaDataOptions}->{$confKey};
my $samlSp = {
confKey => $confKey,
metadata => $metadata,
exportedAttributes => {},
options => $options
};
# Get exported attributes
foreach ( keys %{ $conf->{samlSPMetaDataExportedAttributes}->{$confKey} } )
{
# Extract fields from exportedAttr value
my ( $mandatory, $name, $format, $friendly_name ) =
split( /;/,
$conf->{samlSPMetaDataExportedAttributes}->{$confKey}->{$_} );
$mandatory = !!$mandatory ? 'true' : 'false'; # ????????????
$samlSp->{exportedAttributes}->{$_} = {
name => $name,
mandatory => $mandatory
};
$samlSp->{exportedAttributes}->{$_}->{friendlyName} = $friendly_name
if ( defined $friendly_name && $friendly_name ne '' );
$samlSp->{exportedAttributes}->{$_}->{format} = $format
if ( defined $format && $format ne '' );
}
return $samlSp;
}
sub _getSamlSpByEntityId {
my ( $self, $conf, $entityId ) = @_;
foreach ( keys %{ $conf->{samlSPMetaDataXML} } ) {
return $self->_getSamlSpByConfKey( $conf, $_ )
if (
$self->_readSamlSpEntityId(
$conf->{samlSPMetaDataXML}->{$_}->{samlSPMetaDataXML}
) eq $entityId
);
}
return undef;
}
sub _readSamlSpEntityId {
my ( $self, $metadata ) = @_;
return ( $metadata =~ /entityID=['"](.+?)['"]/ ) ? $1 : undef;
}
sub _readSamlSpExportedAttributes {
my ( $self, $attrs, $mergeWith ) = @_;
my $allowedFormats = [
"urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified",
"urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
"urn:oasis:names:tc:SAML:2.0:attrname-format:basic"
];
foreach ( keys %{$attrs} ) {
return { res => "ko", msg => "Exported attribute $_ has no name" }
unless ( defined $attrs->{$_}->{name} );
my $mandatory = 0;
my $name = $attrs->{$_}->{name};
my $format = '';
my $friendlyName = '';
( $mandatory, $name, $format, $friendlyName ) =
split( /;/, $mergeWith->{$_} )
if ( defined $mergeWith->{$_} );
if ( defined $attrs->{$_}->{mandatory} ) {
$mandatory = (
$attrs->{$_}->{mandatory} eq '1'
or $attrs->{$_}->{mandatory} eq 'true'
) ? 1 : 0;
}
if ( defined $attrs->{$_}->{format} ) {
$format = $attrs->{$_}->{format};
return {
res => "ko",
msg => "Exported attribute $_ format does not exist."
}
unless ( length( grep { /^$format$/ } @{$allowedFormats} ) );
}
$friendlyName = $attrs->{$_}->{friendlyName}
if ( defined $attrs->{$_}->{friendlyName} );
$mergeWith->{$_} = "$mandatory;$name;$format;$friendlyName";
}
return { res => "ok", exportedAttributes => $mergeWith };
}
sub _pushSamlSp {
my ( $self, $conf, $confKey, $push, $replace ) = @_;
if ($replace) {
$conf->{samlSPMetaDataXML}->{$confKey} = {};
$conf->{samlSPMetaDataOptions}->{$confKey} = {};
$conf->{samlSPMetaDataExportedAttributes}->{$confKey} = {};
$push->{options} =
$self->_setDefaultValues( $push->{options}, 'samlSPMetaDataNode' );
}
$conf->{samlSPMetaDataXML}->{$confKey}->{samlSPMetaDataXML} =
$push->{metadata};
if ( defined $push->{options} ) {
my $res = $self->_hasAllowedAttributes( $push->{options},
'samlSPMetaDataNode' );
return $res unless ( $res->{res} eq 'ok' );
foreach ( keys %{ $push->{options} } ) {
$conf->{samlSPMetaDataOptions}->{$confKey}->{$_} =
$push->{options}->{$_};
}
}
if ( defined $push->{exportedAttributes} ) {
my $res =
$self->_readSamlSpExportedAttributes( $push->{exportedAttributes},
$conf->{samlSPMetaDataExportedAttributes}->{$confKey} );
return $res unless ( $res->{res} eq 'ok' );
$conf->{samlSPMetaDataExportedAttributes}->{$confKey} =
$res->{exportedAttributes};
}
# Save configuration
$self->_confAcc->saveConf($conf);
return { res => 'ok' };
}
sub _isNewSamlSpEntityIdUnique {
my ( $self, $conf, $confKey, $newSp ) = @_;
my $newEntityId = $self->_readSamlSpEntityId( $newSp->{metadata} );
my $curEntityId =
$self->_readSamlSpEntityId(
$self->_getSamlSpByConfKey( $conf, $confKey )->{metadata} );
if ( $newEntityId ne $curEntityId ) {
return {
res => 'ko',
msg =>
"An SAML service provide with entityId '$newEntityId' already exists"
}
if ( defined $self->_getSamlSpByEntityId( $conf, $newEntityId ) );
}
return { res => 'ok' };
}
1;

View File

@ -1086,6 +1086,9 @@ qr/(?:(?:https?):\/\/(?:(?:(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.]
'customRegister' => {
'type' => 'text'
},
'customResetCertByMail' => {
'type' => 'text'
},
'customToTrace' => {
'type' => 'lmAttrOrMacro'
},

View File

@ -3602,6 +3602,10 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{1,5})?|ldap(?:s|\+tls)?://\w[\w\-\.]*(?:
type => 'text',
documentation => 'Custom register module',
},
customResetCertByMail => {
type => 'text',
documentation => 'Custom certificateResetByMail module',
},
customAddParams => {
type => 'keyTextContainer',
documentation => 'Custom additional parameters',

View File

@ -435,9 +435,9 @@ sub tree {
title => 'customParams',
help => 'authcustom.html',
nodes => [
'customAuth', 'customUserDB',
'customPassword', 'customRegister',
'customAddParams',
'customAuth', 'customUserDB',
'customPassword', 'customRegister',
'customResetCertByMail', 'customAddParams',
]
},
],

View File

@ -21,17 +21,15 @@ has cfgNum => (
}
);
has sep => ( is => 'rw', isa => 'Str', default => '/' );
has req => ( is => 'ro' );
has log => ( is => 'rw' );
has req => ( is => 'ro' );
has sep => ( is => 'rw', isa => 'Str', default => '/' );
has format => ( is => 'rw', isa => 'Str', default => "%-25s | %-25s | %-25s" );
has yes => ( is => 'rw', isa => 'Bool', default => 0 );
has force => ( is => 'rw', isa => 'Bool', default => 0 );
has log => ( is => 'rw' );
has yes => ( is => 'rw', isa => 'Bool', default => 0 );
has force => ( is => 'rw', isa => 'Bool', default => 0 );
has logger => ( is => 'ro', lazy => 1, builder => sub { $_[0]->mgr->logger } );
has userLogger =>
( is => 'ro', lazy => 1, builder => sub { $_[0]->mgr->userLogger } );
sub get {
my ( $self, @keys ) = @_;
@ -60,6 +58,7 @@ sub set {
die "$key seems to be a hash, modification refused";
}
$oldValue //= '';
$self->logger->info("CLI: Set key $key with $pairs{$key}");
push @list, [ $key, $oldValue, $pairs{$key} ];
}
unless ( $self->yes ) {
@ -96,6 +95,7 @@ sub addKey {
unless ( $root =~ /$simpleHashKeys$/o or $root =~ /$sep/o ) {
die "$root is not a simple hash. Aborting";
}
$self->logger->info("CLI: Append key $root/$newKey $value");
push @list, [ $root, $newKey, $value ];
}
require Clone;
@ -136,6 +136,7 @@ sub delKey {
unless ( $root =~ /$simpleHashKeys$/o or $root =~ /$sep/o ) {
die "$root is not a simple hash. Aborting";
}
$self->logger->info("CLI: Remove key $root/$key");
push @list, [ $root, $key ];
}
require Clone;
@ -191,6 +192,7 @@ sub delKey {
sub lastCfg {
my ($self) = @_;
$self->logger->info("CLI: Retrieve last conf.");
return $self->jsonResponse('/confs/latest')->{cfgNum};
}
@ -218,6 +220,7 @@ sub restore {
close $f;
die "Empty or malformed file $file" unless ( $conf =~ /\w/s );
}
$self->logger->info("CLI: Restore conf.");
my $res = $self->_post( '/confs/raw', '', IO::String->new($conf),
'application/json', length($conf) );
use Data::Dumper;
@ -273,29 +276,34 @@ sub _save {
}
);
unless ( $parser->testNewConf() ) {
$self->logger->error("CLI: Configuration rejected with message: $parser->{message}");
printf STDERR "Modifications rejected: %s:\n", $parser->{message};
}
my $saveParams = { force => $self->force };
if ( $self->force and $self->cfgNum ) {
$self->logger->debug("CLI: cfgNum forced with $self->cfgNum()");
$saveParams->{cfgNum} = $self->cfgNum;
$saveParams->{cfgNumFixed} = 1;
}
$new->{cfgAuthor} = scalar( getpwuid $< ) . '(command-line)';
$new->{cfgAuthor} = scalar( getpwuid $< ) . '(command-line-interface)';
chomp $new->{cfgAuthor};
$new->{cfgAuthorIP} = '127.0.0.1';
$new->{cfgDate} = time;
$new->{cfgVersion} = $Lemonldap::NG::Manager::VERSION;
$new->{cfgLog} = $self->log // 'Modified using LLNG cli';
$new->{cfgLog} = $self->log // 'Modified with LL::NG CLI';
$new->{key} ||= join( '',
map { chr( int( ord( Crypt::URandom::urandom(1) ) * 94 / 256 ) + 33 ) }
( 1 .. 16 ) );
my $s = $self->mgr->confAcc->saveConf( $new, %$saveParams );
if ( $s > 0 ) {
$self->logger->debug("CLI: Configuration $s has been saved by $new->{cfgAuthor}");
$self->logger->info("CLI: Configuration $s saved");
print STDERR "Saved under number $s\n";
$parser->{status} = [ $self->mgr->applyConf($new) ];
}
else {
$self->logger->error("CLI: Configuration not saved!");
printf STDERR "Modifications rejected: %s:\n", $parser->{message};
print STDERR Dumper($parser);
}
@ -336,8 +344,9 @@ sub run {
my $action = shift;
unless ( $action =~ /^(?:get|set|addKey|delKey|save|restore)$/ ) {
die
"unknown action $action. Only get, set, addKey or delKey are accepted";
"Unknown action $action. Only get, set, addKey or delKey allowed";
}
$self->$action(@_);
}
@ -346,7 +355,6 @@ package Lemonldap::NG::Manager::Cli::Request;
use Mouse;
has cfgNum => ( is => 'rw' );
has error => ( is => 'rw' );
sub params {

View File

@ -719,6 +719,21 @@ sub tests {
return 1;
},
# Warn if CertificateResetByMail dependencies seem missing
certResetByMailDependencies => sub {
return 1 unless ( $conf->{portalDisplayCertificateResetByMail} );
return ( 0,
"LDAP RegisterDB is required to enable CertificateResetByMail plugin"
) unless ( $conf->{registerDB} eq 'LDAP' );
eval "use DateTime::Format::RFC3339";
return ( 1,
"DateTime::Format::RFC3339 module is required to enable CertificateResetByMail plugin"
) if ($@);
# Return
return 1;
},
# OIDC redirect URI must not be empty
oidcRPRedirectURINotEmpty => sub {
return 1

View File

@ -0,0 +1,12 @@
#!/usr/bin/perl
use Plack::Handler::FCGI;
use Lemonldap::NG::Manager;
# Roll your own
my $server = Plack::Handler::FCGI->new();
$server->run(
Lemonldap::NG::Manager->run(
{ enabledModules => "api", protection => "none" }
)
);

View File

@ -33,15 +33,20 @@
<option value="configure.png">
<option value="database.png">
<option value="demo.png">
<option value="docs.png">
<option value="folder.png">
<option value="gear.png">
<option value="help.png">
<option value="llng.png">
<option value="mailappt.png">
<option value="money.png">
<option value="network.png">
<option value="terminal.png">
<option value="thumbnail.png">
<option value="tools.png">
<option value="tux.png">
<option value="web.png">
<option value="wheels.png">
</datalist>
</div>
</div>
@ -81,7 +86,7 @@
</div>
<div class="modal-body">
<div class="row text-center">
<div class="col-md-2" ng-repeat="i in ['attach.png', 'bell.png', 'bookmark.png', 'configure.png', 'database.png', 'demo.png', 'folder.png', 'gear.png', 'help.png', 'mailappt.png', 'money.png', 'network.png', 'terminal.png', 'thumbnail.png', 'tux.png']">
<div class="col-md-2" ng-repeat="i in ['attach.png', 'bell.png', 'bookmark.png', 'configure.png', 'database.png', 'demo.png', 'docs.png', 'folder.png', 'gear.png', 'help.png', 'llng.png', 'mailappt.png', 'money.png', 'network.png', 'terminal.png', 'thumbnail.png','tools.png', 'tux.png', 'web.png', 'wheels.png']">
<button class="btn llcontainer" ng-class="{'btn-default':currentNode.data.logo!=i,'btn-info':currentNode.data.logo==i}" ng-click="ok(currentNode.data.logo=i)">
<img ng-src="{{elem('portal').data}}static/common/apps/{{i}}" title="{{i}}" alt="{{i}}" />
</button>

View File

@ -223,6 +223,7 @@
"customPluginsParams":"معايير إضافية",
"customPortalSkin":"غلاف البوابة مخصص",
"customRegister":"وحدة تسجيل مخصص",
"customResetCertByMail":"Custom certificateResetByMail module",
"customToTrace":"REMOTE_CUSTOM",
"customUserDB":"وحدة قاعدة البيانات المخصصة",
"date":"تاريخ",
@ -505,7 +506,7 @@
"next":"التالى",
"nginxCustomHandlers":"معالجات إنجن إكس المخصصة",
"noAjaxHook":"الحفاظ على إعادة توجيه ل أجاكس",
"noDatas":"لا توجد بيانات لعرضها",
"noData":"لا توجد بيانات لعرضها",
"notABoolean":"ليس بولياني",
"notAnInteger":"ليس عددا صحيحا",
"notAValidPerlExpression":"عبارة بيرل ليست صحيحة",

View File

@ -223,6 +223,7 @@
"customPluginsParams":"Additional parameters",
"customPortalSkin":"Custom portal skin",
"customRegister":"Custom register module",
"customResetCertByMail":"Custom certificateResetByMail module",
"customToTrace":"REMOTE_CUSTOM",
"customUserDB":"Custom user DB module",
"date":"Datum",
@ -505,7 +506,7 @@
"next":"Next",
"nginxCustomHandlers":"Custom Nginx handlers",
"noAjaxHook":"Keep redirections for Ajax",
"noDatas":"No datas to display",
"noData":"No data to display",
"notABoolean":"Not a boolean",
"notAnInteger":"Not an integer",
"notAValidPerlExpression":"Not a valid Perl expression",

View File

@ -223,6 +223,7 @@
"customPluginsParams":"Additional parameters",
"customPortalSkin":"Custom portal skin",
"customRegister":"Custom register module",
"customResetCertByMail":"Custom certificateResetByMail module",
"customToTrace":"REMOTE_CUSTOM",
"customUserDB":"Custom user DB module",
"date":"Date",
@ -505,7 +506,7 @@
"next":"Next",
"nginxCustomHandlers":"Custom Nginx handlers",
"noAjaxHook":"Keep redirections for Ajax",
"noDatas":"No datas to display",
"noData":"No data to display",
"notABoolean":"Not a boolean",
"notAnInteger":"Not an integer",
"notAValidPerlExpression":"Not a valid Perl expression",
@ -797,7 +798,7 @@
"saveReport":"Save report",
"savingConfirmation":"Saving confirmation",
"scope":"Scope",
"search":"Search ...",
"search":"Search...",
"secondFactors":"Second factors",
"securedCookie":"Secured Cookie (SSL)",
"security":"Security",

View File

@ -147,10 +147,10 @@
"certificateResetByMailURL":"URL de la page de réinitialisation",
"certificateResetByMailCeaAttribute":"Attribut CEA du certificat",
"certificateResetByMailCertificateAttribute":"Nom de l'attribut du certificat",
"certificateResetByMailStep1Subject":"Sujet du mail de réinitialisation",
"certificateResetByMailStep1Body":"Contenu du mail de réinitialisation",
"certificateResetByMailStep2Subject":"Sujet du mail de confirmation",
"certificateResetByMailStep2Body":"Contenu du mail de confirmation",
"certificateResetByMailStep1Subject":"Sujet du message de réinitialisation",
"certificateResetByMailStep1Body":"Contenu du message de réinitialisation",
"certificateResetByMailStep2Subject":"Sujet du message de confirmation",
"certificateResetByMailStep2Body":"Contenu du message de confirmation",
"certificateResetByMailValidityDelay":"Durée minimun avant expiration",
"portalDisplayCertificateResetByMail":"Réinitialiser votre certificat",
"contentSecurityPolicy":"Politique de sécurité de contenu",
@ -223,6 +223,7 @@
"customPluginsParams":"Paramètres supplémentaires",
"customPortalSkin":"Style personnalisé du portail",
"customRegister":"Module d'enregistrement personnalisé",
"customResetCertByMail":"Module de réinitialisation des certificats personalisé",
"customToTrace":"REMOTE_CUSTOM",
"customUserDB":"Module BD utilisateurs personnalisé",
"date":"Date",
@ -505,7 +506,7 @@
"next":"Suivante",
"nginxCustomHandlers":"Handlers Nginx personnalisés",
"noAjaxHook":"Conserver les redirections pour Ajax",
"noDatas":"Aucune donnée à afficher",
"noData":"Aucune donnée à afficher",
"notABoolean":"Pas un booléen",
"notAnInteger":"Pas un nombre entier",
"notAValidPerlExpression":"Pas une expression Perl valide",

View File

@ -223,6 +223,7 @@
"customPluginsParams":"Parametri aggiuntivi",
"customPortalSkin":"Personalizza faccia del portale ",
"customRegister":"Personalizza modulo di registro",
"customResetCertByMail":"Custom certificateResetByMail module",
"customToTrace":"REMOTE_CUSTOM",
"customUserDB":"Personalizza modulo utente DB",
"date":"Data",
@ -505,7 +506,7 @@
"next":"Seguente",
"nginxCustomHandlers":"Gestori Custom Nginx",
"noAjaxHook":"Tenere i reindirizzamenti per Ajax",
"noDatas":"Nessun dato da visualizzare",
"noData":"Nessun dato da visualizzare",
"notABoolean":"Non un booleano",
"notAnInteger":"Non un numero intero",
"notAValidPerlExpression":"Non una valida espressione Perl",

View File

@ -223,6 +223,7 @@
"customPluginsParams":"Ek parametreler",
"customPortalSkin":"Özel portal dış görünümü",
"customRegister":"Özelleştirilmiş kayıt modülü",
"customResetCertByMail":"Custom certificateResetByMail module",
"customToTrace":"REMOTE_CUSTOM",
"customUserDB":"Özelleştirilmiş kullanıcı veri tabanı modülü",
"date":"Tarih",
@ -505,7 +506,7 @@
"next":"Sonraki",
"nginxCustomHandlers":"Özel Nginx işleyicileri",
"noAjaxHook":"Ajax için yönlendirmeleri tut",
"noDatas":"Görüntülenecek veri yok",
"noData":"Görüntülenecek veri yok",
"notABoolean":"Mantıksal değil",
"notAnInteger":"Bir rakam değil",
"notAValidPerlExpression":"Geçerli bir Perl ifadesi değil",

View File

@ -223,6 +223,7 @@
"customPluginsParams":"Additional parameters",
"customPortalSkin":"Tùy chỉnh giao diện cổng thông tin",
"customRegister":"Module đăng ký tùy chỉnh",
"customResetCertByMail":"Custom certificateResetByMail module",
"customToTrace":"REMOTE_CUSTOM",
"customUserDB":"Mô đun DB người dùng tùy chỉnh",
"date":"Ngày",
@ -505,7 +506,7 @@
"next":"Tiếp theo",
"nginxCustomHandlers":"Tùy chỉnh trình xử lý Nginx ",
"noAjaxHook":"Giữ lại các chuyển hướng cho Ajax",
"noDatas":"Không có dữ liệu để hiển thị",
"noData":"Không có dữ liệu để hiển thị",
"notABoolean":"Không phải là một biến boolean",
"notAnInteger":"Không phải là một số nguyên",
"notAValidPerlExpression":"Không phải là một biểu thức Perl hợp lệ",

View File

@ -223,6 +223,7 @@
"customPluginsParams":"Additional parameters",
"customPortalSkin":"Custom portal skin",
"customRegister":"Custom register module",
"customResetCertByMail":"Custom certificateResetByMail module",
"customToTrace":"REMOTE_CUSTOM",
"customUserDB":"Custom user DB module",
"date":"日期",
@ -505,7 +506,7 @@
"next":"Next",
"nginxCustomHandlers":"Custom Nginx handlers",
"noAjaxHook":"Keep redirections for Ajax",
"noDatas":"No datas to display",
"noData":"No data to display",
"notABoolean":"Not a boolean",
"notAnInteger":"Not an integer",
"notAValidPerlExpression":"Not a valid Perl expression",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -41,12 +41,12 @@
<!-- Tree -->
<div class="text-center"><p class="badge">{{total}} <span trspan="session_s"></span></p></div>
<div ng-show="data.length!=0" class="text-center"><p class="badge">{{total}} <span trspan="session_s"></span></p></div>
<div class="region region-sidebar-first">
<section id="block-superfish-1" class="block block-superfish clearfix">
<div ui-tree data-drag-enabled="false" id="tree-root">
<div ng-show="data.length==0" class="center">
<span class="label label-warning" trspan="noDatas"></span>
<span class="label label-warning" trspan="noData"></span>
</div>
<ol ui-tree-nodes="" ng-model="data">
<li ng-repeat="node in data track by node.id" ui-tree-node ng-include="'nodes_renderer.html'" collapsed="true"></li>

View File

@ -1,7 +1,6 @@
<TMPL_INCLUDE NAME="header.tpl">
<title>LemonLDAP::NG Manager</title>
<link rel="prefetch" href="<TMPL_VAR NAME="STATIC_PREFIX">forms/home.html" />
<title>LemonLDAP::NG Comparator</title>
<link rel="prefetch" href="<TMPL_VAR NAME="STATIC_PREFIX">struct.json" />
</head>
@ -29,12 +28,29 @@
<a ng-show="cfg[1].next" class="input-group-addon link glyphicon glyphicon-arrow-right" href="#!/{{cfg[0].next}}/{{cfg[1].next}}" role="link"></a>
</div>
</div>
<table class="table table-striped">
<tr>
<th>{{translate('date')}}</th>
<td>{{cfg[0].date}}</td>
<td>{{cfg[1].date}}</td>
</tr>
<tr>
<th>{{translate('author')}}</th>
<td>{{cfg[0].cfgAuthor}}</td>
<td>{{cfg[1].cfgAuthor}}</td>
</tr>
<tr ng-if="cfg[0].cfgLog || cfg[1].cfgLog">
<th>{{translate('cfgLog')}}</th>
<td>{{cfg[0].cfgLog}}</td>
<td>{{cfg[1].cfgLog}}</td>
</tr>
</table>
</div>
<div class="region region-sidebar-first">
<section id="block-superfish-1" class="block block-superfish clearfix">
<div ui-tree data-drag-enabled="false" id="tree-root">
<div ng-show="data.length==0" class="center">
<span class="label label-warning" trspan="noDatas"></span>
<span class="label label-warning" trspan="noData"></span>
</div>
<ol ui-tree-nodes="" ng-model="data">
<li ng-repeat="node in data" ui-tree-node ng-include="'nodes_renderer.html'" collapsed="true"></li>

View File

@ -1,6 +1,6 @@
<TMPL_INCLUDE NAME="header.tpl">
<title>LemonLDAP::NG notifications explorer</title>
<title>LemonLDAP::NG Notifications explorer</title>
</head>
<body ng-app="llngNotificationsExplorer" ng-controller="NotificationsExplorerCtrl" ng-csp>
@ -26,7 +26,7 @@
<section id="block-superfish-1" class="block block-superfish clearfix">
<div ui-tree data-drag-enabled="false" id="tree-root">
<div ng-show="data.length==0" class="center">
<span class="label label-warning" trspan="noDatas"></span>
<span class="label label-warning" trspan="noData"></span>
</div>
<ol ui-tree-nodes="" ng-model="data">
<li ng-repeat="node in data track by node.id" ui-tree-node ng-include="'nodes_renderer.html'" collapsed="true"></li>

View File

@ -1,6 +1,6 @@
<TMPL_INCLUDE NAME="header.tpl">
<title>LemonLDAP::NG sessions explorer</title>
<title>LemonLDAP::NG Sessions explorer</title>
</head>
<body ng-app="llngSessionsExplorer" ng-controller="SessionsExplorerCtrl" ng-csp>
@ -30,12 +30,12 @@
</ul>
</div>
</div>
<div class="text-center"><p class="badge">{{total}} <span trspan="session_s"></span></p></div>
<div ng-show="data.length!=0" class="text-center"><p class="badge">{{total}} <span trspan="session_s"></span></p></div>
<div class="region region-sidebar-first">
<section id="block-superfish-1" class="block block-superfish clearfix">
<div ui-tree data-drag-enabled="false" id="tree-root">
<div ng-show="data.length==0" class="center">
<span class="label label-warning" trspan="noDatas"></span>
<span class="label label-warning" trspan="noData"></span>
</div>
<ol ui-tree-nodes="" ng-model="data">
<li ng-repeat="node in data track by node.id" ui-tree-node ng-include="'nodes_renderer.html'" collapsed="true"></li>

View File

@ -6,7 +6,7 @@
<section id="block-superfish-1" class="block block-superfish clearfix">
<div ui-tree data-drag-enabled="false" id="tree-root">
<div ng-show="data.length==0" class="center">
<span class="label label-warning" trspan="noDatas"></span>
<span class="label label-warning" trspan="noData"></span>
</div>
<ol ui-tree-nodes="" ng-model="data">
<li ng-repeat="node in data track by node.id" ui-tree-node ng-include="'nodes_renderer.html'" collapsed="true"></li>

View File

@ -1,7 +1,6 @@
<TMPL_INCLUDE NAME="header.tpl">
<title>LemonLDAP::NG Manager</title>
<link rel="prefetch" href="<TMPL_VAR NAME="STATIC_PREFIX">forms/homeViewer.html" />
<title>LemonLDAP::NG Viewer comparator</title>
<link rel="prefetch" href="<TMPL_VAR NAME="STATIC_PREFIX">struct.json" />
</head>
@ -29,12 +28,29 @@
<a ng-show="cfg[1].next" class="input-group-addon link glyphicon glyphicon-arrow-right" href="#!/{{cfg[0].next}}/{{cfg[1].next}}" role="link"></a>
</div>
</div>
<table class="table table-striped">
<tr>
<th>{{translate('date')}}</th>
<td>{{cfg[0].date}}</td>
<td>{{cfg[1].date}}</td>
</tr>
<tr>
<th>{{translate('author')}}</th>
<td>{{cfg[0].cfgAuthor}}</td>
<td>{{cfg[1].cfgAuthor}}</td>
</tr>
<tr ng-if="cfg[0].cfgLog || cfg[1].cfgLog">
<th>{{translate('cfgLog')}}</th>
<td>{{cfg[0].cfgLog}}</td>
<td>{{cfg[1].cfgLog}}</td>
</tr>
</table>
</div>
<div class="region region-sidebar-first">
<section id="block-superfish-1" class="block block-superfish clearfix">
<div ui-tree data-drag-enabled="false" id="tree-root">
<div ng-show="data.length==0" class="center">
<span class="label label-warning" trspan="noDatas"></span>
<span class="label label-warning" trspan="noData"></span>
</div>
<ol ui-tree-nodes="" ng-model="data">
<li ng-repeat="node in data" ui-tree-node ng-include="'nodes_renderer.html'" collapsed="true"></li>

View File

@ -1,6 +1,6 @@
<TMPL_INCLUDE NAME="header.tpl">
<title>LemonLDAP::NG Manager</title>
<title>LemonLDAP::NG Viewer</title>
<link rel="prefetch" href="<TMPL_VAR NAME="STATIC_PREFIX">forms/home.html" />
<link rel="prefetch" href="<TMPL_VAR NAME="STATIC_PREFIX">struct.json" />
</head>

View File

@ -0,0 +1,355 @@
# Test 2F API
use Test::More;
use strict;
use JSON;
use IO::String;
use Lemonldap::NG::Common::Session;
eval { mkdir 't/sessions' };
`rm -rf t/sessions/*`;
require 't/test-lib.pm';
our $_json = JSON->new->allow_nonref;
sub newSession {
my ( $uid, $ip, $kind, $sfaDevices ) = splice @_;
my $tmp;
ok(
$tmp = Lemonldap::NG::Common::Session->new( {
storageModule => 'Apache::Session::File',
storageModuleOptions => {
Directory => 't/sessions',
LockDirectory => 't/sessions',
backend => 'Apache::Session::File',
generateModule =>
'Lemonldap::NG::Common::Apache::Session::Generate::SHA256',
},
}
),
'Sessions module'
);
count(1);
ok(
$tmp->update( {
ipAddr => $ip,
_whatToTrace => $uid,
uid => $uid,
_session_uid => $uid,
_utime => time,
_session_kind => $kind,
_2fDevices => to_json($sfaDevices),
}
), "New $kind session for $uid"
);
count(1);
}
sub check200 {
my ( $test, $res ) = splice @_;
ok( $res->[0] == 200, "$test: Result code is 200" );
count(1);
checkJson( $test, $res );
}
sub check405 {
my ( $test, $res ) = splice @_;
ok( $res->[0] == 405, "$test: Result code is 405" );
count(1);
checkJson( $test, $res );
}
sub check404 {
my ( $test, $res ) = splice @_;
ok( $res->[0] == 404, "$test: Result code is 404" );
count(1);
checkJson( $test, $res );
}
sub checkJson {
my ( $test, $res ) = splice @_;
my $key;
#diag Dumper($res->[2]->[0]);
ok( $key = from_json( $res->[2]->[0] ), "$test: Response is JSON" );
count(1);
}
sub get {
my ( $test, $uid, $type, $id ) = splice @_;
my ($res);
ok(
$res = &client->_get(
"/api/v1/secondFactor/$uid"
. (
defined $type ? "/type/$type" : ( defined $id ? "/id/$id" : "" )
)
),
"$test: Request succeed"
);
count(1);
return $res;
}
sub checkGet {
my ( $uid, $id ) = splice @_;
my ( $test, $res, $ret );
$test = "$uid should have one 2F with id \"$id\"";
$res = get( $test, $uid, undef, $id );
check200( $test, $res );
#diag Dumper($res);
$ret = from_json( $res->[2]->[0] );
ok( ref $ret eq 'HASH' && $ret->{id} eq $id,
"$test: check returned type is HASH and that ids match" );
count(1);
}
sub checkGet404 {
my ( $uid, $id ) = splice @_;
my ( $test, $res, $ret );
$test = "$uid should not have any 2F with id \"$id\"";
$res = get( $test, $uid, undef, $id );
check404( $test, $res );
}
sub checkGetList {
my ( $expect, $uid, $type ) = splice @_;
my ( $test, $res, $ret );
$test = "$uid should have $expect 2F"
. ( defined $type ? " of type \"$type\"" : "" );
$res = get( $test, $uid, $type );
check200( $test, $res );
#diag Dumper($res);
$ret = from_json( $res->[2]->[0] );
ok(
scalar @$ret eq $expect,
"$test: check if nb of 2F found ("
. scalar @$ret
. ") equals expectation ($expect)"
);
count(1);
return $ret;
}
sub checkGetBadType {
my ( $uid, $type ) = splice @_;
my ( $test, $res );
$test = "Get for uid $uid and type \"$type\" should get rejected.";
$res = get( $test, $uid, $type );
check405( $test, $res );
}
sub checkGetOnIds {
my ( $uid, $ret ) = splice @_;
foreach (@$ret) {
checkGet( $uid, $_->{id} );
}
}
sub checkGetOnIdsNotFound {
my ( $uid, $ret ) = splice @_;
foreach (@$ret) {
checkGet404( $uid, $_->{id} );
}
}
sub del {
my ( $test, $uid, $type, $id ) = splice @_;
my ($res);
ok(
$res = &client->_del(
"/api/v1/secondFactor/$uid"
. (
defined $type ? "/type/$type" : ( defined $id ? "/id/$id" : "" )
)
),
"$test: Request succeed"
);
count(1);
return $res;
}
sub checkDelete {
my ( $uid, $id ) = splice @_;
my ( $test, $res );
$test = "$uid should have a 2F with id \"$id\" to be deleted.";
$res = del( $test, $uid, undef, $id );
check200( $test, $res );
}
sub checkDelete404 {
my ( $uid, $id ) = splice @_;
my ( $test, $res );
$test = "$uid should not have a 2F with id \"$id\" to be deleted.";
$res = del( $test, $uid, undef, $id );
check404( $test, $res );
}
sub checkDeleteList {
my ( $expect, $uid, $type ) = splice @_;
my ( $test, $res, $ret, $countDel );
$test =
"Delete all 2F from $uid" . ( defined $type ? " of type \"$type\"" : "" );
$res = del( $test, $uid, $type );
check200( $test, $res );
$ret = from_json( $res->[2]->[0] );
($countDel) = $ret->{message} =~ m/^Successful operation: ([\d]+) /i;
$countDel = 0 unless ( defined $countDel );
ok(
$countDel eq $expect,
"$test: check nb of 2FA deleted ($countDel) matches expectation ($expect)"
);
count(1);
}
sub checkDeleteBadType {
my ( $uid, $type ) = splice @_;
my ( $test, $res );
$test = "Delete for uid $uid and type \"$type\" should get rejected.";
$res = del( $test, $uid, $type );
check405( $test, $res );
}
my $sfaDevices = [];
my $ret;
## Sessions creation
# msmith
newSession( 'msmith', '127.10.0.1', 'SSO', $sfaDevices );
newSession( 'msmith', '127.10.0.1', 'Persistent', $sfaDevices );
# dwho
$sfaDevices = [ {
"name" => "MyU2FKey",
"type" => "U2F",
"_userKey" => "123456",
"_keyHandle" => "654321",
"epoch" => time
},
{
"name" => "MyTOTP",
"type" => "TOTP",
"_secret" => "123456",
"epoch" => time
},
{
"name" => "MyYubikey",
"type" => "UBK",
"_secret" => "123456",
"epoch" => time
}
];
newSession( 'dwho', '127.10.0.1', 'SSO', $sfaDevices );
newSession( 'dwho', '127.10.0.1', 'Persistent', $sfaDevices );
# rtyler
$sfaDevices = [ {
"name" => "MyU2FKey",
"type" => "U2F",
"_userKey" => "123456",
"_keyHandle" => "654321",
"epoch" => time
},
{
"name" => "MyYubikey",
"type" => "UBK",
"_secret" => "123456",
"epoch" => time
},
{
"name" => "MyYubikey2",
"type" => "UBK",
"_secret" => "654321",
"epoch" => time
}
];
newSession( 'rtyler', '127.10.0.1', 'SSO', $sfaDevices );
newSession( 'rtyler', '127.10.0.1', 'Persistent', $sfaDevices );
# davros
$sfaDevices = [ {
"name" => "MyU2FKey",
"type" => "U2F",
"_userKey" => "123456",
"_keyHandle" => "654321",
"epoch" => time
},
{
"name" => "MyTOTP",
"type" => "TOTP",
"_secret" => "123456",
"epoch" => time
}
];
newSession( 'davros', '127.10.0.1', 'SSO', $sfaDevices );
newSession( 'davros', '127.10.0.1', 'Persistent', $sfaDevices );
# tof
$sfaDevices = [ {
"name" => "MyU2FKey",
"type" => "U2F",
"_userKey" => "123456",
"_keyHandle" => "654321",
"epoch" => time
}
];
newSession( 'tof', '127.10.0.1', 'SSO', $sfaDevices );
newSession( 'tof', '127.10.0.1', 'Persistent', $sfaDevices );
# dwho
checkGetList( 1, 'dwho', 'U2F' );
checkGetList( 1, 'dwho', 'TOTP' );
checkGetList( 1, 'dwho', 'UBK' );
checkGetBadType( 'dwho', 'UBKIKI' );
$ret = checkGetList( 3, 'dwho' );
checkGetOnIds( 'dwho', $ret );
checkDelete( 'dwho', @$ret[0]->{id} );
checkDelete404( 'dwho', @$ret[0]->{id} );
checkGetList( 2, 'dwho' );
checkDeleteList( 2, 'dwho' );
checkGetList( 0, 'dwho' );
checkDeleteList( 0, 'dwho' );
# msmith
checkGetList( 0, 'msmith' );
# rtyler
checkGetList( 1, 'rtyler', 'U2F' );
checkGetList( 0, 'rtyler', 'TOTP' );
checkGetList( 2, 'rtyler', 'UBK' );
$ret = checkGetList( 3, 'rtyler' );
checkGetOnIds( 'rtyler', $ret );
checkDeleteList( 2, 'rtyler', 'UBK' );
$ret = checkGetList( 1, 'rtyler' );
checkDelete( 'rtyler', @$ret[0]->{id} );
checkDelete404( 'rtyler', @$ret[0]->{id} );
checkDeleteList( 0, 'rtyler' );
# davros
checkGetList( 1, 'davros', 'U2F' );
checkGetList( 1, 'davros', 'TOTP' );
checkGetList( 0, 'davros', 'UBK' );
$ret = checkGetList( 2, 'davros' );
checkGetOnIds( 'davros', $ret );
checkDelete( 'davros', @$ret[0]->{id} );
checkDelete404( 'davros', @$ret[0]->{id} );
checkGetList( 1, 'davros' );
checkDeleteList( 1, 'davros', @$ret[1]->{type} );
checkGetList( 0, 'davros' );
checkDeleteList( 0, 'davros' );
# tof
checkGetList( 1, 'tof', 'U2F' );
checkGetList( 0, 'tof', 'TOTP' );
checkGetList( 0, 'tof', 'UBK' );
$ret = checkGetList( 1, 'tof' );
checkGetOnIds( 'tof', $ret );
checkDelete( 'tof', @$ret[0]->{id} );
checkDelete404( 'tof', @$ret[0]->{id} );
checkGetList( 0, 'tof' );
checkDeleteList( 0, 'tof' );
done_testing();

View File

@ -0,0 +1,559 @@
# Test Providers API
use Test::More;
use strict;
use JSON;
use IO::String;
require 't/test-lib.pm';
our $_json = JSON->new->allow_nonref;
sub check200 {
my ( $test, $res ) = splice @_;
#diag Dumper($res);
ok( $res->[0] == 200, "$test: Result code is 200" );
count(1);
checkJson( $test, $res );
}
sub check404 {
my ( $test, $res ) = splice @_;
#diag Dumper($res);
ok( $res->[0] == 404, "$test: Result code is 404" );
count(1);
checkJson( $test, $res );
}
sub check405 {
my ( $test, $res ) = splice @_;
ok( $res->[0] == 405, "$test: Result code is 405" );
count(1);
checkJson( $test, $res );
}
sub checkJson {
my ( $test, $res ) = splice @_;
my $key;
#diag Dumper($res->[2]->[0]);
ok( $key = from_json( $res->[2]->[0] ), "$test: Response is JSON" );
count(1);
}
sub add {
my ( $test, $type, $obj ) = splice @_;
my $j = $_json->encode($obj);
my $res;
#diag Dumper($j);
ok(
$res = &client->_post(
"/api/v1/providers/$type", '',
IO::String->new($j), 'application/json',
length($j)
),
"$test: Request succeed"
);
count(1);
return $res;
}
sub checkAdd {
my ( $test, $type, $add ) = splice @_;
check200( $test, add( $test, $type, $add ) );
}
sub checkAddFailsIfExists {
my ( $test, $type, $add ) = splice @_;
check405( $test, add( $test, $type, $add ) );
}
sub checkAddWithUnknownAttributes {
my ( $test, $type, $add ) = splice @_;
check405( $test, add( $test, $type, $add ) );
}
sub get {
my ( $test, $type, $confKey ) = splice @_;
my $res;
ok( $res = &client->_get( "/api/v1/providers/$type/$confKey", '' ),
"$test: Request succeed" );
count(1);
return $res;
}
sub checkGet {
my ( $test, $type, $confKey, $attrPath, $expectedValue ) = splice @_;
my $res = get( $test, $type, $confKey );
check200( $test, $res );
my @path = split '/', $attrPath;
my $key = from_json( $res->[2]->[0] );
for (@path) {
$key = $key->{$_};
}
ok(
$key eq $expectedValue,
"$test: check if $attrPath value \"$key\" matches expected value \"$expectedValue\""
);
count(1);
}
sub checkGetNotFound {
my ( $test, $type, $confKey ) = splice @_;
check404( $test, get( $test, $type, $confKey ) );
}
sub update {
my ( $test, $type, $confKey, $obj ) = splice @_;
my $j = $_json->encode($obj);
#diag Dumper($j);
my $res;
ok(
$res = &client->_patch(
"/api/v1/providers/$type/$confKey", '',
IO::String->new($j), 'application/json',
length($j)
),
"$test: Request succeed"
);
count(1);
return $res;
}
sub checkUpdate {
my ( $test, $type, $confKey, $update ) = splice @_;
check200( $test, update( $test, $type, $confKey, $update ) );
}
sub checkUpdateNotFound {
my ( $test, $type, $confKey, $update ) = splice @_;
check404( $test, update( $test, $type, $confKey, $update ) );
}
sub checkUpdateFailsIfExists {
my ( $test, $type, $confKey, $update ) = splice @_;
check405( $test, update( $test, $type, $confKey, $update ) );
}
sub checkUpdateWithUnknownAttributes {
my ( $test, $type, $confKey, $update ) = splice @_;
check405( $test, update( $test, $type, $confKey, $update ) );
}
sub replace {
my ( $test, $type, $confKey, $obj ) = splice @_;
my $j = $_json->encode($obj);
my $res;
ok(
$res = &client->_put(
"/api/v1/providers/$type/$confKey", '',
IO::String->new($j), 'application/json',
length($j)
),
"$test: Request succeed"
);
count(1);
return $res;
}
sub checkReplace {
my ( $test, $type, $confKey, $replace ) = splice @_;
check200( $test, replace( $test, $type, $confKey, $replace ) );
}
sub checkReplaceAlreadyThere {
my ( $test, $type, $confKey, $replace ) = splice @_;
check405( $test, replace( $test, $type, $confKey, $replace ) );
}
sub checkReplaceNotFound {
my ( $test, $type, $confKey, $update ) = splice @_;
check404( $test, replace( $test, $type, $confKey, $update ) );
}
sub checkReplaceWithUnknownAttribute {
my ( $test, $type, $confKey, $replace ) = splice @_;
check405( $test, replace( $test, $type, $confKey, $replace ) );
}
sub findByConfKey {
my ( $test, $type, $confKey ) = splice @_;
my $res;
ok(
$res = &client->_get(
"/api/v1/providers/$type/findByConfKey",
"pattern=$confKey"
),
"$test: Request succeed"
);
count(1);
return $res;
}
sub checkFindByConfKey {
my ( $test, $type, $confKey, $expectedHits ) = splice @_;
my $res = findByConfKey( $test, $type, $confKey );
check200( $test, $res );
my $hits = from_json( $res->[2]->[0] );
my $hit;
my $counter = 0;
foreach $hit ( @{$hits} ) {
$counter++;
ok(
$hit->{confKey} =~ $confKey,
"$test: check if confKey value \"$hit->{confKey}\" matches pattern \"$confKey\""
);
count(1);
}
ok(
$counter eq $expectedHits,
"$test: check if nb of hits returned ($counter) matches expectation ($expectedHits)"
);
count(1);
}
sub findByProviderId {
my ( $test, $type, $providerIdName, $providerId ) = splice @_;
my $res;
ok(
$res = &client->_get(
"/api/v1/providers/$type/findBy" . ucfirst $providerIdName,
"$providerIdName=$providerId"
),
"$test: Request succeed"
);
count(1);
return $res;
}
sub checkFindByProviderId {
my ( $test, $type, $providerIdName, $providerId ) = splice @_;
my $res = findByProviderId( $test, $type, $providerIdName, $providerId );
check200( $test, $res );
my $result = from_json( $res->[2]->[0] );
my $gotProviderId;
if ( $providerIdName eq 'entityId' ) {
($gotProviderId) = $result->{metadata} =~ m/entityID=['"](.+?)['"]/i;
}
else {
$gotProviderId = $result->{$providerIdName};
}
ok(
$gotProviderId eq $providerId,
"$test: Check $providerIdName value returned \"$gotProviderId\" matched expected value \"$providerId\""
);
count(1);
}
sub checkFindByProviderIdNotFound {
my ( $test, $type, $providerIdName, $providerId ) = splice @_;
check404( $test,
findByProviderId( $test, $type, $providerIdName, $providerId ) );
}
sub deleteProvider {
my ( $test, $type, $confKey ) = splice @_;
my $res;
ok(
$res = &client->_del(
"/api/v1/providers/$type/$confKey",
'', '', 'application/json', 0
),
"$test: Request succeed"
);
count(1);
return $res;
}
sub checkDelete {
my ( $test, $type, $confKey ) = splice @_;
check200( $test, deleteProvider( $test, $type, $confKey ) );
}
sub checkDeleteNotFound {
my ( $test, $type, $confKey ) = splice @_;
check404( $test, deleteProvider( $test, $type, $confKey ) );
}
my $test;
my $oidcRp = {
confKey => 'myOidcRp1',
clientId => 'myOidcClient1',
exportedVars => {
'sub' => "uid",
family_name => "sn",
given_name => "givenName"
},
extraClaim => {
phone => 'telephoneNumber',
email => 'mail'
},
options => {
oidcRPMetaDataOptionsClientSecret => 'secret',
oidcRPMetaDataOptionsIcon => 'web.png'
}
};
$test = "OidcRp - Add should succeed";
checkAdd( $test, 'oidc/rp', $oidcRp );
checkGet( $test, 'oidc/rp', 'myOidcRp1', 'options/oidcRPMetaDataOptionsIcon',
'web.png' );
checkGet( $test, 'oidc/rp', 'myOidcRp1',
'options/oidcRPMetaDataOptionsClientSecret', 'secret' );
$test = "OidcRp - Check attribute default value was set after add";
checkGet( $test, 'oidc/rp', 'myOidcRp1',
'options/oidcRPMetaDataOptionsIDTokenSignAlg', 'HS512' );
$test = "OidcRp - Add Should fail on duplicate confKey";
checkAddFailsIfExists( $test, 'oidc/rp', $oidcRp );
$test = "OidcRp - Update should succeed and keep existing values";
$oidcRp->{options}->{oidcRPMetaDataOptionsClientSecret} = 'secret2';
$oidcRp->{options}->{oidcRPMetaDataOptionsIDTokenSignAlg} = 'RS512';
delete $oidcRp->{options}->{oidcRPMetaDataOptionsIcon};
delete $oidcRp->{extraClaim};
delete $oidcRp->{exportedVars};
$oidcRp->{exportedVars}->{cn} = 'cn';
checkUpdate( $test, 'oidc/rp', 'myOidcRp1', $oidcRp );
checkGet( $test, 'oidc/rp', 'myOidcRp1',
'options/oidcRPMetaDataOptionsClientSecret', 'secret2' );
checkGet( $test, 'oidc/rp', 'myOidcRp1',
'options/oidcRPMetaDataOptionsIDTokenSignAlg', 'RS512' );
checkGet( $test, 'oidc/rp', 'myOidcRp1', 'options/oidcRPMetaDataOptionsIcon',
'web.png' );
checkGet( $test, 'oidc/rp', 'myOidcRp1', 'exportedVars/cn', 'cn' );
checkGet( $test, 'oidc/rp', 'myOidcRp1', 'exportedVars/family_name', 'sn' );
checkGet( $test, 'oidc/rp', 'myOidcRp1', 'extraClaim/phone',
'telephoneNumber' );
$test = "OidcRp - Update should fail on non existing options";
$oidcRp->{options}->{playingPossum} = 'elephant';
checkUpdateWithUnknownAttributes( $test, 'oidc/rp', 'myOidcRp1', $oidcRp );
delete $oidcRp->{options}->{playingPossum};
$test = "OidcRp - Add Should fail on duplicate clientId";
$oidcRp->{confKey} = 'myOidcRp2';
checkAddFailsIfExists( $test, 'oidc/rp', $oidcRp );
$test = "OidcRp - Add Should fail on non existing options";
$oidcRp->{confKey} = 'myOidcRp2';
$oidcRp->{clientId} = 'myOidcClient2';
$oidcRp->{options}->{playingPossum} = 'ElephantInTheRoom';
checkAddWithUnknownAttributes( $test, 'oidc/rp', $oidcRp );
delete $oidcRp->{options}->{playingPossum};
$test = "OidcRp - 2nd add should succeed";
checkAdd( $test, 'oidc/rp', $oidcRp );
$test = "OidcRp - Update should fail if client id exists";
$oidcRp->{clientId} = 'myOidcClient1';
checkUpdateFailsIfExists( $test, 'oidc/rp', 'myOidcRp2', $oidcRp );
$test = "OidcRp - Update should fail if confKey not found";
$oidcRp->{confKey} = 'myOidcRp3';
checkUpdateNotFound( $test, 'oidc/rp', 'myOidcRp3', $oidcRp );
$test = "OidcRp - Replace should succeed";
$oidcRp->{confKey} = 'myOidcRp2';
$oidcRp->{clientId} = 'myOidcClient2';
delete $oidcRp->{options}->{oidcRPMetaDataOptionsIcon};
delete $oidcRp->{options}->{oidcRPMetaDataOptionsIDTokenSignAlg};
checkReplace( $test, 'oidc/rp', 'myOidcRp2', $oidcRp );
$test = "OidcRp - Check attribute default value was set after replace";
checkGet( $test, 'oidc/rp', 'myOidcRp2',
'options/oidcRPMetaDataOptionsIDTokenSignAlg', 'HS512' );
$test = "OidcRp - Replace should fail on non existing options";
$oidcRp->{options}->{playingPossum} = 'elephant';
checkReplaceWithUnknownAttribute( $test, 'oidc/rp', 'myOidcRp2', $oidcRp );
delete $oidcRp->{options}->{playingPossum};
$test = "OidcRp - Replace should fail if confKey not found";
$oidcRp->{confKey} = 'myOidcRp3';
checkReplaceNotFound( $test, 'oidc/rp', 'myOidcRp3', $oidcRp );
$test = "OidcRp - FindByConfKey should find 2 hits";
checkFindByConfKey( $test, 'oidc/rp', '^myOidcRp.$', 2 );
$test = "OidcRp - FindByConfKey should find 1 hit";
checkFindByConfKey( $test, 'oidc/rp', 'myOidcRp1', 1 );
$test = "OidcRp - FindByConfKey should find 0 hits";
checkFindByConfKey( $test, 'oidc/rp', 'myOidcRp3', 0 );
$test = "OidcRp - FindByClientId should find one entry";
checkFindByProviderId( $test, 'oidc/rp', 'clientId', 'myOidcClient1' );
$test = "OidcRp - FindByClientId should find nothing";
checkFindByProviderIdNotFound( $test, 'oidc/rp', 'clientId', 'myOidcClient3' );
$test = "OidcRp - Clean up";
checkDelete( $test, 'oidc/rp', 'myOidcRp1' );
checkDelete( $test, 'oidc/rp', 'myOidcRp2' );
$test = "OidcRp - Entity should not be found after clean up";
checkDeleteNotFound( $test, 'oidc/rp', 'myOidcRp1' );
my $metadata1 =
"<?xml version=\"1.0\"?><md:EntityDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\" validUntil=\"2019-09-25T16:44:38Z\" cacheDuration=\"PT604800S\" entityID=\"https://myapp.domain.com/saml/metadata\"><md:SPSSODescriptor AuthnRequestsSigned=\"false\" WantAssertionsSigned=\"false\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\"><md:SingleLogoutService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\" Location=\"https://myapp.domain.com/saml/sls\" /><md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat><md:AssertionConsumerService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Location=\"https://myapp.domain.com/saml/acs\" index=\"1\" /></md:SPSSODescriptor></md:EntityDescriptor>";
my $metadata2 =
"<?xml version=\"1.0\"?><md:EntityDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\" validUntil=\"2019-09-25T16:44:38Z\" cacheDuration=\"PT604800S\" entityID=\"https://myapp2.domain.com/saml/metadata\"><md:SPSSODescriptor AuthnRequestsSigned=\"false\" WantAssertionsSigned=\"false\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\"><md:SingleLogoutService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\" Location=\"https://myapp2.domain.com/saml/sls\" /><md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat><md:AssertionConsumerService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Location=\"https://myapp2.domain.com/saml/acs\" index=\"1\" /></md:SPSSODescriptor></md:EntityDescriptor>";
my $samlSp = {
confKey => 'mySamlSp1',
metadata => $metadata1,
exportedAttributes => {
family_name => {
format => "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
friendlyName => "surname",
mandatory => "false",
name => "sn"
},
cn => {
friendlyName => "commonname",
mandatory => "true",
name => "uid"
},
uid => {
mandatory => "true",
name => "uid"
},
phone => {
mandatory => "false",
format => "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified",
name => "telephoneNumber"
},
function => {
name => "title",
mandatory => "false",
format => "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
},
given_name => {
mandatory => "false",
name => "givenName"
}
},
options => {
samlSPMetaDataOptionsCheckSLOMessageSignature => 0,
samlSPMetaDataOptionsEncryptionMode => "assertion",
samlSPMetaDataOptionsSessionNotOnOrAfterTimeout => 36000
}
};
$test = "SamlSp - Add should succeed";
checkAdd( $test, 'saml/sp', $samlSp );
checkGet( $test, 'saml/sp', 'mySamlSp1',
'options/samlSPMetaDataOptionsEncryptionMode', 'assertion' );
checkGet( $test, 'saml/sp', 'mySamlSp1',
'options/samlSPMetaDataOptionsSessionNotOnOrAfterTimeout', 36000 );
$test = "SamlSp - Check attribute default value was set after add";
checkGet( $test, 'saml/sp', 'mySamlSp1',
'options/samlSPMetaDataOptionsNotOnOrAfterTimeout', 72000 );
$test = "SamlSp - Add Should fail on duplicate confKey";
checkAddFailsIfExists( $test, 'saml/sp', $samlSp );
$test = "SamlSp - Update should succeed and keep existing values";
$samlSp->{options}->{samlSPMetaDataOptionsCheckSLOMessageSignature} = 1;
$samlSp->{options}->{samlSPMetaDataOptionsEncryptionMode} = 'nameid';
delete $samlSp->{options}->{samlSPMetaDataOptionsSessionNotOnOrAfterTimeout};
delete $samlSp->{exportedAttributes};
$samlSp->{exportedAttributes}->{cn}->{name} = "cn",
$samlSp->{exportedAttributes}->{cn}->{friendlyName} = "common_name",
$samlSp->{exportedAttributes}->{cn}->{mandatory} = "false",
checkUpdate( $test, 'saml/sp', 'mySamlSp1', $samlSp );
checkGet( $test, 'saml/sp', 'mySamlSp1',
'options/samlSPMetaDataOptionsCheckSLOMessageSignature', 1 );
checkGet( $test, 'saml/sp', 'mySamlSp1',
'options/samlSPMetaDataOptionsSessionNotOnOrAfterTimeout', 36000 );
checkGet( $test, 'saml/sp', 'mySamlSp1', 'exportedAttributes/cn/friendlyName',
'common_name' );
checkGet( $test, 'saml/sp', 'mySamlSp1', 'exportedAttributes/cn/mandatory',
'false' );
checkGet( $test, 'saml/sp', 'mySamlSp1', 'exportedAttributes/cn/mandatory',
'false' );
checkGet( $test, 'saml/sp', 'mySamlSp1', 'exportedAttributes/cn/name', 'uid' );
checkGet( $test, 'saml/sp', 'mySamlSp1', 'exportedAttributes/given_name/name',
'givenName' );
$test = "SamlSp - Update should fail on non existing options";
$samlSp->{options}->{playingPossum} = 'elephant';
checkUpdateWithUnknownAttributes( $test, 'saml/sp', 'mySamlSp1', $samlSp );
delete $samlSp->{options}->{playingPossum};
$test = "SamlSp - Add Should fail on duplicate entityId";
$samlSp->{confKey} = 'mySamlSp2';
checkAddFailsIfExists( $test, 'saml/sp', $samlSp );
$test = "SamlSp - Add Should fail on non existing options";
$samlSp->{confKey} = 'mySamlSp2';
$samlSp->{metadata} = $metadata2;
$samlSp->{options}->{playingPossum} = 'ElephantInTheRoom';
checkAddWithUnknownAttributes( $test, 'saml/sp', $samlSp );
delete $samlSp->{options}->{playingPossum};
$test = "SamlSp - 2nd add should succeed";
checkAdd( $test, 'saml/sp', $samlSp );
$test = "SamlSp - Update should fail if client id exists";
$samlSp->{metadata} = $metadata1;
checkUpdateFailsIfExists( $test, 'saml/sp', 'mySamlSp2', $samlSp );
$test = "SamlSp - Update should fail if confKey not found";
$samlSp->{confKey} = 'mySamlSp3';
checkUpdateNotFound( $test, 'saml/sp', 'mySamlSp3', $samlSp );
$test = "SamlSp - Replace should succeed";
$samlSp->{confKey} = 'mySamlSp2';
$samlSp->{metadata} = $metadata2;
delete $samlSp->{options}->{samlSPMetaDataOptionsEncryptionMode};
checkReplace( $test, 'saml/sp', 'mySamlSp2', $samlSp );
$test = "SamlSp - Check attribute default value was set after replace";
checkGet( $test, 'saml/sp', 'mySamlSp2',
'options/samlSPMetaDataOptionsEncryptionMode', 'none' );
$test = "SamlSp - Replace should fail on non existing options";
$samlSp->{options}->{playingPossum} = 'elephant';
checkReplaceWithUnknownAttribute( $test, 'saml/sp', 'mySamlSp2', $samlSp );
delete $samlSp->{options}->{playingPossum};
$test = "SamlSp - Replace should fail if confKey not found";
$samlSp->{confKey} = 'mySamlSp3';
checkReplaceNotFound( $test, 'saml/sp', 'mySamlSp3', $samlSp );
$test = "SamlSp - FindByConfKey should find 2 hits";
checkFindByConfKey( $test, 'saml/sp', '^mySamlSp.$', 2 );
$test = "SamlSp - FindByConfKey should find 1 hit";
checkFindByConfKey( $test, 'saml/sp', 'mySamlSp1', 1 );
$test = "SamlSp - FindByConfKey should find 0 hits";
checkFindByConfKey( $test, 'saml/sp', 'mySamlSp3', 0 );
$test = "SamlSp - FindByEntityId should find one entry";
checkFindByProviderId( $test, 'saml/sp', 'entityId',
'https://myapp.domain.com/saml/metadata' );
$test = "SamlSp - FindByEntityId should find nothing";
checkFindByProviderIdNotFound( $test, 'saml/sp', 'entityId',
'https://myapp3.domain.com/saml/metadata' );
$test = "SamlSp - Clean up";
checkDelete( $test, 'saml/sp', 'mySamlSp1' );
checkDelete( $test, 'saml/sp', 'mySamlSp2' );
$test = "SamlSp - Entity should not be found after clean up";
checkDeleteNotFound( $test, 'saml/sp', 'mySamlSp1' );
# Clean up generated conf files, except for "lmConf-1.json"
unlink grep { $_ ne "t/conf/lmConf-1.json" } glob "t/conf/lmConf-*.json";
done_testing();

View File

@ -1,10 +1,9 @@
my $tests;
BEGIN { $tests = 5 }
use Test::More tests => $tests;
use Test::More;
use JSON;
use strict;
require 't/test-lib.pm';
my $tests = 9;
use_ok('Lemonldap::NG::Common::Cli');
use_ok('Lemonldap::NG::Manager::Cli');
@ -15,20 +14,60 @@ SKIP: {
if ($@) {
skip 'Test::Output is missing, skipping', $tests - 2;
}
my @cmd;
@cmd = ('save');
my $client =
Lemonldap::NG::Manager::Cli->new( iniFile => 't/lemonldap-ng.ini' );
my $res = Capture::Tiny::capture_stdout( sub { $client->run(@cmd) } );
my @cmd;
my $res;
# Test 'set' command
@cmd = qw(-yes 1 set notification 1);
$res = Capture::Tiny::capture_stdout( sub { $client->run(@cmd) } );
# Test 'get' command
@cmd = qw(get notification);
$res = Capture::Tiny::capture_stdout( sub { $client->run(@cmd) } );
ok( $res =~ /^notification\s+=\s+1$/, '"get notification" OK' )
or diag " $res";
# Test 'addKey' command
@cmd = qw(-yes 1 addKey locationRules/test1.example.com ^/reject deny);
Test::Output::combined_like(
sub { $client->run(@cmd) },
qr#'\^/reject' => 'deny'#s,
'"addKey" OK'
);
# Test 'delKey' command
@cmd = qw(-yes 1 delKey locationRules/test1.example.com ^/reject);
Test::Output::combined_unlike(
sub { $client->run(@cmd) },
qr#'\^/reject' => 'deny'#s,
'"delKey" OK'
);
# Test 'get' command
@cmd = qw(get locationRules/test1.example.com);
$res = Capture::Tiny::capture_stdout( sub { $client->run(@cmd) } );
ok( $res =~ m#(?:/logout|default)#, '"get key/subkey" OK' )
or diag "$res";
# Test 'save' command
@cmd = ('save');
$res = Capture::Tiny::capture_stdout( sub { $client->run(@cmd) } );
ok( $res =~ /^\s*(\{.*\})\s*$/s, '"save" result looks like JSON' );
eval { JSON::from_json($res) };
ok( not($@), ' result is JSON' ) or diag "error: $@";
# Test 'restore' command
close STDIN;
open STDIN, '<', \$res;
@cmd = ( 'restore', '-' );
Test::Output::combined_like( sub { $client->run(@cmd) },
qr/"cfgNum"\s*:\s*"2"/s, 'New config: 2' );
qr/"cfgNum"\s*:\s*"3"/s, 'New config: 3' );
}
count($tests);
done_testing( count() );
&cleanConfFiles;
sub cleanConfFiles {

View File

@ -29,7 +29,7 @@ protection = manager
staticPrefix = app/
languages = fr, en, vi, ar
templateDir = site/templates/
enabledModules = conf, sessions, notifications, 2ndFA, viewer
enabledModules = conf, sessions, notifications, 2ndFA, viewer, api
viewerHiddenKeys = samlIDPMetaDataNodes samlSPMetaDataNodes portalDisplayLogout captcha_login_enabled
viewerAllowBrowser = $env->{REMOTE_ADDR} eq '127.0.0.1'
viewerAllowDiff = 1

View File

@ -47,6 +47,8 @@ lib/Lemonldap/NG/Portal/Auth/SSL.pm
lib/Lemonldap/NG/Portal/Auth/Twitter.pm
lib/Lemonldap/NG/Portal/Auth/WebID.pm
lib/Lemonldap/NG/Portal/CDC.pm
lib/Lemonldap/NG/Portal/CertificateResetByMail/Custom.pm
lib/Lemonldap/NG/Portal/CertificateResetByMail/Demo.pm
lib/Lemonldap/NG/Portal/CertificateResetByMail/LDAP.pm
lib/Lemonldap/NG/Portal/Issuer/CAS.pm
lib/Lemonldap/NG/Portal/Issuer/Get.pm
@ -241,6 +243,7 @@ site/htdocs/static/common/apps/docs.png
site/htdocs/static/common/apps/folder.png
site/htdocs/static/common/apps/gear.png
site/htdocs/static/common/apps/help.png
site/htdocs/static/common/apps/llng.png
site/htdocs/static/common/apps/mailappt.png
site/htdocs/static/common/apps/money.png
site/htdocs/static/common/apps/network.png
@ -451,6 +454,7 @@ site/templates/common/mail/tr.json
site/templates/common/mail/vi.json
site/templates/common/mail/zh_CN.json
site/templates/common/mail_2fcode.tpl
site/templates/common/mail_certificateConfirm.tpl
site/templates/common/mail_certificateReset.tpl
site/templates/common/mail_confirm.tpl
site/templates/common/mail_footer.tpl
@ -592,6 +596,7 @@ t/43-MailPasswordReset-LDAP.t
t/43-MailPasswordReset-with-captcha.t
t/43-MailPasswordReset-with-token.t
t/43-MailPasswordReset.t
t/44-CertificateResetByMail-Demo.t
t/44-CertificateResetByMail-LDAP.t
t/50-IssuerGet.t
t/57-GlobalLogout-without-Timer.t

View File

@ -101,7 +101,7 @@ sub extractFormInfo {
# Security: check for captcha or token
if ( $self->captcha or $self->ottRule->( $req, {} ) ) {
my $token;
unless ( $token = $req->param('token') ) {
unless ( $token = $req->param('token') or $self->captcha ) {
$self->userLogger->error('Authentication tried without token');
$self->ott->setToken($req);
return PE_NOTOKEN;

View File

@ -0,0 +1,22 @@
package Lemonldap::NG::Portal::CertificateResetByMail::Custom;
use strict;
use Mouse;
extends 'Lemonldap::NG::Portal::Main::Plugin';
sub new {
my ( $class, $self ) = @_;
unless ( $self->{conf}->{customRegister} ) {
die 'Custom register module not defined';
}
my $res = $self->{p}->loadModule( $self->{conf}->{customResetCertByMail} );
unless ($res) {
die 'Unable to load register module ' . $self->{conf}->{customResetCertByMail};
}
return $res;
}
1;

View File

@ -0,0 +1,32 @@
package Lemonldap::NG::Portal::CertificateResetByMail::Demo;
use strict;
use Mouse;
use Lemonldap::NG::Portal::Main::Constants qw(PE_OK);
our $VERSION = '2.0.8';
sub init {
1;
}
## @method int modifCertificate
# Do nothing
# @result Lemonldap::NG::Portal constant
sub modifCertificate {
my ( $self, $req, $newCertif, $userCertif ) = @_;
my $uid =
$req->user || $req->userData->{_user} || $req->sessionInfo->{_user};
$Lemonldap::NG::Portal::UserDB::Demo::demoAccounts{$uid} = {
uid => $uid,
cn => $uid . ' ' . uc $uid,
mail => $uid . '@badwolf.org',
newCert => $newCertif,
userCert => $userCertif,
};
return PE_OK;
}
1;

View File

@ -15,7 +15,7 @@ our $VERSION = '2.1.0';
# PRIVATE METHOD
sub modifCertificate {
my ( $self, $newcertif, $usercertif, $req ) = @_;
my ( $self, $req, $newCertif, $userCertif ) = @_;
my $ceaAttribute = $self->conf->{certificateResetByMailCeaAttribute}
|| "description";
my $certificateAttribute =
@ -42,8 +42,8 @@ sub modifCertificate {
my $result = $self->ldap->modify(
$dn,
replace => [
$ceaAttribute => $newcertif,
"$certificateAttribute" => [$usercertif]
$ceaAttribute => $newCertif,
"$certificateAttribute" => [$userCertif]
]
);
@ -55,10 +55,9 @@ sub modifCertificate {
return PE_LDAPERROR;
}
$self->logger->debug("$ceaAttribute set to $newcertif");
$self->logger->debug("$ceaAttribute set to $newCertif");
return PE_OK;
}
1;

View File

@ -64,7 +64,7 @@ sub handler {
and $req->{env}->{HTTP_COOKIE}
and $req->{env}->{HTTP_COOKIE} =~ /$url64/ )
{
$self->logger->debug("Force cleaning pdata");
$self->logger->info("Force cleaning pdata");
$self->logger->warn("pdata cookie domain must be set")
unless ( $self->conf->{pdataDomain} );
$req->pdata( {} );

View File

@ -392,7 +392,8 @@ sub _certificateReset {
# Send mail
unless (
$self->send_mail(
$req->data->{mailAddress}, $subject, $body, $html
$req->data->{mailAddress},
$subject, $body, $html
)
)
{
@ -523,8 +524,8 @@ sub modifyCertificate {
# Modify ldap certificate attribute
$req->user( $req->{sessionInfo}->{_user} );
my $result =
$self->registerModule->modifCertificate( $certificatExactAssertion,
$cert, $req );
$self->registerModule->modifCertificate( $req, $certificatExactAssertion,
$cert );
$self->{user} = undef;
# Mail token can be used only one time, delete the session if all is ok

View File

@ -32,4 +32,5 @@ sub applyLoginRule {
# For now, get first letter of firstname and lastname
return substr( $firstname, 0, 1 ) . $lastname;
}
1;

View File

@ -15,6 +15,7 @@ sub new {
unless ($res) {
die 'Unable to load register module ' . $self->{conf}->{customRegister};
}
return $res;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1001 B

View File

@ -125,6 +125,8 @@
"choose2f":"Choose your second factor",
"chooseApp":"اختر أحد التطبيقات المسموح لك بالدخول إليها",
"cipheredValue":"Ciphered value",
"click2Reset":"Click here to reset your password",
"click2ResetCertificate":"Click here to reset your certificate",
"clickHere":"الرجاء الضغط هنا",
"clickOnYubikey":"Click on your Yubikey",
"closeSSO":"أغلق جلسة الدخول الموحد (سسو)",
@ -218,8 +220,9 @@
"passwordPolicyMinDigit":"Minimal digit characters:",
"ppGrace":"المصادقات المتبقية، غير كلمة المرور الخاصة بك!",
"proxyError":"بوابة سيئة: غير قادر على الانضمام لالخادم البعيد",
"pwdChange":"تغيير كلمة المرور",
"pwd":"كلمة المرور",
"pwdChange":"تغيير كلمة المرور",
"pwdChanged":"Your password has been successfully changed!",
"pwdResetAlreadyIssued":"تم إصدار طلب إعادة تعيين كلمة المرور من قبل",
"pwdWillExpire":"٪ s من الأيام و٪ s من الساعات و٪ s من الدقائق و٪ s من الثواني قبل انتهاء صلاحية كلمة المرور، قم بتغييرها!",
"radius2f":"Radius",

View File

@ -124,6 +124,8 @@
"choose2f":"Wählen deinen Ihren zweiten Faktor",
"chooseApp":"Wählen Sie eine Anwendung aus, auf die du zugreifen darfst",
"cipheredValue":"Ciphered value",
"click2Reset":"Click here to reset your password",
"click2ResetCertificate":"Click here to reset your certificate",
"clickHere":"Bitte hier klicken",
"clickOnYubikey":"Klicke auf deinen Yubikey",
"closeSSO":"Schließe deine SSO-Sitzung",
@ -217,8 +219,9 @@
"passwordPolicyMinDigit":"Minimal digit characters:",
"ppGrace":"verbleibende Authentifizierungen, bitte Passwort ändern !",
"proxyError":"Bad gateway: Der Remote-Server kann nicht verbunden werden",
"pwdChange":"Passwortänderung",
"pwd":"Passwort",
"pwdChange":"Passwortänderung",
"pwdChanged":"Your password has been successfully changed!",
"pwdResetAlreadyIssued":"Eine Anfrage zum Zurücksetzen des Passworts wurde bereits gestellt",
"pwdWillExpire":"%s Tage, %s Stunden, %s Minuten und %s Sekunden bevor dein Passwort abläuft, bitte ändere es!",
"radius2f":"Radius",

View File

@ -125,6 +125,8 @@
"choose2f":"Choose your second factor",
"chooseApp":"Choose an application your are allowed to access to",
"cipheredValue":"Ciphered value",
"click2Reset":"Click here to reset your password",
"click2ResetCertificate":"Click here to reset your certificate",
"clickHere":"Please click here",
"clickOnYubikey":"Click on your Yubikey",
"closeSSO":"Close your SSO session",
@ -218,8 +220,9 @@
"passwordPolicyMinDigit": "Minimal digit characters:",
"ppGrace": "authentications remaining, change your password!",
"proxyError": "Bad gateway: unable to join remote server",
"pwdChange":"Password change",
"pwd":"Password",
"pwdChange":"Password change",
"pwdChanged":"Your password has been successfully changed!",
"pwdResetAlreadyIssued":"A password reset request was already issued on ",
"pwdWillExpire":"%s days, %s hours, %s minutes and %s seconds before password expiration, change it!",
"radius2f":"Radius",

View File

@ -124,6 +124,8 @@
"choose2f":"Seleccione su segundo factor",
"chooseApp":"Elija una aplicación a la cual se le está permitido acceder",
"cipheredValue":"Ciphered value",
"click2Reset":"Click here to reset your password",
"click2ResetCertificate":"Click here to reset your certificate",
"clickHere":"Por favor haga clic aquí",
"clickOnYubikey":"Haga clic en su Yubikey",
"closeSSO":"Cierre su sesión SSO",
@ -217,8 +219,9 @@
"passwordPolicyMinDigit":"Dígitos, como mínimo:",
"ppGrace":"autenticaciones restantes, ¡cambie su contraseña!.",
"proxyError":"Puerta de enlace no válida: servidor remoto inalcanzable",
"pwdChange":"Cambio de contraseña",
"pwd":"Contraseña",
"pwdChange":"Cambio de contraseña",
"pwdChanged":"Your password has been successfully changed!",
"pwdResetAlreadyIssued":"Ya fue expedida una solicitud de reinicio de contraseña",
"pwdWillExpire":"Faltan %s días, %s horas, %s minutos y %s segundos para que su contraseña caduque.",
"radius2f":"Radius",

View File

@ -124,6 +124,8 @@
"choose2f":"Choose your second factor",
"chooseApp":"Choose an application your are allowed to access to",
"cipheredValue":"Ciphered value",
"click2Reset":"Click here to reset your password",
"click2ResetCertificate":"Click here to reset your certificate",
"clickHere":"Please click here",
"clickOnYubikey":"Click on your Yubikey",
"closeSSO":"Sulje SSO istuntosi",
@ -217,8 +219,9 @@
"passwordPolicyMinDigit":"Minimal digit characters:",
"ppGrace":"authentications remaining, change your password!",
"proxyError":"Bad gateway: unable to join remote server",
"pwdChange":"Password change",
"pwd":"Salasana",
"pwdChange":"Password change",
"pwdChanged":"Your password has been successfully changed!",
"pwdResetAlreadyIssued":"A password reset request was already issued on ",
"pwdWillExpire":"%d päivää, %d tuntia, %d minuuttia ja %sekunttia jäljellä salasanan vanhentumiseen, vaihda salasana!",
"radius2f":"Radius",

View File

@ -124,6 +124,8 @@
"choose2f":"Choisissez votre second facteur",
"chooseApp":"Choisissez une application à laquelle vous êtes autorisé à accéder",
"cipheredValue":"Valeur cryptée",
"click2Reset":"Cliquez içi pour réinitialiser votre mot de passe",
"click2ResetCertificate":"Cliquez içi pour réinitialiser votre certificat",
"clickHere":"Cliquez ici",
"clickOnYubikey":"Cliquez sur votre Yubikey",
"closeSSO":"Fermer votre Session SSO",
@ -217,8 +219,9 @@
"passwordPolicyMinDigit": "Minimum de chiffres :",
"ppGrace": "authentifications restantes, changez votre mot de passe !",
"proxyError": "Mauvaise passerelle : impossible de joindre le serveur amont",
"pwdChange":"Changement de mot de passe",
"pwd":"Mot de passe",
"pwdChange":"Changement de mot de passe",
"pwdChanged":"Your password has been successfully changed!",
"pwdResetAlreadyIssued":"Une demande de réinitialisation de mot de passe a déjà été faite le ",
"pwdWillExpire":"%s jours, %s heures, %s minutes et %s secondes avant expiration de votre mot de passe, pensez à le changer !",
"radius2f":"Radius",

View File

@ -124,6 +124,8 @@
"choose2f":"Scegli il tuo secondo fattore",
"chooseApp":"Scegli un'applicazione alla quale ti è consentito l'accesso",
"cipheredValue":"Ciphered value",
"click2Reset":"Click here to reset your password",
"click2ResetCertificate":"Click here to reset your certificate",
"clickHere":"Per favore clicka qui",
"clickOnYubikey":"Clicca sulla tua Yubikey",
"closeSSO":"Chiudi la sessione SSO",
@ -217,8 +219,9 @@
"passwordPolicyMinDigit":"Minimal digit characters:",
"ppGrace":"autenticazioni restanti, modifica la tua password!",
"proxyError":"Gateway errata: impossibile associarsi a un server remoto",
"pwdChange":"Cambio password",
"pwd":"Password",
"pwdChange":"Cambio password",
"pwdChanged":"Your password has been successfully changed!",
"pwdResetAlreadyIssued":"Una richiesta di ripristino della password é già stata rilasciata",
"pwdWillExpire":"%s giorni, %s ore, %s minuti e %s secondi prima della scadenza della password, cambiala!",
"radius2f":"Radius",

View File

@ -124,6 +124,8 @@
"choose2f":"Choose your second factor",
"chooseApp":"Choose an application your are allowed to access to",
"cipheredValue":"Ciphered value",
"click2Reset":"Click here to reset your password",
"click2ResetCertificate":"Click here to reset your certificate",
"clickHere":"Please click here",
"clickOnYubikey":"Click on your Yubikey",
"closeSSO":"Close your SSO session",
@ -217,8 +219,9 @@
"passwordPolicyMinDigit":"Minimal digit characters:",
"ppGrace":"authentications remaining, change your password!",
"proxyError":"Bad gateway: unable to join remote server",
"pwdChange":"Password change",
"pwd":"Password",
"pwdChange":"Password change",
"pwdChanged":"Your password has been successfully changed!",
"pwdResetAlreadyIssued":"A password reset request was already issued on ",
"pwdWillExpire":"%s days, %s hours, %s minutes and %s seconds before password expiration, change it!",
"radius2f":"Radius",

View File

@ -124,6 +124,8 @@
"choose2f":"Choose your second factor",
"chooseApp":"Choose an application your are allowed to access to",
"cipheredValue":"Ciphered value",
"click2Reset":"Click here to reset your password",
"click2ResetCertificate":"Click here to reset your certificate",
"clickHere":"Please click here",
"clickOnYubikey":"Click on your Yubikey",
"closeSSO":"Close your SSO session",
@ -217,8 +219,9 @@
"passwordPolicyMinDigit":"Minimal digit characters:",
"ppGrace":"authentications remaining, change your password!",
"proxyError":"Bad gateway: unable to join remote server",
"pwdChange":"Password change",
"pwd":"Password",
"pwdChange":"Password change",
"pwdChanged":"Your password has been successfully changed!",
"pwdResetAlreadyIssued":"A password reset request was already issued on ",
"pwdWillExpire":"%s days, %s hours, %s minutes and %s seconds before password expiration, change it!",
"radius2f":"Radius",

View File

@ -124,6 +124,8 @@
"choose2f":"Choose your second factor",
"chooseApp":"Choose an application your are allowed to access to",
"cipheredValue":"Ciphered value",
"click2Reset":"Click here to reset your password",
"click2ResetCertificate":"Click here to reset your certificate",
"clickHere":"Please click here",
"clickOnYubikey":"Click on your Yubikey",
"closeSSO":"Close your SSO session",
@ -217,8 +219,9 @@
"passwordPolicyMinDigit":"Minimal digit characters:",
"ppGrace":"authentications remaining, change your password!",
"proxyError":"Bad gateway: unable to join remote server",
"pwdChange":"Password change",
"pwd":"Password",
"pwdChange":"Password change",
"pwdChanged":"Your password has been successfully changed!",
"pwdResetAlreadyIssued":"A password reset request was already issued on ",
"pwdWillExpire":"%s days, %s hours, %s minutes and %s seconds before password expiration, change it!",
"radius2f":"Radius",

View File

@ -125,6 +125,8 @@
"choose2f":"İkinci faktörünüzü seçin",
"chooseApp":"Erişim yetkiniz olan bir uygulama seçin",
"cipheredValue":"Şifrelenmiş değer",
"click2Reset":"Click here to reset your password",
"click2ResetCertificate":"Click here to reset your certificate",
"clickHere":"Lütfen buraya tıklayın",
"clickOnYubikey":"Yubikey'e tıklayın",
"closeSSO":"TOA oturumunuzu kapatın",
@ -218,8 +220,9 @@
"passwordPolicyMinDigit":"Minimum rakam karakter sayısı",
"ppGrace":"kimlik doğrulaması kaldı, parolanızı değiştirin!",
"proxyError":"Kötü ağ geçidi: uzak sunucuya katılamıyor",
"pwdChange":"Parola değişimi",
"pwd":"Parola",
"pwdChange":"Parola değişimi",
"pwdChanged":"Your password has been successfully changed!",
"pwdResetAlreadyIssued":"Parola sıfırlama istediği zaten şu tarihte alındı:",
"pwdWillExpire":"Parola süresinin dolmasına %s gün, %s saat, %s dakika ve %s saniye kaldı, parolayı değiştirin!",
"radius2f":"Radius",

View File

@ -124,6 +124,8 @@
"choose2f":"Choose your second factor",
"chooseApp":"Chọn một ứng dụng bạn được phép truy cập vào",
"cipheredValue":"Ciphered value",
"click2Reset":"Click here to reset your password",
"click2ResetCertificate":"Click here to reset your certificate",
"clickHere":"Vui lòng nhấp vào đây",
"clickOnYubikey":"Click on your Yubikey",
"closeSSO":"Đóng phiên SSO của bạn",
@ -217,8 +219,9 @@
"passwordPolicyMinDigit":"Minimal digit characters:",
"ppGrace":"chứng thực vẫn còn, thay đổi mật khẩu của bạn!",
"proxyError":"Gateway không chính xác: không thể kết nối máy chủ từ xa",
"pwdChange":"Thay đổi mật khẩu",
"pwd":"Mật khẩu",
"pwdChange":"Thay đổi mật khẩu",
"pwdChanged":"Your password has been successfully changed!",
"pwdResetAlreadyIssued":"Yêu cầu đặt lại mật khẩu đã được ban hành",
"pwdWillExpire":"%s ngày, %s giờ, %s phút và %s giây trước khi hết hạn mật khẩu, hãy thay đổi nó!",
"radius2f":"Radius",

View File

@ -124,6 +124,8 @@
"choose2f":"Choose your second factor",
"chooseApp":"Choose an application your are allowed to access to",
"cipheredValue":"Ciphered value",
"click2Reset":"Click here to reset your password",
"click2ResetCertificate":"Click here to reset your certificate",
"clickHere":"请点击这里",
"clickOnYubikey":"Click on your Yubikey",
"closeSSO":"Close your SSO session",
@ -217,8 +219,9 @@
"passwordPolicyMinDigit":"Minimal digit characters:",
"ppGrace":"authentications remaining, change your password!",
"proxyError":"错误的网关:无法连接远程服务器",
"pwdChange":"更改密码",
"pwd":"密码",
"pwdChange":"更改密码",
"pwdChanged":"Your password has been successfully changed!",
"pwdResetAlreadyIssued":"A password reset request was already issued on ",
"pwdWillExpire":"距离密码失效还有 %d 天, %d 小时, %d 分钟, %d 秒, 请修改!",
"radius2f":"Radius",

View File

@ -36,7 +36,6 @@
<span trspan="connect">Connect</span>
</button>
</div>
</TMPL_IF>
<div class="actions">
<TMPL_IF NAME="DISPLAY_RESETPASSWORD">
@ -60,3 +59,4 @@
</a>
</TMPL_IF>
</div>
</TMPL_IF>

View File

@ -0,0 +1,12 @@
<TMPL_INCLUDE NAME="mail_header.tpl">
<p>
<span trspan="hello">Hello</span> $cn,<br />
<br />
<span><img src="cid:arrow:../common/bullet_go.png" /></span>
<a href="$url" style="text-decoration:none;color:orange;">
<span trspan="click2ResetCertificate">Click here to reset your certificate</span>
</a>
</p>
<TMPL_INCLUDE NAME="mail_footer.tpl">

View File

@ -8,7 +8,7 @@
<span><img src="cid:key:../common/key.png" /></span>
<b>$password</b>
<TMPL_ELSE>
<span trspan="pwdChanged">Your password was changed.</span>
<span trspan="pwdChanged">Your password has been successfully changed!</span>
</TMPL_IF>
</p>

View File

@ -48,7 +48,7 @@ ok( defined $register_answer->{client_id},
"Client ID found in answer: " . $register_answer->{client_id} );
# New configuration registered
my $confFile = "t/lmConf-2.json";
my $confFile = "$main::tmpDir/lmConf-2.json";
my $conf = JSON::from_json(`cat $confFile`);
# Check saved data
@ -74,8 +74,7 @@ clean_sessions();
done_testing();
sub op {
return LLNG::Manager::Test->new(
{
return LLNG::Manager::Test->new( {
ini => {
logLevel => $debug,
domain => 'idp.com',

View File

@ -1,14 +1,16 @@
use Test::More;
use strict;
use IO::String;
use JSON;
use Lemonldap::NG::Portal::Main::Constants 'PE_CAPTCHAEMPTY';
require 't/test-lib.pm';
my $res;
my $maintests = 26;
my $maintests = 29;
SKIP: {
eval 'use GD::SecurityImage;use Image::Magick;';
eval 'use GD::SecurityImage; use Image::Magick;';
if ($@) {
skip 'Image::Magick not found', $maintests;
}
@ -25,6 +27,24 @@ SKIP: {
}
);
# Try to authenticate without captcha
# -----------------------------------
ok(
$res = $client->_post(
'/',
IO::String->new('user=dwho&password=dwho'),
length => 23,
),
'Auth query'
);
expectReject($res);
my $json;
ok( $json = eval { from_json( $res->[2]->[0] ) }, 'Response is JSON' )
or print STDERR "$@\n" . Dumper($res);
ok( $json->{error} == PE_CAPTCHAEMPTY, 'Response is PE_CAPTCHAEMPTY' )
or explain( $json, "error => 77" );
# Test normal first access
# ------------------------
ok( $res = $client->_get('/'), 'Unauth JSON request' );

View File

@ -1,6 +1,8 @@
use Test::More;
use strict;
use IO::String;
use JSON;
use Lemonldap::NG::Portal::Main::Constants 'PE_NOTOKEN';
require 't/test-lib.pm';
@ -42,6 +44,13 @@ ok(
count(1);
expectReject($res);
my $json;
ok( $json = eval { from_json( $res->[2]->[0] ) }, 'Response is JSON' )
or print STDERR "$@\n" . Dumper($res);
ok( $json->{error} == PE_NOTOKEN, 'Response is PE_NOTOKEN' )
or explain( $json, "error => 81" );
count(2);
# Try to auth with token
$query .= '&user=dwho&password=dwho';
ok(

View File

@ -0,0 +1,364 @@
#!/usr/bin/perl
use Test::More;
use strict;
use IO::String;
use File::Copy;
use Lemonldap::NG::Portal::Main::Constants qw(
PE_RESETCERTIFICATE_INVALID PE_RESETCERTIFICATE_FORMEMPTY
PE_RESETCERTIFICATE_FIRSTACCESS
);
BEGIN {
eval {
require 't/test-lib.pm';
require 't/smtp.pm';
};
}
my ( $res, $user );
my $maintests = 12;
SKIP: {
eval
'require Email::Sender::Simple; use GD::SecurityImage; use Image::Magick; use Net::SSLeay;
use DateTime::Format::RFC3339;';
if ($@) {
skip 'Missing dependencies ' . $@, $maintests;
}
my $client = LLNG::Manager::Test->new( {
ini => {
logLevel => 'error',
useSafeJail => 1,
portalDisplayRegister => 1,
authentication => 'SSL',
userDB => 'Demo',
passwordDB => 'Demo',
registerDB => 'Custom',
customRegister => '::Register::Demo',
customResetCertByMail => '::CertificateResetByMail::Demo',
captcha_mail_enabled => 0,
portalDisplayCertificateResetByMail => 1,
certificateResetByMailCeaAttribute => 'description',
certificateResetByMailCertificateAttribute =>
'userCertificate;binary',
certificateResetByMailStep1Body =>
'Click here <a href="$url"> to confirm your mail. It will expire $expMailDate',
certificateResetByMailStep2Body =>
'Certificate successfully reset!',
certificateValidityDelay => 30
}
}
);
# Test form
# ------------------------
ok( $res = $client->_get( '/certificateReset', accept => 'text/html' ),
'Reset form', );
my ( $host, $url, $query ) = expectForm( $res, '#', undef, 'mail' );
$query = 'mail=dwho%40badwolf.org';
# Post email
ok(
$res = $client->_post(
'/certificateReset', IO::String->new($query),
length => length($query),
accept => 'text/html'
),
'Post mail'
);
ok( mail() =~ m#a href="http://auth.example.com/certificateReset\?(.*?)"#,
'Found link in mail' );
$query = $1;
my $querymail = $query;
ok(
$res = $client->_get(
'/certificateReset',
query => $query,
accept => 'text/html'
),
'Post mail token received by mail'
);
# print STDERR Dumper($res);
( $host, $url, $query ) = expectForm( $res, '#', undef, 'token' );
ok( $res->[2]->[0] =~ /certif/s, ' Ask for a new certificate file' );
#print STDERR Dumper($query);
my %inputs = split( /[=&]/, $query );
my %querytab = split( /[=&]/, $querymail );
# Create the certificate file
my $cert = "-----BEGIN CERTIFICATE-----
MIIDdzCCAl+gAwIBAgIJAKGx8siw7lkRMA0GCSqGSIb3DQEBCwUAMFExCzAJBgNV
BAYTAkZSMQ8wDQYDVQQIDAZGcmFuY2UxDjAMBgNVBAcMBVBBcmlzMREwDwYDVQQK
DAhMaW5hZ29yYTEOMAwGA1UECwwFTElOSUQwIBcNMTkwNzA0MTcyNjI4WhgPMjEx
OTA2MTAxNzI2MjhaMFExCzAJBgNVBAYTAkZSMQ8wDQYDVQQIDAZGcmFuY2UxDjAM
BgNVBAcMBVBBcmlzMREwDwYDVQQKDAhMaW5hZ29yYTEOMAwGA1UECwwFTElOSUQw
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3iyeNE2vpURgdY7xwxS16
xUJANPuMSrCfy1E/xpCtbP02zK0B11DkT81AnTHgvsWYuiubR1P3Phhh+JLsLRho
Grzu9xjaiKXQ+kT1cAiq6skZljphykXBfKUb73W9CPntHL/zl3XyIfu+dWyCGbqa
jHw0Llomi8JqU/XKB6XAYumsV3QzFMM7ECm5HeV3BxfIBwoIOwfwINDUrAGS3h4k
WH/iiqwG7uSuADupSfdmOrvE7rYZupPas4YATX1m5hmON++9pRRFVEoNeOV1qyGY
G7swH1uoO2hAgwKIw0vinft/pJLqe3qhrJwNCIZFHaDEx/PRERFeeEH9/6HSz5kt
AgMBAAGjUDBOMB0GA1UdDgQWBBTFv6pQT/9IBWEAGhILGCcweVfHmTAfBgNVHSME
GDAWgBTFv6pQT/9IBWEAGhILGCcweVfHmTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3
DQEBCwUAA4IBAQBFYneMW5etMnsA3/PdvOqx/ijBF98aKlB4U4IKZpdDRAcsstdL
BSsHRQbHXtb9VdlDWvUnNg5DmjsA8DkOXKXGPGM9ncu9tQi9EoInbOJTMaEsIr2j
zrLj6PHTvazy+6Au+R/9N5u3WQtq/Z2xoN/+bbQ1dyjXgQmBZFizHP32l5AdgBDT
jF7xMHxJ6Jxz9lkI+d9v0TzpxTStsaC+pbDfoouNc2deZkv84YTIrD0EPSHFDH5d
u5i9b+lrWZeCtpVEPzSYpnBwGfepbZAzfVRKJm7wZPCe7KxqMGXQLVBkD8oN7vA1
lkRrWfQftwmLyNIu3HfSgXlgAZS30ymfbzBU
-----END CERTIFICATE-----";
open my $FH2, '>', '/tmp/v296ZJQ_kG';
print {$FH2} "$cert";
close $FH2;
$res = $client->app->( {
'plack.request.query' => bless( {
'skin' => $querytab{'skin'},
'mail_token' => $querytab{'mail_token'}
},
'Hash::MultiValue'
),
'PATH_INFO' => '/certificateReset',
'HTTP_ACCEPT' =>
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3',
'REQUEST_METHOD' => 'POST',
'HTTP_ORIGIN' => 'http://auth.example.com',
'HTTP_ACCEPT_LANGUAGE' => 'fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3',
'REQUEST_SCHEME' => 'http',
'HTTP_CACHE_CONTROL' => 'max-age=0',
'plack.request.merged' => bless( {
'skin' => $querytab{'skin'},
'mail_token' => $querytab{'mail_token'},
'url' => '',
'token' => $inputs{'token'}
},
'Hash::MultiValue'
),
'REMOTE_PORT' => '36674',
'QUERY_STRING' => $querymail,
'SERVER_SIGNATURE' => '',
'psgix.input.buffered' => 1,
'HTTP_UPGRADE_INSECURE_REQUESTS' => '1',
'CONTENT_TYPE' =>
'multipart/form-data; boundary=----WebKitFormBoundarybabRY9u6K9tERoLr',
'plack.request.upload' => bless( {
'certif' => bless( {
'headers' => bless( {
'content-disposition' =>
'form-data; name="certif"; filename="user.pem"',
'content-type' =>
'application/x-x509-ca-cert',
'::std_case' => {
'content-disposition' =>
'Content-Disposition'
}
},
'HTTP::Headers'
),
'filename' => 'user.pem',
'tempname' => '/tmp/v296ZJQ_kG',
'size' => 1261
},
'Plack::Request::Upload'
)
},
'Hash::MultiValue'
),
'psgi.streaming' => 1,
'plack.request.body' => bless( {
'skin' => 'bootstrap',
'url' => '',
'token' => $inputs{'token'}
},
'Hash::MultiValue'
),
'SCRIPT_URL' => '/certificateReset',
'SERVER_NAME' => 'auth.example.com',
'HTTP_REFERER' => 'http://auth.example.com/certificateReset?'
. $querymail,
'HTTP_CONNECTION' => 'close',
'CONTENT_LENGTH' => '1759',
'SCRIPT_URI' => 'http://auth.example.com/certificateReset',
'plack.cookie.parsed' => {
'llnglanguage' => 'fr'
},
'SERVER_PORT' => '80',
'SERVER_NAME' => 'auth.example.com',
'SERVER_PROTOCOL' => 'HTTP/1.1',
'SCRIPT_NAME' => '',
'HTTP_USER_AGENT' =>
'Mozilla/5.0 (VAX-4000; rv:36.0) Gecko/20350101 Firefox',
'HTTP_COOKIE' => 'llnglanguage=fr',
'REMOTE_ADDR' => '127.0.0.1',
'REQUEST_URI' => '/certificateReset?' . $querymail,
'plack.cookie.string' => 'llnglanguage=fr',
'SERVER_ADDR' => '127.0.0.1',
'psgi.url_scheme' => 'http',
'psgix.harakiri' => '',
'HTTP_HOST' => 'auth.example.com'
}
);
ok( mail() =~ /Certificate successfully reset/,
'Certificate has been reset' );
# Test invalid certificate
# Test form
# ------------------------
ok( $res = $client->_get( '/certificateReset', accept => 'text/html' ),
'Reset form', );
my ( $host, $url, $query ) = expectForm( $res, '#', undef, 'mail' );
$query = 'mail=dwho%40badwolf.org';
# Post email
ok(
$res = $client->_post(
'/certificateReset', IO::String->new($query),
length => length($query),
accept => 'text/html'
),
'Post mail'
);
ok( mail() =~ m#a href="http://auth.example.com/certificateReset\?(.*?)"#,
'Found link in mail' );
$query = $1;
my $querymail = $query;
ok(
$res = $client->_get(
'/certificateReset',
query => $query,
accept => 'text/html'
),
'Post mail token received by mail'
);
# print STDERR Dumper($res);
( $host, $url, $query ) = expectForm( $res, '#', undef, 'token' );
ok( $res->[2]->[0] =~ /certif/s, ' Ask for a new certificate file' );
#print STDERR Dumper($query);
my %inputs = split( /[=&]/, $query );
my %querytab = split( /[=&]/, $querymail );
# Create the certificate file
my $cert = "INVALID CERTIFICATE";
open my $FH2, '>', '/tmp/v296ZJQ_kG';
print {$FH2} "$cert";
close $FH2;
$res = $client->app->( {
'plack.request.query' => bless( {
'skin' => $querytab{'skin'},
'mail_token' => $querytab{'mail_token'}
},
'Hash::MultiValue'
),
'PATH_INFO' => '/certificateReset',
'HTTP_ACCEPT' =>
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3',
'REQUEST_METHOD' => 'POST',
'HTTP_ORIGIN' => 'http://auth.example.com',
'HTTP_ACCEPT_LANGUAGE' => 'fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3',
'REQUEST_SCHEME' => 'http',
'HTTP_CACHE_CONTROL' => 'max-age=0',
'plack.request.merged' => bless( {
'skin' => $querytab{'skin'},
'mail_token' => $querytab{'mail_token'},
'url' => '',
'token' => $inputs{'token'}
},
'Hash::MultiValue'
),
'REMOTE_PORT' => '36674',
'QUERY_STRING' => $querymail,
'SERVER_SIGNATURE' => '',
'psgix.input.buffered' => 1,
'HTTP_UPGRADE_INSECURE_REQUESTS' => '1',
'CONTENT_TYPE' =>
'multipart/form-data; boundary=----WebKitFormBoundarybabRY9u6K9tERoLr',
'plack.request.upload' => bless( {
'certif' => bless( {
'headers' => bless( {
'content-disposition' =>
'form-data; name="certif"; filename="user.pem"',
'content-type' =>
'application/x-x509-ca-cert',
'::std_case' => {
'content-disposition' =>
'Content-Disposition'
}
},
'HTTP::Headers'
),
'filename' => 'user.pem',
'tempname' => '/tmp/v296ZJQ_kG',
'size' => 1261
},
'Plack::Request::Upload'
)
},
'Hash::MultiValue'
),
'psgi.streaming' => 1,
'plack.request.body' => bless( {
'skin' => 'bootstrap',
'url' => '',
'token' => $inputs{'token'}
},
'Hash::MultiValue'
),
'SCRIPT_URL' => '/certificateReset',
'SERVER_NAME' => 'auth.example.com',
'HTTP_REFERER' => 'http://auth.example.com/certificateReset?'
. $querymail,
'HTTP_CONNECTION' => 'close',
'CONTENT_LENGTH' => '1759',
'SCRIPT_URI' => 'http://auth.example.com/certificateReset',
'plack.cookie.parsed' => {
'llnglanguage' => 'fr'
},
'SERVER_PORT' => '80',
'SERVER_NAME' => 'auth.example.com',
'SERVER_PROTOCOL' => 'HTTP/1.1',
'SCRIPT_NAME' => '',
'HTTP_USER_AGENT' =>
'Mozilla/5.0 (VAX-4000; rv:36.0) Gecko/20350101 Firefox',
'HTTP_COOKIE' => 'llnglanguage=fr',
'REMOTE_ADDR' => '127.0.0.1',
'REQUEST_URI' => '/certificateReset?' . $querymail,
'plack.cookie.string' => 'llnglanguage=fr',
'SERVER_ADDR' => '127.0.0.1',
'psgi.url_scheme' => 'http',
'psgix.harakiri' => '',
'HTTP_HOST' => 'auth.example.com'
}
);
my $trmsg = $res->[2]->[0]; # get html response
my @trmsg = split( /\n/, $trmsg ); # split into lines
@trmsg = grep( /trmsg="/, @trmsg ); # only get line corresponding to message
$trmsg = $trmsg[0]; # get the first one only
$trmsg =~ s/.*trmsg="([0-9]+)".*/$1/g; # get error code number
ok( $trmsg == PE_RESETCERTIFICATE_INVALID, 'Invalid certificate' );
}
count($maintests);
done_testing( count() );

View File

@ -17,7 +17,8 @@ my $maintests = 6;
SKIP: {
eval
'require Email::Sender::Simple; use GD::SecurityImage;use Image::Magick;';
'require Email::Sender::Simple; use GD::SecurityImage; use Image::Magick; use Net::SSLeay;
use DateTime::Format::RFC3339;';
if ($@) {
skip 'Missing dependencies ' . $@, $maintests;
@ -210,7 +211,153 @@ lkRrWfQftwmLyNIu3HfSgXlgAZS30ymfbzBU
}
);
ok( mail() =~ /Certificate successfully reset/, 'Certificate has been reset' );
ok( mail() =~ /Certificate successfully reset/,
'Certificate has been reset' );
# Test invalid certificate
# Test form
# ------------------------
ok( $res = $client->_get( '/certificateReset', accept => 'text/html' ),
'Reset form', );
my ( $host, $url, $query ) = expectForm( $res, '#', undef, 'mail' );
$query = 'mail=dwho%40badwolf.org';
# Post email
ok(
$res = $client->_post(
'/certificateReset', IO::String->new($query),
length => length($query),
accept => 'text/html'
),
'Post mail'
);
ok( mail() =~ m#a href="http://auth.example.com/certificateReset\?(.*?)"#,
'Found link in mail' );
$query = $1;
my $querymail = $query;
ok(
$res = $client->_get(
'/certificateReset',
query => $query,
accept => 'text/html'
),
'Post mail token received by mail'
);
# print STDERR Dumper($res);
( $host, $url, $query ) = expectForm( $res, '#', undef, 'token' );
ok( $res->[2]->[0] =~ /certif/s, ' Ask for a new certificate file' );
#print STDERR Dumper($query);
my %inputs = split( /[=&]/, $query );
my %querytab = split( /[=&]/, $querymail );
# Create the certificate file
my $cert = "INVALID CERTIFICATE";
open my $FH2, '>', '/tmp/v296ZJQ_kG';
print {$FH2} "$cert";
close $FH2;
$res = $client->app->( {
'plack.request.query' => bless( {
'skin' => $querytab{'skin'},
'mail_token' => $querytab{'mail_token'}
},
'Hash::MultiValue'
),
'PATH_INFO' => '/certificateReset',
'HTTP_ACCEPT' =>
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3',
'REQUEST_METHOD' => 'POST',
'HTTP_ORIGIN' => 'http://auth.example.com',
'HTTP_ACCEPT_LANGUAGE' => 'fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3',
'REQUEST_SCHEME' => 'http',
'HTTP_CACHE_CONTROL' => 'max-age=0',
'plack.request.merged' => bless( {
'skin' => $querytab{'skin'},
'mail_token' => $querytab{'mail_token'},
'url' => '',
'token' => $inputs{'token'}
},
'Hash::MultiValue'
),
'REMOTE_PORT' => '36674',
'QUERY_STRING' => $querymail,
'SERVER_SIGNATURE' => '',
'psgix.input.buffered' => 1,
'HTTP_UPGRADE_INSECURE_REQUESTS' => '1',
'CONTENT_TYPE' =>
'multipart/form-data; boundary=----WebKitFormBoundarybabRY9u6K9tERoLr',
'plack.request.upload' => bless( {
'certif' => bless( {
'headers' => bless( {
'content-disposition' =>
'form-data; name="certif"; filename="user.pem"',
'content-type' =>
'application/x-x509-ca-cert',
'::std_case' => {
'content-disposition' =>
'Content-Disposition'
}
},
'HTTP::Headers'
),
'filename' => 'user.pem',
'tempname' => '/tmp/v296ZJQ_kG',
'size' => 1261
},
'Plack::Request::Upload'
)
},
'Hash::MultiValue'
),
'psgi.streaming' => 1,
'plack.request.body' => bless( {
'skin' => 'bootstrap',
'url' => '',
'token' => $inputs{'token'}
},
'Hash::MultiValue'
),
'SCRIPT_URL' => '/certificateReset',
'SERVER_NAME' => 'auth.example.com',
'HTTP_REFERER' => 'http://auth.example.com/certificateReset?'
. $querymail,
'HTTP_CONNECTION' => 'close',
'CONTENT_LENGTH' => '1759',
'SCRIPT_URI' => 'http://auth.example.com/certificateReset',
'plack.cookie.parsed' => {
'llnglanguage' => 'fr'
},
'SERVER_PORT' => '80',
'SERVER_NAME' => 'auth.example.com',
'SERVER_PROTOCOL' => 'HTTP/1.1',
'SCRIPT_NAME' => '',
'HTTP_USER_AGENT' =>
'Mozilla/5.0 (VAX-4000; rv:36.0) Gecko/20350101 Firefox',
'HTTP_COOKIE' => 'llnglanguage=fr',
'REMOTE_ADDR' => '127.0.0.1',
'REQUEST_URI' => '/certificateReset?' . $querymail,
'plack.cookie.string' => 'llnglanguage=fr',
'SERVER_ADDR' => '127.0.0.1',
'psgi.url_scheme' => 'http',
'psgix.harakiri' => '',
'HTTP_HOST' => 'auth.example.com'
}
);
my $trmsg = $res->[2]->[0]; # get html response
my @trmsg = split( /\n/, $trmsg ); # split into lines
@trmsg = grep( /trmsg="/, @trmsg ); # only get line corresponding to message
$trmsg = $trmsg[0]; # get the first one only
$trmsg =~ s/.*trmsg="([0-9]+)".*/$1/g; # get error code number
ok( $trmsg == PE_RESETCERTIFICATE_INVALID, 'Invalid certificate' );
}
count($maintests);

View File

@ -14,33 +14,35 @@ my $ini = {
cache_root => 't/',
cache_depth => 0,
},
logLevel => 'error',
cookieName => 'lemonldap',
domain => 'example.com',
templateDir => 'site/templates',
staticPrefix => '/static',
loginHistoryEnabled => 1,
securedCookie => 0,
https => 0,
portalDisplayResetPassword => 1,
portalStatus => 1,
cda => 1,
notification => 1,
portalCheckLogins => 1,
portalDisplayFavApps => 1,
stayConnected => 1,
bruteForceProtection => 1,
grantSessionRules => 1,
upgradeSession => 1,
autoSigninRules => { a => 1 },
checkState => 1,
portalForceAuthn => 1,
checkUser => 1,
impersonationRule => 1,
contextSwitchingRule => 1,
decryptValueRule => 1,
grantSessionRules => { a => 1 },
checkStateSecret => 'x',
logLevel => 'error',
cookieName => 'lemonldap',
domain => 'example.com',
templateDir => 'site/templates',
staticPrefix => '/static',
loginHistoryEnabled => 1,
securedCookie => 0,
https => 0,
portalDisplayResetPassword => 1,
# portalDisplayCertificateResetByMail => 1, Missing dependencies
portalStatus => 1,
cda => 1,
notification => 1,
portalCheckLogins => 1,
portalDisplayFavApps => 1,
stayConnected => 1,
bruteForceProtection => 1,
grantSessionRules => 1,
upgradeSession => 1,
autoSigninRules => { a => 1 },
checkState => 1,
portalForceAuthn => 1,
checkUser => 1,
impersonationRule => 1,
contextSwitchingRule => 1,
decryptValueRule => 1,
globalLogoutRule => 1,
grantSessionRules => { a => 1 },
checkStateSecret => 'x',
};
ok( $p = Lemonldap::NG::Portal::Main->new, 'Portal object' );

View File

@ -76,11 +76,13 @@ $Data::Dumper::Useperl = 1;
my $ini;
use File::Temp 'tempfile', 'tempdir';
use File::Copy 'copy';
our $tmpDir = $LLNG::TMPDIR
|| tempdir( 'tmpSessionXXXXX', DIR => 't/sessions', CLEANUP => 1 );
mkdir "$tmpDir/lock";
mkdir "$tmpDir/saml";
mkdir "$tmpDir/saml/lock";
copy( "t/lmConf-1.json", "$tmpDir/lmConf-1.json" );
=head4 count($inc)
@ -141,8 +143,7 @@ sub count_sessions {
sub getCache {
require Cache::FileCache;
return Cache::FileCache->new(
{
return Cache::FileCache->new( {
namespace => 'lemonldap-ng-session',
cache_root => $tmpDir,
cache_depth => 0,
@ -515,7 +516,7 @@ extends 'Lemonldap::NG::Common::PSGI::Cli::Lib';
our $defaultIni = {
configStorage => {
type => 'File',
dirName => 't',
dirName => "$tmpDir",
},
localSessionStorage => 'Cache::FileCache',
localSessionStorageOptions => {
@ -705,8 +706,7 @@ to test content I<(to launch a C<expectForm()> for example)>.
sub _get {
my ( $self, $path, %args ) = @_;
my $res = $self->app->(
{
my $res = $self->app->( {
'HTTP_ACCEPT' => $args{accept}
|| 'application/json, text/plain, */*',
'HTTP_ACCEPT_LANGUAGE' => 'fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3',
@ -758,8 +758,7 @@ sub _post {
my ( $self, $path, $body, %args ) = @_;
die "$body must be a IO::Handle"
unless ( ref($body) and $body->can('read') );
my $res = $self->app->(
{
my $res = $self->app->( {
'HTTP_ACCEPT' => $args{accept}
|| 'application/json, text/plain, */*',
'HTTP_ACCEPT_LANGUAGE' => 'fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3',