Merge branch 'v2.0'

This commit is contained in:
Christophe Maudoux 2020-04-25 23:15:38 +02:00
commit d2d9988b61
146 changed files with 3645 additions and 964 deletions

View File

@ -96,6 +96,12 @@ Comment: downloaded from
.
Author is unknown and license may be W3C or public-domain
Files: lemonldap-ng-portal/site/htdocs/static/common/modules/GitHub.png
Copyright: GitHub
License: MIT
Comment: downloaded from
https://commons.wikimedia.org/wiki/File:Octicons-mark-github.svg
Files: lemonldap-ng-portal/site/htdocs/static/bootstrap/u2f.png
Copyright: Bautsch <https://commons.wikimedia.org/wiki/User:Bautsch>
License: CC0-1.0

View File

@ -62,6 +62,12 @@
DocumentRoot __MANAGERAPIDIR__
<Location />
# By default, access to this VHost is denied
# If you want to enable the manager APIs, you MUST
# implement a robust authentication scheme to protect this
# VHost since LemonLDAP::NG provides no protection to the
# Manager APIs yet
Require all denied
<IfModule mod_deflate.c>

View File

@ -62,6 +62,12 @@
DocumentRoot __MANAGERAPIDIR__
<Location />
# By default, access to this VHost is denied
# If you want to enable the manager APIs, you MUST
# implement a robust authentication scheme to protect this
# VHost since LemonLDAP::NG provides no protection to the
# Manager APIs yet
<IfVersion >= 2.3>
Require all denied
</IfVersion>

View File

@ -62,6 +62,12 @@
DocumentRoot __MANAGERAPIDIR__
<Location />
# By default, access to this VHost is denied
# If you want to enable the manager APIs, you MUST
# implement a robust authentication scheme to protect this
# VHost since LemonLDAP::NG provides no protection to the
# Manager APIs yet
Order Deny,Allow
Deny from all

View File

@ -1,5 +1,6 @@
server {
listen __PORT__;
listen [::]:__PORT__;
server_name manager-api.__DNSDOMAIN__;
root __MANAGERAPIDIR__;
# Use "lm_app" format to get username in nginx.log (see nginx-lmlog.conf)
@ -36,8 +37,14 @@ server {
# Uncomment this if you use https only
#add_header Strict-Transport-Security "max-age=15768000";
# Access control
# By default, access to this VHost is denied
# If you want to enable the manager APIs, you MUST
# implement a robust authentication scheme to protect this
# VHost since LemonLDAP::NG provides no protection to the
# Manager APIs yet
#
#allow 127.0.0.0/8;
#allow ::1/128;
deny all;
}

View File

@ -18,6 +18,7 @@ include __CONFDIR__/nginx-lmlog.conf;
server {
listen __PORT__;
listen [::]:__PORT__;
server_name reload.__DNSDOMAIN__;
root /var/www/html;
@ -31,7 +32,8 @@ server {
#real_ip_header X-Forwarded-For;
location = /reload {
allow 127.0.0.1;
allow 127.0.0.1/8;
allow ::1/128;
deny all;
# FastCGI configuration
@ -55,7 +57,8 @@ server {
# Uncomment this if status is enabled
#location = /status {
# allow 127.0.0.1;
# allow 127.0.0.1/8;
# allow ::1/128;
# deny all;
# # FastCGI configuration
# include /etc/nginx/fastcgi_params;

View File

@ -1,5 +1,6 @@
server {
listen __PORT__;
listen [::]:__PORT__;
server_name manager.__DNSDOMAIN__;
root __MANAGERSITEDIR__;
# Use "lm_app" format to get username in nginx.log (see nginx-lmlog.conf)
@ -43,6 +44,7 @@ server {
index manager.psgi;
try_files $uri $uri/ =404;
allow 127.0.0.0/8;
allow ::1/128;
deny all;
}

View File

@ -12,6 +12,7 @@ upstream llng_portal_upstream {
server {
listen __PORT__;
listen [::]:__PORT__;
server_name auth.__DNSDOMAIN__;
root __PORTALSITEDIR__;
# Use "lm_app" format to get username in nginx.log (see nginx-lmlog.conf)

View File

@ -1,5 +1,6 @@
server {
listen __PORT__;
listen [::]:__PORT__;
server_name test1.__DNSDOMAIN__ test2.__DNSDOMAIN__;
root __TESTDIR__;
@ -113,7 +114,8 @@ server {
}
#location = /status {
# allow 127.0.0.1;
# allow 127.0.0.1/8;
# allow ::1/128;
# deny all;
# include /etc/nginx/fastcgi_params;
# fastcgi_pass unix:__FASTCGISOCKDIR__/llng-fastcgi.sock;

View File

@ -24,7 +24,7 @@ use constant MANAGERSECTION => "manager";
use constant SESSIONSEXPLORERSECTION => "sessionsExplorer";
use constant APPLYSECTION => "apply";
our $hashParameters = qr/^(?:(?:l(?:o(?:ca(?:lSessionStorageOption|tionRule)|goutService)|dapExportedVar|wp(?:Ssl)?Opt)|(?:(?:d(?:emo|bi)|facebook|webID)ExportedVa|exported(?:Heade|Va)|issuerDBGetParamete)r|re(?:moteGlobalStorageOption|st2f(?:Verify|Init)Arg|loadUrl)|g(?:r(?:antSessionRule|oup)|lobalStorageOption)|n(?:otificationStorageOption|ginxCustomHandler)|macro)s|o(?:idc(?:S(?:ervice(?:DynamicRegistrationEx(?:portedVar|traClaim)s|MetaDataAuthnContext)|torageOptions)|RPMetaData(?:(?:Option(?:sExtraClaim)?|ExportedVar|Macro)s|Node)|OPMetaData(?:(?:ExportedVar|Option)s|J(?:SON|WKS)|Node))|penIdExportedVars)|s(?:aml(?:S(?:PMetaData(?:(?:ExportedAttribute|Option|Macro)s|Node|XML)|torageOptions)|IDPMetaData(?:(?:ExportedAttribute|Option)s|Node|XML))|essionDataToRemember|laveExportedVars|fExtra)|c(?:as(?:A(?:ppMetaData(?:(?:ExportedVar|Option|Macro)s|Node)|ttributes)|S(?:rvMetaData(?:(?:ExportedVar|Option)s|Node)|torageOptions))|(?:ustom(?:Plugins|Add)Param|ombModule)s)|p(?:ersistentStorageOptions|o(?:rtalSkinRules|st))|a(?:ut(?:hChoiceMod|oSigninR)ules|pplicationList)|v(?:hostOptions|irtualHost)|S(?:MTPTLSOpts|SLVarIf))$/;
our $boolKeys = qr/^(?:s(?:aml(?:IDP(?:MetaDataOptions(?:(?:Check(?:S[LS]OMessageSignatur|Audienc|Tim)|IsPassiv)e|A(?:llow(?:LoginFromIDP|ProxiedAuthn)|daptSessionUtime)|Force(?:Authn|UTF8)|StoreSAMLToken|RelayStateURL)|SSODescriptorWantAuthnRequestsSigned)|S(?:P(?:MetaDataOptions(?:(?:CheckS[LS]OMessageSignatur|OneTimeUs)e|EnableIDPInitiatedURL|ForceUTF8)|SSODescriptor(?:WantAssertion|AuthnRequest)sSigned)|erviceUseCertificateInResponse)|DiscoveryProtocol(?:Activation|IsPassive)|CommonDomainCookieActivation|UseQueryStringSpecific|MetadataForceUTF8)|ingle(?:Session(?:UserByIP)?|(?:UserBy)?IP)|oap(?:Session|Config)Server|t(?:ayConnecte|orePasswor)d|kipRenewConfirmation|fRemovedUseNotif|laveDisplayLogo|howLanguages|slByAjax)|o(?:idc(?:RPMetaDataOptions(?:Re(?:freshToken|quirePKCE)|LogoutSessionRequired|IDTokenForceClaims|BypassConsent|AllowOffline|Public)|ServiceAllow(?:(?:AuthorizationCode|Implicit|Hybrid)Flow|DynamicRegistration)|OPMetaDataOptions(?:(?:CheckJWTSignatur|UseNonc)e|StoreIDToken))|ldNotifFormat)|c(?:a(?:ptcha_(?:register|login|mail)_enabled|sSrvMetaDataOptions(?:Gateway|Renew))|heck(?:User(?:Display(?:Empty(?:Header|Value)s|PersistentInfo))?|State|XSS)|o(?:ntextSwitchingStopWithLogout|mpactConf|rsEnabled)|da)|p(?:ortal(?:Display(?:Re(?:setPassword|gister)|GeneratePassword|PasswordPolicy)|ErrorOn(?:ExpiredSession|MailNotFound)|(?:CheckLogin|Statu)s|OpenLinkInNewWindow|RequireOldPassword|ForceAuthn|AntiFrame)|roxyUseSoap)|l(?:dap(?:(?:Group(?:DecodeSearchedValu|Recursiv)|UsePasswordResetAttribut)e|(?:AllowResetExpired|Set)Password|ChangePasswordAsUser|PpolicyControl|ITDS)|oginHistoryEnabled)|no(?:tif(?:ication(?:Server(?:(?:POS|GE)T|DELETE)?|sExplorer)?|y(?:Deleted|Other))|AjaxHook)|i(?:ssuerDB(?:OpenID(?:Connect)?|SAML|CAS|Get)Activation|mpersonationSkipEmptyValues)|to(?:tp2f(?:UserCan(?:Chang|Remov)eKey|DisplayExistingSecret)|kenUseGlobalStorage)|u(?:se(?:RedirectOn(?:Forbidden|Error)|SafeJail)|2fUserCanRemoveKey|pgradeSession)|br(?:uteForceProtection(?:IncrementalTempo)?|owsersDontStorePassword)|re(?:st(?:(?:Session|Config)Server|ExportSecretKeys)|freshSessions)|(?:mai(?:lOnPasswordChang|ntenanc)|vhostMaintenanc)e|d(?:isablePersistentStorage|biDynamicHashEnabled)|g(?:roupsBeforeMacros|lobalLogoutTimer)|h(?:ideOldPassword|ttpOnly)|yubikey2fUserCanRemoveKey|(?:activeTim|wsdlServ)er|krb(?:RemoveDomain|ByJs))$/;
our $boolKeys = qr/^(?:s(?:aml(?:IDP(?:MetaDataOptions(?:(?:Check(?:S[LS]OMessageSignatur|Audienc|Tim)|IsPassiv)e|A(?:llow(?:LoginFromIDP|ProxiedAuthn)|daptSessionUtime)|Force(?:Authn|UTF8)|StoreSAMLToken|RelayStateURL)|SSODescriptorWantAuthnRequestsSigned)|S(?:P(?:MetaDataOptions(?:(?:CheckS[LS]OMessageSignatur|OneTimeUs)e|EnableIDPInitiatedURL|ForceUTF8)|SSODescriptor(?:WantAssertion|AuthnRequest)sSigned)|erviceUseCertificateInResponse)|DiscoveryProtocol(?:Activation|IsPassive)|CommonDomainCookieActivation|UseQueryStringSpecific|MetadataForceUTF8)|oap(?:Session|Config)Server|t(?:ayConnecte|orePasswor)d|kipRenewConfirmation|fRemovedUseNotif|laveDisplayLogo|howLanguages|slByAjax)|o(?:idc(?:RPMetaDataOptions(?:Allow(?:PasswordGrant|Offline)|Re(?:freshToken|quirePKCE)|LogoutSessionRequired|IDTokenForceClaims|BypassConsent|Public)|ServiceAllow(?:(?:AuthorizationCode|Implicit|Hybrid)Flow|DynamicRegistration)|OPMetaDataOptions(?:(?:CheckJWTSignatur|UseNonc)e|StoreIDToken))|ldNotifFormat)|p(?:ortal(?:Display(?:Re(?:setPassword|gister)|GeneratePassword|PasswordPolicy)|ErrorOn(?:ExpiredSession|MailNotFound)|(?:CheckLogin|Statu)s|OpenLinkInNewWindow|RequireOldPassword|ForceAuthn|AntiFrame)|roxyUseSoap)|l(?:dap(?:(?:Group(?:DecodeSearchedValu|Recursiv)|UsePasswordResetAttribut)e|(?:AllowResetExpired|Set)Password|ChangePasswordAsUser|PpolicyControl|ITDS)|oginHistoryEnabled)|c(?:a(?:ptcha_(?:register|login|mail)_enabled|sSrvMetaDataOptions(?:Gateway|Renew))|o(?:ntextSwitchingStopWithLogout|mpactConf|rsEnabled)|heck(?:State|User|XSS)|da)|no(?:tif(?:ication(?:Server(?:(?:POS|GE)T|DELETE)?|sExplorer)?|y(?:Deleted|Other))|AjaxHook)|i(?:ssuerDB(?:OpenID(?:Connect)?|SAML|CAS|Get)Activation|mpersonationSkipEmptyValues)|to(?:tp2f(?:UserCan(?:Chang|Remov)eKey|DisplayExistingSecret)|kenUseGlobalStorage)|u(?:se(?:RedirectOn(?:Forbidden|Error)|SafeJail)|2fUserCanRemoveKey|pgradeSession)|br(?:uteForceProtection(?:IncrementalTempo)?|owsersDontStorePassword)|re(?:st(?:(?:Session|Config)Server|ExportSecretKeys)|freshSessions)|(?:mai(?:lOnPasswordChang|ntenanc)|vhostMaintenanc)e|d(?:isablePersistentStorage|biDynamicHashEnabled)|g(?:roupsBeforeMacros|lobalLogoutTimer)|h(?:ideOldPassword|ttpOnly)|yubikey2fUserCanRemoveKey|(?:activeTim|wsdlServ)er|krb(?:RemoveDomain|ByJs))$/;
our @sessionTypes = ( 'remoteGlobal', 'global', 'localSession', 'persistent', 'saml', 'oidc', 'cas' );

View File

@ -8,7 +8,7 @@ sub defaultValues {
'activeTimer' => 1,
'ADPwdExpireWarning' => 0,
'ADPwdMaxAge' => 0,
'apacheAuthnLevel' => 4,
'apacheAuthnLevel' => 3,
'applicationList' => {
'default' => {
'catname' => 'Default category',
@ -36,6 +36,9 @@ sub defaultValues {
'http://auth.example.com/certificateReset',
'certificateResetByMailValidityDelay' => 0,
'checkTime' => 600,
'checkUserDisplayEmptyHeaders' => 0,
'checkUserDisplayEmptyValues' => 0,
'checkUserDisplayPersistentInfo' => 0,
'checkUserHiddenAttributes' => '_loginHistory _session_id hGroups',
'checkUserIdRule' => 1,
'checkXSS' => 1,
@ -80,6 +83,9 @@ sub defaultValues {
'failedLoginNumber' => 5,
'favAppsMaxNumber' => 3,
'formTimeout' => 120,
'githubAuthnLevel' => 1,
'githubScope' => 'user:email',
'githubUserField' => 'login',
'globalLogoutRule' => 0,
'globalLogoutTimer' => 1,
'globalStorage' => 'Apache::Session::File',
@ -326,6 +332,9 @@ sub defaultValues {
'sfRemovedNotifTitle' => 'Second factor notification',
'sfRequired' => 0,
'showLanguages' => 1,
'singleIP' => 0,
'singleSession' => 0,
'singleUserByIP' => 0,
'slaveAuthnLevel' => 2,
'slaveExportedVars' => {},
'SMTPServer' => '',

View File

@ -733,7 +733,7 @@ sub sfExtra {
$tmp->{id} = "sfExtra/$mod";
$tmp->{type} = 'sfExtra';
$tmp->{data}->{$_} = $val->{$mod}->{$_}
foreach (qw(type rule logo label));
foreach (qw(type rule logo level label));
my $over = $val->{$mod}->{over} // {};
$tmp->{data}->{over} = [ map { [ $_, $over->{$_} ] } keys %$over ];
push @$res, $tmp;

View File

@ -27,7 +27,7 @@ our $specialNodeKeys = '(?:(?:(?:saml(?:ID|S)|oidc[OR])P|cas(?:App|Srv))MetaData
our $casAppMetaDataNodeKeys = 'casAppMetaData(?:Options(?:UserAttribut|Servic|Rul)e|(?:ExportedVar|Macro)s)';
our $casSrvMetaDataNodeKeys = 'casSrvMetaData(?:Options(?:ProxiedServices|DisplayName|SortNumber|Gateway|Renew|Icon|Url)|ExportedVars)';
our $oidcOPMetaDataNodeKeys = 'oidcOPMetaData(?:Options(?:C(?:lient(?:Secret|ID)|heckJWTSignature|onfigurationURI)|S(?:toreIDToken|ortNumber|cope)|TokenEndpointAuthMethod|(?:JWKSTimeou|Promp)t|I(?:DTokenMaxAge|con)|U(?:iLocales|seNonce)|Display(?:Name)?|AcrValues|MaxAge)|ExportedVars|J(?:SON|WKS))';
our $oidcRPMetaDataNodeKeys = 'oidcRPMetaData(?:Options(?:A(?:(?:uthorizationCode|ccessToken)Expiration|llowOffline)|I(?:DToken(?:ForceClaims|Expiration|SignAlg)|con)|R(?:e(?:directUris|freshToken|quirePKCE)|ule)|Logout(?:SessionRequired|Type|Url)|P(?:ostLogoutRedirectUris|ublic)|OfflineSessionExpiration|Client(?:Secret|ID)|BypassConsent|DisplayName|ExtraClaims|UserIDAttr)|(?:ExportedVar|Macro)s)';
our $oidcRPMetaDataNodeKeys = 'oidcRPMetaData(?:Options(?:A(?:(?:uthorizationCode|ccessToken)Expiration|llow(?:PasswordGrant|Offline)|dditionalAudiences)|I(?:DToken(?:ForceClaims|Expiration|SignAlg)|con)|R(?:e(?:directUris|freshToken|quirePKCE)|ule)|Logout(?:SessionRequired|Type|Url)|P(?:ostLogoutRedirectUris|ublic)|OfflineSessionExpiration|Client(?:Secret|ID)|BypassConsent|DisplayName|ExtraClaims|UserIDAttr)|(?:ExportedVar|Macro)s)';
our $samlIDPMetaDataNodeKeys = 'samlIDPMetaData(?:Options(?:(?:Check(?:S[LS]OMessageSignatur|Audienc|Tim)|EncryptionMod|UserAttribut|DisplayNam)e|S(?:ignS[LS]OMessage|toreSAMLToken|[LS]OBinding|ortNumber)|A(?:llow(?:LoginFromIDP|ProxiedAuthn)|daptSessionUtime)|Re(?:questedAuthnContext|solutionRule|layStateURL)|Force(?:Authn|UTF8)|I(?:sPassive|con)|NameIDFormat)|ExportedAttributes|XML)';
our $samlSPMetaDataNodeKeys = 'samlSPMetaData(?:Options(?:N(?:ameID(?:SessionKey|Format)|otOnOrAfterTimeout)|S(?:essionNotOnOrAfterTimeout|ignS[LS]OMessage)|(?:CheckS[LS]OMessageSignatur|OneTimeUs|Rul)e|En(?:ableIDPInitiatedURL|cryptionMode)|ForceUTF8)|(?:ExportedAttribute|Macro)s|XML)';
our $virtualHostKeys = '(?:vhost(?:A(?:uthnLevel|liases)|(?:Maintenanc|Typ)e|ServiceTokenTTL|Https|Port)|(?:exportedHeader|locationRule)s|post)';
@ -42,6 +42,7 @@ our $authParameters = {
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)],
githubParams => [qw(githubAuthnLevel githubClientID githubClientSecret githubUserField githubScope)],
gpgParams => [qw(gpgAuthnLevel gpgDb)],
kerberosParams => [qw(krbAuthnLevel krbKeytab krbByJs krbRemoveDomain)],
ldapParams => [qw(ldapAuthnLevel ldapExportedVars ldapServer ldapPort ldapBase managerDn managerPassword ldapTimeout ldapVersion ldapRaw LDAPFilter AuthLDAPFilter mailLDAPFilter ldapSearchDeref ldapGroupBase ldapGroupObjectClass ldapGroupAttributeName ldapGroupAttributeNameUser ldapGroupAttributeNameSearch ldapGroupDecodeSearchedValue ldapGroupRecursive ldapGroupAttributeNameGroup ldapPpolicyControl ldapSetPassword ldapChangePasswordAsUser ldapPwdEnc ldapUsePasswordResetAttribute ldapPasswordResetAttribute ldapPasswordResetAttributeValue ldapAllowResetExpiredPassword ldapITDS)],

View File

@ -72,6 +72,7 @@ sub encodings { $_[0]->env->{HTTP_ACCEPT_ENCODING} }
sub languages { $_[0]->env->{HTTP_ACCEPT_LANGUAGE} }
sub authorization { $_[0]->env->{HTTP_AUTHORIZATION} }
sub hostname { $_[0]->env->{HTTP_HOST} }
sub origin { $_[0]->env->{HTTP_ORIGIN} }
sub referer { $_[0]->env->{REFERER} }
sub query_string { $_[0]->env->{QUERY_STRING} }

View File

@ -8,7 +8,7 @@ sub run {
my ( $class, $req, $rule, $protection ) = @_;
my $uri = $req->{env}->{REQUEST_URI};
my $cn = $class->tsv->{cookieName};
my ( $id, $ret, $session );
my ( $id, $session );
if ( $uri =~ s/[\?&;]${cn}cda=(\w+)$//oi ) {
if ( $id = $class->fetchId($req)
and $session = $class->retrieveSession( $req, $id ) )
@ -48,10 +48,8 @@ sub run {
return $class->REDIRECT;
}
}
( $ret, $session ) =
$class->Lemonldap::NG::Handler::Main::run( $req, $rule, $protection );
return $ret;
return $class->Lemonldap::NG::Handler::Main::run( $req, $rule,
$protection );
}
## @rmethod protected hash getCDAInfos(id)

View File

@ -172,7 +172,9 @@ sub tplParams {
sub javascript {
my ( $self, $req ) = @_;
my $res = eval {
$self->hLoadedPlugins->{viewer}->diffRule->( $req, $req->{userData} );
$self->hLoadedPlugins->{viewer}
&& $self->hLoadedPlugins->{viewer}
->diffRule->( $req, $req->{userData} );
} || 0;
print STDERR $@ if $@;
my $impPrefix = $self->{impersonationPrefix} || 'real_';

View File

@ -353,7 +353,7 @@ sub _checkType {
return {
res => "ko",
code => 405,
code => 400,
msg =>
"Invalid input: Type \"$type\" does not exist. Allowed values for type are: \"U2F\", \"TOTP\" or \"UBK\""
}

View File

@ -20,17 +20,16 @@ sub _isSimpleKeyValueHash {
return 1;
}
sub _setDefaultValues {
my ( $self, $attrs, $rootNode ) = @_;
sub _getDefaultValues {
my ( $self, $rootNode ) = @_;
my @allAttrs = $self->_listAttributes($rootNode);
my $defaultAttrs = Lemonldap::NG::Manager::Build::Attributes::attributes();
my $attrs = {};
foreach $attr (@allAttrs) {
unless ( defined $attrs->{$attr} ) {
$attrs->{$attr} = $defaultAttrs->{$attr}->{default}
if ( defined $defaultAttrs->{$attr}
&& defined $defaultAttrs->{$attr}->{default} );
}
$attrs->{$attr} = $defaultAttrs->{$attr}->{default}
if ( defined $defaultAttrs->{$attr}
&& defined $defaultAttrs->{$attr}->{default} );
}
return $attrs;
@ -47,7 +46,7 @@ sub _hasAllowedAttributes {
msg => "Invalid input: Attribute $attribute is not a string."
};
}
unless ( grep { /^$attribute$/ } @allowedAttributes ) {
unless ( grep { $_ eq $attribute } @allowedAttributes ) {
return {
res => "ko",
msg => "Invalid input: Attribute $attribute does not exist."
@ -76,4 +75,41 @@ sub _listNodeAttributes {
return @attributes;
}
sub _translateOptionApiToConf {
my ( $self, $optionName, $prefix ) = @_;
# For consistency
$optionName =~ s/^clientId$/clientID/;
return $prefix . "MetaDataOptions" . ( ucfirst $optionName );
}
sub _translateOptionConfToApi {
my ( $self, $optionName ) = @_;
$optionName =~ s/^(\w+)MetaDataOptions//;
$optionName = lcfirst $optionName;
# iDToken looks ugly
$optionName =~ s/^iDToken/IDToken/;
# For consistency
$optionName =~ s/^clientID/clientId/;
return $optionName;
}
sub _getRegexpFromPattern {
my ( $self, $pattern ) = @_;
return unless ( $pattern =~ /[\w\.\-\*]+/ );
# . is allowed, and must be escaped
$pattern =~ s/\./\\\./g;
$pattern =~ s/\*/\.\*/g;
# anchor string, unless * was provided
$pattern = "^$pattern\$" if ( $pattern =~ /\*/ );
return qr/$pattern/;
}
1;

View File

@ -7,6 +7,7 @@ package Lemonldap::NG::Manager::Api;
use 5.10.0;
use utf8;
use Mouse;
use Lemonldap::NG::Manager::Conf::Parser;
extends 'Lemonldap::NG::Manager::Api::Common';
@ -40,9 +41,14 @@ sub findOidcRpByConfKey {
: ( defined $req->params('pattern') ? $req->params('pattern') : undef )
);
return $self->sendError( $req, 'Invalid input: pattern is missing', 405 )
return $self->sendError( $req, 'Invalid input: pattern is missing', 400 )
unless ( defined $pattern );
unless ( $pattern = $self->_getRegexpFromPattern($pattern) ) {
return $self->sendError( $req, 'Invalid input: pattern is invalid',
400 );
}
$self->logger->debug(
"[API] Find OIDC RPs by confKey regexp $pattern requested");
@ -67,7 +73,7 @@ sub findOidcRpByClientId {
)
);
return $self->sendError( $req, 'Invalid input: clientId is missing', 405 )
return $self->sendError( $req, 'Invalid input: clientId is missing', 400 )
unless ( defined $clientId );
$self->logger->debug("[API] Find OIDC RPs by clientId $clientId requested");
@ -87,15 +93,31 @@ sub addOidcRp {
my ( $self, $req ) = @_;
my $add = $req->jsonBodyToObj;
return $self->sendError( $req, "Invalid input: " . $req->error, 405 )
return $self->sendError( $req, "Invalid input: " . $req->error, 400 )
unless ($add);
return $self->sendError( $req, 'Invalid input: confKey is missing', 405 )
return $self->sendError( $req, 'Invalid input: confKey is missing', 400 )
unless ( defined $add->{confKey} );
return $self->sendError( $req, 'Invalid input: clientId is missing', 405 )
return $self->sendError( $req, 'Invalid input: confKey is not a string',
400 )
if ( ref $add->{confKey} );
return $self->sendError( $req, 'Invalid input: clientId is missing', 400 )
unless ( defined $add->{clientId} );
return $self->sendError( $req, 'Invalid input: clientId is not a string',
400 )
if ( ref $add->{clientId} );
return $self->sendError( $req, 'Invalid input: redirectUris is missing',
400 )
unless ( defined $add->{redirectUris} );
return $self->sendError( $req,
'Invalid input: redirectUris must be an array', 400 )
unless ( ref( $add->{redirectUris} ) eq "ARRAY" );
$self->logger->debug(
"[API] Add OIDC RP with confKey $add->{confKey} and clientId $add->{clientId} requested"
);
@ -106,25 +128,29 @@ sub addOidcRp {
return $self->sendError(
$req,
"Invalid input: An OIDC RP with confKey $add->{confKey} already exists",
405
409
) if ( defined $self->_getOidcRpByConfKey( $conf, $add->{confKey} ) );
return $self->sendError(
$req,
"Invalid input: An OIDC RP with clientId $add->{clientId} already exists",
405
409
) if ( defined $self->_getOidcRpByClientId( $conf, $add->{clientId} ) );
$add->{options} = {} unless ( defined $add->{options} );
$add->{options}->{oidcRPMetaDataOptionsClientID} = $add->{clientId};
$add->{options}->{clientId} = $add->{clientId};
$add->{options}->{redirectUris} = $add->{redirectUris};
my $res = $self->_pushOidcRp( $conf, $add->{confKey}, $add, 1 );
return $self->sendError( $req, $res->{msg}, 405 )
return $self->sendError( $req, $res->{msg}, 400 )
unless ( $res->{res} eq 'ok' );
return $self->sendJSONresponse( $req,
{ message => "Successful operation" } );
return $self->sendJSONresponse(
$req,
{ message => "Successful operation" },
code => 201
);
}
sub updateOidcRp {
@ -134,7 +160,7 @@ sub updateOidcRp {
my $update = $req->jsonBodyToObj;
return $self->sendError( $req, "Invalid input: " . $req->error, 405 )
return $self->sendError( $req, "Invalid input: " . $req->error, 400 )
unless ($update);
$self->logger->debug(
@ -154,16 +180,15 @@ sub updateOidcRp {
# check if new clientID exists already
my $res = $self->_isNewOidcRpClientIdUnique( $conf, $confKey, $update );
return $self->sendError( $req, $res->{msg}, 405 )
return $self->sendError( $req, $res->{msg}, 409 )
unless ( $res->{res} eq 'ok' );
$res = $self->_pushOidcRp( $conf, $confKey, $update, 0 );
return $self->sendError( $req, $res->{msg}, 405 )
return $self->sendError( $req, $res->{msg}, 400 )
unless ( $res->{res} eq 'ok' );
return $self->sendJSONresponse( $req,
{ message => "Successful operation" } );
return $self->sendJSONresponse( $req, undef, code => 204 );
}
sub replaceOidcRp {
@ -173,11 +198,31 @@ sub replaceOidcRp {
my $replace = $req->jsonBodyToObj;
return $self->sendError( $req, "Invalid input: " . $req->error, 405 )
return $self->sendError( $req, "Invalid input: " . $req->error, 400 )
unless ($replace);
return $self->sendError( $req, 'Invalid input: clientId is missing', 405 )
return $self->sendError( $req, 'Invalid input: confKey is missing', 400 )
unless ( defined $replace->{confKey} );
return $self->sendError( $req, 'Invalid input: confKey is not a string',
400 )
if ( ref $replace->{confKey} );
return $self->sendError( $req, 'Invalid input: clientId is missing', 400 )
unless ( defined $replace->{clientId} );
return $self->sendError( $req, 'Invalid input: clientId is not a string',
400 )
if ( ref $replace->{clientId} );
return $self->sendError( $req, 'Invalid input: redirectUris is missing',
400 )
unless ( defined $replace->{redirectUris} );
return $self->sendError( $req,
'Invalid input: redirectUris must be an array', 400 )
unless ( ref( $replace->{redirectUris} ) eq "ARRAY" );
$self->logger->debug(
"[API] OIDC RP $confKey configuration replace requested");
@ -192,15 +237,18 @@ sub replaceOidcRp {
# check if new clientID exists already
my $res = $self->_isNewOidcRpClientIdUnique( $conf, $confKey, $replace );
return $self->sendError( $req, $res->{msg}, 405 )
return $self->sendError( $req, $res->{msg}, 409 )
unless ( $res->{res} eq 'ok' );
$replace->{options} = {} unless ( defined $replace->{options} );
$replace->{options}->{clientId} = $replace->{clientId};
$replace->{options}->{redirectUris} = $replace->{redirectUris};
$res = $self->_pushOidcRp( $conf, $confKey, $replace, 1 );
return $self->sendError( $req, $res->{msg}, 405 )
return $self->sendError( $req, $res->{msg}, 400 )
unless ( $res->{res} eq 'ok' );
return $self->sendJSONresponse( $req,
{ message => "Successful operation" } );
return $self->sendJSONresponse( $req, undef, code => 204 );
}
sub deleteOidcRp {
@ -222,12 +270,12 @@ sub deleteOidcRp {
delete $conf->{oidcRPMetaDataOptions}->{$confKey};
delete $conf->{oidcRPMetaDataExportedVars}->{$confKey};
delete $conf->{oidcRPMetaDataOptionsExtraClaims}->{$confKey};
delete $conf->{oidcRPMetaDataMacros}->{$confKey};
# Save configuration
$self->_confAcc->saveConf($conf);
return $self->sendJSONresponse( $req,
{ message => "Successful operation" } );
return $self->sendJSONresponse( $req, undef, code => 204 );
}
sub _getOidcRpByConfKey {
@ -244,16 +292,37 @@ sub _getOidcRpByConfKey {
my $exportedVars = $conf->{oidcRPMetaDataExportedVars}->{$confKey};
# Get extra claim
my $extraClaim = $conf->{oidcRPMetaDataOptionsExtraClaims}->{$confKey};
my $extraClaims = $conf->{oidcRPMetaDataOptionsExtraClaims}->{$confKey};
# Get macros
my $macros = $conf->{oidcRPMetaDataMacros}->{$confKey} || {};
# Get options
my $options = $conf->{oidcRPMetaDataOptions}->{$confKey};
my $options = {};
for
my $configOption ( keys %{ $conf->{oidcRPMetaDataOptions}->{$confKey} } )
{
# redirectUris is handled as an array
if ( $configOption eq "oidcRPMetaDataOptionsRedirectUris" ) {
$options->{ $self->_translateOptionConfToApi($configOption) } = [
split(
/\s+/,
$conf->{oidcRPMetaDataOptions}->{$confKey}->{$configOption}
)
];
}
else {
$options->{ $self->_translateOptionConfToApi($configOption) } =
$conf->{oidcRPMetaDataOptions}->{$confKey}->{$configOption};
}
}
return {
confKey => $confKey,
clientId => $clientId,
exportedVars => $exportedVars,
extraClaim => $extraClaim,
extraClaims => $extraClaims,
macros => $macros,
options => $options
};
}
@ -275,7 +344,7 @@ sub _isNewOidcRpClientIdUnique {
my $curClientId = $self->_getOidcRpByConfKey( $conf, $confKey )->{clientId};
my $newClientId =
$oidcRp->{clientId}
|| $oidcRp->{options}->{oidcRPMetaDataOptionsClientID}
|| $oidcRp->{options}->{clientId}
|| "";
if ( $newClientId ne '' && $newClientId ne $curClientId ) {
return {
@ -292,23 +361,46 @@ sub _isNewOidcRpClientIdUnique {
sub _pushOidcRp {
my ( $self, $conf, $confKey, $push, $replace ) = @_;
my $translatedOptions = {};
if ($replace) {
$conf->{oidcRPMetaDataOptions}->{$confKey} = {};
$conf->{oidcRPMetaDataExportedVars}->{$confKey} = {};
$conf->{oidcRPMetaDataOptionsExtraClaims}->{$confKey} = {};
$push->{options} =
$self->_setDefaultValues( $push->{options}, 'oidcRPMetaDataNode' );
$conf->{oidcRPMetaDataMacros}->{$confKey} = {};
$translatedOptions = $self->_getDefaultValues('oidcRPMetaDataNode');
}
if ( defined $push->{options} ) {
my $res = $self->_hasAllowedAttributes( $push->{options},
foreach ( keys %{ $push->{options} } ) {
# redirectUris is handled as an array
if ( $_ eq 'redirectUris' ) {
my $option = $push->{options}->{$_};
return {
res => 'ko',
msg => "Invalid input: redirectUris is not an array"
}
unless ( ref($option) eq "ARRAY" );
$translatedOptions->{ $self->_translateOptionApiToConf( $_,
'oidcRP' ) } = join( ' ', @{ $push->{options}->{$_} } );
}
else {
$translatedOptions->{ $self->_translateOptionApiToConf( $_,
'oidcRP' ) } = $push->{options}->{$_};
}
}
my $res = $self->_hasAllowedAttributes( $translatedOptions,
'oidcRPMetaDataNode' );
return $res unless ( $res->{res} eq 'ok' );
foreach ( keys %{ $push->{options} } ) {
foreach ( keys %{$translatedOptions} ) {
$conf->{oidcRPMetaDataOptions}->{$confKey}->{$_} =
$push->{options}->{$_};
$translatedOptions->{$_};
}
}
$conf->{oidcRPMetaDataOptions}->{$confKey}->{oidcRPMetaDataOptionsClientID}
@ -331,22 +423,54 @@ sub _pushOidcRp {
}
}
if ( defined $push->{extraClaim} ) {
if ( $self->_isSimpleKeyValueHash( $push->{extraClaim} ) ) {
foreach ( keys %{ $push->{extraClaim} } ) {
$conf->{oidcRPMetaDataOptionsExtraClaims}->{$confKey}->{$_} =
$push->{extraClaim}->{$_};
if ( defined $push->{macros} ) {
if ( $self->_isSimpleKeyValueHash( $push->{macros} ) ) {
foreach ( keys %{ $push->{macros} } ) {
$conf->{oidcRPMetaDataMacros}->{$confKey}->{$_} =
$push->{macros}->{$_};
}
}
else {
return {
res => 'ko',
msg =>
"Invalid input: extraClaim is not a hash object with \"key\":\"value\" attributes"
"Invalid input: macros is not a hash object with \"key\":\"value\" attributes"
};
}
}
if ( defined $push->{extraClaims} ) {
if ( $self->_isSimpleKeyValueHash( $push->{extraClaims} ) ) {
foreach ( keys %{ $push->{extraClaims} } ) {
$conf->{oidcRPMetaDataOptionsExtraClaims}->{$confKey}->{$_} =
$push->{extraClaims}->{$_};
}
}
else {
return {
res => 'ko',
msg =>
"Invalid input: extraClaims is not a hash object with \"key\":\"value\" attributes"
};
}
}
# Test new configuration
my $parser = Lemonldap::NG::Manager::Conf::Parser->new( {
refConf => $self->_confAcc->getConf,
newConf => $conf,
req => {},
}
);
unless ( $parser->testNewConf( $self->p ) ) {
return {
res => 'ko',
code => 400,
msg => "Configuration error: "
. join( ". ", map { $_->{message} } @{ $parser->errors } ),
};
}
# Save configuration
$self->_confAcc->saveConf($conf);

View File

@ -38,9 +38,14 @@ sub findSamlSpByConfKey {
: ( defined $req->params('pattern') ? $req->params('pattern') : undef )
);
return $self->sendError( $req, 'Invalid input: pattern is missing', 405 )
return $self->sendError( $req, 'Invalid input: pattern is missing', 400 )
unless ( defined $pattern );
unless ( $pattern = $self->_getRegexpFromPattern($pattern) ) {
return $self->sendError( $req, 'Invalid input: pattern is invalid',
400 );
}
$self->logger->debug(
"[API] Find SAML SPs by confKey regexp $pattern requested");
@ -63,7 +68,7 @@ sub findSamlSpByEntityId {
)
);
return $self->sendError( $req, 'entityId is missing', 405 )
return $self->sendError( $req, 'entityId is missing', 400 )
unless ( defined $entityId );
$self->logger->debug("[API] Find SAML SPs by entityId $entityId requested");
@ -82,19 +87,19 @@ sub addSamlSp {
my ( $self, $req ) = @_;
my $add = $req->jsonBodyToObj;
return $self->sendError( $req, "Invalid input: " . $req->error, 405 )
return $self->sendError( $req, "Invalid input: " . $req->error, 400 )
unless ($add);
return $self->sendError( $req, 'Invalid input: confKey is missing', 405 )
return $self->sendError( $req, 'Invalid input: confKey is missing', 400 )
unless ( defined $add->{confKey} );
return $self->sendError( $req, 'Invalid input: metadata is missing', 405 )
return $self->sendError( $req, 'Invalid input: metadata is missing', 400 )
unless ( defined $add->{metadata} );
my $entityId = $self->_readSamlSpEntityId( $add->{metadata} );
return $self->sendError( $req,
'Invalid input: entityID is missing in metadata', 405 )
'Invalid input: entityID is missing in metadata', 400 )
unless ( defined $entityId );
$self->logger->debug(
@ -107,20 +112,23 @@ sub addSamlSp {
return $self->sendError(
$req,
"Invalid input: A SAML SP with confKey $add->{confKey} already exists",
405
409
) if ( defined $self->_getSamlSpByConfKey( $conf, $add->{confKey} ) );
return $self->sendError( $req,
"Invalid input: A SAML SP with entityID $entityId already exists", 405 )
"Invalid input: A SAML SP with entityID $entityId already exists", 409 )
if ( defined $self->_getSamlSpByEntityId( $conf, $entityId ) );
my $res = $self->_pushSamlSp( $conf, $add->{confKey}, $add, 1 );
return $self->sendError( $req, $res->{msg}, 405 )
return $self->sendError( $req, $res->{msg}, 400 )
unless ( $res->{res} eq 'ok' );
return $self->sendJSONresponse( $req,
{ message => "Successful operation" } );
return $self->sendJSONresponse(
$req,
{ message => "Successful operation" },
code => 201
);
}
sub replaceSamlSp {
@ -131,10 +139,10 @@ sub replaceSamlSp {
my $replace = $req->jsonBodyToObj;
return $self->sendError( $req, "Invalid input: " . $req->error, 405 )
return $self->sendError( $req, "Invalid input: " . $req->error, 400 )
unless ($replace);
return $self->sendError( $req, 'Invalid input: metadata is missing', 405 )
return $self->sendError( $req, 'Invalid input: metadata is missing', 400 )
unless ( defined $replace->{metadata} );
$self->logger->debug(
@ -152,16 +160,15 @@ sub replaceSamlSp {
# check if new entityId exists already
my $res = $self->_isNewSamlSpEntityIdUnique( $conf, $confKey, $replace );
return $self->sendError( $req, $res->{msg}, 405 )
return $self->sendError( $req, $res->{msg}, 409 )
unless ( $res->{res} eq 'ok' );
$res = $self->_pushSamlSp( $conf, $confKey, $replace, 1 );
return $self->sendError( $req, $res->{msg}, 405 )
return $self->sendError( $req, $res->{msg}, 400 )
unless ( $res->{res} eq 'ok' );
return $self->sendJSONresponse( $req,
{ message => "Successful operation" } );
return $self->sendJSONresponse( $req, undef, code => 204 );
}
sub updateSamlSp {
@ -172,7 +179,7 @@ sub updateSamlSp {
my $update = $req->jsonBodyToObj;
return $self->sendError( $req, "Invalid input: " . $req->error, 405 )
return $self->sendError( $req, "Invalid input: " . $req->error, 400 )
unless ($update);
$self->logger->debug(
@ -192,17 +199,16 @@ sub updateSamlSp {
# check if new entityId exists already
$res = $self->_isNewSamlSpEntityIdUnique( $conf, $confKey, $update );
return $self->sendError( $req, $res->{msg}, 405 )
return $self->sendError( $req, $res->{msg}, 409 )
unless ( $res->{res} eq 'ok' );
}
$res = $self->_pushSamlSp( $conf, $confKey, $update, 0 );
return $self->sendError( $req, $res->{msg}, 405 )
return $self->sendError( $req, $res->{msg}, 400 )
unless ( $res->{res} eq 'ok' );
return $self->sendJSONresponse( $req,
{ message => "Successful operation" } );
return $self->sendJSONresponse( $req, undef, code => 204 );
}
sub deleteSamlSp {
@ -225,12 +231,12 @@ sub deleteSamlSp {
delete $conf->{samlSPMetaDataXML}->{$confKey};
delete $conf->{samlSPMetaDataOptions}->{$confKey};
delete $conf->{samlSPMetaDataExportedAttributes}->{$confKey};
delete $conf->{samlSPMetaDataMacros}->{$confKey};
# Save configuration
$self->_confAcc->saveConf($conf);
return $self->sendJSONresponse( $req,
{ message => "Successful operation" } );
return $self->sendJSONresponse( $req, undef, code => 204 );
}
sub _getSamlSpByConfKey {
@ -243,12 +249,21 @@ sub _getSamlSpByConfKey {
my $metadata = $conf->{samlSPMetaDataXML}->{$confKey}->{samlSPMetaDataXML};
# Get options
my $options = $conf->{samlSPMetaDataOptions}->{$confKey};
my $options = {};
for my $confOption ( keys %{ $conf->{samlSPMetaDataOptions}->{$confKey} } )
{
$options->{ $self->_translateOptionConfToApi($confOption) } =
$conf->{samlSPMetaDataOptions}->{$confKey}->{$confOption};
}
# Get macros
my $macros = $conf->{samlSPMetaDataMacros}->{$confKey} || {};
my $samlSp = {
confKey => $confKey,
metadata => $metadata,
exportedAttributes => {},
macros => $macros,
options => $options
};
@ -344,25 +359,48 @@ sub _readSamlSpExportedAttributes {
sub _pushSamlSp {
my ( $self, $conf, $confKey, $push, $replace ) = @_;
my $translatedOptions = {};
if ($replace) {
$conf->{samlSPMetaDataXML}->{$confKey} = {};
$conf->{samlSPMetaDataOptions}->{$confKey} = {};
$conf->{samlSPMetaDataMacros}->{$confKey} = {};
$conf->{samlSPMetaDataExportedAttributes}->{$confKey} = {};
$push->{options} =
$self->_setDefaultValues( $push->{options}, 'samlSPMetaDataNode' );
$translatedOptions = $self->_getDefaultValues('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} } ) {
$translatedOptions->{ $self->_translateOptionApiToConf( $_,
'samlSP' ) } = $push->{options}->{$_};
}
my $res = $self->_hasAllowedAttributes( $translatedOptions,
'samlSPMetaDataNode' );
return $res unless ( $res->{res} eq 'ok' );
foreach ( keys %{$translatedOptions} ) {
$conf->{samlSPMetaDataOptions}->{$confKey}->{$_} =
$push->{options}->{$_};
$translatedOptions->{$_};
}
}
if ( defined $push->{macros} ) {
if ( $self->_isSimpleKeyValueHash( $push->{macros} ) ) {
foreach ( keys %{ $push->{macros} } ) {
$conf->{samlSPMetaDataMacros}->{$confKey}->{$_} =
$push->{macros}->{$_};
}
}
else {
return {
res => 'ko',
msg =>
"Invalid input: macros is not a hash object with \"key\":\"value\" attributes"
};
}
}
@ -376,6 +414,22 @@ sub _pushSamlSp {
$res->{exportedAttributes};
}
# Test new configuration
my $parser = Lemonldap::NG::Manager::Conf::Parser->new( {
refConf => $self->_confAcc->getConf,
newConf => $conf,
req => {},
}
);
unless ( $parser->testNewConf( $self->p ) ) {
return {
res => 'ko',
code => 400,
msg => "Configuration error: "
. join( ". ", map { $_->{message} } @{ $parser->errors } ),
};
}
# Save configuration
$self->_confAcc->saveConf($conf);

View File

@ -224,6 +224,7 @@ m[^(?:(?:\-+\s*BEGIN\s+(?:PUBLIC\s+KEY|CERTIFICATE)\s*\-+\r?\n)?[a-zA-Z0-9/\+\r\
},
'select' => {
'test' => sub {
return 0, 'Value is not a scalar' if ref $_[0];
my $test = grep( { $_ eq $_[0]; }
map( { $_->{'k'}; } @{ $_[2]{'select'}; } ) );
return $test
@ -271,7 +272,7 @@ sub attributes {
'type' => 'int'
},
'apacheAuthnLevel' => {
'default' => 4,
'default' => 3,
'type' => 'int'
},
'applicationList' => {
@ -314,6 +315,10 @@ sub attributes {
'k' => 'Facebook',
'v' => 'Facebook'
},
{
'k' => 'GitHub',
'v' => 'GitHub'
},
{
'k' => 'GPG',
'v' => 'GPG'
@ -509,6 +514,10 @@ sub attributes {
'k' => 'Facebook',
'v' => 'Facebook'
},
{
'k' => 'GitHub',
'v' => 'GitHub'
},
{
'k' => 'GPG',
'v' => 'GPG'
@ -842,15 +851,15 @@ qr/(?:(?:https?):\/\/(?:(?:(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.]
},
'checkUserDisplayEmptyHeaders' => {
'default' => 0,
'type' => 'bool'
'type' => 'boolOrExpr'
},
'checkUserDisplayEmptyValues' => {
'default' => 0,
'type' => 'bool'
'type' => 'boolOrExpr'
},
'checkUserDisplayPersistentInfo' => {
'default' => 0,
'type' => 'bool'
'type' => 'boolOrExpr'
},
'checkUserHiddenAttributes' => {
'default' => '_loginHistory _session_id hGroups',
@ -894,6 +903,10 @@ qr/(?:(?:https?):\/\/(?:(?:(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.]
'k' => 'Facebook',
'v' => 'Facebook'
},
{
'k' => 'GitHub',
'v' => 'GitHub'
},
{
'k' => 'GPG',
'v' => 'GPG'
@ -1301,6 +1314,24 @@ qr/^(?:\*\.)?(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*(?:[a-zA-Z][
'default' => 120,
'type' => 'int'
},
'githubAuthnLevel' => {
'default' => 1,
'type' => 'int'
},
'githubClientID' => {
'type' => 'text'
},
'githubClientSecret' => {
'type' => 'password'
},
'githubScope' => {
'default' => 'user:email',
'type' => 'text'
},
'githubUserField' => {
'default' => 'login',
'type' => 'text'
},
'globalLogoutCustomParam' => {
'type' => 'text'
},
@ -2098,10 +2129,17 @@ qr/^(?:\*\.)?(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*(?:[a-zA-Z][
'oidcRPMetaDataOptionsAccessTokenExpiration' => {
'type' => 'int'
},
'oidcRPMetaDataOptionsAdditionalAudiences' => {
'type' => 'text'
},
'oidcRPMetaDataOptionsAllowOffline' => {
'default' => 0,
'type' => 'bool'
},
'oidcRPMetaDataOptionsAllowPasswordGrant' => {
'default' => 0,
'type' => 'bool'
},
'oidcRPMetaDataOptionsAuthorizationCodeExpiration' => {
'type' => 'int'
},
@ -2120,6 +2158,7 @@ qr/^(?:\*\.)?(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*(?:[a-zA-Z][
},
'oidcRPMetaDataOptionsExtraClaims' => {
'default' => {},
'keyTest' => qr/^[\x21\x23-\x5B\x5D-\x7E]+$/,
'type' => 'keyTextContainer'
},
'oidcRPMetaDataOptionsIcon' => {
@ -2129,7 +2168,8 @@ qr/^(?:\*\.)?(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*(?:[a-zA-Z][
'type' => 'int'
},
'oidcRPMetaDataOptionsIDTokenForceClaims' => {
'type' => 'bool'
'default' => 0,
'type' => 'bool'
},
'oidcRPMetaDataOptionsIDTokenSignAlg' => {
'default' => 'HS512',
@ -2173,10 +2213,6 @@ qr/^(?:\*\.)?(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*(?:[a-zA-Z][
'select' => [ {
'k' => 'front',
'v' => 'Front Channel'
},
{
'k' => 'back',
'v' => 'Back Channel'
}
],
'type' => 'select'
@ -2246,7 +2282,8 @@ qr/^(?:\*\.)?(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*(?:[a-zA-Z][
'type' => 'keyTextContainer'
},
'oidcServiceDynamicRegistrationExtraClaims' => {
'type' => 'keyTextContainer'
'keyTest' => qr/^[\x21\x23-\x5B\x5D-\x7E]+$/,
'type' => 'keyTextContainer'
},
'oidcServiceIDTokenExpiration' => {
'default' => 3600,
@ -3593,19 +3630,15 @@ qr/(?:(?:https?):\/\/(?:(?:(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.]
},
'singleIP' => {
'default' => 0,
'type' => 'bool'
'type' => 'boolOrExpr'
},
'singleSession' => {
'default' => 0,
'type' => 'bool'
},
'singleSessionUserByIP' => {
'default' => 0,
'type' => 'bool'
'type' => 'boolOrExpr'
},
'singleUserByIP' => {
'default' => 0,
'type' => 'bool'
'type' => 'boolOrExpr'
},
'skipRenewConfirmation' => {
'default' => 0,

View File

@ -25,7 +25,8 @@ sub perlExpr {
$Lemonldap::NG::Common::Safelib::functions );
$cpt->reval("BEGIN { 'warnings'->unimport; } $val");
my $err = join( '',
grep { $_ =~ /(?:Undefined subroutine|Devel::StackTrace)/ ? () : $_ } split( /\n/, $@ ) );
grep { $_ =~ /(?:Undefined subroutine|Devel::StackTrace)/ ? () : $_ }
split( /\n/, $@ ) );
return $err ? ( -1, "__badExpression__: $err" ) : (1);
}
@ -54,8 +55,8 @@ sub types {
msgFail => '__badUrl__',
},
PerlModule => {
form => 'text',
test => qr/^(?:[a-zA-Z][a-zA-Z0-9]*)*(?:::[a-zA-Z][a-zA-Z0-9]*)*$/,
form => 'text',
test => qr/^(?:[a-zA-Z][a-zA-Z0-9]*)*(?:::[a-zA-Z][a-zA-Z0-9]*)*$/,
msgFail => '__badPerlPackageName__',
},
hostname => {
@ -115,6 +116,7 @@ sub types {
},
select => {
test => sub {
return ( 0, "Value is not a scalar" ) if ref( $_[0] );
my $test = grep ( { $_ eq $_[0] }
map ( { $_->{k} } @{ $_[2]->{select} } ) );
return $test
@ -466,20 +468,20 @@ sub attributes {
},
checkUserDisplayPersistentInfo => {
default => 0,
type => 'bool',
documentation => 'Display persistent session info',
type => 'boolOrExpr',
documentation => 'Display persistent session info rule',
flags => 'p',
},
checkUserDisplayEmptyValues => {
default => 0,
type => 'bool',
documentation => 'Display session empty values',
type => 'boolOrExpr',
documentation => 'Display session empty values rule',
flags => 'p',
},
checkUserDisplayEmptyHeaders => {
default => 0,
type => 'bool',
documentation => 'Display empty headers',
type => 'boolOrExpr',
documentation => 'Display empty headers rule',
flags => 'p',
},
globalLogoutRule => {
@ -811,21 +813,22 @@ sub attributes {
'Brute force attack protection -> Max allowed failed login',
},
bruteForceProtectionMaxLockTime => {
default => 900,
type => 'int',
documentation =>
'Brute force attack protection -> Max lock time',
default => 900,
type => 'int',
documentation => 'Brute force attack protection -> Max lock time',
},
bruteForceProtectionIncrementalTempo => {
default => 0,
help => 'bruteforceprotection.html',
type => 'bool',
documentation => 'Enable incremental lock time for brute force attack protection',
default => 0,
help => 'bruteforceprotection.html',
type => 'bool',
documentation =>
'Enable incremental lock time for brute force attack protection',
},
bruteForceProtectionLockTimes => {
type => 'text',
default => '5 15 60 300 600',
documentation => 'Incremental lock time values for brute force attack protection',
type => 'text',
default => '5 15 60 300 600',
documentation =>
'Incremental lock time values for brute force attack protection',
},
grantSessionRules => {
type => 'grantContainer',
@ -1978,24 +1981,19 @@ sub attributes {
},
singleSession => {
default => 0,
type => 'bool',
type => 'boolOrExpr',
documentation => 'Allow only one session per user',
},
singleIP => {
default => 0,
type => 'bool',
type => 'boolOrExpr',
documentation => 'Allow only one session per IP',
},
singleUserByIP => {
default => 0,
type => 'bool',
type => 'boolOrExpr',
documentation => 'Allow only one user per IP',
},
singleSessionUserByIP => {
default => 0,
type => 'bool',
documentation => 'Allow only one session per user on an IP',
},
# REST server
restSessionServer => {
@ -2914,6 +2912,7 @@ sub attributes {
{ k => 'AD', v => 'Active Directory' },
{ k => 'DBI', v => 'Database (DBI)' },
{ k => 'Facebook', v => 'Facebook' },
{ k => 'GitHub', v => 'GitHub' },
{ k => 'GPG', v => 'GPG' },
{ k => 'Kerberos', v => 'Kerberos' },
{ k => 'LDAP', v => 'LDAP' },
@ -3425,6 +3424,17 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{1,5})?|ldap(?:s|\+tls)?://\w[\w\-\.]*(?:
linkedInScope =>
{ type => 'text', default => 'r_liteprofile r_emailaddress' },
# GitHub
githubAuthnLevel => {
type => 'int',
default => 1,
documentation => 'GitHub authentication level',
},
githubClientID => { type => 'text', },
githubClientSecret => { type => 'password', },
githubScope => { type => 'text', default => 'user:email' },
githubUserField => { type => 'text', default => 'login' },
# WebID
webIDAuthnLevel => {
type => 'int',
@ -3485,7 +3495,7 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{1,5})?|ldap(?:s|\+tls)?://\w[\w\-\.]*(?:
# Apache
apacheAuthnLevel => {
type => 'int',
default => 4,
default => 3,
documentation => 'Apache authentication level',
},
@ -3571,6 +3581,7 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{1,5})?|ldap(?:s|\+tls)?://\w[\w\-\.]*(?:
{ k => 'DBI', v => 'Database (DBI)' },
{ k => 'Demo', v => 'Demo' },
{ k => 'Facebook', v => 'Facebook' },
{ k => 'GitHub', v => 'GitHub' },
{ k => 'GPG', v => 'GPG' },
{ k => 'Kerberos', v => 'Kerberos' },
{ k => 'LDAP', v => 'LDAP' },
@ -3643,6 +3654,7 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{1,5})?|ldap(?:s|\+tls)?://\w[\w\-\.]*(?:
{ k => 'AD', v => 'Active Directory' },
{ k => 'DBI', v => 'Database (DBI)' },
{ k => 'Facebook', v => 'Facebook' },
{ k => 'GitHub', v => 'GitHub' },
{ k => 'GPG', v => 'GPG' },
{ k => 'Kerberos', v => 'Kerberos' },
{ k => 'LDAP', v => 'LDAP' },
@ -3836,7 +3848,8 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{1,5})?|ldap(?:s|\+tls)?://\w[\w\-\.]*(?:
'OpenID Connect exported variables for dynamic registration',
},
oidcServiceDynamicRegistrationExtraClaims => {
type => 'keyTextContainer',
type => 'keyTextContainer',
keyTest => qr/^[\x21\x23-\x5B\x5D-\x7E]+$/,
documentation =>
'OpenID Connect extra claims for dynamic registration',
},
@ -3950,14 +3963,20 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{1,5})?|ldap(?:s|\+tls)?://\w[\w\-\.]*(?:
],
default => 'HS512',
},
oidcRPMetaDataOptionsIDTokenExpiration => { type => 'int' },
oidcRPMetaDataOptionsIDTokenForceClaims => { type => 'bool' },
oidcRPMetaDataOptionsIDTokenExpiration => { type => 'int' },
oidcRPMetaDataOptionsIDTokenForceClaims =>
{ type => 'bool', default => 0 },
oidcRPMetaDataOptionsAdditionalAudiences =>
{ type => 'text' },
oidcRPMetaDataOptionsAccessTokenExpiration => { type => 'int' },
oidcRPMetaDataOptionsAuthorizationCodeExpiration => { type => 'int' },
oidcRPMetaDataOptionsOfflineSessionExpiration => { type => 'int' },
oidcRPMetaDataOptionsRedirectUris => { type => 'text', },
oidcRPMetaDataOptionsExtraClaims =>
{ type => 'keyTextContainer', default => {} },
oidcRPMetaDataOptionsExtraClaims => {
type => 'keyTextContainer',
keyTest => qr/^[\x21\x23-\x5B\x5D-\x7E]+$/,
default => {}
},
oidcRPMetaDataOptionsBypassConsent => {
type => 'bool',
help => 'openidconnectclaims.html',
@ -3972,7 +3991,8 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{1,5})?|ldap(?:s|\+tls)?://\w[\w\-\.]*(?:
type => 'select',
select => [
{ k => 'front', v => 'Front Channel' },
{ k => 'back', v => 'Back Channel' },
#TODO #1194
# { k => 'back', v => 'Back Channel' },
],
default => 'front',
documentation => 'Logout type',
@ -3997,6 +4017,12 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{1,5})?|ldap(?:s|\+tls)?://\w[\w\-\.]*(?:
default => 0,
documentation => 'Allow offline access',
},
oidcRPMetaDataOptionsAllowPasswordGrant => {
type => 'bool',
default => 0,
documentation =>
'Allow OAuth2 Resource Owner Password Credentials Grant',
},
oidcRPMetaDataOptionsRefreshToken => {
type => 'bool',
default => 0,

View File

@ -191,27 +191,49 @@ sub cTrees {
'oidcRPMetaDataOptionsExtraClaims',
{
title => 'oidcRPMetaDataOptions',
help => 'idpopenidconnect.html#options',
nodes => [ {
title => 'oidcRPMetaDataOptionsAuthentication',
title => 'oidcRPMetaDataOptionsBasic',
form => 'simpleInputContainer',
nodes => [
'oidcRPMetaDataOptionsClientID',
'oidcRPMetaDataOptionsClientSecret',
'oidcRPMetaDataOptionsPublic',
'oidcRPMetaDataOptionsRequirePKCE',
'oidcRPMetaDataOptionsRedirectUris',
]
},
{
title => 'oidcRPMetaDataOptionsAdvanced',
form => 'simpleInputContainer',
nodes => [
'oidcRPMetaDataOptionsBypassConsent',
'oidcRPMetaDataOptionsUserIDAttr',
'oidcRPMetaDataOptionsIDTokenForceClaims',
'oidcRPMetaDataOptionsAdditionalAudiences',
'oidcRPMetaDataOptionsRefreshToken',
]
},
{
title => 'security',
form => 'simpleInputContainer',
nodes => [
'oidcRPMetaDataOptionsIDTokenSignAlg',
'oidcRPMetaDataOptionsRequirePKCE',
'oidcRPMetaDataOptionsAllowOffline',
'oidcRPMetaDataOptionsAllowPasswordGrant',
'oidcRPMetaDataOptionsRule',
]
},
{
title => 'oidcRPMetaDataOptionsTimeouts',
form => 'simpleInputContainer',
nodes => [
'oidcRPMetaDataOptionsAuthorizationCodeExpiration',
'oidcRPMetaDataOptionsIDTokenExpiration',
'oidcRPMetaDataOptionsAccessTokenExpiration',
'oidcRPMetaDataOptionsOfflineSessionExpiration',
]
},
'oidcRPMetaDataOptionsUserIDAttr',
'oidcRPMetaDataOptionsIDTokenSignAlg',
'oidcRPMetaDataOptionsIDTokenExpiration',
'oidcRPMetaDataOptionsIDTokenForceClaims',
'oidcRPMetaDataOptionsAccessTokenExpiration',
'oidcRPMetaDataOptionsAuthorizationCodeExpiration',
'oidcRPMetaDataOptionsAllowOffline',
'oidcRPMetaDataOptionsRefreshToken',
'oidcRPMetaDataOptionsOfflineSessionExpiration',
'oidcRPMetaDataOptionsRedirectUris',
'oidcRPMetaDataOptionsBypassConsent',
{
title => 'logout',
form => 'simpleInputContainer',
@ -222,7 +244,6 @@ sub cTrees {
'oidcRPMetaDataOptionsLogoutSessionRequired',
]
},
'oidcRPMetaDataOptionsRule',
]
},
'oidcRPMetaDataMacros',

View File

@ -314,6 +314,16 @@ sub tree {
'linkedInScope'
]
},
{
title => 'githubParams',
help => 'authgithub.html',
form => 'simpleInputContainer',
nodes => [
'githubAuthnLevel', 'githubClientID',
'githubClientSecret', 'githubUserField',
'githubScope'
]
},
{
title => 'combinationParams',
help => 'authcombination.html',
@ -569,7 +579,7 @@ sub tree {
form => 'simpleInputContainer',
nodes => [
'singleSession', 'singleIP',
'singleUserByIP', 'singleSessionUserByIP',
'singleUserByIP',
'notifyDeleted', 'notifyOther'
]
},
@ -720,8 +730,11 @@ sub tree {
title => 'globalLogout',
help => 'globallogout.html',
form => 'simpleInputContainer',
nodes =>
[ 'globalLogoutRule', 'globalLogoutTimer','globalLogoutCustomParam' ],
nodes => [
'globalLogoutRule',
'globalLogoutTimer',
'globalLogoutCustomParam'
],
},
{
title => 'stateCheck',

View File

@ -831,7 +831,7 @@ sub _scanNodes {
foreach my $node ( @{ $leaf->{nodes} } ) {
my $tmp;
$tmp->{$_} = $node->{data}->{$_}
foreach (qw(type rule logo label));
foreach (qw(type rule logo level label));
$tmp->{over} = {};
foreach ( @{ $node->{data}->{over} } ) {
$tmp->{over}->{ $_->[0] } = $_->[1];
@ -1131,10 +1131,25 @@ sub _unitTest {
next;
}
if ( $attr->{type} and $attr->{type} eq 'subContainer' ) {
# Vhost, CAS, SAML, OIDC options
if ( $key =~
/^(?^:(?:(?:(?:saml(?:ID|S)|oidc[OR])P|cas(?:App|Srv))MetaData|vhost)Options)$/
)
{
# TODO Recursive for SAML/OIDC nodes
# Iterate on vhost names, or saml/cas/oidc configuration keys
for my $vhost ( keys %{ $conf->{$key} } ) {
my $options = $conf->{$key}->{$vhost};
if ( ref($options) eq "HASH" ) {
# Recurse on option list,
# FIXME this does check for oidcRPMetaDataOptionsXXX appearing under
# samlSPMetadataOptions
$res = 0 unless $self->_unitTest( $options, $localConf, "$key/$vhost/" );
}
}
}
else {
# Check if key exists

View File

@ -13,8 +13,11 @@ require Lemonldap::NG::Common::Notifications;
use feature 'state';
extends 'Lemonldap::NG::Manager::Plugin',
'Lemonldap::NG::Common::Conf::AccessLib';
extends qw(
Lemonldap::NG::Manager::Plugin
Lemonldap::NG::Common::Conf::AccessLib
Lemonldap::NG::Common::PSGI::Router
);
our $VERSION = '2.1.0';
@ -271,7 +274,7 @@ sub newNotification {
return $self->sendError( $req, undef, 200 );
}
$json->{reference} =~ s/_/-/g; # Remove underscores (#2135)
$json->{reference} =~ s/_/-/g; # Remove underscores (#2135)
foreach my $r (qw(uid reference xml)) {
return $self->sendError( $req, "Missing $r", 200 )

View File

@ -339,6 +339,7 @@ llapp.controller 'TreeCtrl', [
type: ''
rule: ''
logo: ''
level: ''
label: ''
over: []

View File

@ -320,6 +320,9 @@ llapp.controller 'SessionsExplorerCtrl', ['$scope', '$translator', '$location',
for key, value of l
if !key.match /^(_utime|ipAddr|error)$/
cv += ", #{key} : #{value}"
tab = cv.split ', '
tab.sort()
cv = tab.join ', '
tmp.push
t: l._utime
title: $scope.localeDate l._utime
@ -331,6 +334,9 @@ llapp.controller 'SessionsExplorerCtrl', ['$scope', '$translator', '$location',
for key, value of l
if !key.match /^(_utime|ipAddr|error)$/
cv += ", #{key} : #{value}"
tab = cv.split ', '
tab.sort()
cv = tab.join ', '
tmp.push
t: l._utime
title: $scope.localeDate l._utime

View File

@ -5,11 +5,12 @@
<table class="table table-striped">
<thead>
<tr>
<th width="12%" trspan="name"></th>
<th width="11%" trspan="type"></th>
<th width="33%" trspan="label"></th>
<th width="11%" trspan="logo"></th>
<th width="33%" trspan="rule"></th>
<th trspan="name"></th>
<th trspan="type"></th>
<th trspan="label"></th>
<th trspan="logo"></th>
<th trspan="level"></th>
<th trspan="rule"></th>
<th />
</tr>
</thead>
@ -29,6 +30,9 @@
<td>
<input class="form-control" ng-model="currentNode.data.logo" />
</td>
<td>
<input class="form-control" ng-model="currentNode.data.level" />
</td>
<td>
<input class="form-control" ng-model="currentNode.data.rule" />
</td>

View File

@ -5,11 +5,12 @@
<table class="table table-striped">
<thead>
<tr>
<th width="12%" trspan="name"></th>
<th width="11%" trspan="type"></th>
<th width="33%" trspan="label"></th>
<th width="11%" trspan="logo"></th>
<th width="33%" trspan="rule"></th>
<th trspan="name"></th>
<th trspan="type"></th>
<th trspan="label"></th>
<th trspan="logo"></th>
<th trspan="level"></th>
<th trspan="rule"></th>
<th />
</tr>
</thead>
@ -29,6 +30,9 @@
<td>
<input class="form-control" ng-model="s.data.logo" />
</td>
<td>
<input class="form-control" ng-model="s.data.level" />
</td>
<td>
<input class="form-control" ng-model="s.data.rule" />
</td>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -426,116 +426,155 @@ function templates(tpl,key) {
"title" : "oidcRPMetaDataOptionsPublic",
"type" : "bool"
},
{
"get" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsRedirectUris",
"id" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsRedirectUris",
"title" : "oidcRPMetaDataOptionsRedirectUris"
}
],
"id" : "oidcRPMetaDataOptionsBasic",
"title" : "oidcRPMetaDataOptionsBasic",
"type" : "simpleInputContainer"
},
{
"_nodes" : [
{
"default" : 0,
"get" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsBypassConsent",
"help" : "openidconnectclaims.html",
"id" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsBypassConsent",
"title" : "oidcRPMetaDataOptionsBypassConsent",
"type" : "bool"
},
{
"get" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsUserIDAttr",
"id" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsUserIDAttr",
"title" : "oidcRPMetaDataOptionsUserIDAttr"
},
{
"default" : 0,
"get" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsIDTokenForceClaims",
"id" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsIDTokenForceClaims",
"title" : "oidcRPMetaDataOptionsIDTokenForceClaims",
"type" : "bool"
},
{
"get" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsAdditionalAudiences",
"id" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsAdditionalAudiences",
"title" : "oidcRPMetaDataOptionsAdditionalAudiences"
},
{
"default" : 0,
"get" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsRefreshToken",
"id" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsRefreshToken",
"title" : "oidcRPMetaDataOptionsRefreshToken",
"type" : "bool"
}
],
"id" : "oidcRPMetaDataOptionsAdvanced",
"title" : "oidcRPMetaDataOptionsAdvanced",
"type" : "simpleInputContainer"
},
{
"_nodes" : [
{
"default" : "HS512",
"get" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsIDTokenSignAlg",
"id" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsIDTokenSignAlg",
"select" : [
{
"k" : "none",
"v" : "None"
},
{
"k" : "HS256",
"v" : "HS256"
},
{
"k" : "HS384",
"v" : "HS384"
},
{
"k" : "HS512",
"v" : "HS512"
},
{
"k" : "RS256",
"v" : "RS256"
},
{
"k" : "RS384",
"v" : "RS384"
},
{
"k" : "RS512",
"v" : "RS512"
}
],
"title" : "oidcRPMetaDataOptionsIDTokenSignAlg",
"type" : "select"
},
{
"default" : 0,
"get" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsRequirePKCE",
"id" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsRequirePKCE",
"title" : "oidcRPMetaDataOptionsRequirePKCE",
"type" : "bool"
},
{
"default" : 0,
"get" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsAllowOffline",
"id" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsAllowOffline",
"title" : "oidcRPMetaDataOptionsAllowOffline",
"type" : "bool"
},
{
"default" : 0,
"get" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsAllowPasswordGrant",
"id" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsAllowPasswordGrant",
"title" : "oidcRPMetaDataOptionsAllowPasswordGrant",
"type" : "bool"
},
{
"get" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsRule",
"id" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsRule",
"title" : "oidcRPMetaDataOptionsRule"
}
],
"id" : "oidcRPMetaDataOptionsAuthentication",
"title" : "oidcRPMetaDataOptionsAuthentication",
"id" : "security",
"title" : "security",
"type" : "simpleInputContainer"
},
{
"get" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsUserIDAttr",
"id" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsUserIDAttr",
"title" : "oidcRPMetaDataOptionsUserIDAttr"
},
{
"default" : "HS512",
"get" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsIDTokenSignAlg",
"id" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsIDTokenSignAlg",
"select" : [
"_nodes" : [
{
"k" : "none",
"v" : "None"
"get" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsAuthorizationCodeExpiration",
"id" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsAuthorizationCodeExpiration",
"title" : "oidcRPMetaDataOptionsAuthorizationCodeExpiration",
"type" : "int"
},
{
"k" : "HS256",
"v" : "HS256"
"get" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsIDTokenExpiration",
"id" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsIDTokenExpiration",
"title" : "oidcRPMetaDataOptionsIDTokenExpiration",
"type" : "int"
},
{
"k" : "HS384",
"v" : "HS384"
"get" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsAccessTokenExpiration",
"id" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsAccessTokenExpiration",
"title" : "oidcRPMetaDataOptionsAccessTokenExpiration",
"type" : "int"
},
{
"k" : "HS512",
"v" : "HS512"
},
{
"k" : "RS256",
"v" : "RS256"
},
{
"k" : "RS384",
"v" : "RS384"
},
{
"k" : "RS512",
"v" : "RS512"
"get" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsOfflineSessionExpiration",
"id" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsOfflineSessionExpiration",
"title" : "oidcRPMetaDataOptionsOfflineSessionExpiration",
"type" : "int"
}
],
"title" : "oidcRPMetaDataOptionsIDTokenSignAlg",
"type" : "select"
},
{
"get" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsIDTokenExpiration",
"id" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsIDTokenExpiration",
"title" : "oidcRPMetaDataOptionsIDTokenExpiration",
"type" : "int"
},
{
"get" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsIDTokenForceClaims",
"id" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsIDTokenForceClaims",
"title" : "oidcRPMetaDataOptionsIDTokenForceClaims",
"type" : "bool"
},
{
"get" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsAccessTokenExpiration",
"id" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsAccessTokenExpiration",
"title" : "oidcRPMetaDataOptionsAccessTokenExpiration",
"type" : "int"
},
{
"get" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsAuthorizationCodeExpiration",
"id" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsAuthorizationCodeExpiration",
"title" : "oidcRPMetaDataOptionsAuthorizationCodeExpiration",
"type" : "int"
},
{
"default" : 0,
"get" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsAllowOffline",
"id" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsAllowOffline",
"title" : "oidcRPMetaDataOptionsAllowOffline",
"type" : "bool"
},
{
"default" : 0,
"get" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsRefreshToken",
"id" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsRefreshToken",
"title" : "oidcRPMetaDataOptionsRefreshToken",
"type" : "bool"
},
{
"get" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsOfflineSessionExpiration",
"id" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsOfflineSessionExpiration",
"title" : "oidcRPMetaDataOptionsOfflineSessionExpiration",
"type" : "int"
},
{
"get" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsRedirectUris",
"id" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsRedirectUris",
"title" : "oidcRPMetaDataOptionsRedirectUris"
},
{
"default" : 0,
"get" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsBypassConsent",
"help" : "openidconnectclaims.html",
"id" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsBypassConsent",
"title" : "oidcRPMetaDataOptionsBypassConsent",
"type" : "bool"
"id" : "oidcRPMetaDataOptionsTimeouts",
"title" : "oidcRPMetaDataOptionsTimeouts",
"type" : "simpleInputContainer"
},
{
"_nodes" : [
@ -557,10 +596,6 @@ function templates(tpl,key) {
{
"k" : "front",
"v" : "Front Channel"
},
{
"k" : "back",
"v" : "Back Channel"
}
],
"title" : "oidcRPMetaDataOptionsLogoutType",
@ -577,13 +612,9 @@ function templates(tpl,key) {
"id" : "logout",
"title" : "logout",
"type" : "simpleInputContainer"
},
{
"get" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsRule",
"id" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsRule",
"title" : "oidcRPMetaDataOptionsRule"
}
],
"help" : "idpopenidconnect.html#options",
"id" : "oidcRPMetaDataOptions",
"title" : "oidcRPMetaDataOptions"
},

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
(function(){var e;(e=angular.module("llApp",["ngAria"])).provider("$translator",function(){var e,t,n,r,a,i,o,s,l,u,c,g;if(g={},decodeURIComponent(document.cookie).match(/llnglanguage=(\w+)/))g.lang=RegExp.$1;else if(navigator){for(r=[],a=[],l=[navigator.language],navigator.languages&&(l=navigator.languages),t=0,i=l.length;t<i;t++)for(s=l[t],console.log("Navigator lang",s),n=0,o=(c=window.availableLanguages).length;n<o;n++)e=c[n],console.log(" Available lang",e),u=new RegExp("^"+e+"-?"),s.match(u)?(console.log(" Matching lang =",e),r.push(e)):e.substring(0,1)===s.substring(0,1)&&a.push(e);g.lang=r[0]?r[0]:a[0]?a[0]:"en"}else g.lang="en";return console.log("Selected lang ->",g.lang),g.deferredTr=[],g.translationFields={},g.translate=function(e){return g.translationFields[e]&&(e=g.translationFields[e]),e},g.translateField=function(e,t){return g.translate(e[t])},g.translateP=function(e){return e&&g.translationFields.portal&&(e=e.replace(/__(\w+)__/g,function(e,t){return g.translate(t)})),e},this.$get=["$q","$http",function(t,n){return g.last="",g.init=function(e){var i;return e=e||g.lang,(i=new Date).setTime(i.getTime()+2592e6),document.cookie="llnglanguage="+e+"; expires="+i.toUTCString()+"; path=/",i=t.defer(),g.last!==e?(g.last=e,n.get(window.staticPrefix+"languages/"+e+".json").then(function(e){var t,n,r,a;for(g.translationFields=e.data,n=0,r=(a=g.deferredTr).length;n<r;n++)(t=a[n]).e[t.f](g.translationFields[t.m]);return g.deferredTr=[],i.resolve("Translation files loaded")},function(e){return i.reject("")})):i.resolve("No change"),i.promise},g}],this}),e.directive("trspan",["$translator",function(r){return{restrict:"A",replace:!1,transclude:!0,scope:{trspan:"@"},link:function(e,t,n){return r.translationFields.portal?n.trspan=r.translate(n.trspan):r.deferredTr.push({e:t,f:"text",m:n.trspan}),t.text(n.trspan)},template:""}}]),e.provider("$htmlParams",function(){return this.$get=function(){var n;return n={},{set:function(e,t){return n[e]=t},menu:function(){return n.menu},params:function(){return n.params}}},this}),e.directive("script",["$htmlParams",function(a){return{restrict:"E",terminal:!0,compile:function(e,t){var n,r;if(t.type&&(r=t.type.match(/text\/(menu|parameters)/)))try{return a.set(r[1],JSON.parse(e[0].text))}catch(e){n=e,console.log("Parsing error:",n)}}}}]),e.controller("ModalInstanceCtrl",["$scope","$uibModalInstance","elem","set","init",function(a,e,t,n,r){var i,o;return null,a.elem=t,a.set=n,a.result=r,a.staticPrefix=window.staticPrefix,i=t("currentNode"),a.translateP=t("translateP"),i&&(o=i.data,a.currentNode=i),a.ok=function(){return n("result",a.result),e.close(!0)},a.cancel=function(){return i&&(a.currentNode.data=o),e.dismiss("cancel")},a.inSelect=function(e){var t,n,r;for(t=0,n=(r=a.currentNode.select).length;t<n;t++)if(r[t].k===e)return!0;return!1}}]),e.directive("onReadFile",["$parse",function(a){return{restrict:"A",scope:!1,link:function(n,e,t){var r;return r=a(t.onReadFile),e.on("change",function(e){var t;return(t=new FileReader).onload=function(e){return n.$apply(function(){return r(n,{$fileContent:e.target.result})})},t.readAsText((e.srcElement||e.target).files[0])})}}}]),e.directive("resizer",["$document",function(i){var o,s;return s=o=null,function(e,t,r){var n,a;return t.on("mousedown",function(e){return"vertical"===r.resizer?s=$(r.resizerRight).width()+$(r.resizerLeft).width():o=$(r.resizerTop).height()+$(r.resizerBottom).height(),e.preventDefault(),i.on("mousemove",n),i.on("mouseup",a)}),n=function(e){var t,n;return"vertical"===r.resizer?(t=e.pageX,r.resizerMax&&t>r.resizerMax&&(t=parseInt(r.resizerMax)),$(r.resizerLeft).css({width:t+"px"}),$(r.resizerRight).css({width:s-t+"px"})):(n=e.pageY-$("#navbar").height(),$(r.resizerTop).css({height:n+"px"}),$(r.resizerBottom).css({height:o-n+"px"}))},a=function(){return i.unbind("mousemove",n),i.unbind("mouseup",a)}}}]),e.factory("$lmhttp",["$q","$location",function(t,e){return{responseError:function(e){return 401===e.status&&window.portal?window.location=window.portal+"?url="+window.btoa(window.location).replace(/\//,"_"):t.reject(e)}}}]),e.config(["$httpProvider",function(e){return e.interceptors.push("$lmhttp")}])}).call(this);
(function(){var e;(e=angular.module("llApp",["ngAria"])).provider("$translator",function(){var e,t,n,r,a,i,o,s,l,u,c,g;if(g={},decodeURIComponent(document.cookie).match(/llnglanguage=(\w+)/))g.lang=RegExp.$1;else if(navigator){for(r=[],a=[],l=[navigator.language],navigator.languages&&(l=navigator.languages),t=0,i=l.length;t<i;t++)for(s=l[t],console.log("Navigator lang",s),n=0,o=(c=window.availableLanguages).length;n<o;n++)e=c[n],console.log(" Available lang",e),u=new RegExp("^"+e+"-?"),s.match(u)?(console.log(" Matching lang =",e),r.push(e)):e.substring(0,1)===s.substring(0,1)&&a.push(e);g.lang=r[0]?r[0]:a[0]?a[0]:"en"}else g.lang="en";return console.log("Selected lang ->",g.lang),g.deferredTr=[],g.translationFields={},g.translate=function(e){return g.translationFields[e]&&(e=g.translationFields[e]),e},g.translateField=function(e,t){return g.translate(e[t])},g.translateP=function(e){return e&&g.translationFields.portal&&(e=e.replace(/__(\w+)__/g,function(e,t){return g.translate(t)})),e},this.$get=["$q","$http",function(t,n){return g.last="",g.init=function(e){var i;return e||(e=g.lang),(i=new Date).setTime(i.getTime()+2592e6),document.cookie="llnglanguage="+e+"; expires="+i.toUTCString()+"; path=/",i=t.defer(),g.last!==e?(g.last=e,n.get(window.staticPrefix+"languages/"+e+".json").then(function(e){var t,n,r,a;for(g.translationFields=e.data,n=0,r=(a=g.deferredTr).length;n<r;n++)(t=a[n]).e[t.f](g.translationFields[t.m]);return g.deferredTr=[],i.resolve("Translation files loaded")},function(e){return i.reject("")})):i.resolve("No change"),i.promise},g}],this}),e.directive("trspan",["$translator",function(r){return{restrict:"A",replace:!1,transclude:!0,scope:{trspan:"@"},link:function(e,t,n){return r.translationFields.portal?n.trspan=r.translate(n.trspan):r.deferredTr.push({e:t,f:"text",m:n.trspan}),t.text(n.trspan)},template:""}}]),e.provider("$htmlParams",function(){return this.$get=function(){var n;return n={},{set:function(e,t){return n[e]=t},menu:function(){return n.menu},params:function(){return n.params}}},this}),e.directive("script",["$htmlParams",function(a){return{restrict:"E",terminal:!0,compile:function(e,t){var n,r;if(t.type&&(r=t.type.match(/text\/(menu|parameters)/)))try{return a.set(r[1],JSON.parse(e[0].text))}catch(e){n=e,console.log("Parsing error:",n)}}}}]),e.controller("ModalInstanceCtrl",["$scope","$uibModalInstance","elem","set","init",function(a,e,t,n,r){var i,o;return null,a.elem=t,a.set=n,a.result=r,a.staticPrefix=window.staticPrefix,i=t("currentNode"),a.translateP=t("translateP"),i&&(o=i.data,a.currentNode=i),a.ok=function(){return n("result",a.result),e.close(!0)},a.cancel=function(){return i&&(a.currentNode.data=o),e.dismiss("cancel")},a.inSelect=function(e){var t,n,r;for(t=0,n=(r=a.currentNode.select).length;t<n;t++)if(r[t].k===e)return!0;return!1}}]),e.directive("onReadFile",["$parse",function(a){return{restrict:"A",scope:!1,link:function(n,e,t){var r;return r=a(t.onReadFile),e.on("change",function(e){var t;return(t=new FileReader).onload=function(e){return n.$apply(function(){return r(n,{$fileContent:e.target.result})})},t.readAsText((e.srcElement||e.target).files[0])})}}}]),e.directive("resizer",["$document",function(i){var o,s;return s=o=null,function(e,t,r){var n,a;return t.on("mousedown",function(e){return"vertical"===r.resizer?s=$(r.resizerRight).width()+$(r.resizerLeft).width():o=$(r.resizerTop).height()+$(r.resizerBottom).height(),e.preventDefault(),i.on("mousemove",n),i.on("mouseup",a)}),n=function(e){var t,n;return"vertical"===r.resizer?(t=e.pageX,r.resizerMax&&t>r.resizerMax&&(t=parseInt(r.resizerMax)),$(r.resizerLeft).css({width:t+"px"}),$(r.resizerRight).css({width:s-t+"px"})):(n=e.pageY-$("#navbar").height(),$(r.resizerTop).css({height:n+"px"}),$(r.resizerBottom).css({height:o-n+"px"}))},a=function(){return i.unbind("mousemove",n),i.unbind("mouseup",a)}}}]),e.factory("$lmhttp",["$q","$location",function(t,e){return{responseError:function(e){return 401===e.status&&window.portal?window.location=window.portal+"?url="+window.btoa(window.location).replace(/\//,"_"):t.reject(e)}}}]),e.config(["$httpProvider",function(e){return e.interceptors.push("$lmhttp")}])}).call(this);

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
// Generated by CoffeeScript 1.12.7
// Generated by CoffeeScript 1.12.8
/*
LemonLDAP::NG Manager client
@ -394,6 +394,7 @@ This file contains:
type: '',
rule: '',
logo: '',
level: '',
label: '',
over: []
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -228,7 +228,7 @@
$scope.displaySession = function(scope) {
var sessionId, transformSession;
transformSession = function(session) {
var _insert, array, attr, attrs, category, cv, element, epoch, i, id, j, k, key, l, len, len1, len2, len3, len4, len5, m, name, o, oidcConsent, p, real, ref, ref1, res, sfDevice, spoof, subres, time, title, tmp, value;
var _insert, array, attr, attrs, category, cv, element, epoch, i, id, j, k, key, l, len, len1, len2, len3, len4, len5, m, name, o, oidcConsent, p, real, ref, ref1, res, sfDevice, spoof, subres, tab, time, title, tmp, value;
_insert = function(re, title) {
var key, reg, tmp, value;
tmp = [];
@ -374,6 +374,9 @@
cv += ", " + key + " : " + value;
}
}
tab = cv.split(', ');
tab.sort();
cv = tab.join(', ');
tmp.push({
t: l._utime,
title: $scope.localeDate(l._utime),
@ -392,6 +395,9 @@
cv += ", " + key + " : " + value;
}
}
tab = cv.split(', ');
tab.sort();
cv = tab.join(', ');
tmp.push({
t: l._utime,
title: $scope.localeDate(l._utime),

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -319,6 +319,12 @@
"forms":"إستمارات",
"friendlyName":"اسم مألوف",
"generalParameters":"المعاييرالعامة",
"githubAuthnLevel":"مستوى إثبات الهوية",
"githubClientID":"Client ID",
"githubClientSecret":"Client secret",
"githubParams":"GitHub Parameters",
"githubScope":"Scope",
"githubUserField":"Field containing user identifier",
"globalLogout":"Global logout",
"globalLogoutCustomParam":"Custom parameter",
"globalLogoutRule":"تفعيل",
@ -430,6 +436,7 @@
"ldapTimeout":"مهلة",
"ldapUsePasswordResetAttribute":"استخدام سمة إعادة الضبط",
"ldapVersion":"الإصدار",
"level":"Level",
"linkedInAuthnLevel":"مستوى إثبات الهوية",
"linkedInClientID":"معرف العميل",
"linkedInClientSecret":"سرالعميل",
@ -559,7 +566,10 @@
"oidcOPMetaDataJWKS":"بيانات JWKS",
"oidcOPMetaDataNode":" أوبين أيدي كونيكت بروفيدر",
"oidcOPMetaDataOptions":"الخيارات",
"oidcRPMetaDataOptionsAuthentication":"إثبات الهوية",
"oidcRPMetaDataOptionsBasic":"Basique",
"oidcRPMetaDataOptionsAdditionalAudiences":"Additional audiences",
"oidcRPMetaDataOptionsAdvanced":"Avancé",
"oidcRPMetaDataOptionsTimeouts":"Expiration",
"oidcRPMetaDataOptionsAllowOffline":"Allow offline access",
"oidcOPMetaDataOptionsCheckJWTSignature":"توقيع",
"oidcOPMetaDataOptionsClientID":"معرف العميل",
@ -591,6 +601,7 @@
"oidcRPMetaDataNode":"الأطراف المعتمد لي أوبين أيدي كونيكت",
"oidcRPMetaDataOptions":"الخيارات",
"oidcRPMetaDataOptionsAccessTokenExpiration":"انتهاء صلاحية التوكن",
"oidcRPMetaDataOptionsAllowPasswordGrant":"Allow OAuth2.0 Password Grant",
"oidcRPMetaDataOptionsAuthorizationCodeExpiration":"Authorization Code expiration",
"oidcRPMetaDataOptionsBypassConsent":"تخطى الموافقة ",
"oidcRPMetaDataOptionsClientID":"معرف العميل",
@ -746,7 +757,7 @@
"radius2fSecret":"سر مشترك",
"radius2fUsernameSessionKey":"Session key containing login",
"radius2fTimeout":"Authentication timeout",
"radius2fAuthnLevel":"Authentication level",
"radius2fAuthnLevel":"مستوى إثبات الهوية",
"radius2fLogo":"شعار",
"radius2fLabel":"Label",
"radiusAuthnLevel":"مستوى إثبات الهوية",
@ -803,7 +814,7 @@
"rule":"القاعدة",
"ruleAuthnLevel":"مستوى إثبات الهوية واجب",
"rules":"القواعد",
"rulesAuthnLevel":"Required auth levels",
"rulesAuthnLevel":"مستوى إثبات الهوية واجب",
"Same":"نفسه",
"sameSite":"Cookie SameSite value",
"save":"حفظ",
@ -825,23 +836,22 @@
"sessionStartedAt":"بدأت الجلسة",
"sessionStorage":"تخزين الجلسات",
"sessionTitle":"محتوى الجلسة",
"sfaTitle":"Second Factors Authentication",
"sfaTitle":"Second factors authentication",
"sfExtra":"Additional second factors",
"sfRequired":"Require 2FA",
"sfRemovedNotification":"Warn if an expired SF is removed",
"sfRequired":"Force 2FA registration at login",
"sfRemovedNotification":"Warn if an expired 2FA is removed",
"sfRemovedMsgRule":"تفعيل",
"sfRemovedUseNotif":"Use Notifications plugin",
"sfRemovedNotifMsg":"Notification message",
"sfRemovedNotifRef":"Notification reference",
"sfRemovedNotifTitle":"Notification title",
"sfRemovedMsg":"Display a message if an expired SF is removed",
"sfRemovedMsg":"Display a message if an expired 2FA is removed",
"show":"عرض",
"showHelp":"عرض المساعدة",
"showLanguages":"Show languages choice",
"singleIP":"عنوان آي بي واحد لكل مستخدم",
"singleSession":"جلسة واحدة فقط من قبل المستخدم",
"singleUserByIP":"مستخدم واحد لكل عنوان آي بي",
"singleSessionUserByIP":"جلسة واحدة بواسطة عنوان الآي بي",
"singleIP":"One IP address per user",
"singleSession":"One session per user",
"singleUserByIP":"One user per IP address",
"skipRenewConfirmation":"Skip re-auth confirmation",
"slaveAuthnLevel":"مستوى إثبات الهوية",
"slaveDisplayLogo":"Display authentication logo",

View File

@ -319,6 +319,12 @@
"forms":"Forms",
"friendlyName":"Friendly name",
"generalParameters":"General Parameters",
"githubAuthnLevel":"Authentication level",
"githubClientID":"Client ID",
"githubClientSecret":"Client secret",
"githubParams":"GitHub Parameters",
"githubScope":"Scope",
"githubUserField":"Field containing user identifier",
"globalLogout":"Global logout",
"globalLogoutCustomParam":"Custom parameter",
"globalLogoutRule":"Activation",
@ -430,6 +436,7 @@
"ldapTimeout":"Timeout",
"ldapUsePasswordResetAttribute":"Use reset attribute",
"ldapVersion":"Version",
"level":"Level",
"linkedInAuthnLevel":"Authentication level",
"linkedInClientID":"Client ID",
"linkedInClientSecret":"Client secret",
@ -559,7 +566,10 @@
"oidcOPMetaDataJWKS":"JWKS data",
"oidcOPMetaDataNode":"OpenID Connect Providers",
"oidcOPMetaDataOptions":"Optionen",
"oidcRPMetaDataOptionsAuthentication":"Authentication",
"oidcRPMetaDataOptionsBasic":"Basic",
"oidcRPMetaDataOptionsAdditionalAudiences":"Additional audiences",
"oidcRPMetaDataOptionsAdvanced":"Advanced",
"oidcRPMetaDataOptionsTimeouts":"Timeouts",
"oidcRPMetaDataOptionsAllowOffline":"Allow offline access",
"oidcOPMetaDataOptionsCheckJWTSignature":"Check JWT signature",
"oidcOPMetaDataOptionsClientID":"Client ID",
@ -591,6 +601,7 @@
"oidcRPMetaDataNode":"OpenID Connect Relying Parties",
"oidcRPMetaDataOptions":"Options",
"oidcRPMetaDataOptionsAccessTokenExpiration":"Access Token expiration",
"oidcRPMetaDataOptionsAllowPasswordGrant":"Allow OAuth2.0 Password Grant",
"oidcRPMetaDataOptionsAuthorizationCodeExpiration":"Authorization Code expiration",
"oidcRPMetaDataOptionsBypassConsent":"Bypass consent",
"oidcRPMetaDataOptionsClientID":"Client ID",
@ -825,23 +836,22 @@
"sessionStartedAt":"Session started on",
"sessionStorage":"Sessions Storage",
"sessionTitle":"Session content",
"sfaTitle":"Second Factors Authentication",
"sfaTitle":"Second factors authentication",
"sfExtra":"Additional second factors",
"sfRequired":"Require 2FA",
"sfRemovedNotification":"Warn if an expired SF is removed",
"sfRequired":"Force 2FA registration at login",
"sfRemovedNotification":"Warn if an expired 2FA is removed",
"sfRemovedMsgRule":"Activation",
"sfRemovedUseNotif":"Use Notifications plugin",
"sfRemovedNotifMsg":"Notification message",
"sfRemovedNotifRef":"Notification reference",
"sfRemovedNotifTitle":"Notification title",
"sfRemovedMsg":"Display a message if an expired SF is removed",
"sfRemovedMsg":"Display a message if an expired 2FA is removed",
"show":"Show",
"showHelp":"Show help",
"showLanguages":"Show languages choice",
"singleIP":"One IP only by user",
"singleSession":"One session only by user",
"singleUserByIP":"One user by IP address",
"singleSessionUserByIP":"One session by IP address",
"singleIP":"One IP address per user",
"singleSession":"One session per user",
"singleUserByIP":"One user per IP address",
"skipRenewConfirmation":"Skip re-auth confirmation",
"slaveAuthnLevel":"Authentication level",
"slaveDisplayLogo":"Display authentication logo",

View File

@ -319,6 +319,12 @@
"forms":"Forms",
"friendlyName":"Friendly name",
"generalParameters":"General Parameters",
"githubAuthnLevel":"Authentication level",
"githubClientID":"Client ID",
"githubClientSecret":"Client secret",
"githubParams":"GitHub Parameters",
"githubScope":"Scope",
"githubUserField":"Field containing user identifier",
"globalLogout":"Global logout",
"globalLogoutCustomParam":"Custom parameter",
"globalLogoutRule":"Activation",
@ -430,6 +436,7 @@
"ldapTimeout":"Timeout",
"ldapUsePasswordResetAttribute":"Use reset attribute",
"ldapVersion":"Version",
"level":"Level",
"linkedInAuthnLevel":"Authentication level",
"linkedInClientID":"Client ID",
"linkedInClientSecret":"Client secret",
@ -559,7 +566,10 @@
"oidcOPMetaDataJWKS":"JWKS data",
"oidcOPMetaDataNode":"OpenID Connect Providers",
"oidcOPMetaDataOptions":"Options",
"oidcRPMetaDataOptionsAuthentication":"Authentication",
"oidcRPMetaDataOptionsBasic":"Basic",
"oidcRPMetaDataOptionsAdditionalAudiences":"Additional audiences",
"oidcRPMetaDataOptionsAdvanced":"Advanced",
"oidcRPMetaDataOptionsTimeouts":"Timeouts",
"oidcRPMetaDataOptionsAllowOffline":"Allow offline access",
"oidcOPMetaDataOptionsCheckJWTSignature":"Check JWT signature",
"oidcOPMetaDataOptionsClientID":"Client ID",
@ -591,6 +601,7 @@
"oidcRPMetaDataNode":"OpenID Connect Relying Parties",
"oidcRPMetaDataOptions":"Options",
"oidcRPMetaDataOptionsAccessTokenExpiration":"Access Token expiration",
"oidcRPMetaDataOptionsAllowPasswordGrant":"Allow OAuth2.0 Password Grant",
"oidcRPMetaDataOptionsAuthorizationCodeExpiration":"Authorization Code expiration",
"oidcRPMetaDataOptionsBypassConsent":"Bypass consent",
"oidcRPMetaDataOptionsClientID":"Client ID",
@ -825,23 +836,22 @@
"sessionStartedAt":"Session started on",
"sessionStorage":"Sessions Storage",
"sessionTitle":"Session content",
"sfaTitle":"Second Factors Authentication",
"sfaTitle":"Second factors authentication",
"sfExtra":"Additional second factors",
"sfRequired":"Require 2FA",
"sfRemovedNotification":"Warn if an expired SF is removed",
"sfRequired":"Force 2FA registration at login",
"sfRemovedNotification":"Warn if an expired 2FA is removed",
"sfRemovedMsgRule":"Activation",
"sfRemovedUseNotif":"Use Notifications plugin",
"sfRemovedNotifMsg":"Notification message",
"sfRemovedNotifRef":"Notification reference",
"sfRemovedNotifTitle":"Notification title",
"sfRemovedMsg":"Display a message if an expired SF is removed",
"sfRemovedMsg":"Display a message if an expired 2FA is removed",
"show":"Show",
"showHelp":"Show help",
"showLanguages":"Show languages choice",
"singleIP":"One IP only by user",
"singleSession":"One session only by user",
"singleUserByIP":"One user by IP address",
"singleSessionUserByIP":"One session by IP address",
"singleIP":"One IP address per user",
"singleSession":"One session per user",
"singleUserByIP":"One user per IP address",
"skipRenewConfirmation":"Skip re-auth confirmation",
"slaveAuthnLevel":"Authentication level",
"slaveDisplayLogo":"Display authentication logo",

View File

@ -319,6 +319,12 @@
"forms":"Formulaires",
"friendlyName":"Nom alternatif",
"generalParameters":"Paramètres généraux",
"githubAuthnLevel":"Niveau d'authentification",
"githubClientID":"Identifiant",
"githubClientSecret":"Mot de passe",
"githubParams":"Paramètres GitHub",
"githubScope":"Scope",
"githubUserField":"Champ contenant l'identifiant de l'utilisateur",
"globalLogout":"Déconnexion globale",
"globalLogoutCustomParam":"Paramètre personnalisé",
"globalLogoutRule":"Activation",
@ -430,6 +436,7 @@
"ldapTimeout":"Temps maximum d'inactivité",
"ldapUsePasswordResetAttribute":"Utiliser l'attribut de réinitialisation",
"ldapVersion":"Version",
"level":"Niveau",
"linkedInAuthnLevel":"Niveau d'authentification",
"linkedInClientID":"Identifiant",
"linkedInClientSecret":"Mot de passe",
@ -559,7 +566,10 @@
"oidcOPMetaDataJWKS":"Données JWKS",
"oidcOPMetaDataNode":"Fournisseurs OpenID Connect",
"oidcOPMetaDataOptions":"Options",
"oidcRPMetaDataOptionsAuthentication":"Authentification",
"oidcRPMetaDataOptionsBasic":"Basiques",
"oidcRPMetaDataOptionsAdvanced":"Avancées",
"oidcRPMetaDataOptionsAdditionalAudiences":"Audiences supplémentaires",
"oidcRPMetaDataOptionsTimeouts":"Expiration",
"oidcRPMetaDataOptionsAllowOffline":"Autoriser l'accès hors ligne",
"oidcOPMetaDataOptionsCheckJWTSignature":"Vérifier la signature des jetons",
"oidcOPMetaDataOptionsClientID":"Identifiant",
@ -591,6 +601,7 @@
"oidcRPMetaDataNode":"Clients OpenID Connect",
"oidcRPMetaDataOptions":"Options",
"oidcRPMetaDataOptionsAccessTokenExpiration":"Expiration des jetons d'accès",
"oidcRPMetaDataOptionsAllowPasswordGrant":"Autoriser le Password Grant OAuth2.0",
"oidcRPMetaDataOptionsAuthorizationCodeExpiration":"Expiration des codes d'autorisation",
"oidcRPMetaDataOptionsBypassConsent":"Contourner le consentement",
"oidcRPMetaDataOptionsClientID":"Identifiant",
@ -825,10 +836,10 @@
"sessionStartedAt":"Session démarrée le ",
"sessionStorage":"Stockage des sessions",
"sessionTitle":"Contenu de la session",
"sfaTitle":"Seconds Facteurs d'Authentification",
"sfaTitle":"Seconds facteurs d'authentification",
"sfExtra":"Seconds facteurs additionnels",
"sfRequired":"Exiger 2FA",
"sfRemovedNotification":"Avertir si un SF expiré a été supprimé",
"sfRequired":"Exiger l'enrôlement d'un SF à l'authentification",
"sfRemovedNotification":"Avertir si un SF expiré est supprimé",
"sfRemovedMsgRule":"Activation",
"sfRemovedUseNotif":"Utiliser les notifications",
"sfRemovedNotifMsg":"Message de la notification",
@ -838,10 +849,9 @@
"show":"Montrer",
"showHelp":"Montrer l'aide",
"showLanguages":"Afficher le choix des langues",
"singleIP":"Une seule session par couple utilisateur/IP",
"singleIP":"Une seule adresse IP par utilisateur",
"singleSession":"Une seule session par utilisateur",
"singleUserByIP":"Un seul utilisateur par IP",
"singleSessionUserByIP":"Une seule session par IP",
"singleUserByIP":"Un seul utilisateur par adresse IP",
"skipRenewConfirmation":"Éviter la confirmation de ré-authentification",
"slaveAuthnLevel":"Niveau d'authentification",
"slaveDisplayLogo":"Afficher le logo d'authentification",

View File

@ -319,6 +319,12 @@
"forms":"Moduli",
"friendlyName":"Nome amichevole",
"generalParameters":"Parametri generali",
"githubAuthnLevel":"Livello di autenticazione",
"githubClientID":"Client ID",
"githubClientSecret":"Client secret",
"githubParams":"GitHub Parameters",
"githubScope":"Scope",
"githubUserField":"Field containing user identifier",
"globalLogout":"Global logout",
"globalLogoutCustomParam":"Custom parameter",
"globalLogoutRule":"Attivazione",
@ -430,6 +436,7 @@
"ldapTimeout":"Timeout",
"ldapUsePasswordResetAttribute":"Utilizza l'attributo di ripristino",
"ldapVersion":"Versione",
"level":"Livello",
"linkedInAuthnLevel":"Livello di autenticazione",
"linkedInClientID":"Client ID",
"linkedInClientSecret":"Client segreto",
@ -559,7 +566,10 @@
"oidcOPMetaDataJWKS":"Dati di JWKS",
"oidcOPMetaDataNode":"Provider di OpenID Connect",
"oidcOPMetaDataOptions":"Opzioni",
"oidcRPMetaDataOptionsAuthentication":"Autenticazione",
"oidcRPMetaDataOptionsBasic":"Basic",
"oidcRPMetaDataOptionsAdditionalAudiences":"Additional audiences",
"oidcRPMetaDataOptionsAdvanced":"Advanced",
"oidcRPMetaDataOptionsTimeouts":"Timeouts",
"oidcRPMetaDataOptionsAllowOffline":"Allow offline access",
"oidcOPMetaDataOptionsCheckJWTSignature":"Controllare la firma JWT",
"oidcOPMetaDataOptionsClientID":"ID Client",
@ -591,6 +601,7 @@
"oidcRPMetaDataNode":"Parti basate su OpenID Connect",
"oidcRPMetaDataOptions":"Opzioni",
"oidcRPMetaDataOptionsAccessTokenExpiration":"Scadenza accesso token",
"oidcRPMetaDataOptionsAllowPasswordGrant":"Allow OAuth2.0 Password Grant",
"oidcRPMetaDataOptionsAuthorizationCodeExpiration":"Authorization Code expiration",
"oidcRPMetaDataOptionsBypassConsent":"Consenso di bypass",
"oidcRPMetaDataOptionsClientID":"ID Client",
@ -827,21 +838,20 @@
"sessionTitle":"Contenuto della sessione",
"sfaTitle":"Autenticazione a due fattori",
"sfExtra":"Additional second factors",
"sfRequired":"Richiedi 2FA",
"sfRemovedNotification":"Warn if an expired SF is removed",
"sfRequired":"Force 2FA registration at login",
"sfRemovedNotification":"Warn if an expired 2FA is removed",
"sfRemovedMsgRule":"Attivazione",
"sfRemovedUseNotif":"Use Notifications plugin",
"sfRemovedNotifMsg":"Notification message",
"sfRemovedNotifRef":"Notification reference",
"sfRemovedNotifTitle":"Notification title",
"sfRemovedMsg":"Display a message if an expired SF is removed",
"sfRemovedMsg":"Display a message if an expired 2FA is removed",
"show":"Mostra",
"showHelp":"Mostra aiuto",
"showLanguages":"Mostra la scelta delle lingue",
"singleIP":"Solo un IP per utente",
"singleSession":"Una sola sessione per utente",
"singleUserByIP":"Un utente per indirizzo IP",
"singleSessionUserByIP":"Una sessione per indirizzo IP",
"singleIP":"One IP address per user",
"singleSession":"One session per user",
"singleUserByIP":"One user per IP address",
"skipRenewConfirmation":"Salta la conferma di re-auth",
"slaveAuthnLevel":"Livello di autenticazione",
"slaveDisplayLogo":"Display authentication logo",

View File

@ -319,6 +319,12 @@
"forms":"Formlar",
"friendlyName":"Kolay ad",
"generalParameters":"Genel Parametreler",
"githubAuthnLevel":"Doğrulama seviyesi",
"githubClientID":"Client ID",
"githubClientSecret":"Client secret",
"githubParams":"GitHub Parameters",
"githubScope":"Scope",
"githubUserField":"Field containing user identifier",
"globalLogout":"Global çıkış",
"globalLogoutCustomParam":"Custom parameter",
"globalLogoutRule":"Aktivasyon",
@ -430,6 +436,7 @@
"ldapTimeout":"Zaman aşımı",
"ldapUsePasswordResetAttribute":"Sıfırlama niteliklerini kullan",
"ldapVersion":"Sürüm",
"level":"Seviyesi",
"linkedInAuthnLevel":"Doğrulama seviyesi",
"linkedInClientID":"İstemci ID",
"linkedInClientSecret":"İstemci sırrı",
@ -559,7 +566,10 @@
"oidcOPMetaDataJWKS":"JWKS verisi",
"oidcOPMetaDataNode":"OpenID Connect Sağlayıcıları",
"oidcOPMetaDataOptions":"Seçenekler",
"oidcRPMetaDataOptionsAuthentication":"Doğrulama",
"oidcRPMetaDataOptionsBasic":"Basic",
"oidcRPMetaDataOptionsAdditionalAudiences":"Additional audiences",
"oidcRPMetaDataOptionsAdvanced":"Advanced",
"oidcRPMetaDataOptionsTimeouts":"Timeouts",
"oidcRPMetaDataOptionsAllowOffline":"Çevrimdışı erişime izin ver",
"oidcOPMetaDataOptionsCheckJWTSignature":"JWT imzasını kontrol et",
"oidcOPMetaDataOptionsClientID":"İstemci ID",
@ -591,6 +601,7 @@
"oidcRPMetaDataNode":"OpenID Connect Relying Parties",
"oidcRPMetaDataOptions":"Seçenekler",
"oidcRPMetaDataOptionsAccessTokenExpiration":"Erişim jetonu sona erme",
"oidcRPMetaDataOptionsAllowPasswordGrant":"Allow OAuth2.0 Password Grant",
"oidcRPMetaDataOptionsAuthorizationCodeExpiration":"Yetkilendirme Kodu sona erme",
"oidcRPMetaDataOptionsBypassConsent":"İzni es geç",
"oidcRPMetaDataOptionsClientID":"İstemci ID",
@ -827,21 +838,20 @@
"sessionTitle":"Oturum içeriği",
"sfaTitle":"İki Faktörlü Kimlik Doğrulaması",
"sfExtra":"Ek ikinci faktörler",
"sfRequired":"2FA gerektir",
"sfRemovedNotification":"Warn if an expired SF is removed",
"sfRequired":"Force 2FA registration at login",
"sfRemovedNotification":"Warn if an expired 2FA is removed",
"sfRemovedMsgRule":"Aktivasyon",
"sfRemovedUseNotif":"Bildirimler eklentisini kullan",
"sfRemovedNotifMsg":"Bildirim mesajı",
"sfRemovedNotifRef":"Bildirim kaynağı",
"sfRemovedNotifTitle":"Bildirim başlığı",
"sfRemovedMsg":"Süresi dolan bir SF kaldırıldığında bir mesaj göster",
"sfRemovedMsg":"Süresi dolan bir 2FA kaldırıldığında bir mesaj göster",
"show":"Görüntüle",
"showHelp":"Yardımı görüntüle",
"showLanguages":"Dil tercihini görüntüle",
"singleIP":"Kullanıcıdan yalnızca bir IP",
"singleSession":"Kullanıcıdan yalnızca bir oturum",
"singleUserByIP":"IP adresinden bir kullanıcı",
"singleSessionUserByIP":"IP adresinden bir oturum",
"singleIP":"One IP address per user",
"singleSession":"One session per user",
"singleUserByIP":"One user per IP address",
"skipRenewConfirmation":"Yeniden yetkilendirme doğrulamasını geç",
"slaveAuthnLevel":"Doğrulama seviyesi",
"slaveDisplayLogo":"Doğrulama logosunu görüntüle",

View File

@ -319,6 +319,12 @@
"forms":"Biểu mẫu",
"friendlyName":"Tên thân thiện",
"generalParameters":"Thông số chung",
"githubAuthnLevel":"Mức xác thực",
"githubClientID":"Client ID",
"githubClientSecret":"Client secret",
"githubParams":"GitHub Parameters",
"githubScope":"Scope",
"githubUserField":"Field containing user identifier",
"globalLogout":"Global logout",
"globalLogoutCustomParam":"Custom parameter",
"globalLogoutRule":"Activation",
@ -430,6 +436,7 @@
"ldapTimeout":"Thời gian chờ",
"ldapUsePasswordResetAttribute":"Sử dụng thuộc tính đặt lại",
"ldapVersion":"Phiên bản",
"level":"Mức",
"linkedInAuthnLevel":"Mức xác thực",
"linkedInClientID":"Client ID",
"linkedInClientSecret":"Trình khách bí mật",
@ -559,7 +566,10 @@
"oidcOPMetaDataJWKS":"Dữ liệu JWKS",
"oidcOPMetaDataNode":"Nhà cung cấp Kết nối OpenID",
"oidcOPMetaDataOptions":"Tùy chọn",
"oidcRPMetaDataOptionsAuthentication":"Xác thực",
"oidcRPMetaDataOptionsBasic":"Basic",
"oidcRPMetaDataOptionsAdditionalAudiences":"Additional audiences",
"oidcRPMetaDataOptionsAdvanced":"Advanced",
"oidcRPMetaDataOptionsTimeouts":"Timeouts",
"oidcRPMetaDataOptionsAllowOffline":"Allow offline access",
"oidcOPMetaDataOptionsCheckJWTSignature":"Kiểm tra chữ ký JWT",
"oidcOPMetaDataOptionsClientID":"Client ID",
@ -591,6 +601,7 @@
"oidcRPMetaDataNode":"OpenID Connect Relying Parties",
"oidcRPMetaDataOptions":"Tùy chọn",
"oidcRPMetaDataOptionsAccessTokenExpiration":"Access Token expiration",
"oidcRPMetaDataOptionsAllowPasswordGrant":"Allow OAuth2.0 Password Grant",
"oidcRPMetaDataOptionsAuthorizationCodeExpiration":"Authorization Code expiration",
"oidcRPMetaDataOptionsBypassConsent":"Bỏ qua sự đồng ý",
"oidcRPMetaDataOptionsClientID":"Client ID",
@ -825,23 +836,22 @@
"sessionStartedAt":"Phiên bắt đầu lúc",
"sessionStorage":"Sessions lưu trữ",
"sessionTitle":"Nội dung phiên",
"sfaTitle":"Second Factors Authentication",
"sfaTitle":"Second factors authentication",
"sfExtra":"Additional second factors",
"sfRequired":"Require 2FA",
"sfRemovedNotification":"Warn if an expired SF is removed",
"sfRequired":"Force 2FA registration at login",
"sfRemovedNotification":"Warn if an expired 2FA is removed",
"sfRemovedMsgRule":"Kích hoạt",
"sfRemovedUseNotif":"Use Notifications plugin",
"sfRemovedNotifMsg":"Notification message",
"sfRemovedNotifRef":"Notification reference",
"sfRemovedNotifTitle":"Notification title",
"sfRemovedMsg":"Display a message if an expired SF is removed",
"sfRemovedMsg":"Display a message if an expired 2FA is removed",
"show":"Hiển thị",
"showHelp":"Hiển thị trợ giúp",
"showLanguages":"Show languages choice",
"singleIP":"Chỉ một địa chỉ IP bởi người dùng",
"singleSession":"Một phiên chỉ bởi người dùng",
"singleUserByIP":"Một người dùng theo địa chỉ IP",
"singleSessionUserByIP":"Một phiên theo địa chỉ IP",
"singleIP":"One IP address per user",
"singleSession":"One session per user",
"singleUserByIP":"One user per IP address",
"skipRenewConfirmation":"Skip re-auth confirmation",
"slaveAuthnLevel":"Mức xác thực",
"slaveDisplayLogo":"Display authentication logo",

View File

@ -319,6 +319,12 @@
"forms":"Forms",
"friendlyName":"Friendly name",
"generalParameters":"通用参数",
"githubAuthnLevel":"认证等级",
"githubClientID":"Client ID",
"githubClientSecret":"Client secret",
"githubParams":"GitHub Parameters",
"githubScope":"Scope",
"githubUserField":"Field containing user identifier",
"globalLogout":"Global logout",
"globalLogoutCustomParam":"Custom parameter",
"globalLogoutRule":"Activation",
@ -430,6 +436,7 @@
"ldapTimeout":"Timeout",
"ldapUsePasswordResetAttribute":"Use reset attribute",
"ldapVersion":"版本",
"level":"Level",
"linkedInAuthnLevel":"认证等级",
"linkedInClientID":"Client ID",
"linkedInClientSecret":"Client secret",
@ -559,7 +566,10 @@
"oidcOPMetaDataJWKS":"JWKS data",
"oidcOPMetaDataNode":"OpenID Connect Providers",
"oidcOPMetaDataOptions":"Options",
"oidcRPMetaDataOptionsAuthentication":"Authentication",
"oidcRPMetaDataOptionsBasic":"Basic",
"oidcRPMetaDataOptionsAdditionalAudiences":"Additional audiences",
"oidcRPMetaDataOptionsAdvanced":"Advanced",
"oidcRPMetaDataOptionsTimeouts":"Timeouts",
"oidcRPMetaDataOptionsAllowOffline":"Allow offline access",
"oidcOPMetaDataOptionsCheckJWTSignature":"Check JWT signature",
"oidcOPMetaDataOptionsClientID":"Client ID",
@ -591,6 +601,7 @@
"oidcRPMetaDataNode":"OpenID Connect Relying Parties",
"oidcRPMetaDataOptions":"Options",
"oidcRPMetaDataOptionsAccessTokenExpiration":"Access Token expiration",
"oidcRPMetaDataOptionsAllowPasswordGrant":"Allow OAuth2.0 Password Grant",
"oidcRPMetaDataOptionsAuthorizationCodeExpiration":"Authorization Code expiration",
"oidcRPMetaDataOptionsBypassConsent":"Bypass consent",
"oidcRPMetaDataOptionsClientID":"Client ID",
@ -746,7 +757,7 @@
"radius2fSecret":"Shared secret",
"radius2fUsernameSessionKey":"Session key containing login",
"radius2fTimeout":"Authentication timeout",
"radius2fAuthnLevel":"Authentication level",
"radius2fAuthnLevel":"认证等级",
"radius2fLogo":"Logo",
"radius2fLabel":"Label",
"radiusAuthnLevel":"认证等级",
@ -825,23 +836,22 @@
"sessionStartedAt":"Session started on",
"sessionStorage":"Sessions Storage",
"sessionTitle":"Session content",
"sfaTitle":"Second Factors Authentication",
"sfaTitle":"Second factors authentication",
"sfExtra":"Additional second factors",
"sfRequired":"Require 2FA",
"sfRemovedNotification":"Warn if an expired SF is removed",
"sfRequired":"Force 2FA registration at login",
"sfRemovedNotification":"Warn if an expired 2FA is removed",
"sfRemovedMsgRule":"激活",
"sfRemovedUseNotif":"Use Notifications plugin",
"sfRemovedNotifMsg":"Notification message",
"sfRemovedNotifRef":"Notification reference",
"sfRemovedNotifTitle":"Notification title",
"sfRemovedMsg":"Display a message if an expired SF is removed",
"sfRemovedMsg":"Display a message if an expired 2FA is removed",
"show":"Show",
"showHelp":"Show help",
"showLanguages":"Show languages choice",
"singleIP":"One IP only by user",
"singleSession":"One session only by user",
"singleUserByIP":"One user by IP address",
"singleSessionUserByIP":"One session by IP address",
"singleIP":"One IP address per user",
"singleSession":"One session per user",
"singleUserByIP":"One user per IP address",
"skipRenewConfirmation":"Skip re-auth confirmation",
"slaveAuthnLevel":"认证等级",
"slaveDisplayLogo":"Display authentication logo",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -54,9 +54,9 @@ sub check200 {
checkJson( $test, $res );
}
sub check405 {
sub check400 {
my ( $test, $res ) = splice @_;
ok( $res->[0] == 405, "$test: Result code is 405" );
ok( $res->[0] == 400, "$test: Result code is 400" );
count(1);
checkJson( $test, $res );
}
@ -140,7 +140,7 @@ sub checkGetBadType {
my ( $test, $res );
$test = "Get for uid $uid and type \"$type\" should get rejected.";
$res = get( $test, $uid, $type );
check405( $test, $res );
check400( $test, $res );
}
sub checkGetOnIds {
@ -211,7 +211,7 @@ sub checkDeleteBadType {
my ( $test, $res );
$test = "Delete for uid $uid and type \"$type\" should get rejected.";
$res = del( $test, $uid, $type );
check405( $test, $res );
check400( $test, $res );
}
my $sfaDevices = [];

View File

@ -8,28 +8,62 @@ require 't/test-lib.pm';
our $_json = JSON->new->allow_nonref;
sub check201 {
my ( $test, $res ) = splice @_;
#diag Dumper($res);
is( $res->[0], "201", "$test: Result code is 201" )
or diag explain $res->[2];
count(1);
checkJson( $test, $res );
}
sub check204 {
my ( $test, $res ) = splice @_;
#diag Dumper($res);
is( $res->[0], "204", "$test: Result code is 204" )
or diag explain $res->[2];
count(1);
is( $res->[2]->[0], undef, "204 code returns no content" );
}
sub check200 {
my ( $test, $res ) = splice @_;
#diag Dumper($res);
ok( $res->[0] == 200, "$test: Result code is 200" );
is( $res->[0], "200", "$test: Result code is 200" )
or diag explain $res->[2];
count(1);
checkJson( $test, $res );
}
sub check409 {
my ( $test, $res ) = splice @_;
#diag Dumper($res);
is( $res->[0], "409", "$test: Result code is 409" )
or diag explain $res->[2];
count(1);
checkJson( $test, $res );
}
sub check404 {
my ( $test, $res ) = splice @_;
#diag Dumper($res);
ok( $res->[0] == 404, "$test: Result code is 404" );
is( $res->[0], "404", "$test: Result code is 404" )
or diag explain $res->[2];
count(1);
checkJson( $test, $res );
}
sub check405 {
sub check400 {
my ( $test, $res ) = splice @_;
ok( $res->[0] == 405, "$test: Result code is 405" );
is( $res->[0], "400", "$test: Result code is 400" )
or diag explain $res->[2];
count(1);
count(1);
checkJson( $test, $res );
}
@ -63,17 +97,17 @@ sub add {
sub checkAdd {
my ( $test, $type, $add ) = splice @_;
check200( $test, add( $test, $type, $add ) );
check201( $test, add( $test, $type, $add ) );
}
sub checkAddFailsIfExists {
my ( $test, $type, $add ) = splice @_;
check405( $test, add( $test, $type, $add ) );
check409( $test, add( $test, $type, $add ) );
}
sub checkAddWithUnknownAttributes {
my ( $test, $type, $add ) = splice @_;
check405( $test, add( $test, $type, $add ) );
check400( $test, add( $test, $type, $add ) );
}
sub get {
@ -92,7 +126,12 @@ sub checkGet {
my @path = split '/', $attrPath;
my $key = from_json( $res->[2]->[0] );
for (@path) {
$key = $key->{$_};
if ( ref($key) eq 'ARRAY' ) {
$key = $key->[$_];
}
else {
$key = $key->{$_};
}
}
ok(
$key eq $expectedValue,
@ -126,7 +165,7 @@ sub update {
sub checkUpdate {
my ( $test, $type, $confKey, $update ) = splice @_;
check200( $test, update( $test, $type, $confKey, $update ) );
check204( $test, update( $test, $type, $confKey, $update ) );
}
sub checkUpdateNotFound {
@ -136,12 +175,12 @@ sub checkUpdateNotFound {
sub checkUpdateFailsIfExists {
my ( $test, $type, $confKey, $update ) = splice @_;
check405( $test, update( $test, $type, $confKey, $update ) );
check409( $test, update( $test, $type, $confKey, $update ) );
}
sub checkUpdateWithUnknownAttributes {
my ( $test, $type, $confKey, $update ) = splice @_;
check405( $test, update( $test, $type, $confKey, $update ) );
check400( $test, update( $test, $type, $confKey, $update ) );
}
sub replace {
@ -162,12 +201,12 @@ sub replace {
sub checkReplace {
my ( $test, $type, $confKey, $replace ) = splice @_;
check200( $test, replace( $test, $type, $confKey, $replace ) );
check204( $test, replace( $test, $type, $confKey, $replace ) );
}
sub checkReplaceAlreadyThere {
my ( $test, $type, $confKey, $replace ) = splice @_;
check405( $test, replace( $test, $type, $confKey, $replace ) );
check400( $test, replace( $test, $type, $confKey, $replace ) );
}
sub checkReplaceNotFound {
@ -175,9 +214,9 @@ sub checkReplaceNotFound {
check404( $test, replace( $test, $type, $confKey, $update ) );
}
sub checkReplaceWithUnknownAttribute {
sub checkReplaceWithInvalidAttribute {
my ( $test, $type, $confKey, $replace ) = splice @_;
check405( $test, replace( $test, $type, $confKey, $replace ) );
check400( $test, replace( $test, $type, $confKey, $replace ) );
}
sub findByConfKey {
@ -194,21 +233,19 @@ sub findByConfKey {
return $res;
}
sub checkFindByConfKeyError {
my ( $test, $type, $pattern ) = splice @_;
my $res = findByConfKey( $test, $type, $pattern );
check400( $test, $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);
}
my $counter = @{$hits};
ok(
$counter eq $expectedHits,
"$test: check if nb of hits returned ($counter) matches expectation ($expectedHits)"
@ -271,7 +308,7 @@ sub deleteProvider {
sub checkDelete {
my ( $test, $type, $confKey ) = splice @_;
check200( $test, deleteProvider( $test, $type, $confKey ) );
check204( $test, deleteProvider( $test, $type, $confKey ) );
}
sub checkDeleteNotFound {
@ -283,65 +320,74 @@ my $test;
my $oidcRp = {
confKey => 'myOidcRp1',
clientId => 'myOidcClient1',
clientId => 'myOidcClient0',
exportedVars => {
'sub' => "uid",
family_name => "sn",
given_name => "givenName"
},
extraClaim => {
macros => {
given_name => '$cn',
},
redirectUris => [ "http://url/1", "http://url/2", ],
extraClaims => {
phone => 'telephoneNumber',
email => 'mail'
email => 'mail',
},
options => {
oidcRPMetaDataOptionsClientSecret => 'secret',
oidcRPMetaDataOptionsIcon => 'web.png'
clientSecret => 'secret',
icon => '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' );
checkGet( $test, 'oidc/rp', 'myOidcRp1', 'options/icon', 'web.png' );
checkGet( $test, 'oidc/rp', 'myOidcRp1', 'options/clientId', 'myOidcClient0' );
checkGet( $test, 'oidc/rp', 'myOidcRp1', 'options/redirectUris/0',
'http://url/1' );
checkGet( $test, 'oidc/rp', 'myOidcRp1', 'options/clientSecret', 'secret' );
$test = "OidcRp - Check attribute default value was set after add";
checkGet( $test, 'oidc/rp', 'myOidcRp1',
'options/oidcRPMetaDataOptionsIDTokenSignAlg', 'HS512' );
checkGet( $test, 'oidc/rp', 'myOidcRp1', 'options/IDTokenSignAlg', 'HS512' );
$test = "OidcRp - Add Should fail on duplicate confKey";
$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};
$oidcRp->{options}->{clientId} = 'myOidcClient1';
$oidcRp->{options}->{clientSecret} = 'secret2';
$oidcRp->{options}->{IDTokenSignAlg} = 'RS512';
delete $oidcRp->{options}->{icon};
delete $oidcRp->{extraClaims};
delete $oidcRp->{exportedVars};
$oidcRp->{exportedVars}->{cn} = 'cn';
delete $oidcRp->{clientId};
$oidcRp->{macros}->{given_name} = '$givenName';
$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', 'options/clientSecret', 'secret2' );
checkGet( $test, 'oidc/rp', 'myOidcRp1', 'options/clientId', 'myOidcClient1' );
checkGet( $test, 'oidc/rp', 'myOidcRp1', 'options/IDTokenSignAlg', 'RS512' );
checkGet( $test, 'oidc/rp', 'myOidcRp1', 'options/icon', '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',
checkGet( $test, 'oidc/rp', 'myOidcRp1', 'macros/given_name', '$givenName' );
checkGet( $test, 'oidc/rp', 'myOidcRp1', 'extraClaims/phone',
'telephoneNumber' );
checkGet( $test, 'oidc/rp', 'myOidcRp1', 'options/redirectUris/1',
'http://url/2' );
$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';
$test = "OidcRp - Add should fail on duplicate clientId";
$oidcRp->{clientId} = "myOidcClient1";
$oidcRp->{confKey} = 'myOidcRp2';
checkAddFailsIfExists( $test, 'oidc/rp', $oidcRp );
$test = "OidcRp - Add Should fail on non existing options";
$test = "OidcRp - Add should fail on non existing options";
$oidcRp->{confKey} = 'myOidcRp2';
$oidcRp->{clientId} = 'myOidcClient2';
$oidcRp->{options}->{playingPossum} = 'ElephantInTheRoom';
@ -359,28 +405,35 @@ $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};
$test = "OidcRp - Replace should succeed";
$oidcRp->{confKey} = 'myOidcRp2';
$oidcRp->{clientId} = 'myOidcClient2';
$oidcRp->{redirectUris} = ["http://url/3"];
delete $oidcRp->{options}->{icon};
delete $oidcRp->{options}->{IDTokenSignAlg};
checkReplace( $test, 'oidc/rp', 'myOidcRp2', $oidcRp );
$test = "OidcRp - Check attribute default value was set after replace";
checkGet( $test, 'oidc/rp', 'myOidcRp2',
'options/oidcRPMetaDataOptionsIDTokenSignAlg', 'HS512' );
checkGet( $test, 'oidc/rp', 'myOidcRp2', 'options/IDTokenSignAlg', 'HS512' );
checkGet( $test, 'oidc/rp', 'myOidcRp2', 'options/redirectUris/0',
'http://url/3' );
$test = "OidcRp - Replace should fail on non existing options";
$test = "OidcRp - Replace should fail on non existing or invalid options";
$oidcRp->{options}->{playingPossum} = 'elephant';
checkReplaceWithUnknownAttribute( $test, 'oidc/rp', 'myOidcRp2', $oidcRp );
checkReplaceWithInvalidAttribute( $test, 'oidc/rp', 'myOidcRp2', $oidcRp );
delete $oidcRp->{options}->{playingPossum};
$oidcRp->{options}->{IDTokenExpiration} = "XXX";
checkReplaceWithInvalidAttribute( $test, 'oidc/rp', 'myOidcRp2', $oidcRp );
$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 );
checkFindByConfKey( $test, 'oidc/rp', '*', 2 );
$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 );
@ -388,6 +441,10 @@ checkFindByConfKey( $test, 'oidc/rp', 'myOidcRp1', 1 );
$test = "OidcRp - FindByConfKey should find 0 hits";
checkFindByConfKey( $test, 'oidc/rp', 'myOidcRp3', 0 );
$test = "OidcRp - FindByConfKey should err on invalid patterns";
checkFindByConfKeyError( $test, 'oidc/rp', '' );
checkFindByConfKeyError( $test, 'oidc/rp', '$' );
$test = "OidcRp - FindByClientId should find one entry";
checkFindByProviderId( $test, 'oidc/rp', 'clientId', 'myOidcClient1' );
@ -440,40 +497,43 @@ my $samlSp = {
name => "givenName"
}
},
macros => {
given_name => '$givenName',
},
options => {
samlSPMetaDataOptionsCheckSLOMessageSignature => 0,
samlSPMetaDataOptionsEncryptionMode => "assertion",
samlSPMetaDataOptionsSessionNotOnOrAfterTimeout => 36000
checkSLOMessageSignature => 0,
encryptionMode => "assertion",
sessionNotOnOrAfterTimeout => 36000
}
};
$test = "SamlSp - Add should succeed";
checkAdd( $test, 'saml/sp', $samlSp );
checkGet( $test, 'saml/sp', 'mySamlSp1',
'options/samlSPMetaDataOptionsEncryptionMode', 'assertion' );
'options/encryptionMode', 'assertion' );
checkGet( $test, 'saml/sp', 'mySamlSp1',
'options/samlSPMetaDataOptionsSessionNotOnOrAfterTimeout', 36000 );
'options/sessionNotOnOrAfterTimeout', 36000 );
$test = "SamlSp - Check attribute default value was set after add";
checkGet( $test, 'saml/sp', 'mySamlSp1',
'options/samlSPMetaDataOptionsNotOnOrAfterTimeout', 72000 );
checkGet( $test, 'saml/sp', 'mySamlSp1', 'options/notOnOrAfterTimeout', 72000 );
$test = "SamlSp - Add Should fail on duplicate confKey";
$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};
$samlSp->{options}->{checkSLOMessageSignature} = 1;
$samlSp->{options}->{encryptionMode} = 'nameid';
delete $samlSp->{options}->{sessionNotOnOrAfterTimeout};
delete $samlSp->{exportedAttributes};
$samlSp->{exportedAttributes}->{cn}->{name} = "cn",
$samlSp->{macros}->{family_name} = '$sn',
$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 );
'options/checkSLOMessageSignature', 1 );
checkGet( $test, 'saml/sp', 'mySamlSp1',
'options/samlSPMetaDataOptionsSessionNotOnOrAfterTimeout', 36000 );
'options/sessionNotOnOrAfterTimeout', 36000 );
checkGet( $test, 'saml/sp', 'mySamlSp1', 'exportedAttributes/cn/friendlyName',
'common_name' );
checkGet( $test, 'saml/sp', 'mySamlSp1', 'exportedAttributes/cn/mandatory',
@ -483,17 +543,19 @@ checkGet( $test, 'saml/sp', 'mySamlSp1', 'exportedAttributes/cn/mandatory',
checkGet( $test, 'saml/sp', 'mySamlSp1', 'exportedAttributes/cn/name', 'uid' );
checkGet( $test, 'saml/sp', 'mySamlSp1', 'exportedAttributes/given_name/name',
'givenName' );
checkGet( $test, 'saml/sp', 'mySamlSp1', 'macros/family_name', '$sn' );
checkGet( $test, 'saml/sp', 'mySamlSp1', 'macros/given_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";
$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";
$test = "SamlSp - Add should fail on non existing options";
$samlSp->{confKey} = 'mySamlSp2';
$samlSp->{metadata} = $metadata2;
$samlSp->{options}->{playingPossum} = 'ElephantInTheRoom';
@ -514,31 +576,42 @@ checkUpdateNotFound( $test, 'saml/sp', 'mySamlSp3', $samlSp );
$test = "SamlSp - Replace should succeed";
$samlSp->{confKey} = 'mySamlSp2';
$samlSp->{metadata} = $metadata2;
delete $samlSp->{options}->{samlSPMetaDataOptionsEncryptionMode};
delete $samlSp->{options}->{encryptionMode};
checkReplace( $test, 'saml/sp', 'mySamlSp2', $samlSp );
$test = "SamlSp - Check attribute default value was set after replace";
checkGet( $test, 'saml/sp', 'mySamlSp2',
'options/samlSPMetaDataOptionsEncryptionMode', 'none' );
checkGet( $test, 'saml/sp', 'mySamlSp2', 'options/encryptionMode', 'none' );
$test = "SamlSp - Replace should fail on non existing options";
$samlSp->{options}->{playingPossum} = 'elephant';
checkReplaceWithUnknownAttribute( $test, 'saml/sp', 'mySamlSp2', $samlSp );
checkReplaceWithInvalidAttribute( $test, 'saml/sp', 'mySamlSp2', $samlSp );
delete $samlSp->{options}->{playingPossum};
$samlSp->{options}->{notOnOrAfterTimeout} = "XXX";
checkReplaceWithInvalidAttribute( $test, 'saml/sp', 'mySamlSp2', $samlSp );
$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 );
checkFindByConfKey( $test, 'saml/sp', '*', 2 );
$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 1 hit";
checkFindByConfKey( $test, 'saml/sp', '*Sp1', 1 );
$test = "SamlSp - FindByConfKey should find 0 hits";
checkFindByConfKey( $test, 'saml/sp', 'mySamlSp3', 0 );
$test = "SamlSp - FindByConfKey should err on invalid patterns";
checkFindByConfKeyError( $test, 'saml/sp', '' );
checkFindByConfKeyError( $test, 'saml/sp', '$' );
$test = "SamlSp - FindByEntityId should find one entry";
checkFindByProviderId( $test, 'saml/sp', 'entityId',
'https://myapp.domain.com/saml/metadata' );

View File

@ -1172,11 +1172,6 @@
"id": "singleUserByIP",
"title": "singleUserByIP",
"type": "bool"
}, {
"default": 0,
"id": "singleSessionUserByIP",
"title": "singleSessionUserByIP",
"type": "bool"
}, {
"default": 1,
"id": "notifyDeleted",

View File

@ -1314,12 +1314,6 @@
"title": "singleUserByIP",
"type": "bool",
"data": 0
}, {
"default": 0,
"id": "singleSessionUserByIP",
"title": "singleSessionUserByIP",
"type": "bool",
"data": 0
}, {
"default": 1,
"id": "notifyDeleted",

View File

@ -1289,12 +1289,6 @@
"title": "singleUserByIP",
"type": "bool",
"data": 0
}, {
"default": 0,
"id": "singleSessionUserByIP",
"title": "singleSessionUserByIP",
"type": "bool",
"data": 0
}, {
"default": 1,
"id": "notifyDeleted",

View File

@ -1325,12 +1325,6 @@
"title": "singleUserByIP",
"type": "bool",
"data": 0
}, {
"default": 0,
"id": "singleSessionUserByIP",
"title": "singleSessionUserByIP",
"type": "bool",
"data": 0
}, {
"default": 1,
"id": "notifyDeleted",

View File

@ -1327,12 +1327,6 @@
"title": "singleUserByIP",
"type": "bool",
"data": 0
}, {
"default": 0,
"id": "singleSessionUserByIP",
"title": "singleSessionUserByIP",
"type": "bool",
"data": 0
}, {
"default": 1,
"id": "notifyDeleted",

View File

@ -1309,12 +1309,6 @@
"title": "singleUserByIP",
"type": "bool",
"data": 0
}, {
"default": 0,
"id": "singleSessionUserByIP",
"title": "singleSessionUserByIP",
"type": "bool",
"data": 0
}, {
"default": 1,
"id": "notifyDeleted",

View File

@ -1833,12 +1833,6 @@
"title": "singleUserByIP",
"type": "bool"
},
{
"default": 0,
"id": "singleSessionUserByIP",
"title": "singleSessionUserByIP",
"type": "bool"
},
{
"default": 1,
"id": "notifyDeleted",

View File

@ -2221,12 +2221,6 @@
"title" : "singleUserByIP",
"type" : "bool"
},
{
"default" : 0,
"id" : "singleSessionUserByIP",
"title" : "singleSessionUserByIP",
"type" : "bool"
},
{
"default" : 1,
"id" : "notifyDeleted",

View File

@ -1174,11 +1174,6 @@
"id": "singleUserByIP",
"title": "singleUserByIP",
"type": "bool"
}, {
"default": 0,
"id": "singleSessionUserByIP",
"title": "singleSessionUserByIP",
"type": "bool"
}, {
"default": 1,
"id": "notifyDeleted",

View File

@ -23,7 +23,7 @@ useRedirectOnError = 0
[manager]
skippedUnitTests = cookieExpiration
skippedGlobalTests = cookieTTL
skippedGlobalTests = cookieTTL oidcRPNeedRSAKey
protection = manager
staticPrefix = app/

View File

@ -29,6 +29,7 @@ lib/Lemonldap/NG/Portal/Auth/Custom.pm
lib/Lemonldap/NG/Portal/Auth/DBI.pm
lib/Lemonldap/NG/Portal/Auth/Demo.pm
lib/Lemonldap/NG/Portal/Auth/Facebook.pm
lib/Lemonldap/NG/Portal/Auth/GitHub.pm
lib/Lemonldap/NG/Portal/Auth/GPG.pm
lib/Lemonldap/NG/Portal/Auth/Kerberos.pm
lib/Lemonldap/NG/Portal/Auth/LDAP.pm
@ -355,6 +356,7 @@ site/htdocs/static/common/modules/Apache.png
site/htdocs/static/common/modules/CAS.png
site/htdocs/static/common/modules/CustomAuth.png
site/htdocs/static/common/modules/Facebook.png
site/htdocs/static/common/modules/GitHub.png
site/htdocs/static/common/modules/Google.png
site/htdocs/static/common/modules/Kerberos.png
site/htdocs/static/common/modules/LinkedIn.png
@ -365,6 +367,7 @@ site/htdocs/static/common/modules/SSL.png
site/htdocs/static/common/modules/Twitter.png
site/htdocs/static/common/modules/WebID.png
site/htdocs/static/common/nl.png
site/htdocs/static/common/pl.png
site/htdocs/static/common/pt.png
site/htdocs/static/common/ro.png
site/htdocs/static/common/tr.png
@ -378,6 +381,7 @@ site/htdocs/static/languages/fi.json
site/htdocs/static/languages/fr.json
site/htdocs/static/languages/it.json
site/htdocs/static/languages/nl.json
site/htdocs/static/languages/pl.json
site/htdocs/static/languages/pt.json
site/htdocs/static/languages/ro.json
site/htdocs/static/languages/tr.json
@ -400,6 +404,7 @@ site/templates/bootstrap/customLoginFooter.tpl
site/templates/bootstrap/customLoginHeader.tpl
site/templates/bootstrap/decryptvalue.tpl
site/templates/bootstrap/error.tpl
site/templates/bootstrap/errormsg.tpl
site/templates/bootstrap/ext2fcheck.tpl
site/templates/bootstrap/footer.tpl
site/templates/bootstrap/globallogout.tpl
@ -612,6 +617,7 @@ t/44-CertificateResetByMail-LDAP.t
t/50-IssuerGet.t
t/57-GlobalLogout-without-Timer.t
t/57-GlobalLogout.t
t/57-LogoutForward.t
t/58-DecryptValue-with-custom-function.t
t/58-DecryptValue-with-internal-function.t
t/59-Double-cookies-for-a-Single-session.t
@ -677,8 +683,10 @@ t/76-2F-Ext-with-History.t
t/77-2F-Extra.t
t/77-2F-Mail-with-global-storage.t
t/77-2F-Mail.t
t/78-2F-Upgrade-Many.t
t/78-2F-Upgrade.t
t/90-Translations.t
t/91-Memory-Leak.t
t/99-Dont-load-Dumper.t
t/99-pod.t
t/gpghome/key.asc

View File

@ -113,11 +113,13 @@ sub init {
my $rule = $self->conf->{sfExtra}->{$extraKey}->{rule} || 1;
my $prefix = $m->prefix;
# Overwrite logo and label from user configuration
# Overwrite logo, label, level from user configuration
$m->logo( $self->conf->{sfExtra}->{$extraKey}->{logo} )
if $self->conf->{sfExtra}->{$extraKey}->{logo};
$m->label( $self->conf->{sfExtra}->{$extraKey}->{label} )
if $self->conf->{sfExtra}->{$extraKey}->{label};
$m->authnLevel( $self->conf->{sfExtra}->{$extraKey}->{level} )
if $self->conf->{sfExtra}->{$extraKey}->{level};
# Compile rule
$rule = $self->p->HANDLER->substitute($rule);
@ -161,6 +163,8 @@ sub init {
# Enable REST request only if more than 1 2F module is enabled
if ( @{ $self->{sfModules} } > 1 ) {
$self->addAuthRoute( '2fchoice' => '_choice', ['POST'] );
$self->addAuthRoute( '2fchoice' => '_redirect', ['GET'] );
$self->addUnauthRoute( '2fchoice' => '_choice', ['POST'] );
$self->addUnauthRoute( '2fchoice' => '_redirect', ['GET'] );
}

View File

@ -13,6 +13,10 @@ extends 'Lemonldap::NG::Portal::Auth::_WebForm',
sub init {
my ($self) = @_;
foreach (qw(dbiAuthTable dbiAuthLoginCol dbiAuthPasswordCol)) {
$self->logger->warn( ref($self) . " seems not configured: missing $_" )
unless $self->conf->{$_};
}
return ( $self->Lemonldap::NG::Portal::Auth::_WebForm::init
and $self->Lemonldap::NG::Portal::Lib::DBI::init );
}

View File

@ -0,0 +1,238 @@
package Lemonldap::NG::Portal::Auth::GitHub;
use strict;
use JSON;
use Mouse;
use MIME::Base64 qw/encode_base64 decode_base64/;
use Lemonldap::NG::Common::FormEncode;
use Lemonldap::NG::Common::UserAgent;
use Lemonldap::NG::Portal::Main::Constants qw(PE_OK PE_ERROR PE_REDIRECT);
our $VERSION = '2.0.8';
extends 'Lemonldap::NG::Portal::Main::Auth';
# INITIALIZATION
# return LWP::UserAgent object
has ua => (
is => 'rw',
lazy => 1,
builder => sub {
# TODO : LWP options to use a proxy for example
my $ua = Lemonldap::NG::Common::UserAgent->new( $_[0]->{conf} );
$ua->env_proxy();
return $ua;
}
);
has githubAuthorizationEndpoint => (
is => 'ro',
lazy => 1,
default => sub {
$_[0]->conf->{githubAuthorizationEndpoint}
|| 'https://github.com/login/oauth/authorize';
}
);
has githubTokenEndpoint => (
is => 'ro',
lazy => 1,
default => sub {
$_[0]->conf->{githubTokenEndpoint}
|| 'https://github.com/login/oauth/access_token';
}
);
has githubUserEndpoint => (
is => 'ro',
lazy => 1,
default => sub {
$_[0]->conf->{githubUserEndpoint}
|| 'https://api.github.com/user';
}
);
sub init {
my ($self) = @_;
my $ret = 1;
foreach my $arg (qw(githubClientID githubClientSecret)) {
unless ( $self->conf->{$arg} ) {
$ret = 0;
$self->error("Parameter $arg is required");
}
}
return $ret;
}
# RUNNING METHODS
sub extractFormInfo {
my ( $self, $req ) = @_;
my $nonce = time;
# Build redirect_uri
my $callback_url = $self->conf->{portal};
# Check return values
my $code = $req->param("code");
my $state = $req->param("state");
# Code
if ($code) {
my %form;
$form{"code"} = $code;
$form{"state"} = $state if $state;
$form{"client_id"} = $self->conf->{githubClientID};
$form{"client_secret"} = $self->conf->{githubClientSecret};
$form{"redirect_uri"} = $callback_url;
my $response = $self->ua->post(
$self->githubTokenEndpoint,
\%form,
"Content-Type" => 'application/x-www-form-urlencoded',
'Accept' => 'application/json'
);
if ( $response->is_error ) {
$self->logger->error(
"Bad authorization response: " . $response->message );
$self->logger->debug( $response->content );
return PE_ERROR;
}
my $content = $response->decoded_content;
my $json_hash;
eval { $json_hash = from_json( $content, { allow_nonref => 1 } ); };
if ($@) {
$self->logger->error("Unable to decode JSON $content");
return PE_ERROR;
}
my $access_token = $json_hash->{access_token};
$self->logger->debug("Get access token $access_token from GitHub");
# Call People EndPoint URI
$self->logger->debug(
"Call GitHub People Endpoint " . $self->githubUserEndpoint );
my $people_response = $self->ua->get( $self->githubUserEndpoint,
"Authorization" => "token $access_token" );
if ( $people_response->is_error ) {
$self->logger->error(
"Bad authorization response: " . $people_response->message );
$self->logger->debug( $people_response->content );
return PE_ERROR;
}
my $user_content = $people_response->decoded_content;
$self->logger->debug("Response from GitHub People API: $user_content");
eval {
$json_hash = from_json( $user_content, { allow_nonref => 1 } ); };
if ($@) {
$self->logger->error("Unable to decode JSON $user_content");
return PE_ERROR;
}
foreach ( keys %$json_hash ) {
$req->data->{githubData}->{$_} = $json_hash->{$_};
}
$req->user(
$req->data->{githubData}->{ $self->conf->{githubUserField} } );
$self->logger->debug( "Good GitHub authentication for " . $req->user );
# Extract state
if ($state) {
my $stateSession = $self->p->getApacheSession( $state, 1 );
$req->urldc( $stateSession->data->{urldc} );
$req->{checkLogins} = $stateSession->data->{checkLogins};
$stateSession->remove;
}
return PE_OK;
}
# No code, redirect to GitHub
else {
$self->logger->debug('Redirection to GitHub');
# Store state
my $stateSession =
$self->p->getApacheSession( undef, 1, 0, 'GitHubState' );
my $stateInfos = {};
$stateInfos->{_utime} = time() + $self->conf->{timeout};
$stateInfos->{urldc} = $req->urldc;
$stateInfos->{checkLogins} = $req->{checkLogins};
$stateSession->update($stateInfos);
my $authn_uri = $self->githubAuthorizationEndpoint;
my $client_id = $self->conf->{githubClientID};
my $scope = $self->conf->{githubScope};
$authn_uri .= '?'
. build_urlencoded(
response_type => 'code',
client_id => $client_id,
redirect_uri => $callback_url,
scope => $scope,
state => $stateSession->id,
);
$req->urldc($authn_uri);
$self->logger->debug( "Redirect user to " . $req->urldc );
return PE_REDIRECT;
}
}
sub setAuthSessionInfo {
my ( $self, $req ) = @_;
$req->{sessionInfo}->{authenticationLevel} =
$self->conf->{githubAuthnLevel};
foreach ( keys %{ $req->data->{githubData} } ) {
$req->{sessionInfo}->{ 'github_' . $_ } =
$req->data->{githubData}->{$_};
}
PE_OK;
}
sub authenticate {
PE_OK;
}
sub authFinish {
PE_OK;
}
sub authLogout {
PE_OK;
}
sub authForce {
return 0;
}
sub getDisplayType {
return "logo";
}
1;

View File

@ -26,6 +26,13 @@ sub init {
# @return Lemonldap::NG::Portal constant
sub extractFormInfo {
my ( $self, $req ) = @_;
# If this is the ajax query, allow response to contain HTML code
# to update the portal error message
if ( $req->wantJSON ) {
$req->wantErrorRender(1);
}
my $field = $self->conf->{SSLVar};
if ( $req->env->{SSL_CLIENT_I_DN} ) {
$self->logger->debug(
@ -50,6 +57,16 @@ sub extractFormInfo {
return PE_BADCERTIFICATE;
}
elsif ( $self->conf->{sslByAjax} and not $req->param('nossl') ) {
# If this is the AJAX query
if ( $req->wantJSON ) {
return PE_CERTIFICATEREQUIRED;
}
$self->logger->debug( 'Append ' . $self->{Name} . ' init/script' );
$req->data->{customScript} .= $self->{AjaxInitScript};
$self->logger->debug(
"Send init/script -> " . $req->data->{customScript} );
$req->data->{waitingMessage} = 1;
return PE_FIRSTACCESS;
}
@ -58,6 +75,7 @@ sub extractFormInfo {
$self->logger->debug( 'Append ' . $self->{Name} . ' init/script' );
$self->logger->debug(
"Send init/script -> " . $req->data->{customScript} );
return PE_BADCERTIFICATE;
}
$self->userLogger->warn('No certificate found');
return PE_CERTIFICATEREQUIRED;

View File

@ -99,7 +99,9 @@ sub extractFormInfo {
}
# Security: check for captcha or token
if ( $self->captcha or $self->ottRule->( $req, {} ) ) {
if ( not $req->data->{'skipToken'}
and ( $self->captcha or $self->ottRule->( $req, {} ) ) )
{
my $token;
unless ( $token = $req->param('token') or $self->captcha ) {
$self->userLogger->error('Authentication tried without token');

View File

@ -12,6 +12,7 @@ use Lemonldap::NG::Portal::Main::Constants qw(
PE_LOGOUT_OK
PE_REDIRECT
PE_OK
PE_PASSWORD_OK
PE_UNAUTHORIZEDPARTNER
PE_OIDC_SERVICE_NOT_ALLOWED
);
@ -77,7 +78,7 @@ sub init {
$hd->substitute( $self->conf->{issuerDBOpenIDConnectRule} ) );
unless ($rule) {
my $error = $hd->tsv->{jail}->error || '???';
$self->error( "Bad OIDC activation rule -> $error" );
$self->error("Bad OIDC activation rule -> $error");
return 0;
}
$self->{rule} = $rule;
@ -346,14 +347,20 @@ sub run {
}
# Check scope validity
unless ( $oidc_request->{'scope'} =~ /^[a-zA-Z_\-\s]+$/ ) {
$self->logger->error( "Submitted scope is not valid: "
. $oidc_request->{'scope'} );
# We use a slightly more relaxed version of
# https://tools.ietf.org/html/rfc6749#appendix-A.4
# To be tolerant of user error (trailing spaces, etc.)
# Scope names are restricted to printable ASCII characters,
# excluding double quote and backslash
unless (
$oidc_request->{'scope'} =~ /^[\x20\x21\x23-\x5B\x5D-\x7E]*$/ )
{
$self->logger->error("Submitted scope is not valid");
return PE_ERROR;
}
# Check openid scope
unless ( $oidc_request->{'scope'} =~ /\bopenid\b/ ) {
unless ( $self->_hasScope( 'openid', $oidc_request->{'scope'} ) ) {
$self->logger->debug("No openid scope found");
#TODO manage standard OAuth request
@ -467,7 +474,12 @@ sub run {
foreach my $requested_scope (
split( /\s+/, $oidc_request->{'scope'} ) )
{
if ( $consent_scope =~ /\b$requested_scope\b/ ) {
if (
$self->_hasScope(
$requested_scope, $consent_scope
)
)
{
$self->logger->debug(
"Scope $requested_scope already accepted");
}
@ -490,11 +502,17 @@ sub run {
{
$RPoidcConsent[0]{epoch} = time;
$RPoidcConsent[0]{scope} = $oidc_request->{'scope'};
push @{$_oidcConsents}, @RPoidcConsent;
# Build new consent list by removing all references
# to the current RP from the old list and appending the
# new consent
my @newoidcConsents =
grep { $_->{rp} ne $rp } @$_oidcConsents;
push @newoidcConsents, $RPoidcConsent[0];
$self->logger->debug(
"Append Relying Party $rp Consent");
$self->p->updatePersistentSession( $req,
{ _oidcConsents => to_json($_oidcConsents) } );
{ _oidcConsents => to_json( \@newoidcConsents ) } );
$self->logger->debug(
"Consent given for Relying Party $rp");
@ -537,7 +555,8 @@ sub run {
my $display_name =
$self->conf->{oidcRPMetaDataOptions}->{$rp}
->{oidcRPMetaDataOptionsDisplayName};
my $icon = $self->conf->{oidcRPMetaDataOptions}->{$rp}
my $icon =
$self->conf->{oidcRPMetaDataOptions}->{$rp}
->{oidcRPMetaDataOptionsIcon};
my $imgSrc;
@ -558,7 +577,7 @@ sub run {
};
my @list;
foreach my $requested_scope (
split( /\s/, $oidc_request->{'scope'} ) )
split( /\s+/, $oidc_request->{'scope'} ) )
{
if ( my $message =
$scope_messages->{$requested_scope} )
@ -614,7 +633,9 @@ sub run {
# WIP: Offline access
my $offline = 0;
if ( $oidc_request->{'scope'} =~ /\boffline_access\b/ ) {
if (
$self->_hasScope( 'offline_access', $oidc_request->{'scope'} ) )
{
$offline = 1;
# MUST ensure that the prompt parameter contains consent unless
@ -649,8 +670,10 @@ sub run {
}
# Strip offline_access from scopes from now on
$oidc_request->{'scope'} = join " ", grep !/^offline_access$/,
split /\s+/, $oidc_request->{'scope'};
$oidc_request->{'scope'} = join " ",
grep !/^offline_access$/,
split /\s+/,
$oidc_request->{'scope'};
}
# Authorization Code Flow
@ -724,7 +747,8 @@ sub run {
"Generated access token: $access_token");
# Compute hash to store in at_hash
my $alg = $self->conf->{oidcRPMetaDataOptions}->{$rp}
my $alg =
$self->conf->{oidcRPMetaDataOptions}->{$rp}
->{oidcRPMetaDataOptionsIDTokenSignAlg};
my ($hash_level) = ( $alg =~ /(?:\w{2})(\d{3})/ );
$at_hash = $self->createHash( $access_token, $hash_level )
@ -986,6 +1010,19 @@ sub token {
return $self->_handleRefreshTokenGrant( $req, $rp );
}
# Resource Owner Password Credenials
elsif ( $grant_type eq 'password' ) {
unless (
$self->oidcRPList->{$rp}->{oidcRPMetaDataOptionsAllowPasswordGrant}
)
{
$self->logger->warn(
"Access to grant_type=password, is not allowed for RP $rp");
return $self->p->sendError( $req, 'unauthorized_client', 400 );
}
return $self->_handlePasswordGrant( $req, $rp );
}
# Unknown or unspecified grant type
else {
$self->userLogger->error(
@ -998,6 +1035,137 @@ sub token {
}
sub _handlePasswordGrant {
my ( $self, $req, $rp ) = @_;
my $client_id = $self->oidcRPList->{$rp}->{oidcRPMetaDataOptionsClientID};
my $scope = $req->param('scope') || 'openid';
my $username = $req->param('username');
my $password = $req->param('password');
unless ( $username and $password ) {
$self->logger->error("Missing username or password");
# FIXME
return $self->p->sendError( $req, 'invalid_request', 400 );
}
####
# Authenticate user by running through the regular login process
# minus the buildCookie step
$req->parameters->{user} = ($username);
$req->parameters->{password} = $password;
$req->data->{skipToken} = 1;
$req->steps( [
@{ $self->p->beforeAuth },
$self->p->authProcess,
@{ $self->p->betweenAuthAndData },
$self->p->sessionData,
@{ $self->p->afterData },
'storeHistory',
@{ $self->p->endAuth },
]
);
my $result = $self->p->process($req);
$self->logger->debug( "Credentials check returned "
. $self->p->_formatProcessResult($result) )
if $result;
## Make sure we returned successfuly from the process AND we were able to create a session
unless ( $result == PE_OK and $req->id and $req->user ) {
return $self->p->sendError( $req, 'invalid_grant', 400 );
}
## Make sure the current user is allowed to use this RP
if ( my $rule = $self->spRules->{$rp} ) {
unless ( $rule->( $req, $req->sessionInfo ) ) {
$self->userLogger->warn( 'User '
. $req->sessionInfo->{ $self->conf->{whatToTrace} }
. " is not authorized to access to $rp" );
$self->p->deleteSession($req);
return $self->p->sendError( $req, 'invalid_grant', 400 );
}
}
my $user_id_attribute =
$self->conf->{oidcRPMetaDataOptions}->{$rp}
->{oidcRPMetaDataOptionsUserIDAttr}
|| $self->conf->{whatToTrace};
my $user_id = $req->sessionInfo->{$user_id_attribute};
$self->logger->debug("Found corresponding user: $user_id");
# Generate access_token
my $accessTokenSession = $self->newAccessToken(
$rp,
{
scope => $scope,
rp => $rp,
user_session_id => $req->id,
}
);
unless ($accessTokenSession) {
$self->userLogger->error(
"Unable to create OIDC session for access_token");
#FIXME: should be an error 500
return $self->p->sendError( $req, 'invalid_request', 400 );
}
my $access_token = $accessTokenSession->id;
$self->logger->debug("Generated access token: $access_token");
# Generate refresh_token
my $refresh_token = undef;
if ( $self->conf->{oidcRPMetaDataOptions}->{$rp}
->{oidcRPMetaDataOptionsRefreshToken} )
{
my $refreshTokenSession = $self->newRefreshToken(
$rp,
{
scope => $req->param('scope'),
client_id => $client_id,
user_session_id => $req->id,
},
0,
);
unless ($refreshTokenSession) {
$self->userLogger->error(
"Unable to create OIDC session for refresh_token");
return $self->p->sendError( $req,
'Could not create refresh token session', 500 );
}
$refresh_token = $refreshTokenSession->id;
$self->logger->debug("Generated refresh token: $refresh_token");
}
# Send token response
my $expires_in =
$self->conf->{oidcRPMetaDataOptions}->{$rp}
->{oidcRPMetaDataOptionsAccessTokenExpiration}
|| $self->conf->{oidcServiceAccessTokenExpiration};
my $token_response = {
access_token => $access_token,
token_type => 'Bearer',
expires_in => $expires_in,
( $refresh_token ? ( refresh_token => $refresh_token ) : () ),
};
$self->logger->debug("Send token response");
return $self->p->sendJSONresponse( $req, $token_response );
}
sub _handleAuthorizationCodeGrant {
my ( $self, $req, $rp ) = @_;
@ -1174,7 +1342,7 @@ sub _handleAuthorizationCodeGrant {
my $id_token_payload_hash = {
iss => $self->iss, # Issuer Identifier
sub => $user_id, # Subject Identifier
aud => [$client_id], # Audience
aud => $self->getAudiences($rp), # Audience
exp => $id_token_exp, # expiration
iat => time, # Issued time
auth_time => $apacheSession->data->{_lastAuthnUTime}
@ -1400,11 +1568,11 @@ sub _handleRefreshTokenGrant {
my $id_token_acr = "loa-0";
my $id_token_payload_hash = {
iss => $self->iss, # Issuer Identifier
sub => $user_id, # Subject Identifier
aud => [$client_id], # Audience
exp => $id_token_exp, # expiration
iat => time, # Issued time
iss => $self->iss, # Issuer Identifier
sub => $user_id, # Subject Identifier
aud => $self->getAudiences($rp), # Audience
exp => $id_token_exp, # expiration
iat => time, # Issued time
# TODO: is this the right value when using refresh tokens??
auth_time => $auth_time, # Authentication time
acr => $id_token_acr, # Authentication Context Class Reference
@ -1494,42 +1662,18 @@ sub userInfo {
my $rp = $accessTokenSession->data->{rp};
my $user_session_id = $accessTokenSession->data->{user_session_id};
my $session;
# If using a refreshed access token
if ($user_session_id) {
# Get user identifier
$session = $self->p->getApacheSession($user_session_id);
unless ($session) {
$self->logger->error("Unable to find user session");
return $self->returnBearerError( 'invalid_request',
'Invalid request', 401 );
}
}
else {
my $offline_session_id =
$accessTokenSession->data->{offline_session_id};
unless ($offline_session_id) {
return $self->returnBearerError( 'invalid_request',
'Invalid request', 401 );
}
$session = $self->getRefreshToken($offline_session_id);
unless ($session) {
$self->logger->error("Unable to find refresh session");
return $self->returnBearerError( 'invalid_request',
'Invalid request', 401 );
}
my $session =
$self->_getSessionFromAccessTokenData( $accessTokenSession->data );
unless ($session) {
return $self->returnBearerError( 'invalid_request',
'Invalid request', 401 );
}
my $userinfo_response =
$self->buildUserInfoResponse( $req, $scope, $rp, $session );
unless ($userinfo_response) {
return $self->returnBearerError( 'invalid_request', 'Invalid request',
401 );
return $self->returnBearerError( 'invalid_request',
'Invalid request', 401 );
}
my $userinfo_sign_alg = $self->conf->{oidcRPMetaDataOptions}->{$rp}
@ -1553,6 +1697,32 @@ sub userInfo {
}
}
sub _getSessionFromAccessTokenData {
my ( $self, $tokenData ) = @_;
my $session;
# If using a refreshed access token
if ( $tokenData->{user_session_id} ) {
# Get user identifier
$session = $self->p->getApacheSession( $tokenData->{user_session_id} );
unless ($session) {
$self->logger->error("Unable to find user session");
}
}
else {
my $offline_session_id = $tokenData->{offline_session_id};
if ($offline_session_id) {
$session = $self->getRefreshToken($offline_session_id);
unless ($session) {
$self->logger->error("Unable to find refresh session");
}
}
}
return $session;
}
sub introspection {
my ( $self, $req ) = @_;
$self->logger->debug("URL detected as an OpenID Connect INTROSPECTION URL");
@ -1580,35 +1750,31 @@ sub introspection {
my $response = { active => JSON::false };
my $oidcSession = $self->getOpenIDConnectSession($token);
if ($oidcSession) {
if ( my $user_session_id = $oidcSession->{data}->{user_session_id} ) {
my $apacheSession =
$self->_getSessionFromAccessTokenData( $oidcSession->{data} );
if ($apacheSession) {
# Get user identifier
my $apacheSession = $self->p->getApacheSession($user_session_id);
if ($apacheSession) {
$response->{active} = JSON::true;
$response->{active} = JSON::true;
# The ID attribute we choose is the one of the calling webservice,
# which might be different from the OIDC client the token was issued to.
my $user_id_attribute =
$self->conf->{oidcRPMetaDataOptions}->{$rp}
->{oidcRPMetaDataOptionsUserIDAttr}
|| $self->conf->{whatToTrace};
$response->{sub} = $apacheSession->data->{$user_id_attribute};
$response->{scope} = $oidcSession->{data}->{scope}
if $oidcSession->{data}->{scope};
$response->{client_id} =
$self->oidcRPList->{ $oidcSession->{data}->{rp} }
->{oidcRPMetaDataOptionsClientID}
if $oidcSession->{data}->{rp};
$response->{iss} = $self->iss;
$response->{exp} =
$oidcSession->{data}->{_utime} + $self->conf->{timeout};
}
my $user_id_attribute =
$self->conf->{oidcRPMetaDataOptions}->{$rp}
->{oidcRPMetaDataOptionsUserIDAttr}
|| $self->conf->{whatToTrace};
$response->{sub} = $apacheSession->data->{$user_id_attribute};
$response->{scope} = $oidcSession->{data}->{scope}
if $oidcSession->{data}->{scope};
$response->{client_id} =
$self->oidcRPList->{ $oidcSession->{data}->{rp} }
->{oidcRPMetaDataOptionsClientID}
if $oidcSession->{data}->{rp};
$response->{iss} = $self->iss;
$response->{exp} =
$oidcSession->{data}->{_utime} + $self->conf->{timeout};
}
else {
$self->logger->error(
"Could not find user session ID in access token object");
$self->logger->error("Count not find session tied to Access Token");
}
}
return $self->p->sendJSONresponse( $req, $response );
@ -1688,7 +1854,7 @@ sub registration {
my $redirect_uris = $client_metadata->{redirect_uris};
# Register RP in global configuration
my $conf = $self->confAcc->getConf( { raw => 1 } );
my $conf = $self->confAcc->getConf( { raw => 1, noCache => 1 } );
$conf->{cfgAuthor} = "OpenID Connect Registration ($client_name)";
$conf->{cfgAuthorIP} = $source_ip;
@ -1857,7 +2023,7 @@ sub logout {
. '</iframe>' );
}
else {
# TODO
# TODO #1194
}
}
}
@ -1897,6 +2063,17 @@ sub metadata {
push( @$response_types, "id_token", "id_token token" );
push( @$grant_types, "implicit" );
}
# If one of the RPs has password grant enabled
if (
grep {
$self->oidcRPList->{$_}->{oidcRPMetaDataOptionsAllowPasswordGrant}
} keys %{ $self->oidcRPList }
)
{
push( @$grant_types, "password" );
}
if ( $self->conf->{oidcServiceAllowHybridFlow} ) {
push( @$response_types,
"code id_token",
@ -1920,8 +2097,8 @@ sub metadata {
introspection_endpoint => $baseUrl . $introspection_uri,
# Logout capabilities
backchannel_logout_supported => JSON::true,
backchannel_logout_session_supported => JSON::true,
backchannel_logout_supported => JSON::false,
backchannel_logout_session_supported => JSON::false,
frontchannel_logout_supported => JSON::true,
frontchannel_logout_session_supported => JSON::true,
(
@ -2016,6 +2193,11 @@ sub exportRequestParameters {
return PE_OK;
}
sub _hasScope {
my ( $self, $scope, $scopelist ) = @_;
return scalar grep { $_ eq $scope } ( split /\s+/, $scopelist );
}
sub _convertOldFormatConsents {
my ( $self, $req ) = @_;
my @oidcConsents = ();
@ -2073,11 +2255,10 @@ sub _convertOldFormatConsents {
}
sub _generateIDToken {
my ( $self, $req, $oidc_request, $rp, $extra_claims )
= @_;
my ( $self, $req, $oidc_request, $rp, $extra_claims ) = @_;
my $response_type = $oidc_request->{'response_type'};
my $client_id = $oidc_request->{'client_id'};
my $client_id = $oidc_request->{'client_id'};
my $id_token_exp =
$self->conf->{oidcRPMetaDataOptions}->{$rp}
@ -2106,7 +2287,7 @@ sub _generateIDToken {
my $id_token_payload_hash = {
iss => $self->iss, # Issuer Identifier
sub => $user_id, # Subject Identifier
aud => [$client_id], # Audience
aud => $self->getAudiences($rp), # Audience
exp => $id_token_exp, # expiration
iat => time, # Issued time
auth_time => $req->{sessionInfo}->{_lastAuthnUTime}

View File

@ -75,6 +75,11 @@ sub init {
my ($self) = @_;
$self->_dbh
or $self->logger->error("DBI connection has failed, but let's continue");
unless ( $self->table ) {
$self->logger->error(
"SQL Table name is not set, can't load " . ref($self) );
return 0;
}
return 1;
}

View File

@ -384,8 +384,7 @@ sub buildHybridAuthnResponse {
: ()
),
(
$id_token
? ( id_token => $id_token )
$id_token ? ( id_token => $id_token )
: ()
),
( $expires_in ? ( expires_in => $expires_in ) : () ),
@ -1717,6 +1716,26 @@ sub force_id_claims {
->{oidcRPMetaDataOptionsIDTokenForceClaims};
}
# https://openid.net/specs/openid-connect-core-1_0.html#IDToken
# Audience(s) that this ID Token is intended for. It MUST contain the OAuth 2.0
# client_id of the Relying Party as an audience value. It MAY also contain
# identifiers for other audiences. In the general case, the aud value is an
# array of case sensitive strings. In the common special case when there is one
# audience, the aud value MAY be a single case sensitive string.
sub getAudiences {
my ( $self, $rp ) = @_;
my $client_id = $self->oidcRPList->{$rp}->{oidcRPMetaDataOptionsClientID};
my @addAudiences = split /\s+/,
( $self->oidcRPList->{$rp}->{oidcRPMetaDataOptionsAdditionalAudiences}
|| '' );
my $result = [$client_id];
push @{$result}, @addAudiences;
return $result;
}
1;
__END__

View File

@ -1743,6 +1743,8 @@ sub replayProtection {
return 0;
}
}
} else {
$self->logger->warn( "No assertion session found for request ID ".$samlID);
}
return 0;

View File

@ -8,6 +8,7 @@ package Lemonldap::NG::Portal::Main;
use strict;
use Mouse;
use JSON;
use URI;
use constant CommonPrms => {
MAIN_LOGO => 'portalMainLogo',
@ -147,13 +148,13 @@ sub display {
AUTH_ERROR_TYPE => $req->error_type,
MSG => $info,
URL => $req->{urldc},
HIDDEN_INPUTS => $self->buildHiddenForm($req),
HIDDEN_INPUTS => $self->buildOutgoingHiddenForm( $req, $method ),
ACTIVE_TIMER => $req->data->{activeTimer},
CHOICE_PARAM => $self->conf->{authChoiceParam},
CHOICE_VALUE => $req->data->{_authChoice},
FORM_METHOD => $method,
(
$method ne 'get' ? ( SEND_PARAMS => 1 )
(not $req->{urldc}) ? ( SEND_PARAMS => 1 )
: ()
),
(
@ -191,13 +192,14 @@ sub display {
# 2.1 Redirection
elsif ( $req->{error} == PE_REDIRECT ) {
my $method = $req->data->{redirectFormMethod} || 'get';
$skinfile = "redirect";
%templateParams = (
MAIN_LOGO => $self->conf->{portalMainLogo},
LANGS => $self->conf->{showLanguages},
URL => $req->{urldc},
HIDDEN_INPUTS => $self->buildHiddenForm($req),
FORM_METHOD => $req->data->{redirectFormMethod} || 'get',
HIDDEN_INPUTS => $self->buildOutgoingHiddenForm( $req, $method ),
FORM_METHOD => $method,
(
$req->data->{customScript}
? ( CUSTOM_SCRIPT => $req->data->{customScript} )
@ -410,6 +412,29 @@ sub staticFile {
];
}
sub buildOutgoingHiddenForm {
my ( $self, $req, $method ) = @_;
my @keys = keys %{ $req->{portalHiddenFormValues} };
# Redirection URL contains query string. Before displaying a form,
# we must set the query string parameters as form fields so they can
# be preserved #2085
my $uri = URI->new( $req->{urldc} );
my %query_params = $uri->query_form;
if (%query_params) {
$self->logger->debug(
"urldc contains query parameters, setting them as hidden form values"
);
$self->clearHiddenFormValue($req);
foreach ( keys %query_params ) {
$self->setHiddenFormValue( $req, $_, $query_params{$_}, "", 0 );
}
}
return $self->buildHiddenForm($req);
}
sub buildHiddenForm {
my ( $self, $req ) = @_;
my @keys = keys %{ $req->{portalHiddenFormValues} };
@ -507,6 +532,7 @@ sub mkSessionArray {
ip => $session->{ipAddr},
values => [ map { { v => $session->{$_} } } @fields ],
error => $session->{error},
displayUser => $displayUser,
displayError => $displayError,
}
} @$sessions

View File

@ -135,7 +135,18 @@ sub init {
return 0;
}
# Handle requests (other path may be declared in enabled plugins)
# Default routes must point to routines declared above
$self->defaultAuthRoute('');
$self->defaultUnauthRoute('');
return 1;
}
sub setPortalRoutes {
my ($self) = @_;
$self->authRoutes(
{ GET => {}, POST => {}, PUT => {}, DELETE => {}, OPTIONS => {} } );
$self->unAuthRoutes(
{ GET => {}, POST => {}, PUT => {}, DELETE => {}, OPTIONS => {} } );
$self
# "/" or undeclared paths
@ -173,11 +184,15 @@ sub init {
$self->defaultAuthRoute('');
$self->defaultUnauthRoute('');
return 1;
}
sub reloadConf {
my ( $self, $conf ) = @_;
# Handle requests (other path may be declared in enabled plugins)
$self->setPortalRoutes;
# Reinitialize $self->conf
%{ $self->{conf} } = %{ $self->localConfig };
@ -518,4 +533,25 @@ sub displayError {
'Portal error, contact your administrator', 500 );
}
# This helper method builds a rule from a string expression
# - $rule: rule text
# - $ruleDesc optional hint of what the rule is for, to display in error message
# returns undef if the rule syntax was invalid
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 || '???';
$self->logger->error( "Bad" . $ruleDesc . "rule: " . $error );
}
return $compiledRule,;
}
1;

View File

@ -39,15 +39,19 @@ sub process {
}
}
}
$self->logger->debug( "Returned "
. ( $err > 0 ? "error" : "status" )
. ": $err ("
. portalConsts->{$err}
. ")" )
$self->logger->debug( "Returned " . $self->_formatProcessResult($err) )
if ($err);
return $err;
}
sub _formatProcessResult {
my ( $self, $err ) = @_;
return ( ( $err > 0 ? "error" : "status" )
. ": $err ("
. portalConsts->{$err}
. ")" );
}
# First process block: check args
# -------------------------------
@ -198,7 +202,20 @@ sub deleteSession {
}
}
# TODO
# Merge logoutServices from user context (for example CAS logoutServices
# url) and from global configuration
if ( $self->conf->{logoutServices}
and %{ $self->conf->{logoutServices} } )
{
# Initialize logoutServices (if not already done)
$req->data->{logoutServices} ||= {};
$req->data->{logoutServices} = {
%{ $req->data->{logoutServices} },
%{ $self->conf->{logoutServices} }
};
}
# Collect logout services and build hidden iFrames
if ( $req->data->{logoutServices} and %{ $req->data->{logoutServices} } ) {
@ -212,15 +229,12 @@ sub deleteSession {
);
foreach ( keys %{ $req->data->{logoutServices} } ) {
my $logoutServiceName = $_;
my $logoutServiceUrl =
$req->data->{logoutServices}->{$logoutServiceName};
my $logoutServiceUrl = $req->data->{logoutServices}->{$_};
$self->logger->debug(
"Find logout service $logoutServiceName ($logoutServiceUrl)");
$self->logger->debug("Find logout service $_ ($logoutServiceUrl)");
my $iframe =
qq'<iframe src="$logoutServiceUrl" alt="$logoutServiceName"'
qq'<iframe src="$logoutServiceUrl" alt="$_"'
. ' marginwidth="0" marginheight="0" scrolling="no"'
. ' class="hiddenFrame" width="0" height="0"'
. ' frameborder="0"></iframe>';

View File

@ -80,6 +80,10 @@ has captcha => ( is => 'rw' );
# Token
has token => ( is => 'rw' );
# Whether or not to include a HTML render of the error message
# in error responses
has wantErrorRender => ( is => 'rw' );
# Error type
sub error_type {
my $req = shift;

View File

@ -260,16 +260,25 @@ sub do {
if ( !$self->conf->{noAjaxHook} and $req->wantJSON ) {
$self->logger->debug('Processing to JSON response');
if ( ( $err > 0 and !$req->id ) or $err eq PE_SESSIONNOTGRANTED ) {
my $s = qq'{"result":0,"error":$err}';
return [
401,
[
my $json = { result => 0, error => $err };
if ( $req->wantErrorRender ) {
$json->{html} = $self->loadTemplate(
$req,
'errormsg',
params => {
AUTH_ERROR => $err,
AUTH_ERROR_TYPE => $req->error_type,
}
);
}
return $self->sendJSONresponse(
$req, $json,
code => 401,
headers => [
'WWW-Authenticate' => "SSO " . $self->conf->{portal},
'Content-Type' => 'application/json',
'Content-Length' => length($s)
"Content-Type" => "application/javascript"
],
[$s]
];
);
}
elsif ( $err > 0 ) {
return $self->sendJSONresponse(
@ -364,7 +373,10 @@ sub autoRedirect {
$req->data->{redirectFormMethod} = "get";
}
else {
if ( $req->{pdata}->{_url} eq encode_base64( $req->{urldc}, '' ) ) {
if ( $req->{pdata}->{_url}
and $req->{pdata}->{_url} eq encode_base64( $req->{urldc}, '' )
)
{
$self->logger->info("Force cleaning pdata");
$req->pdata( {} );
}
@ -1099,7 +1111,19 @@ sub corsPreflight {
sub sendJSONresponse {
my ( $self, $req, $j, %args ) = @_;
my $res = Lemonldap::NG::Common::PSGI::sendJSONresponse(@_);
if ( $self->conf->{corsEnabled} ) {
# If this is a cross-domain request from the portal itself
# (Ajax SSL to a different VHost)
# we allow CORS
if ( $req->origin and index( $self->conf->{portal}, $req->origin ) == 0 ) {
$self->logger->debug('AJAX request from portal, allowing CORS');
push @{ $res->[1] },
"Access-Control-Allow-Origin" => $req->origin,
"Access-Control-Allow-Methods" => "*",
"Access-Control-Allow-Credentials" => "true";
}
elsif ( $self->conf->{corsEnabled} ) {
my @cors = split /;/, $self->cors;
push @{ $res->[1] }, @cors;
$self->logger->debug('Apply following CORS policy :');

View File

@ -34,6 +34,13 @@ has prefix => ( is => 'rw' );
has logo => ( is => 'rw', default => '2f.png' );
has label => ( is => 'rw' );
has noRoute => ( is => 'ro' );
has authnLevel => (
is => 'rw',
lazy => 1,
default => sub {
return $_[0]->conf->{ $_[0]->prefix . '2fAuthnLevel' };
}
);
sub init {
my ($self) = @_;
@ -122,7 +129,7 @@ sub _verify {
. '2F verification for '
. $req->sessionInfo->{ $self->conf->{whatToTrace} } );
if ( my $l = $self->conf->{ $self->prefix . '2fAuthnLevel' } ) {
if ( my $l = $self->authnLevel ) {
$self->logger->debug(
"Update sessionInfo with new authenticationLevel: $l");
$req->sessionInfo->{authenticationLevel} = $l;

View File

@ -28,9 +28,12 @@ has ott => (
return $ott;
}
);
has idRule => ( is => 'rw', default => sub { 1 } );
has sorted => ( is => 'rw', default => sub { 0 } );
has merged => ( is => 'rw', default => '' );
has idRule => ( is => 'rw', default => sub { 1 } );
has displayEmptyValuesRule => ( is => 'rw', default => sub { 0 } );
has displayEmptyHeadersRule => ( is => 'rw', default => sub { 0 } );
has displayPersistentInfoRule => ( is => 'rw', default => sub { 0 } );
has sorted => ( is => 'rw', default => sub { 0 } );
has merged => ( is => 'rw', default => '' );
sub hAttr {
$_[0]->{conf}->{checkUserHiddenAttributes} . ' '
@ -44,25 +47,44 @@ sub persistentAttrs {
sub init {
my ($self) = @_;
my $hd = $self->p->HANDLER;
$self->addAuthRoute( checkuser => 'check', ['POST'] );
$self->addAuthRouteWithRedirect( checkuser => 'display', ['GET'] );
# Parse identity rule
$self->logger->debug(
"checkUser identities rule -> " . $self->conf->{checkUserIdRule} );
my $rule =
$hd->buildSub( $hd->substitute( $self->conf->{checkUserIdRule} ) );
unless ($rule) {
my $error = $hd->tsv->{jail}->error || '???';
$self->error("Bad checkUser identities rule -> $error");
return 0;
}
$self->idRule($rule);
# Parse checkUser rules
$self->idRule(
$self->p->buildRule( $self->conf->{checkUserIdRule}, 'checkUserId' ) );
return 0 unless $self->idRule;
$self->displayEmptyValuesRule(
$self->p->buildRule(
$self->conf->{checkUserDisplayEmptyValues},
'checkUserDisplayEmptyValues'
)
);
return 0 unless $self->displayEmptyValuesRule;
$self->displayEmptyHeadersRule(
$self->p->buildRule(
$self->conf->{checkUserDisplayEmptyHeaders},
'checkUserDisplayEmptyHeaders'
)
);
return 0 unless $self->displayEmptyHeadersRule;
$self->displayPersistentInfoRule(
$self->p->buildRule(
$self->conf->{checkUserDisplayPersistentInfo},
'checkUserDisplayPersistentInfo'
)
);
return 0 unless $self->displayPersistentInfoRule;
# Init. other options
$self->sorted( $self->conf->{impersonationRule}
|| $self->conf->{contextSwitchingRule} );
$self->merged( $self->conf->{impersonationMergeSSOgroups}
&& $self->conf->{impersonationRule} ? 'Merged' : '' );
return 1;
}
@ -77,11 +99,11 @@ sub display {
$attrs = $req->userData;
$attrs = $self->_removePersistentAttributes($attrs)
unless $self->conf->{checkUserDisplayPersistentInfo};
unless $self->displayPersistentInfoRule->( $req, $req->userData );
# Create an array of hashes for template loop
$self->logger->debug("Delete hidden or empty attributes");
if ( $self->conf->{checkUserDisplayEmptyValues} ) {
if ( $self->displayEmptyValuesRule->( $req, $req->userData ) ) {
foreach my $k ( sort keys %$attrs ) {
# Ignore hidden attributes
@ -246,7 +268,7 @@ sub check {
else {
$msg = 'checkUser' . $self->merged;
$attrs = $self->_removePersistentAttributes($attrs)
unless $self->conf->{checkUserDisplayPersistentInfo};
unless $self->displayPersistentInfoRule->( $req, $req->userData );
if ($compute) {
$msg = 'checkUserComputeSession';
@ -273,7 +295,7 @@ sub check {
# Create an array of hashes for template loop
$self->logger->debug("Delete hidden or empty attributes");
if ( $self->conf->{checkUserDisplayEmptyValues} ) {
if ( $self->displayEmptyValuesRule->( $req, $req->userData ) ) {
foreach my $k ( sort keys %$attrs ) {
# Ignore hidden attributes
@ -453,7 +475,7 @@ sub _headers {
$self->logger->debug(
"Return \"$attrs->{ $self->{conf}->{whatToTrace} }\" headers");
return $self->p->HANDLER->checkHeaders( $req, $attrs )
if ( $self->conf->{checkUserDisplayEmptyHeaders} );
if ( $self->displayEmptyHeadersRule->( $req, $req->userData ) );
$self->logger->debug("Remove empty headers");
my @headers = grep $_->{value} =~ /.+/,

View File

@ -39,33 +39,25 @@ has idRule => ( is => 'rw', default => sub { 1 } );
sub init {
my ($self) = @_;
my $hd = $self->p->HANDLER;
$self->addAuthRoute( switchcontext => 'run', ['POST'] )
->addAuthRoute( switchcontext => 'display', ['GET'] );
# Parse activation rule
$self->logger->debug(
'ContextSwitching rule -> ' . $self->conf->{contextSwitchingRule} );
my $rule =
$hd->buildSub( $hd->substitute( $self->conf->{contextSwitchingRule} ) );
unless ($rule) {
my $error = $hd->tsv->{jail}->error || '???';
$self->error("Bad contextSwitching rule -> $error");
return 0;
}
$self->rule($rule);
# Parse ContextSwitching rules
$self->rule(
$self->p->buildRule(
$self->conf->{contextSwitchingRule},
'contextSwitching'
)
);
return 0 unless $self->rule;
# Parse identity rule
$self->logger->debug( "ContextSwitching identities rule -> "
. $self->conf->{contextSwitchingIdRule} );
$rule =
$hd->buildSub( $hd->substitute( $self->conf->{contextSwitchingIdRule} ) );
unless ($rule) {
my $error = $hd->tsv->{jail}->error || '???';
$self->error("Bad contextSwitching identities rule -> $error");
return 0;
}
$self->idRule($rule);
$self->idRule(
$self->p->buildRule(
$self->conf->{contextSwitchingIdRule},
'contextSwitchingId'
)
);
return 0 unless $self->idRule;
return 1;
}
@ -74,13 +66,15 @@ sub init {
sub display {
my ( $self, $req ) = @_;
my $realSessionId =
$req->userData->{"$self->{conf}->{contextSwitchingPrefix}_session_id"};
my $realSession;
unless ( $realSession = $self->p->getApacheSession($realSessionId) ) {
$self->userLogger->info(
"ContextSwitching: session $realSessionId expired");
return $self->p->do( $req, [ sub { PE_SESSIONEXPIRED } ] );
my ( $realSession, $realSessionId );
if ( $realSessionId =
$req->userData->{"$self->{conf}->{contextSwitchingPrefix}_session_id"} )
{
unless ( $realSession = $self->p->getApacheSession($realSessionId) ) {
$self->userLogger->info(
"ContextSwitching: session $realSessionId expired");
return $self->p->do( $req, [ sub { PE_SESSIONEXPIRED } ] );
}
}
# Check access rules
@ -241,7 +235,8 @@ sub _switchContext {
delete $req->sessionInfo->{groups};
# Compute groups & macros again with real authenticationLevel
$req->steps( [ $self->p->groupsAndMacros, 'setLocalGroups' ] );
$req->steps(
[ 'setSessionInfo', $self->p->groupsAndMacros, 'setLocalGroups' ] );
if ( my $error = $self->p->process($req) ) {
$self->logger->debug(
"ContextSwitching: Process returned error: $error");

View File

@ -30,21 +30,14 @@ has ott => (
sub init {
my ($self) = @_;
my $hd = $self->p->HANDLER;
$self->addAuthRoute( decryptvalue => 'run', ['POST'] )
->addAuthRouteWithRedirect( decryptvalue => 'display', ['GET'] );
# Parse activation rule
$self->logger->debug(
'DecryptValue rule -> ' . $self->conf->{decryptValueRule} );
my $rule =
$hd->buildSub( $hd->substitute( $self->conf->{decryptValueRule} ) );
unless ($rule) {
my $error = $hd->tsv->{jail}->error || '???';
$self->error("Bad decryptValue rule -> $error");
return 0;
}
$self->rule($rule);
$self->rule(
$self->p->buildRule( $self->conf->{decryptValueRule}, 'decryptValue' )
);
return 0 unless $self->rule;
return 1;
}

View File

@ -3,6 +3,7 @@ package Lemonldap::NG::Portal::Plugins::GlobalLogout;
use strict;
use Mouse;
use JSON qw(from_json to_json);
use Time::Local;
use Lemonldap::NG::Portal::Main::Constants qw(
PE_OK
PE_ERROR
@ -39,17 +40,10 @@ sub init {
$self->addAuthRoute( globallogout => 'globalLogout', [ 'POST', 'GET' ] );
# Parse activation rule
my $hd = $self->p->HANDLER;
$self->logger->debug(
"GlobalLogout rule -> " . $self->conf->{globalLogoutRule} );
my $rule =
$hd->buildSub( $hd->substitute( $self->conf->{globalLogoutRule} ) );
unless ($rule) {
my $error = $hd->tsv->{jail}->error || '???';
$self->error("Bad globalLogout rule -> $error");
return 0;
}
$self->rule($rule);
$self->rule(
$self->p->buildRule( $self->conf->{globalLogoutRule}, 'globalLogout' )
);
return 0 unless $self->rule;
return 1;
}
@ -112,6 +106,7 @@ sub run {
sub globalLogout {
my ( $self, $req ) = @_;
my $res = PE_OK;
my $count = 0;
if ( $req->param('all') ) {
@ -122,7 +117,7 @@ sub globalLogout {
my $sessions = eval { from_json( $token->{sessions} ) };
if ($@) {
$self->logger->error("Bad encoding in OTT: $@");
return PE_ERROR;
$res = PE_ERROR;
}
my $as;
foreach (@$sessions) {
@ -145,23 +140,24 @@ sub globalLogout {
else {
$self->userLogger->warn(
"GlobalLogout called with an unvalid token");
return PE_TOKENEXPIRED;
$res = PE_TOKENEXPIRED;
}
}
}
else {
$self->userLogger->error(
"GlobalLogout called with an expired token");
return PE_TOKENEXPIRED;
$res = PE_TOKENEXPIRED;
}
}
else {
$self->userLogger->error('GlobalLogout called without token');
return PE_NOTOKEN;
$res = PE_NOTOKEN;
}
}
$self->userLogger->info("$count remaining session(s) removed");
return $self->p->do( $req, [ sub { $res } ] ) if $res;
$self->userLogger->info("$count remaining session(s) removed");
return $self->p->do( $req, [ 'authLogout', 'deleteSession' ] );
}
@ -169,7 +165,6 @@ sub activeSessions {
my ( $self, $req ) = @_;
my $activeSessions = [];
my $sessions = {};
my $regex = '^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$';
my $user = $req->{userData}->{ $self->conf->{whatToTrace} };
my $customParam = $self->conf->{globalLogoutCustomParam} || '';
@ -183,21 +178,29 @@ sub activeSessions {
$user );
$self->logger->debug("Building array ref with sessions info...");
@$activeSessions = map { {
@$activeSessions =
map {
my $epoch;
my $regex = '^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$';
$_->{startTime} =~ /$regex/;
$epoch = timelocal( $6, $5, $4, $3, $2 - 1, $1 );
$_->{startTime} = $epoch;
if ( $_->{updateTime} ) {
$_->{updateTime} =~ /$regex/;
$epoch = timelocal( $6, $5, $4, $3, $2 - 1, $1 );
$_->{updateTime} = $epoch;
}
$_;
}
sort { $b->{startTime} cmp $a->{startTime} } map { {
id => $_,
customParam => $sessions->{$_}->{$customParam},
ipAddr => $sessions->{$_}->{'ipAddr'},
authLevel => $sessions->{$_}->{'authenticationLevel'},
startTime => $sessions->{$_}->{'_startTime'} =~
s/$regex/$1-$2-$3 $4:$5:$6/ro,
updateTime => (
$sessions->{$_}->{'_updateTime'}
? $sessions->{$_}->{'_updateTime'} =~
s/$regex/$1-$2-$3 $4:$5:$6/ro
: ''
),
ipAddr => $sessions->{$_}->{ipAddr},
authLevel => $sessions->{$_}->{authenticationLevel},
startTime => $sessions->{$_}->{_startTime},
updateTime => $sessions->{$_}->{_updateTime}
};
} keys %$sessions;
} keys %$sessions;
return $activeSessions;
}

View File

@ -19,6 +19,7 @@ sub init { 1 }
sub run {
my ( $self, $req ) = @_;
if ( $req->param('checkLogins') ) {
$self->logger->debug('History asked');
$req->info( (

View File

@ -23,31 +23,23 @@ sub hAttr {
sub init {
my ($self) = @_;
my $hd = $self->p->HANDLER;
# Parse activation rule
$self->logger->debug(
"Impersonation rule -> " . $self->conf->{impersonationRule} );
my $rule =
$hd->buildSub( $hd->substitute( $self->conf->{impersonationRule} ) );
unless ($rule) {
my $error = $hd->tsv->{jail}->error || '???';
$self->error("Bad impersonation rule -> $error");
return 0;
}
$self->rule($rule);
# Parse Impersonation rules
$self->rule(
$self->p->buildRule(
$self->conf->{impersonationRule}, 'impersonation'
)
);
return 0 unless $self->rule;
$self->idRule(
$self->p->buildRule(
$self->conf->{impersonationIdRule},
'impersonationId'
)
);
return 0 unless $self->idRule;
# Parse identity rule
$self->logger->debug( "Impersonation identities rule -> "
. $self->conf->{impersonationIdRule} );
$rule =
$hd->buildSub( $hd->substitute( $self->conf->{impersonationIdRule} ) );
unless ($rule) {
my $error = $hd->tsv->{jail}->error || '???';
$self->error("Bad impersonation identities rule -> $error");
return 0;
}
$self->idRule($rule);
return 1;
}
@ -235,7 +227,8 @@ sub _userData {
$req->sessionInfo->{authenticationLevel} =
$realSession->{real_authenticationLevel};
delete $req->sessionInfo->{groups};
$req->steps( [ $self->p->groupsAndMacros, 'setLocalGroups' ] );
$req->steps(
[ 'setSessionInfo', $self->p->groupsAndMacros, 'setLocalGroups' ] );
if ( my $error = $self->p->process($req) ) {
$self->logger->debug("Impersonation: Process returned error: $error");
$req->error($error);

View File

@ -321,12 +321,16 @@ sub updateSession {
my $force = 0;
if ( my $s = delete $infos->{__secret} ) {
my $t;
if ( $t =
$self->conf->{cipher}->decrypt($s)
and $t <= time + $self->conf->{restClockTolerance}
and $t > time - $self->conf->{restClockTolerance} )
{
$force = 1;
if ( $t = $self->conf->{cipher}->decrypt($s) ) {
if ( $t <= time + $self->conf->{restClockTolerance}
and $t > time - $self->conf->{restClockTolerance} )
{
$force = 1;
}
else {
$self->userLogger->error( 'Clock drift between servers is'
. ' beyond tolerance, force denied.' );
}
}
else {
$self->userLogger->error('Bad key, force denied');

View File

@ -2,7 +2,14 @@ package Lemonldap::NG::Portal::Plugins::SingleSession;
use strict;
use Mouse;
use Lemonldap::NG::Portal::Main::Constants qw(PE_OK);
use MIME::Base64;
use JSON qw(from_json to_json);
use Lemonldap::NG::Portal::Main::Constants qw(
PE_OK
PE_ERROR
PE_TOKENEXPIRED
PE_NOTOKEN
);
our $VERSION = '2.1.0';
@ -11,14 +18,49 @@ extends 'Lemonldap::NG::Portal::Main::Plugin',
use constant endAuth => 'run';
sub init { 1 }
has singleIPRule => ( is => 'rw' );
has singleSessionRule => ( is => 'rw' );
has singleUserByIPRule => ( is => 'rw' );
has ott => (
is => 'rw',
lazy => 1,
default => sub {
my $ott =
$_[0]->{p}->loadModule('Lemonldap::NG::Portal::Lib::OneTimeToken');
$ott->timeout( $_[0]->conf->{formTimeout} );
return $ott;
}
);
sub init {
my ($self) = @_;
$self->addAuthRoute( removeOther => 'removeOther', ['GET'] );
# Build triggering rules from configuration
$self->singleIPRule(
$self->p->buildRule( $self->conf->{singleIP}, 'singleIP' ) );
return 0 unless $self->singleIPRule;
$self->singleSessionRule(
$self->p->buildRule( $self->conf->{singleSession}, 'singleSession' ) );
return 0 unless $self->singleSessionRule;
$self->singleUserByIPRule(
$self->p->buildRule( $self->conf->{singleUserByIP}, 'singleUserByIP' )
);
return 0 unless $self->singleUserByIPRule;
return 1;
}
sub run {
my ( $self, $req ) = @_;
my ( $linkedSessionId, $token, $html ) = ( '', '', '' );
my $deleted = [];
my $otherSessions = [];
my $linkedSessionId = '';
my $moduleOptions = $self->conf->{globalStorageOptions} || {};
my @otherSessionsId = ();
my $moduleOptions = $self->conf->{globalStorageOptions} || {};
$moduleOptions->{backend} = $self->conf->{globalStorage};
my $sessions = $self->module->searchOn(
@ -42,8 +84,8 @@ sub run {
next if ( $linkedSessionId and $id eq $linkedSessionId );
my $session = $self->p->getApacheSession($id) or next;
if (
$self->conf->{singleSession}
or ( $self->conf->{singleIP}
$self->singleSessionRule->( $req, $req->sessionInfo )
or ( $self->singleIPRule->( $req, $req->sessionInfo )
and $req->{sessionInfo}->{ipAddr} ne $session->data->{ipAddr} )
)
{
@ -51,13 +93,21 @@ sub run {
$self->p->_deleteSession( $req, $session, 1 );
}
else {
push @$otherSessions, $self->p->_sumUpSession( $session->data );
push @$otherSessions, $self->p->_sumUpSession( $session->data );
push @otherSessionsId, $id;
}
}
if ( $self->conf->{singleUserByIP} ) {
$token = $self->ott->createToken( {
user => $req->{sessionInfo}->{ $self->conf->{whatToTrace} },
sessions => to_json( \@otherSessionsId )
}
) if @otherSessionsId;
if ( $self->singleUserByIPRule->( $req, $req->sessionInfo ) ) {
my $sessions =
$self->module->searchOn( $moduleOptions, 'ipAddr',
$req->sessionInfo->ipAddr );
$req->sessionInfo->{ipAddr} );
foreach my $id ( keys %$sessions ) {
next if ( $req->id eq $id );
my $session = $self->p->getApacheSession($id) or next;
@ -69,29 +119,79 @@ sub run {
}
}
}
$req->info(
$self->p->mkSessionArray( $req, $deleted, 'sessionsDeleted', 1 ) )
$html = $self->p->mkSessionArray( $req, $deleted, 'sessionsDeleted', 1 )
if ( $self->conf->{notifyDeleted} and @$deleted );
$req->info(
$self->p->mkSessionArray( $req, $otherSessions, 'otherSessions', 1 )
. $self->_mkRemoveOtherLink($req) )
$html .=
$self->p->mkSessionArray( $req, $otherSessions, 'otherSessions', 1 )
. $self->_mkRemoveOtherLink( $req, $token )
if ( $self->conf->{notifyOther} and @$otherSessions );
$req->info($html);
return PE_OK;
}
sub removeOther {
my ( $self, $req ) = @_;
my $res = PE_OK;
$req->{urldc} = decode_base64( $req->param('url') );
if ( my $token = $req->param('token') ) {
if ( $token = $self->ott->getToken($token) ) {
# Read sessions from token
my $sessions = eval { from_json( $token->{sessions} ) };
if ($@) {
$self->logger->error("Bad encoding in OTT: $@");
$res = PE_ERROR;
}
my $as;
foreach my $id (@$sessions) {
unless ( $as = $self->p->getApacheSession($id) ) {
$self->userLogger->info(
"SingleSession: session $id expired");
next;
}
my $user = $token->{user};
if ( $req->{userData}->{ $self->{conf}->{whatToTrace} } eq
$user )
{
$self->userLogger->info("Remove \"$user\" session: $id");
$self->p->_deleteSession( $req, $as, 1 );
}
else {
$self->userLogger->warn(
"SingleSession called with an unvalid token");
$res = PE_TOKENEXPIRED;
}
}
}
else {
$self->userLogger->error(
"SingleSession called with an expired token");
$res = PE_TOKENEXPIRED;
}
}
else {
$self->userLogger->error('SingleSession called without token');
$res = PE_NOTOKEN;
}
return $self->p->do( $req, [ sub { $res } ] ) if $res;
$req->mustRedirect(1);
return $self->p->autoRedirect($req);
}
# Build the removeOther link
# Last part of URL is built trough javascript
# @return removeOther link in HTML code
sub _mkRemoveOtherLink {
my ( $self, $req ) = @_;
my ( $self, $req, $token ) = @_;
# TODO: remove this
return $self->loadTemplate(
$req,
'removeOther',
params => {
link => $self->conf->{portal} . "?removeOther=1"
link => $self->conf->{portal} . "removeOther?token=$token"
}
);
}

View File

@ -13,5 +13,8 @@ timer = () ->
window.setTimeout timer, 1000
$(document).ready ->
$(".data-epoch").each ->
myDate = new Date($(this).text() * 1000)
$(this).text(myDate.toLocaleString())
window.setTimeout go, 30000
window.setTimeout timer, 1000

View File

@ -227,6 +227,11 @@ datas = {}
$(window).on 'load', () ->
# Get application/init variables
datas = getValues()
# Keep the currently selected tab
if "datas" of window && "choicetab" of window.datas
datas.choicetab = window.datas.choicetab;
# Export datas for other scripts
window.datas = datas
@ -280,26 +285,48 @@ $(window).on 'load', () ->
# Complete removeOther link
if $("p.removeOther").length
action = $("form.login").attr "action"
method = $("form.login").attr "method"
action = $("#form").attr "action"
method = $("#form").attr "method"
console.log 'method=', method
hiddenParams = ""
if $("#form input[type=hidden]")
console.log 'Parse hidden values'
$("#form input[type=hidden]").each (index) ->
console.log ' ->', $(this).attr("name"), $(this).val()
hiddenParams += "&" + $(this).attr("name") + "=" + $(this).val()
back_url = ""
if action.indexOf("?") != -1
action.substring(0, action.indexOf("?")) + "?"
else
back_url = action + "?"
if action
console.log 'action=', action
if action.indexOf("?") != -1
action.substring(0, action.indexOf("?")) + "?"
else
back_url = action + "?"
back_url += hiddenParams
hiddenParams = ""
$("form.login input[type=hidden]").each (index) ->
back_url += "&" + $(this).attr("name") + "=" + $(this).val()
link = $("p.removeOther a").attr("href") + "&method=" + method + "&url=" + btoa(back_url)
link = $("p.removeOther a").attr("href") + "&method=" + method + hiddenParams
link += "&url=" + btoa(back_url) if back_url
$("p.removeOther a").attr "href", link
# Language detection. Priority order:
# 0 - llnglanguage parameter
# 1 - cookie value
# 2 - first navigator.languages item that exists in window.availableLanguages
# 3 - first value of window.availableLanguages
lang = getCookie 'llnglanguage'
queryString = window.location.search
if queryString
console.log 'Parsed queryString:', queryString
urlParams = new URLSearchParams queryString
if urlParams
queryLang = urlParams.get('llnglanguage')
console.log 'Get lang from parameter' if queryLang
setCookieLang = urlParams.get('setCookieLang')
console.log 'Set lang cookie' if setCookieLang == 1
if !lang
lang = getCookie 'llnglanguage'
console.log 'Get lang from cookie' if lang && !queryLang
if !lang
if navigator
langs = []
@ -320,13 +347,26 @@ $(window).on 'load', () ->
else if al.substring(0, 1) == nl.substring(0, 1)
langs2.push al
lang = if langs[0] then langs[0] else if langs2[0] then langs2[0] else window.availableLanguages[0]
console.log 'Get lang from navigator' if lang && !queryLang
else
lang = window.availableLanguages[0]
console.log 'Get lang from window' if lang && !queryLang
else if lang not in window.availableLanguages
lang = window.availableLanguages[0]
console.log 'Selected lang ->', lang
setCookie 'llnglanguage', lang
translatePage(lang)
console.log 'Lang not available -> Get default lang' if !queryLang
if queryLang
if queryLang not in window.availableLanguages
console.log 'Lang not available -> Get default lang'
queryLang = window.availableLanguages[0]
console.log 'Selected lang ->', queryLang
if setCookieLang
console.log 'Set cookie lang ->', queryLang
setCookie 'llnglanguage', queryLang
translatePage(queryLang)
else
console.log 'Selected lang ->', lang
setCookie 'llnglanguage', lang
translatePage(lang)
# Build language icons
langdiv = ''

View File

@ -5,12 +5,9 @@ tryssl = () ->
console.log 'path -> ', path
console.log 'Call URL -> ', window.datas.sslHost
$.ajax window.datas.sslHost,
dataType: 'jsonp'
# PE_BADCREDENTIALS
statusCode:
401: () ->
$('#lform').submit()
console.log 'Error code 401'
dataType: 'json',
xhrFields:
withCredentials: true
# If request succeed, cookie is set, posting form to get redirection
# or menu
success: (data) ->
@ -18,9 +15,23 @@ tryssl = () ->
console.log 'Success -> ', data
# Case else, will display PE_BADCREDENTIALS or fallback to next auth
# backend
error: () ->
sendUrl path
console.log 'Error'
error: (result) ->
# If the AJAX query didn't fire at all, it's probably
# a bad certificate
if result.status == 0
# We couldn't send the request.
# if client verification is optional, this means
# the certificate was rejected (or some network error)
sendUrl path
# For compatibility with earlier configs, handle PE9 by posting form
if result.responseJSON && 'error' of result.responseJSON && result.responseJSON.error == "9"
sendUrl path
# If the server sent a html error description, display it
if result.responseJSON && 'html' of result.responseJSON
$('#errormsg').html(result.responseJSON.html);
$(window).trigger('load');
console.log 'Error during AJAX SSL authentication', result
false
sendUrl = (path) ->
@ -34,4 +45,4 @@ sendUrl = (path) ->
$('#lform').submit()
$(document).ready ->
$('.sslclick').on 'click', tryssl
$('.sslclick').on 'click', tryssl

View File

@ -5,12 +5,9 @@ tryssl = () ->
console.log 'path -> ', path
console.log 'Call URL -> ', window.datas.sslHost
$.ajax window.datas.sslHost,
dataType: 'jsonp'
# PE_BADCREDENTIALS
statusCode:
401: () ->
$('#lformSSL').submit()
console.log 'Error code 401'
dataType: 'json',
xhrFields:
withCredentials: true
# If request succeed, cookie is set, posting form to get redirection
# or menu
success: (data) ->
@ -18,9 +15,23 @@ tryssl = () ->
console.log 'Success -> ', data
# Case else, will display PE_BADCREDENTIALS or fallback to next auth
# backend
error: () ->
sendUrl path
console.log 'Error'
error: (result) ->
# If the AJAX query didn't fire at all, it's probably
# a bad certificate
if result.status == 0
# We couldn't send the request.
# if client verification is optional, this means
# the certificate was rejected (or some network error)
sendUrl path
# For compatibility with earlier configs, handle PE9 by posting form
if result.responseJSON && 'error' of result.responseJSON && result.responseJSON.error == "9"
sendUrl path
# If the server sent a html error description, display it
if result.responseJSON && 'html' of result.responseJSON
$('#errormsg').html(result.responseJSON.html);
$(window).trigger('load');
console.log 'Error during AJAX SSL authentication', result
false
sendUrl = (path) ->
@ -34,4 +45,4 @@ sendUrl = (path) ->
$('#lformSSL').submit()
$(document).ready ->
$('.sslclick').on 'click', tryssl
$('.sslclick').on 'click', tryssl

View File

@ -1,4 +1,4 @@
$(document).ready(function() {
$(window).on("load", function() {
// Adapt some class to fit Bootstrap theme
$("div.message-positive").addClass("alert-success");
@ -16,4 +16,9 @@ $(document).ready(function() {
}
});
// Remember selected tab
$('#authMenu .nav-link').on('click', function (e) {
window.datas.choicetab = e.target.hash.substr(1)
});
});

View File

@ -1 +1 @@
$(document).ready(function(){$("div.message-positive").addClass("alert-success"),$("div.message-warning").addClass("alert-warning"),$("div.message-negative").addClass("alert-danger"),$("table.info").addClass("table"),$(".notifCheck").addClass("checkbox"),$('.collapse li[class!="dropdown"]').on("click",function(){$(".navbar-toggler").hasClass("collapsed")||$(".navbar-toggler").trigger("click")})});
$(window).on("load",function(){$("div.message-positive").addClass("alert-success"),$("div.message-warning").addClass("alert-warning"),$("div.message-negative").addClass("alert-danger"),$("table.info").addClass("table"),$(".notifCheck").addClass("checkbox"),$('.collapse li[class!="dropdown"]').on("click",function(){$(".navbar-toggler").hasClass("collapsed")||$(".navbar-toggler").trigger("click")}),$("#authMenu .nav-link").on("click",function(a){window.datas.choicetab=a.target.hash.substr(1)})});

View File

@ -1 +1 @@
{"version":3,"sources":["lemonldap-ng-portal/site/htdocs/static/bootstrap/js/skin.js"],"names":["$","document","ready","addClass","on","hasClass","trigger"],"mappings":"AAAAA,EAAEC,UAAUC,MAAM,WAGhBF,EAAE,wBAAwBG,SAAS,iBACnCH,EAAE,uBAAuBG,SAAS,iBAClCH,EAAE,wBAAwBG,SAAS,gBAEnCH,EAAE,cAAcG,SAAS,SAEzBH,EAAE,eAAeG,SAAS,YAG1BH,EAAE,mCAAmCI,GAAG,QAAS,WAC1CJ,EAAE,mBAAmBK,SAAS,cACjCL,EAAE,mBAAmBM,QAAQ"}
{"version":3,"sources":["lemonldap-ng-portal/site/htdocs/static/bootstrap/js/skin.js"],"names":["$","window","on","addClass","hasClass","trigger","e","datas","choicetab","target","hash","substr"],"mappings":"AAAAA,EAAEC,QAAQC,GAAG,OAAQ,WAGnBF,EAAE,wBAAwBG,SAAS,iBACnCH,EAAE,uBAAuBG,SAAS,iBAClCH,EAAE,wBAAwBG,SAAS,gBAEnCH,EAAE,cAAcG,SAAS,SAEzBH,EAAE,eAAeG,SAAS,YAG1BH,EAAE,mCAAmCE,GAAG,QAAS,WAC1CF,EAAE,mBAAmBI,SAAS,cACjCJ,EAAE,mBAAmBK,QAAQ,WAKjCL,EAAE,uBAAuBE,GAAG,QAAS,SAAUI,GAC3CL,OAAOM,MAAMC,UAAYF,EAAEG,OAAOC,KAAKC,OAAO"}

View File

@ -20,6 +20,11 @@
};
$(document).ready(function() {
$(".data-epoch").each(function() {
var myDate;
myDate = new Date($(this).text() * 1000);
return $(this).text(myDate.toLocaleString());
});
window.setTimeout(go, 30000);
return window.setTimeout(timer, 1000);
});

View File

@ -1 +1 @@
(function(){var t,e,n;e=30,t=function(){return $("#globallogout").submit()},n=function(){var t;return t=$("#timer").html(),0<e&&e--,t=t.replace(/\d+/,e),$("#timer").html(t),window.setTimeout(n,1e3)},$(document).ready(function(){return window.setTimeout(t,3e4),window.setTimeout(n,1e3)})}).call(this);
(function(){var t,e,n;e=30,t=function(){return $("#globallogout").submit()},n=function(){var t;return t=$("#timer").html(),0<e&&e--,t=t.replace(/\d+/,e),$("#timer").html(t),window.setTimeout(n,1e3)},$(document).ready(function(){return $(".data-epoch").each(function(){var t;return t=new Date(1e3*$(this).text()),$(this).text(t.toLocaleString())}),window.setTimeout(t,3e4),window.setTimeout(n,1e3)})}).call(this);

Some files were not shown because too many files have changed in this diff Show More