Merge branch 'extra2f' into 'v2.0'

Allow multi instanciation of 2F modules (#1860)

See merge request lemonldap-ng/lemonldap-ng!87
This commit is contained in:
Xavier Guimard 2019-07-22 20:40:06 +02:00
commit 349a40717b
29 changed files with 4685 additions and 22 deletions

View File

@ -23,7 +23,7 @@ use constant HANDLERSECTION => "handler";
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(?:RPMetaData(?:(?:Option(?:sExtraClaim)?|ExportedVar)s|Node)|OPMetaData(?:(?:ExportedVar|Option)s|J(?:SON|WKS)|Node)|S(?:erviceMetaDataAuthnContext|torageOptions))|penIdExportedVars)|s(?:aml(?:S(?:PMetaData(?:(?:ExportedAttribute|Option)s|Node|XML)|torageOptions)|IDPMetaData(?:(?:ExportedAttribute|Option)s|Node|XML))|essionDataToRemember|laveExportedVars)|c(?:as(?:S(?:rvMetaData(?:(?:ExportedVar|Option)s|Node)|torageOptions)|A(?:ppMetaData(?:(?:ExportedVar|Option)s|Node)|ttributes))|(?:ustomAddParam|ombModule)s)|p(?:ersistentStorageOptions|o(?:rtalSkinRules|st))|a(?:ut(?:hChoiceMod|oSigninR)ules|pplicationList)|v(?:hostOptions|irtualHost)|S(?:MTPTLSOpts|SLVarIf))$/;
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(?:RPMetaData(?:(?:Option(?:sExtraClaim)?|ExportedVar)s|Node)|OPMetaData(?:(?:ExportedVar|Option)s|J(?:SON|WKS)|Node)|S(?:erviceMetaDataAuthnContext|torageOptions))|penIdExportedVars)|s(?:aml(?:S(?:PMetaData(?:(?:ExportedAttribute|Option)s|Node|XML)|torageOptions)|IDPMetaData(?:(?:ExportedAttribute|Option)s|Node|XML))|essionDataToRemember|laveExportedVars|fExtra)|c(?:as(?:S(?:rvMetaData(?:(?:ExportedVar|Option)s|Node)|torageOptions)|A(?:ppMetaData(?:(?:ExportedVar|Option)s|Node)|ttributes))|(?:ustomAddParam|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|howLanguages|slByAjax)|o(?:idc(?:ServiceAllow(?:(?:AuthorizationCode|Implicit|Hybrid)Flow|DynamicRegistration)|RPMetaDataOptions(?:LogoutSessionRequired|BypassConsent|RequirePKCE|Public)|OPMetaDataOptions(?:(?:CheckJWTSignatur|UseNonc)e|StoreIDToken))|ldNotifFormat)|c(?:a(?:ptcha_(?:register|login|mail)_enabled|sSrvMetaDataOptions(?:Gateway|Renew))|heck(?:User(?:Display(?:PersistentInfo|EmptyValues))?|State|XSS)|o(?:ntextSwitchingStopWithLogout|rsEnabled)|da)|p(?:ortal(?:ErrorOn(?:ExpiredSession|MailNotFound)|DisplayRe(?:setPassword|gister)|(?:CheckLogin|Statu)s|OpenLinkInNewWindow|RequireOldPassword|ForceAuthn|AntiFrame)|roxyUseSoap)|l(?:dap(?:(?:Group(?:DecodeSearchedValu|Recursiv)|UsePasswordResetAttribut)e|(?:AllowResetExpired|Set)Password|ChangePasswordAsUser|PpolicyControl)|oginHistoryEnabled)|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)|no(?:tif(?:ication(?:Server)?|y(?:Deleted|Other))|AjaxHook)|(?:mai(?:lOnPasswordChang|ntenanc)|vhostMaintenanc)e|d(?:isablePersistentStorage|biDynamicHashEnabled)|rest(?:(?:Session|Config)Server|ExportSecretKeys)|h(?:ideOldPassword|ttpOnly)|yubikey2fUserCanRemoveKey|(?:activeTim|wsdlServ)er|krb(?:RemoveDomain|ByJs)|bruteForceProtection)$/;
our @sessionTypes = ( 'remoteGlobal', 'global', 'localSession', 'persistent', 'saml', 'oidc', 'cas' );

View File

@ -698,6 +698,25 @@ sub combModules {
return $self->sendJSONresponse( $req, $res );
}
sub sfExtra {
my ( $self, $req, $key ) = @_;
return $self->sendError( $req, 'Subkeys forbidden for sfExtra', 400 )
if ($key);
my $val = $self->getConfKey( $req, 'sfExtra' ) // {};
my $res = [];
foreach my $mod ( keys %$val ) {
my $tmp;
$tmp->{title} = $mod;
$tmp->{id} = "sfExtra/$mod";
$tmp->{type} = 'sfExtra';
$tmp->{data}->{$_} = $val->{$mod}->{$_} foreach (qw(type rule));
my $over = $val->{$mod}->{over} // {};
$tmp->{data}->{over} = [ map { [ $_, $over->{$_} ] } keys %$over ];
push @$res, $tmp;
}
return $self->sendJSONresponse( $req, $res );
}
# 33 - Root queries
# -----------

View File

@ -22,7 +22,7 @@ our $specialNodeHash = {
};
our $doubleHashKeys = 'issuerDBGetParameters';
our $simpleHashKeys = '(?:(?:l(?:o(?:calSessionStorageOption|goutService)|dapExportedVar|wp(?:Ssl)?Opt)|re(?:moteGlobalStorageOption|st2f(?:Verify|Init)Arg|loadUrl)|c(?:as(?:StorageOption|Attribute)|ustomAddParam|ombModule)|(?:(?:d(?:emo|bi)|facebook|webID)E|e)xportedVar|g(?:r(?:antSessionRule|oup)|lobalStorageOption)|n(?:otificationStorageOption|ginxCustomHandler)|p(?:ersistentStorageOption|ortalSkinRule)|macro)s|o(?:idcS(?:erviceMetaDataAuthnContext|torageOptions)|penIdExportedVars)|s(?:(?:amlStorageOption|laveExportedVar)s|essionDataToRemember)|a(?:ut(?:hChoiceMod|oSigninR)ules|pplicationList)|S(?:MTPTLSOpts|SLVarIf))';
our $simpleHashKeys = '(?:(?:l(?:o(?:calSessionStorageOption|goutService)|dapExportedVar|wp(?:Ssl)?Opt)|re(?:moteGlobalStorageOption|st2f(?:Verify|Init)Arg|loadUrl)|c(?:as(?:StorageOption|Attribute)|ustomAddParam|ombModule)|(?:(?:d(?:emo|bi)|facebook|webID)E|e)xportedVar|g(?:r(?:antSessionRule|oup)|lobalStorageOption)|n(?:otificationStorageOption|ginxCustomHandler)|p(?:ersistentStorageOption|ortalSkinRule)|macro)s|o(?:idcS(?:erviceMetaDataAuthnContext|torageOptions)|penIdExportedVars)|s(?:(?:amlStorageOption|laveExportedVar)s|essionDataToRemember|fExtra)|a(?:ut(?:hChoiceMod|oSigninR)ules|pplicationList)|S(?:MTPTLSOpts|SLVarIf))';
our $specialNodeKeys = '(?:(?:(?:saml(?:ID|S)|oidc[OR])P|cas(?:App|Srv))MetaDataNode|virtualHost)s';
our $casAppMetaDataNodeKeys = 'casAppMetaData(?:Options(?:UserAttribut|Servic|Rul)e|ExportedVars)';
our $casSrvMetaDataNodeKeys = 'casSrvMetaData(?:Options(?:ProxiedServices|DisplayName|SortNumber|Gateway|Renew|Icon|Url)|ExportedVars)';

View File

@ -3212,6 +3212,26 @@ qr/(?:(?:https?):\/\/(?:(?:(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.]
'default' => '::2F::Engines::Default',
'type' => 'text'
},
'sfExtra' => {
'keyTest' => qr/^\w+$/,
'select' => [ {
'k' => 'Mail2F',
'v' => 'E-Mail'
},
{
'k' => 'REST',
'v' => 'REST'
},
{
'k' => 'Ext2F',
'v' => 'External'
}
],
'test' => sub {
1;
},
'type' => 'sfExtraContainer'
},
'sfRemovedMsgRule' => {
'default' => 0,
'type' => 'boolOrExpr'

View File

@ -3278,6 +3278,17 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{1,5})?|ldap(?:s|\+tls)?://\w[\w\-\.]*(?:
{ k => 'Custom', v => 'customModule' },
],
},
sfExtra => {
type => 'sfExtraContainer',
keyTest => qr/^\w+$/,
test => sub { 1 },
documentation => 'Extra second factors',
select => [
{ k => 'Mail2F', v => 'E-Mail' },
{ k => 'REST', v => 'REST' },
{ k => 'Ext2F', v => 'External' },
],
},
# Custom auth modules
customAuth => {

View File

@ -775,6 +775,7 @@ sub tree {
],
},
'sfRequired',
'sfExtra',
]
},
{

View File

@ -69,7 +69,7 @@ sub addRoutes {
qw(virtualHosts samlIDPMetaDataNodes samlSPMetaDataNodes
applicationList oidcOPMetaDataNodes oidcRPMetaDataNodes
casSrvMetaDataNodes casAppMetaDataNodes
authChoiceModules grantSessionRules combModules
authChoiceModules grantSessionRules combModules sfExtra
openIdIDPList)
]
},

View File

@ -793,6 +793,25 @@ sub _scanNodes {
next;
}
# sfExtra: just to replace "over" key
if ( $name eq 'sfExtra' ) {
hdebug(' sfExtra');
$self->newConf->{$name} = {};
foreach my $node ( @{ $leaf->{nodes} } ) {
my $tmp;
$tmp->{$_} = $node->{data}->{$_} foreach (qw(type rule));
$tmp->{over} = {};
foreach ( @{ $node->{data}->{over} } ) {
$tmp->{over}->{ $_->[0] } = $_->[1];
}
$self->newConf->{$name}->{ $node->{title} } = $tmp;
}
# TODO: check changes
$self->confChanged(1);
next;
}
$subNodes //= [];
my $count = 0;
my @old = (

View File

@ -328,6 +328,23 @@ llapp.controller 'TreeCtrl', [
over: []
$scope.execFilters $scope._findScopeByKey 'authParams'
$scope.newSfExtra = ->
node = $scope._findContainer()
node.nodes.push
id: "#{node.id}/n#{id++}"
title: 'new'
type: 'sfExtra'
data:
type: ''
rule: ''
over: []
$scope.newSfOver = ->
d = $scope.currentNode.data
d.over = [] unless d.over
d.over.push ["new#{id++}", '']
$scope.newCmbOver = ->
d = $scope.currentNode.data
d.over = [] unless d.over
@ -624,16 +641,17 @@ llapp.controller 'TreeCtrl', [
node = scope.$modelValue
# regexp-assemble of:
# authChoice
# keyText
# cmbModule
# virtualHost
# rule
# menuCat
# keyText
# menuApp
# menuCat
# rule
# samlAttribute
# samlIDPMetaDataNode
# samlSPMetaDataNode
return if node.type and node.type.match /^(?:(?:saml(?:(?:ID|S)PMetaDataNod|Attribut)|(?:cmbMod|r)ul|authChoic)e|(?:virtualHos|keyTex)t|menu(?:App|Cat))$/ then true else false
# sfExtra
# virtualHost
return if node.type and node.type.match /^(?:s(?:aml(?:(?:ID|S)PMetaDataNod|Attribut)e|fExtra)|(?:(?:cmbMod|r)ul|authChoic)e|(?:virtualHos|keyTex)t|menu(?:App|Cat))$/ then true else false
# RSA keys generation
$scope.newRSAKey = ->

View File

@ -0,0 +1,54 @@
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{{currentNode.title}}</h3>
</div>
<table class="table table-striped">
<thead>
<tr>
<th width="25%" trspan="name"></th>
<th width="25%" trspan="type"></th>
<th width="50%" trspan="rule"></th>
<th />
</tr>
</thead>
<tbody>
<tr>
<td>
<input class="form-control" ng-model="currentNode.title" />
</td>
<td>
<select class="form-control" ng-model="currentNode.data.type">
<option ng-repeat="item in _findContainer().select" ng-selected="item.k===currentNode.data.type" value="{{item.k}}">{{item.v}}</option>
</select>
</td>
<td>
<input class="form-control" ng-model="currentNode.data.rule" />
</td>
</tr>
</tbody>
</table>
<h4 trspan="overPrm"></h4>
<table class="table">
<tr ng-repeat="t in currentNode.data.over">
<td>
<input class="form-control" ng-model="t[0]" />
</td>
<td>
<input class="form-control" ng-model="t[1]" />
</td>
<td>
<span class="link text-danger glyphicon glyphicon-minus-sign" ng-click="del(currentNode.data.over,$index)"/>
<span ng-if="$last" class="link text-success glyphicon glyphicon-plus-sign" ng-click="menuClick({title:'newSfOver'})"/>
</td>
</tr>
</table>
</div>
<script type="text/menu">
[{
"title": "newSfOver",
"icon": "plus-sign"
},{
"title": "deleteEntry",
"icon": "minus-sign"
}]
</script>

View File

@ -0,0 +1,40 @@
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{{translateTitle(currentNode)}}</h3>
</div>
<table class="table table-striped">
<thead>
<tr>
<th width="25%" trspan="name"></th>
<th width="25%" trspan="type"></th>
<th width="50%" trspan="rule"></th>
<th />
</tr>
</thead>
<tbody>
<tr ng-repeat="s in currentNode.nodes">
<td>
<input class="form-control" ng-model="s.title" />
</td>
<td>
<select class="form-control" ng-model="s.data.type">
<option ng-repeat="item in currentNode.select" ng-selected="item.k==s.data.type" value="{{item.k}}">{{item.v}}</option>
</select>
</td>
<td>
<input class="form-control" ng-model="s.data.rule" />
</td>
<td>
<span class="link text-danger glyphicon glyphicon-minus-sign" ng-click="del(currentNode.nodes,$index)"/>
<span ng-if="$last" class="link text-success glyphicon glyphicon-plus-sign" ng-click="menuClick({title:'newSfExtra'})"/>
</td>
</tr>
</tbody>
</table>
</div>
<script type="text/menu">
[{
"title": "newSfExtra",
"icon": "plus-sign"
}]
</script>

View File

@ -1,4 +1,4 @@
// Generated by CoffeeScript 1.12.7
// Generated by CoffeeScript 1.12.8
/*
LemonLDAP::NG Manager client
@ -382,6 +382,28 @@ This file contains:
});
return $scope.execFilters($scope._findScopeByKey('authParams'));
};
$scope.newSfExtra = function() {
var node;
node = $scope._findContainer();
return node.nodes.push({
id: node.id + "/n" + (id++),
title: 'new',
type: 'sfExtra',
data: {
type: '',
rule: '',
over: []
}
});
};
$scope.newSfOver = function() {
var d;
d = $scope.currentNode.data;
if (!d.over) {
d.over = [];
}
return d.over.push(["new" + (id++), '']);
};
$scope.newCmbOver = function() {
var d;
d = $scope.currentNode.data;
@ -767,7 +789,7 @@ This file contains:
$scope.keyWritable = function(scope) {
var node;
node = scope.$modelValue;
if (node.type && node.type.match(/^(?:(?:saml(?:(?:ID|S)PMetaDataNod|Attribut)|(?:cmbMod|r)ul|authChoic)e|(?:virtualHos|keyTex)t|menu(?:App|Cat))$/)) {
if (node.type && node.type.match(/^(?:s(?:aml(?:(?:ID|S)PMetaDataNod|Attribut)e|fExtra)|(?:(?:cmbMod|r)ul|authChoic)e|(?:virtualHos|keyTex)t|menu(?:App|Cat))$/)) {
return true;
} else {
return false;

File diff suppressed because one or more lines are too long

View File

@ -465,6 +465,8 @@
"newPostVar":"متغير جديد",
"newRSAKey":"مفاتيح جديدة",
"newRule":"قاعدة جديدة",
"newSfOver":"معايير جديدة",
"newSfExtra":"New second factor",
"newValue":"قيمة جديدة",
"next":"التالى",
"nginxCustomHandlers":"معالجات إنجن إكس المخصصة",
@ -734,6 +736,7 @@
"sessionStorage":"تخزين الجلسات",
"sessionTitle":"محتوى الجلسة",
"sfaTitle":"Second Factors Authentication",
"sfExtra":"Additional second factors",
"sfRequired":"Require 2FA",
"sfRemovedNotification":"Display a message if an expired SF is removed",
"sfRemovedMsgRule":"تفعيل",
@ -1015,4 +1018,4 @@
"samlRelayStateTimeout":"تناوب حالة مهلة الجلسة ",
"samlUseQueryStringSpecific":"استخدام أسلوب query_string المعين",
"samlOverrideIDPEntityID":"Override Entity ID when acting as IDP"
}
}

View File

@ -465,6 +465,8 @@
"newPostVar":"New variable",
"newRSAKey":"New keys",
"newRule":"New rule",
"newSfOver":"New parameter",
"newSfExtra":"New second factor",
"newValue":"New value",
"next":"Next",
"nginxCustomHandlers":"Custom Nginx handlers",
@ -734,6 +736,7 @@
"sessionStorage":"Sessions Storage",
"sessionTitle":"Session content",
"sfaTitle":"Second Factors Authentication",
"sfExtra":"Additional second factors",
"sfRequired":"Require 2FA",
"sfRemovedNotification":"Display a message if an expired SF is removed",
"sfRemovedMsgRule":"Activation",
@ -1015,4 +1018,4 @@
"samlRelayStateTimeout":"RelayState session timeout",
"samlUseQueryStringSpecific":"Use specific query_string method",
"samlOverrideIDPEntityID":"Override Entity ID when acting as IDP"
}
}

View File

@ -465,6 +465,8 @@
"newPostVar":"New variable",
"newRSAKey":"New keys",
"newRule":"New rule",
"newSfOver":"New parameter",
"newSfExtra":"New second factor",
"newValue":"New value",
"next":"Next",
"nginxCustomHandlers":"Custom Nginx handlers",
@ -734,6 +736,7 @@
"sessionStorage":"Sessions Storage",
"sessionTitle":"Session content",
"sfaTitle":"Second Factors Authentication",
"sfExtra":"Additional second factors",
"sfRequired":"Require 2FA",
"sfRemovedNotification":"Display a message if an expired SF is removed",
"sfRemovedMsgRule":"Activation",

View File

@ -465,6 +465,8 @@
"newPostVar":"Nouvelle variable",
"newRSAKey":"Nouvelles clefs",
"newRule":"Nouvelle règle",
"newSfOver":"Nouveau paramètre",
"newSfExtra":"Nouveau second facteur",
"newValue":"Nouvelle valeur",
"next":"Suivante",
"nginxCustomHandlers":"Handlers Nginx personnalisés",
@ -734,6 +736,7 @@
"sessionStorage":"Stockage des sessions",
"sessionTitle":"Contenu de la session",
"sfaTitle":"Seconds Facteurs d'Authentification",
"sfExtra":"Seconds Facteurs additionnels",
"sfRequired":"Exiger 2FA",
"sfRemovedNotification":"Afficher un message si un SF expiré a été supprimé",
"sfRemovedMsgRule":"Activation",

View File

@ -465,6 +465,8 @@
"newPostVar":"Nuova variabile",
"newRSAKey":"Nuove chiavi",
"newRule":"Nuova regola",
"newSfOver":"Nuovo parametro",
"newSfExtra":"New second factor",
"newValue":"Nuovo valore",
"next":"Seguente",
"nginxCustomHandlers":"Gestori Custom Nginx",
@ -734,6 +736,7 @@
"sessionStorage":"Conservazione di sessioni",
"sessionTitle":"Contenuto della sessione",
"sfaTitle":"Autenticazione a due fattori",
"sfExtra":"Additional second factors",
"sfRequired":"Richiedi 2FA",
"sfRemovedNotification":"Display a message if an expired SF is removed",
"sfRemovedMsgRule":"Attivazione",
@ -1015,4 +1018,4 @@
"samlRelayStateTimeout":"Timeout di sessione di RelayState",
"samlUseQueryStringSpecific":"Utilizza il metodo specifico query_string",
"samlOverrideIDPEntityID":"Sostituisci l'ID entità quando agisce come IDP"
}
}

View File

@ -465,6 +465,8 @@
"newPostVar":"Biến mới",
"newRSAKey":"Khóa mới",
"newRule":"Quy tắc mới",
"newSfOver":"Tham số mới",
"newSfExtra":"New second factor",
"newValue":"Giá trị mới",
"next":"Tiếp theo",
"nginxCustomHandlers":"Tùy chỉnh trình xử lý Nginx ",
@ -734,6 +736,7 @@
"sessionStorage":"Sessions lưu trữ",
"sessionTitle":"Nội dung phiên",
"sfaTitle":"Second Factors Authentication",
"sfExtra":"Additional second factors",
"sfRequired":"Require 2FA",
"sfRemovedNotification":"Display a message if an expired SF is removed",
"sfRemovedMsgRule":"Kích hoạt",
@ -1015,4 +1018,4 @@
"samlRelayStateTimeout":"Thời gian hết hạn phiên RelayState ",
"samlUseQueryStringSpecific":"Sử dụng phương pháp query_string cụ thể",
"samlOverrideIDPEntityID":"Override Entity ID when acting as IDP"
}
}

View File

@ -465,6 +465,8 @@
"newPostVar":"New variable",
"newRSAKey":"New keys",
"newRule":"New rule",
"newSfOver":"New parameter",
"newSfExtra":"New second factor",
"newValue":"New value",
"next":"Next",
"nginxCustomHandlers":"Custom Nginx handlers",
@ -734,6 +736,7 @@
"sessionStorage":"Sessions Storage",
"sessionTitle":"Session content",
"sfaTitle":"Second Factors Authentication",
"sfExtra":"Additional second factors",
"sfRequired":"Require 2FA",
"sfRemovedNotification":"Display a message if an expired SF is removed",
"sfRemovedMsgRule":"激活",
@ -1015,4 +1018,4 @@
"samlRelayStateTimeout":"RelayState session timeout",
"samlUseQueryStringSpecific":"Use specific query_string method",
"samlOverrideIDPEntityID":"Override Entity ID when acting as IDP"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,38 @@
# Verify that bas changes are detected
use Test::More;
use strict;
use JSON;
require 't/test-lib.pm';
my $struct = 't/jsonfiles/17-extra2f.json';
sub body {
return IO::File->new( $struct, 'r' );
}
unlink 't/conf/lmConf-2.json';
mkdir 't/sessions';
my ( $res, $resBody );
ok( $res = &client->_post( '/confs/', 'cfgNum=1', &body, 'application/json' ),
"Request succeed" );
ok( $res->[0] == 200, "Result code is 200" );
ok( $resBody = from_json( $res->[2]->[0] ), "Result body contains JSON text" );
ok( $resBody->{result} == 1, "JSON response contains \"result:1\"" )
or print STDERR Dumper($res);
ok( $res = &client->_get( '/confs/2/sfExtra', 'application/json' ),
'Get combModules' );
ok( $resBody = from_json( $res->[2]->[0] ), "Result body contains JSON text" );
use Data::Dumper;
print Dumper($resBody);
count(6);
done_testing( count() );
unlink 't/conf/lmConf-2.json';
`rm -rf t/sessions`;

File diff suppressed because it is too large Load Diff

View File

@ -24,6 +24,7 @@ use Lemonldap::NG::Portal::Main::Constants qw(
our $VERSION = '2.0.5';
extends 'Lemonldap::NG::Portal::Main::Plugin';
with 'Lemonldap::NG::Portal::Lib::OverConf';
# INITIALIZATION
@ -93,6 +94,37 @@ sub init {
}
}
# Extra 2F modules
$self->logger->debug('Processing Extra 2F modules');
foreach my $extraKey (sort keys %{ $self->conf->{sfExtra} } ) {
my $moduleType = $self->conf->{sfExtra}->{$extraKey}->{type};
next unless ($moduleType);
my %over = %{ $self->conf->{sfExtra}->{$extraKey}->{over} or {} };
$self->logger->debug(
"Loading extra 2F module $extraKey of type $moduleType");
my $m =
$self->loadPlugin( "::2F::$moduleType",
{ sfPrefix => $extraKey, %over } )
or return 0;
# Rule and prefix may be modified by 2F module, reread them
my $rule = $self->conf->{sfExtra}->{$extraKey}->{rule} || 1;
my $prefix = $m->prefix;
# Compile rule
$rule = $self->p->HANDLER->substitute($rule);
unless ( $rule = $self->p->HANDLER->buildSub($rule) ) {
$self->error( 'External 2F rule error: '
. $self->p->HANDLER->tsv->{jail}->error );
return 0;
}
# Store module
push @{ $self->{'sfModules'} }, { p => $prefix, m => $m, r => $rule };
}
unless (
$self->sfReq(
$self->p->HANDLER->buildSub(

View File

@ -16,7 +16,7 @@ extends 'Lemonldap::NG::Portal::Main::SecondFactor';
# INITIALIZATION
has prefix => ( is => 'ro', default => 'ext' );
has prefix => ( is => 'rw', default => 'ext' );
has random => ( is => 'rw' );
sub init {
@ -28,6 +28,8 @@ sub init {
return 0;
}
}
$self->prefix( $self->conf->{sfPrefix} )
if ( $self->conf->{sfPrefix} );
$self->logo( $self->conf->{ext2fLogo} )
if ( $self->conf->{ext2fLogo} );
return $self->SUPER::init();
@ -38,6 +40,8 @@ sub init {
return 0;
}
$self->random( Lemonldap::NG::Common::Crypto::srandom() );
$self->prefix( $self->conf->{sfPrefix} )
if ( $self->conf->{sfPrefix} );
$self->logo( $self->conf->{ext2fLogo} )
if ( $self->conf->{ext2fLogo} );
return $self->SUPER::init();
@ -82,6 +86,7 @@ sub run {
MAIN_LOGO => $self->conf->{portalMainLogo},
SKIN => $self->p->getSkin($req),
TOKEN => $token,
TARGET => '/' . $self->prefix . '2fcheck',
CHECKLOGINS => $checkLogins
}
);

View File

@ -18,7 +18,7 @@ extends 'Lemonldap::NG::Portal::Main::SecondFactor',
# INITIALIZATION
has prefix => ( is => 'ro', default => 'mail' );
has prefix => ( is => 'rw', default => 'mail' );
has random => (
is => 'rw',
default => sub {
@ -47,6 +47,8 @@ sub init {
}
$self->logo( $self->conf->{mail2fLogo} )
if ( $self->conf->{mail2fLogo} );
$self->prefix( $self->conf->{sfPrefix} )
if ( $self->conf->{sfPrefix} );
return $self->SUPER::init();
}

View File

@ -17,7 +17,7 @@ extends 'Lemonldap::NG::Portal::Main::SecondFactor',
# INITIALIZATION
has prefix => ( is => 'ro', default => 'rest' );
has prefix => ( is => 'rw', default => 'rest' );
has initAttrs => ( is => 'rw', default => sub { {} } );
@ -29,6 +29,8 @@ sub init {
$self->logger->error('Missing REST verification URL');
return 0;
}
$self->prefix( $self->conf->{sfPrefix} )
if ( $self->conf->{sfPrefix} );
$self->logo( $self->conf->{rest2fLogo} ) if ( $self->conf->{rest2fLogo} );
foreach my $k ( keys %{ $self->conf->{rest2fInitArgs} } ) {
my $attr = $self->conf->{rest2fInitArgs}->{$k};
@ -92,7 +94,7 @@ sub run {
MAIN_LOGO => $self->conf->{portalMainLogo},
SKIN => $self->p->getSkin($req),
TOKEN => $token,
TARGET => '/rest2fcheck',
TARGET => '/' . $self->prefix . '2fcheck',
CHECKLOGINS => $checkLogins
}
);

View File

@ -0,0 +1,162 @@
use Test::More;
use strict;
use IO::String;
use Data::Dumper;
require 't/test-lib.pm';
require 't/smtp.pm';
use_ok('Lemonldap::NG::Common::FormEncode');
count(1);
my $client = LLNG::Manager::Test->new( {
ini => {
logLevel => 'error',
authentication => 'Demo',
userDB => 'Same',
'sfExtra' => {
'home' => {
'over' => {
mail2fCodeRegex => '\w{4}',
mail2fLogo => 'home.jpg',
},
'rule' => '$uid eq "dwho" or $uid eq "msmith"',
'type' => 'Mail2F'
},
'work' => {
'over' => {
mail2fLogo => 'work.jpg',
mail2fCodeRegex => '\d{8}',
},
'rule' => '$uid eq "dwho" or $uid eq "rtyler"',
'type' => 'Mail2F'
}
},
}
}
);
# Login with rtyler
# -----------------
ok(
my $res = $client->_post(
'/',
IO::String->new('user=rtyler&password=rtyler'),
length => 27,
accept => 'text/html',
),
'Auth query'
);
count(1);
# Only "work" option is available
my ( $host, $url, $query ) =
expectForm( $res, undef, '/work2fcheck', 'token', 'code' );
ok(
$res->[2]->[0] =~
qr%<input name="code" value="" class="form-control" id="extcode" trplaceholder="code" autocomplete="off" />%,
'Found EXTCODE input'
) or print STDERR Dumper( $res->[2]->[0] );
count(1);
ok( mail() =~ m%<b>(\d{8})</b>%, 'Found 2F code in mail' )
or print STDERR Dumper( mail() );
my $code = $1;
count(1);
$query =~ s/code=/code=${code}/;
ok(
$res = $client->_post(
'/work2fcheck',
IO::String->new($query),
length => length($query),
accept => 'text/html',
),
'Post code'
);
count(1);
my $id = expectCookie($res);
$client->logout($id);
clean_sessions();
# Login with dwho
# ---------------
ok(
my $res = $client->_post(
'/',
IO::String->new('user=dwho&password=dwho'),
length => 23,
accept => 'text/html',
),
'Auth query'
);
count(1);
# Expect choice page
my ( $host, $url, $query ) =
expectForm( $res, undef, '/2fchoice', 'token', 'checkLogins' );
ok(
$res->[2]->[0] =~
qq%<img src="/static/bootstrap/work.jpg" alt="work2F" title="work2F" />%,
'Found work.png'
) or print STDERR Dumper( $res->[2]->[0] );
count(1);
ok(
$res->[2]->[0] =~
qq%<img src="/static/bootstrap/home.jpg" alt="home2F" title="home2F" />%,
'Found home.png'
) or print STDERR Dumper( $res->[2]->[0] );
count(1);
$query .= '&sf=home';
ok(
$res = $client->_post(
'/2fchoice',
IO::String->new($query),
length => length($query),
accept => 'text/html',
),
'Post ext2f choice'
);
count(1);
my ( $host, $url, $query ) =
expectForm( $res, undef, '/home2fcheck', 'token', 'code' );
ok(
$res->[2]->[0] =~
qr%<input name="code" value="" class="form-control" id="extcode" trplaceholder="code" autocomplete="off" />%,
'Found EXTCODE input'
) or print STDERR Dumper( $res->[2]->[0] );
count(1);
ok( mail() =~ m%<b>(\w{4})</b>%, 'Found 2F code in mail' )
or print STDERR Dumper( mail() );
my $code = $1;
count(1);
$query =~ s/code=/code=${code}/;
ok(
$res = $client->_post(
'/home2fcheck',
IO::String->new($query),
length => length($query),
accept => 'text/html',
),
'Post code'
);
count(1);
my $id = expectCookie($res);
$client->logout($id);
clean_sessions();
done_testing( count() );