Merge branch 'v2.0'
This commit is contained in:
commit
aab0dcca14
1
Makefile
1
Makefile
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)],
|
||||
|
|
|
@ -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->( {
|
||||
|
|
|
@ -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 );
|
||||
|
|
|
@ -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 ) = @_;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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($_) }
|
||||
|
|
153
lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Api.pm
Normal file
153
lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Api.pm
Normal 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;
|
345
lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Api/2F.pm
Normal file
345
lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Api/2F.pm
Normal 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;
|
79
lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Api/Common.pm
Normal file
79
lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Api/Common.pm
Normal 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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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'
|
||||
},
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -437,7 +437,7 @@ sub tree {
|
|||
nodes => [
|
||||
'customAuth', 'customUserDB',
|
||||
'customPassword', 'customRegister',
|
||||
'customAddParams',
|
||||
'customResetCertByMail', 'customAddParams',
|
||||
]
|
||||
},
|
||||
],
|
||||
|
|
|
@ -21,17 +21,15 @@ has cfgNum => (
|
|||
}
|
||||
);
|
||||
|
||||
has sep => ( is => 'rw', isa => 'Str', default => '/' );
|
||||
|
||||
has req => ( is => 'ro' );
|
||||
|
||||
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 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 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 {
|
||||
|
|
|
@ -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
|
||||
|
|
12
lemonldap-ng-manager/site/htdocs/api.fcgi
Executable file
12
lemonldap-ng-manager/site/htdocs/api.fcgi
Executable 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" }
|
||||
)
|
||||
);
|
|
@ -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>
|
||||
|
|
|
@ -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":"عبارة بيرل ليست صحيحة",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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ệ",
|
||||
|
|
|
@ -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
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
355
lemonldap-ng-manager/t/04-2F-api.t
Normal file
355
lemonldap-ng-manager/t/04-2F-api.t
Normal 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();
|
559
lemonldap-ng-manager/t/04-providers-api.t
Normal file
559
lemonldap-ng-manager/t/04-providers-api.t
Normal 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();
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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( {} );
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -32,4 +32,5 @@ sub applyLoginRule {
|
|||
# For now, get first letter of firstname and lastname
|
||||
return substr( $firstname, 0, 1 ) . $lastname;
|
||||
}
|
||||
|
||||
1;
|
||||
|
|
|
@ -15,6 +15,7 @@ sub new {
|
|||
unless ($res) {
|
||||
die 'Unable to load register module ' . $self->{conf}->{customRegister};
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
|
BIN
lemonldap-ng-portal/site/htdocs/static/common/apps/llng.png
Normal file
BIN
lemonldap-ng-portal/site/htdocs/static/common/apps/llng.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1001 B |
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
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;';
|
||||
if ($@) {
|
||||
|
@ -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' );
|
||||
|
|
|
@ -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(
|
||||
|
|
364
lemonldap-ng-portal/t/44-CertificateResetByMail-Demo.t
Normal file
364
lemonldap-ng-portal/t/44-CertificateResetByMail-Demo.t
Normal 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() );
|
|
@ -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);
|
||||
|
|
|
@ -23,6 +23,7 @@ my $ini = {
|
|||
securedCookie => 0,
|
||||
https => 0,
|
||||
portalDisplayResetPassword => 1,
|
||||
# portalDisplayCertificateResetByMail => 1, Missing dependencies
|
||||
portalStatus => 1,
|
||||
cda => 1,
|
||||
notification => 1,
|
||||
|
@ -39,6 +40,7 @@ my $ini = {
|
|||
impersonationRule => 1,
|
||||
contextSwitchingRule => 1,
|
||||
decryptValueRule => 1,
|
||||
globalLogoutRule => 1,
|
||||
grantSessionRules => { a => 1 },
|
||||
checkStateSecret => 'x',
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in New Issue
Block a user