our
$VERSION
=
'2.20.0'
;
has
localConfig
=> (
is
=>
'rw'
,
default
=>
sub
{ {} } );
has
conf
=> (
is
=>
'rw'
,
default
=>
sub
{ {} } );
has
trOver
=> (
is
=>
'rw'
,
default
=>
sub
{ {
all
=> {} } } );
has
_authentication
=> (
is
=>
'rw'
);
has
_userDB
=> (
is
=>
'rw'
);
has
_passwordDB
=> (
is
=>
'rw'
);
has
_loadedServices
=> (
is
=>
'rw'
,
default
=>
sub
{ {} } );
sub
_captcha {
$_
[0]->getService(
'captcha'
) }
sub
_trustedBrowser {
$_
[0]->getService(
'trustedBrowser'
) }
sub
_sfEngine {
$_
[0]->getService(
'secondFactor'
) }
sub
menu {
$_
[0]->getService(
'menu'
) }
has
_ppRules
=> (
is
=>
'rw'
,
default
=>
sub
{ {} } );
has
loadedModules
=> (
is
=>
'rw'
);
has
_macros
=> (
is
=>
'rw'
);
has
_groups
=> (
is
=>
'rw'
);
has
_jsRedirect
=> (
is
=>
'rw'
);
has
trustedDomainsRe
=> (
is
=>
'rw'
);
has
additionalTrustedDomains
=> (
is
=>
'rw'
,
default
=>
sub
{ [] } );
has
_pluginEntryPoints
=>
(
is
=>
'rw'
,
isa
=>
'ArrayRef'
,
default
=>
sub
{ [] } );
my
@entryPoints
;
BEGIN {
@entryPoints
= (
qw(beforeAuth betweenAuthAndData afterData endAuth)
,
'forAuthUser'
,
'beforeLogout'
,
'authCancel'
,
);
foreach
(
@entryPoints
) {
has
$_
=> (
is
=>
'rw'
,
isa
=>
'ArrayRef'
,
default
=>
sub
{ [] }
);
}
}
has
afterSub
=> (
is
=>
'rw'
,
default
=>
sub
{ {} } );
has
aroundSub
=> (
is
=>
'rw'
,
default
=>
sub
{ {} } );
has
hook
=> (
is
=>
'rw'
,
default
=>
sub
{ {} } );
has
spRules
=> (
is
=>
'rw'
,
default
=>
sub
{ {} }
);
has
customParameters
=> (
is
=>
'rw'
,
default
=>
sub
{ {} } );
has
csp
=> (
is
=>
'rw'
);
has
cors
=> (
is
=>
'rw'
);
has
cookieSameSite
=> (
is
=>
'rw'
);
has
pluginSessionDataToRemember
=>
(
is
=>
'rw'
,
isa
=>
"HashRef"
,
default
=>
sub
{ {} } );
sub
_resetPluginsAndServices {
my
(
$self
) =
@_
;
$self
->loadedModules( {} );
$self
->_loadedServices( {} );
$self
->afterSub( {} );
$self
->aroundSub( {} );
$self
->spRules( {} );
$self
->hook( {} );
$self
->pluginSessionDataToRemember( {} );
$self
->_pluginEntryPoints( [] );
foreach
(
qw(_macros _groups)
,
@entryPoints
) {
$self
->{
$_
} = [];
}
}
sub
init {
my
(
$self
,
$args
) =
@_
;
$args
||= {};
my
$confAcc
= Lemonldap::NG::Common::Conf->new(
$args
->{configStorage} );
unless
(
$confAcc
) {
die
(
'Could not read configuration: '
.
$Lemonldap::NG::Common::Conf::msg
);
}
$self
->localConfig( { %{
$confAcc
->getLocalConf(
'portal'
) },
%$args
} );
foreach
my
$k
(
keys
%{
$self
->localConfig } ) {
if
(
$k
=~ /tpl_(.*)/ ) {
$self
->customParameters->{$1} =
$self
->localConfig->{
$k
};
}
elsif
(
$k
=~ /error_(?:(\w+?)_)?(\d+)$/ ) {
my
$lang
= $1 ||
'all'
;
$self
->trOver->{
$lang
}->{
"PE$2"
} =
$self
->localConfig->{
$k
};
}
elsif
(
$k
=~ /msg_(?:([a-z][a-z](?:_[A-Z][A-Z])?)_)?(\w+)$/ ) {
my
$lang
= $1 ||
'all'
;
$self
->trOver->{
$lang
}->{$2} =
$self
->localConfig->{
$k
};
}
else
{
$self
->conf->{
$k
} =
$self
->localConfig->{
$k
};
}
}
$self
->trOver( JSON::to_json(
$self
->trOver ) );
$self
->_resetPluginsAndServices;
Lemonldap::NG::Handler::Main->onReload(
$self
,
'reloadConf'
);
unless
(
$self
->SUPER::init(
$self
->localConfig ) ) {
$self
->logger->error(
'Initialization failed: '
.
$self
->error );
return
0;
}
if
(
$self
->error ) {
$self
->logger->error(
$self
->error );
return
0;
}
$self
->defaultAuthRoute(
''
);
$self
->defaultUnauthRoute(
''
);
return
1;
}
sub
setPortalRoutes {
my
(
$self
) =
@_
;
$self
->authRoutes( {
GET
=> {},
POST
=> {},
PUT
=> {},
PATCH
=> {},
DELETE
=> {},
OPTIONS
=> {}
}
);
$self
->unAuthRoutes( {
GET
=> {},
POST
=> {},
PUT
=> {},
PATCH
=> {},
DELETE
=> {},
OPTIONS
=> {}
}
);
$self
->addUnauthRoute(
'*'
=>
'login'
, [
'GET'
] )
->addUnauthRoute(
'*'
=>
'postLogin'
, [
'POST'
] )
->addAuthRoute(
'*'
=>
'authenticatedRequest'
, [
'GET'
] )
->addAuthRoute(
'*'
=>
'postAuthenticatedRequest'
, [
'POST'
] )
->addUnauthRoute(
'psgi.js'
=>
'sendJs'
, [
'GET'
] )
->addAuthRoute(
'psgi.js'
=>
'sendJs'
, [
'GET'
] )
->addUnauthRoute(
'portal.css'
=>
'sendCss'
, [
'GET'
] )
->addAuthRoute(
'portal.css'
=>
'sendCss'
, [
'GET'
] )
->addUnauthRoute(
lmerror
=> {
':code'
=>
'lmError'
}, [
'GET'
] )
->addAuthRoute(
lmerror
=> {
':code'
=>
'lmError'
}, [
'GET'
] )
->addUnauthRoute(
ping
=>
'pleaseAuth'
, [
'GET'
] )
->addAuthRoute(
ping
=>
'authenticated'
, [
'GET'
] )
->addAuthRoute(
refresh
=>
'refresh'
, [
'GET'
] )
->addAuthRoute(
'*'
=>
'corsPreflight'
, [
'OPTIONS'
] )
->addUnauthRoute(
'*'
=>
'corsPreflight'
, [
'OPTIONS'
] )
->addAuthRoute(
logout
=>
'logout'
, [
'GET'
] )
->addUnauthRoute(
logout
=>
'unauthLogout'
, [
'GET'
] );
$self
->defaultAuthRoute(
''
);
$self
->defaultUnauthRoute(
''
);
return
1;
}
sub
reloadConf {
my
(
$self
,
$conf
) =
@_
;
$self
->portal( Lemonldap::NG::Handler::Main->tsv->{portal}->() );
$self
->setPortalRoutes;
%{
$self
->{conf} } = %{
$self
->localConfig };
$self
->_resetPluginsAndServices;
foreach
my
$key
(
keys
%$conf
) {
$self
->{conf}->{
$key
} ||=
$conf
->{
$key
};
}
my
$csp
=
''
;
foreach
(
qw(default img src style font connect script)
) {
my
$prm
=
$self
->conf->{
'csp'
.
ucfirst
(
$_
) };
$csp
.=
"$_-src $prm;"
if
(
$prm
);
}
$self
->csp(
$csp
);
$self
->logger->debug(
"Initialized CSP headers : "
.
$self
->csp );
my
$cors
=
''
;
foreach
(
qw(Allow_Origin Allow_Credentials Allow_Headers Allow_Methods Expose_Headers Max_Age)
)
{
my
$header
=
$_
;
my
$prm
=
$self
->conf->{
'cors'
.
$_
};
if
(
$header
and
$prm
) {
$header
=~ s/_/-/;
$prm
=~ s/\s+//;
$cors
.=
"Access-Control-$header;$prm;"
;
}
}
$self
->cors(
$cors
);
$self
->logger->debug(
"Initialized CORS headers : "
.
$self
->cors );
$self
->{staticPrefix} =
$self
->conf->{staticPrefix} ||
'/static'
;
$self
->{languages} =
$self
->conf->{languages} ||
'en'
;
unless
(
$self
->conf->{globalStorage} ) {
$self
->error(
'globalStorage not defined (perhaps configuration can not be read)'
);
return
$self
->fail;
}
unless
(
$self
->conf->{persistentStorage} ) {
$self
->conf->{persistentStorage} =
$self
->conf->{globalStorage};
$self
->conf->{persistentStorageOptions} =
$self
->conf->{globalStorageOptions};
}
$self
->cookieSameSite( getSameSite(
$self
->conf ) );
$self
->logger->debug(
"Cookies will use SameSite="
.
$self
->cookieSameSite );
$self
->displayInit;
my
$mod
;
for
my
$type
(
qw(authentication userDB)
) {
unless
(
$self
->conf->{
$type
} ) {
$self
->error(
"$type is not set"
);
return
$self
->fail;
}
$mod
=
$self
->conf->{
$type
}
unless
(
$self
->conf->{
$type
} eq
'Same'
);
my
$module
=
'::'
.
ucfirst
(
$type
) .
'::'
.
$mod
;
$module
=~ s/Authentication/Auth/;
return
$self
->fail
unless
(
$self
->{
"_$type"
} =
$self
->loadPlugin(
$module
) );
}
foreach
my
$type
(
qw(macros groups)
) {
$self
->{
"_$type"
} = {};
if
(
$self
->conf->{
$type
} ) {
for
my
$name
(
sort
keys
%{
$self
->conf->{
$type
} } ) {
my
$sub
=
HANDLER->buildSub(
HANDLER->substitute(
$self
->conf->{
$type
}->{
$name
} ) );
if
(
$sub
) {
$self
->{
"_$type"
}->{
$name
} =
$sub
;
}
else
{
$self
->logger->error(
"$type $name returns an error: "
. HANDLER->tsv->{jail}->error );
}
}
}
}
$self
->{_jsRedirect} =
HANDLER->buildSub( HANDLER->substitute(
$self
->conf->{jsRedirect} ) )
or
$self
->logger->error(
'jsRedirect returns an error: '
. HANDLER->tsv->{jail}->error );
foreach
my
$service
(
$self
->enabledServices ) {
$self
->loadService(
@$service
) or
return
$self
->fail;
}
foreach
my
$plugin
(
$self
->enabledPlugins ) {
$self
->loadPlugin(
$plugin
) or
return
$self
->fail;
}
if
(
$self
->conf->{trustedDomains}
and
$self
->conf->{trustedDomains} =~ /^\s*\*\s*$/ )
{
$self
->trustedDomainsRe(
qr#^https?://#
);
}
else
{
my
$re
= Regexp::Assemble->new();
if
(
my
$td
=
$self
->conf->{trustedDomains} ) {
$td
=~ s/^\s*(.*?)\s*/$1/;
foreach
(
split
( /\s+/,
$td
) ) {
next
unless
(
$td
);
s
$self
->logger->debug(
"Domain $_ added in trusted domains"
);
s/\./\\./g;
$_
=~
s/\*\\\./(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9]\\.)*/g;
$re
->add(
"$_"
);
}
}
my
$default_portal
= HANDLER->tsv->{portal}->();
foreach
( @{
$self
->{additionalTrustedDomains} },
$default_portal
) {
my
$p
=
$_
;
$p
=~ s
$re
->add(
quotemeta
(
$p
) );
}
foreach
my
$vhost
(
sort
keys
%{
$self
->conf->{locationRules} } ) {
my
$expr
=
quotemeta
(
$vhost
);
if
(
$vhost
=~ /[\%\*]/ ) {
$expr
=~ s/\\\*/[A-Za-z0-9\-\.]\*/;
$expr
=~ s/\\\%/[A-Za-z0-9\-]\*/;
}
$re
->add(
$expr
);
$self
->logger->debug(
"Vhost $vhost added in trusted domains"
);
$self
->conf->{vhostOptions} ||= {};
if
(
my
$tmp
=
$self
->conf->{vhostOptions}->{
$vhost
}->{vhostAliases} )
{
foreach
my
$alias
(
split
/\s+/,
$tmp
) {
$self
->logger->debug(
"Alias $alias added in trusted domains"
);
$re
->add(
quotemeta
(
$alias
) );
}
}
}
my
$tmp
=
'^https?://'
.
$re
->as_string .
'(?::\d+)?(?:/|$)'
;
$self
->trustedDomainsRe(
qr/$tmp/
);
}
push
@{
$self
->endAuth },
sub
{
my
$tmp
=
$_
[0]->pdata->{keepPdata} //= [];
foreach
my
$k
(
keys
%{
$_
[0]->pdata } ) {
unless
(
grep
{
$_
eq
$k
}
@$tmp
) {
$self
->logger->debug(
"Removing $k from pdata"
);
delete
$_
[0]->pdata->{
$k
};
}
}
my
$user_log
=
$_
[0]->{sessionInfo}->{
$self
->conf->{whatToTrace} };
my
$auth_module
=
$_
[0]->{sessionInfo}->{_auth};
my
$ipAddr
=
$_
[0]->{sessionInfo}->{ipAddr};
if
(
$user_log
) {
$self
->auditLog(
$_
[0],
message
=> (
"User "
.
$user_log
.
" connected from $auth_module ($ipAddr)"
),
code
=>
"LOGIN"
,
user
=>
$user_log
,
);
}
if
(
@$tmp
) {
$self
->logger->debug(
'Add '
.
join
(
','
,
@$tmp
) .
' in keepPdata'
);
$_
[0]->pdata->{keepPdata} =
$tmp
;
}
return
PE_OK;
};
my
$default_portal_uri
= URI->new( HANDLER->tsv->{portal}->() );
my
$default_portal_host
=
eval
{
$default_portal_uri
->host };
if
(
$default_portal_host
) {
HANDLER->tsv->{defaultCondition}->{
$default_portal_host
} ||=
sub
{ 1 };
}
1;
}
sub
loadService {
my
(
$self
,
$name
,
$plugin
) =
@_
;
$self
->logger->debug(
"Loading service $name from $plugin"
);
return
$self
->_loadedServices->{
$name
} =
$self
->loadPlugin(
$plugin
);
}
sub
getService {
my
(
$self
,
$name
) =
@_
;
return
$_
[0]->_loadedServices->{
$name
};
}
sub
loadPlugin {
my
(
$self
,
$plugin
) =
@_
;
unless
(
$plugin
) {
Carp::confess(
'Calling loadPugin without arg !'
);
}
my
$obj
;
return
0
unless
(
$obj
=
$self
->loadModule(
"$plugin"
) );
return
$self
->findEP(
$plugin
,
$obj
);
}
sub
findEP {
my
(
$self
,
$plugin
,
$obj
) =
@_
;
foreach
my
$sub
(
@entryPoints
) {
if
(
$obj
->can(
$sub
) ) {
$self
->logger->debug(
" Found $sub entry point:"
);
if
(
my
$callback
=
$obj
->
$sub
) {
push
@{
$self
->{
$sub
} },
sub
{
eval
{
$obj
->logger->debug(
"Launching ${plugin}::$callback"
);
};
$obj
->
$callback
(
@_
);
};
$self
->logger->debug(
" -> $callback"
);
}
}
}
if
(
$obj
->can(
'afterSub'
) ) {
$self
->logger->debug(
"Found afterSub in $plugin"
);
my
$h
=
$obj
->afterSub;
unless
(
ref
$h
and
ref
(
$h
) eq
'HASH'
) {
$self
->logger->error(
'"afterSub" endpoint must be a hashref, skipped'
);
}
else
{
foreach
my
$ep
(
keys
%$h
) {
my
$callback
=
$h
->{
$ep
};
push
@{
$self
->afterSub->{
$ep
} },
sub
{
eval
{
$obj
->logger->debug(
"Launching ${plugin}::$callback afterSub $ep"
);
};
$obj
->
$callback
(
@_
);
};
}
}
}
if
(
$obj
->can(
'aroundSub'
) ) {
$self
->logger->debug(
"Found aroundSub in $plugin"
);
my
$h
=
$obj
->aroundSub;
unless
(
ref
$h
and
ref
(
$h
) eq
'HASH'
) {
$self
->logger->error(
'"aroundSub" endpoint must be a hashref, skipped'
);
}
else
{
foreach
my
$ep
(
keys
%$h
) {
my
$callback
=
$h
->{
$ep
};
my
$previousSub
=
$self
->aroundSub->{
$ep
} ||=
sub
{
$self
->logger->debug(
"$ep launched inside ${plugin}::$callback"
);
$self
->
$ep
(
@_
);
};
$self
->aroundSub->{
$ep
} =
sub
{
$self
->logger->debug(
"Launching ${plugin}::$callback instead of $ep"
);
$obj
->
$callback
(
$previousSub
,
@_
);
};
}
}
}
if
(
$obj
->can(
'hook'
) ) {
$self
->logger->debug(
"Found hook in $plugin"
);
my
$h
=
$obj
->hook;
unless
(
ref
$h
and
ref
(
$h
) eq
'HASH'
) {
$self
->logger->error(
'"hook" endpoint must be a hashref, skipped'
);
}
else
{
foreach
my
$hookname
(
keys
%$h
) {
my
$callback
=
$h
->{
$hookname
};
push
@{
$self
->hook->{
$hookname
} },
sub
{
eval
{
$obj
->logger->debug(
"Launching ${plugin}::$callback on hook $hookname"
);
};
$obj
->
$callback
(
@_
);
};
}
}
}
if
(
$obj
->can(
'spRules'
) ) {
foreach
my
$k
(
keys
%{
$obj
->spRules } ) {
$self
->logger->info(
"$k is defined more than one time, it can have some bad effects on Menu display"
)
if
(
$self
->spRules->{
$k
} );
$self
->spRules->{
$k
} =
$obj
->spRules->{
$k
};
}
}
for
my
$ep
( @{
$self
->_pluginEntryPoints } ) {
if
( (
$ep
->{can} and
$obj
->can(
$ep
->{can} ) )
or (
$ep
->{isa} and
$obj
->isa(
$ep
->{isa} ) )
or (
$ep
->{does} and
$obj
->does(
$ep
->{does} ) ) )
{
my
@args
= @{
$ep
->{args} || [] };
if
(
my
$callback
=
$ep
->{callback} ) {
$self
->logger->debug(
"Invoking callback registered by $ep->{_pkg}"
);
$callback
->(
$obj
,
@args
);
}
elsif
(
$ep
->{service} &&
$ep
->{method} ) {
my
$service
=
$self
->getService(
$ep
->{service} );
if
(
$service
) {
if
(
my
$method
=
$service
->can(
$ep
->{method} ) ) {
$self
->logger->debug(
"Invoking $ep->{method} on $ep->{service}"
.
" on behalf of $ep->{_pkg}"
);
$service
->
$method
(
$obj
,
@args
);
}
else
{
$self
->logger->
warn
(
"Service $ep->{service} has no $ep->{method} method"
.
" in entrypoint added by $ep->{_pkg}"
);
}
}
else
{
$self
->logger->
warn
(
"Could not find service $ep->{service}"
.
" in entrypoint added by $ep->{_pkg}"
);
}
}
}
}
$self
->logger->debug(
"Plugin $plugin initialized"
);
return
$obj
;
}
sub
loadModule {
my
(
$self
,
$module
,
$conf
,
%args
) =
@_
;
$conf
//=
$self
->conf;
my
$obj
;
$module
=
"Lemonldap::NG::Portal$module"
if
(
$module
=~ /^::/ );
eval
"require $module"
;
if
($@) {
$self
->logger->error(
"$module load error: $@"
);
$self
->error(
"$module load error: $@"
);
return
0;
}
eval
{
$obj
=
$module
->new( {
p
=>
$self
,
conf
=>
$conf
,
%args
} );
$self
->logger->debug(
"Module $module loaded"
);
};
if
($@) {
$self
->logger->error(
"Unable to build $module object: $@"
);
return
0;
}
unless
(
$obj
) {
$self
->logger->error(
"$module new() method returned undef"
);
return
0;
}
if
(
$obj
->can(
"init"
) and ( !
$obj
->init ) ) {
$self
->logger->error(
"$module init failed"
);
$self
->error(
"$module init failed"
);
return
0;
}
$self
->loadedModules->{
$module
} =
$obj
;
return
$obj
;
}
sub
fail {
$_
[0]->logger->error(
$_
[0]->error );
$_
[0]->addUnauthRoute(
'*'
=>
'displayError'
);
$_
[0]->addAuthRoute(
'*'
=>
'displayError'
);
return
0;
}
sub
displayError {
my
(
$self
,
$req
) =
@_
;
return
$self
->sendError(
$req
,
'Portal error, contact your administrator'
, 500 );
}
sub
buildRule {
my
(
$self
,
$rule
,
$ruleDesc
) =
@_
;
if
(
$ruleDesc
) {
$ruleDesc
=
" $ruleDesc "
;
}
else
{
$ruleDesc
=
" "
;
}
my
$compiledRule
=
$self
->HANDLER->buildSub(
$self
->HANDLER->substitute(
$rule
) );
unless
(
$compiledRule
) {
my
$error
=
$self
->HANDLER->tsv->{jail}->error ||
'Unable to compile rule'
;
$self
->logger->error(
"Bad"
.
$ruleDesc
.
"rule: "
.
$error
);
}
return
$compiledRule
,;
}
sub
addPasswordPolicyDisplay {
my
(
$self
,
$id
,
$options
) =
@_
;
$self
->_ppRules->{
$id
} = {
%$options
};
}
sub
_addPluginEntryPoint {
my
(
$self
,
%entryPointDescription
) =
@_
;
push
@{
$self
->_pluginEntryPoints },
{
_pkg
=>
"[unknown]"
,
%entryPointDescription
};
}
1;