Combination override conf (#1151)

TODO: lot of job in the manager...
This commit is contained in:
Xavier Guimard 2017-02-05 23:04:28 +00:00
parent b78022558d
commit 8a3bb7b0f9
15 changed files with 268 additions and 49 deletions

View File

@ -30,6 +30,7 @@ lib/Lemonldap/NG/Common/Conf/ReConstants.pm
lib/Lemonldap/NG/Common/Conf/RESTServer.pm
lib/Lemonldap/NG/Common/Conf/SAML/Metadata.pm
lib/Lemonldap/NG/Common/Conf/Serializer.pm
lib/Lemonldap/NG/Common/Conf/Wrapper.pm
lib/Lemonldap/NG/Common/Crypto.pm
lib/Lemonldap/NG/Common/FormEncode.pm
lib/Lemonldap/NG/Common/Module.pm

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|wpSslOpt)|(?:(?:d(?:emo|bi)|facebook|webID)ExportedVa|exported(?:Heade|Va))r|g(?:r(?:antSessionRule|oup)|lobalStorageOption)|n(?:otificationStorageOption|ginxCustomHandler)|re(?:moteGlobalStorageOption|loadUrl)|cas(?:StorageOption|Attribute)|CAS_proxiedService|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)|p(?:ersistentStorageOptions|o(?:rtalSkinRules|st))|a(?:uthChoiceModules|pplicationList)|v(?:hostOptions|irtualHost))$/;
our $hashParameters = qr/^(?:(?:l(?:o(?:ca(?:lSessionStorageOption|tionRule)|goutService)|dapExportedVar|wpSslOpt)|(?:(?:d(?:emo|bi)|facebook|webID)ExportedVa|exported(?:Heade|Va))r|g(?:r(?:antSessionRule|oup)|lobalStorageOption)|n(?:otificationStorageOption|ginxCustomHandler)|re(?:moteGlobalStorageOption|loadUrl)|CAS_proxiedService|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(?:StorageOption|Attribute)s|omb(?:Modules|Over))|p(?:ersistentStorageOptions|o(?:rtalSkinRules|st))|a(?:uthChoiceModules|pplicationList)|v(?:hostOptions|irtualHost))$/;
our @sessionTypes = ( 'remoteGlobal', 'cas', 'global', 'localSession', 'persistent', 'saml', 'oidc' );

View File

@ -20,7 +20,7 @@ our $specialNodeHash = {
};
our $doubleHashKeys = 'issuerDBGetParameters';
our $simpleHashKeys = '(?:(?:l(?:o(?:calSessionStorageOption|goutService)|dapExportedVar|wpSslOpt)|(?:(?:d(?:emo|bi)|facebook|webID)E|e)xportedVar|g(?:r(?:antSessionRule|oup)|lobalStorageOption)|n(?:otificationStorageOption|ginxCustomHandler)|p(?:ersistentStorageOption|ortalSkinRule)|re(?:moteGlobalStorageOption|loadUrl)|cas(?:StorageOption|Attribute)|CAS_proxiedService|macro)s|o(?:idcS(?:erviceMetaDataAuthnContext|torageOptions)|penIdExportedVars)|s(?:(?:amlStorageOption|laveExportedVar)s|essionDataToRemember)|a(?:uthChoiceModules|pplicationList))';
our $simpleHashKeys = '(?:(?:l(?:o(?:calSessionStorageOption|goutService)|dapExportedVar|wpSslOpt)|(?:(?:d(?:emo|bi)|facebook|webID)E|e)xportedVar|g(?:r(?:antSessionRule|oup)|lobalStorageOption)|n(?:otificationStorageOption|ginxCustomHandler)|p(?:ersistentStorageOption|ortalSkinRule)|re(?:moteGlobalStorageOption|loadUrl)|CAS_proxiedService|macro)s|o(?:idcS(?:erviceMetaDataAuthnContext|torageOptions)|penIdExportedVars)|s(?:(?:amlStorageOption|laveExportedVar)s|essionDataToRemember)|c(?:as(?:StorageOption|Attribute)s|omb(?:Modules|Over))|a(?:uthChoiceModules|pplicationList))';
our $specialNodeKeys = '(?:(?:saml(?:ID|S)|oidc[OR])PMetaDataNode|virtualHost)s';
our $oidcOPMetaDataNodeKeys = 'oidcOPMetaData(?:Options(?:C(?:lient(?:Secret|ID)|heckJWTSignature|onfigurationURI)|TokenEndpointAuthMethod|(?:JWKSTimeou|Promp)t|I(?:DTokenMaxAge|con)|S(?:toreIDToken|cope)|U(?:iLocales|seNonce)|Display(?:Name)?|AcrValues|MaxAge)|ExportedVars|J(?:SON|WKS))';
our $oidcRPMetaDataNodeKeys = 'oidcRPMetaData(?:Options(?:I(?:DToken(?:Expiration|SignAlg)|con)|(?:RedirectUri|ExtraClaim)s|AccessTokenExpiration|Client(?:Secret|ID)|BypassConsent|DisplayName|UserIDAttr)|ExportedVars)';
@ -33,11 +33,11 @@ our $authParameters = {
apacheParams => [qw(apacheAuthnLevel)],
casParams => [qw(CAS_authnLevel CAS_url CAS_CAFile CAS_renew CAS_gateway CAS_pgtFile CAS_proxiedServices)],
choiceParams => [qw(authChoiceParam authChoiceModules)],
combinationParams => [qw(combination combModules combOver)],
dbiParams => [qw(dbiAuthnLevel dbiExportedVars dbiAuthChain dbiAuthUser dbiAuthPassword dbiUserChain dbiUserUser dbiUserPassword dbiAuthTable dbiUserTable dbiAuthLoginCol dbiAuthPasswordCol dbiPasswordMailCol userPivot dbiAuthPasswordHash)],
demoParams => [qw(demoExportedVars)],
facebookParams => [qw(facebookAuthnLevel facebookExportedVars facebookAppId facebookAppSecret)],
ldapParams => [qw(ldapAuthnLevel ldapExportedVars ldapServer ldapPort ldapBase managerDn managerPassword ldapTimeout ldapVersion ldapRaw LDAPFilter AuthLDAPFilter mailLDAPFilter ldapSearchDeref ldapGroupBase ldapGroupObjectClass ldapGroupAttributeName ldapGroupAttributeNameUser ldapGroupAttributeNameSearch ldapGroupRecursive ldapGroupAttributeNameGroup ldapPpolicyControl ldapSetPassword ldapChangePasswordAsUser ldapPwdEnc ldapUsePasswordResetAttribute ldapPasswordResetAttribute ldapPasswordResetAttributeValue ldapAllowResetExpiredPassword)],
multiParams => [qw(multiAuthStack multiUserDBStack)],
nullParams => [qw(nullAuthnLevel)],
oidcParams => [qw(oidcAuthnLevel oidcRPCallbackGetParam oidcRPStateTimeout)],
openidParams => [qw(openIdAuthnLevel openIdExportedVars openIdSecret openIdIDPList)],

View File

@ -0,0 +1,58 @@
package Lemonldap::NG::Common::Conf::Wrapper;
use strict;
our $VERSION = '2.0.0';
sub TIEHASH {
my ( $class, $conf, $overrides ) = @_;
return bless {
_wrapC => $conf,
_wrapO => $overrides,
}, $class;
}
sub FETCH {
my ( $self, $key ) = @_;
return (
exists( $self->{_wrapO}->{$key} )
? $self->{_wrapO}->{$key}
: $self->{_wrapC}->{$key}
);
}
sub STORE {
my ( $self, $key, $value ) = @_;
return $self->{_wrapO}->{$key} = $value;
}
sub DELETE {
my ( $self, $key ) = @_;
my $res = $self->{_wrapO}->{$key} // $self->{_wrapC}->{$key};
$self->{_wrapO}->{$key} = undef;
return $res;
}
sub EXISTS {
my ( $self, $key ) = @_;
return (
exists( $self->{_wrapC}->{$key} )
or exists( $self->{_wrapO}->{$key} )
);
}
sub DESTROY {
my $self = shift;
delete $self->{_wrapO};
delete $self->{_wrapC};
}
sub FIRSTKEY {
return each %{ $_[0]->{_wrapC} };
}
sub NEXTKEY {
return each %{ $_[0]->{_wrapC} };
}
1;

View File

@ -654,6 +654,15 @@ qr/(?:(?:https?):\/\/(?:(?:(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.]
'default' => 1,
'type' => 'bool'
},
'combination' => {
'type' => 'text'
},
'combModules' => {
'type' => 'keyTextContainer'
},
'combOver' => {
'type' => 'keyTextContainer'
},
'confirmFormMethod' => {
'default' => 'post',
'select' => [
@ -1359,12 +1368,6 @@ qr/^(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*(?:[a-zA-Z][-a-zA-Z0-
'test' => qr/^\S*$/,
'type' => 'password'
},
'multiAuthStack' => {
'type' => 'authParamsText'
},
'multiUserDBStack' => {
'type' => 'authParamsText'
},
'multiValuesSeparator' => {
'default' => '; ',
'type' => 'authParamsText'

View File

@ -342,6 +342,11 @@ sub attributes {
default => '',
documentation => 'Syslog facility',
},
multiValuesSeparator => {
type => 'authParamsText',
default => '; ',
documentation => 'Separator for multiple values',
},
# Manager
protection => {
@ -1796,11 +1801,11 @@ sub attributes {
userDB => {
type => 'select',
select => [
{ k => 'Same', v => 'Same' },
{ k => 'AD', v => 'Active Directory' },
{ k => 'DBI', v => 'Database (DBI)' },
{ k => 'LDAP', v => 'LDAP' },
{ k => 'Null', v => 'None' },
{ k => 'Same', v => 'Same' },
{ k => 'AD', v => 'Active Directory' },
{ k => 'DBI', v => 'Database (DBI)' },
{ k => 'LDAP', v => 'LDAP' },
{ k => 'Null', v => 'None' },
],
default => 'Same',
documentation => 'User module',
@ -2262,17 +2267,17 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{1,5})?|ldap(?:s|\+tls)?://\w[\w\-\.]*(?:
],
},
# Multi
multiAuthStack => {
type => 'authParamsText',
# Combination
combination => {
type => 'text',
},
multiUserDBStack => {
type => 'authParamsText',
combModules => {
type => 'keyTextContainer',
},
multiValuesSeparator => {
type => 'authParamsText',
default => '; ',
documentation => 'Separator for multiple values',
combOver => {
#TODO: create new type
type => 'keyTextContainer',
},
# Yubikey

View File

@ -250,10 +250,10 @@ sub tree {
]
},
{
title => 'multiParams',
help => 'authmulti.html',
form => 'authParamsTextContainer',
nodes => [ 'multiAuthStack', 'multiUserDBStack' ]
title => 'combinationParams',
help => 'authcombination.html',
nodes =>
[ 'combination', 'combModules', 'combOver' ]
},
{
title => 'nullParams',

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,7 @@
use Test::More;
use strict;
use Data::Dumper;
my $formDir = 'site/static/forms';
@ -31,7 +32,7 @@ $count += 4;
my %types = %{ getTypes( $tree, values(%$ctrees), $attr ) };
foreach (qw(home menuCat menuApp)) {
foreach (qw(home menuCat menuApp authParamsTextContainer)) {
ok( $forms{$_}, "Found $_ form" );
$count++;
delete $forms{$_};
@ -39,10 +40,13 @@ foreach (qw(home menuCat menuApp)) {
foreach my $type ( keys %types ) {
delete $types{$type};
next
if ( $type =~
if ( $type =~
/^(?:authParamsText|url|PerlModule|hostname|pcre|lmAttrOrMacro|subContainer|RSAP(?:ublic|rivate)Key(?:OrCertificate)?)$/
);
)
{
delete $forms{$type};
next;
}
ok( $forms{$type}, "Found $type" );
delete $forms{$type};
$count++;
@ -53,7 +57,7 @@ foreach my $type ( keys %types ) {
$count++;
}
}
ok( !%forms, "No unused forms" );
ok( !%forms, "No unused forms" ) or print "Found:\n" . Dumper( \%forms );
$count++;
done_testing($count);

View File

@ -372,6 +372,7 @@ t/34-Auth-Proxy-and-REST-Server.t
t/34-Auth-Proxy-and-SOAP-Server.t
t/35-REST-sessions-with-REST-server.t
t/35-SOAP-sessions-with-SOAP-server.t
t/36-Combination-with-over.t
t/36-Combination.t
t/40-Notifications-JSON-DBI.t
t/40-Notifications-JSON-File-with-token.t

View File

@ -16,31 +16,49 @@ has stackSub => ( is => 'rw' );
sub init {
my ($self) = @_;
# Check if expression exists
unless ( $self->conf->{combination} ) {
$self->error('No combination found');
return 0;
}
# Load all declared modules
my %mods;
foreach my $mod ( @{ $self->conf->{combModules} } ) {
my @tmp = ( undef, undef );
# TODO: override params
# Override parameters
my $over = $self->conf->{combOver}->{ $mod->{name} };
# "for" key can have 3 values:
# 0: this module will be used for Auth and UserDB
# 1: this module will be user for Auth only
# 2: this module will be user for UserDB only
# Load Auth module
if ( $mod->{for} < 2 ) {
$tmp[0] = $self->p->loadPlugin("::Auth::$mod->{type}");
$tmp[0] = $self->loadPlugin( "::Auth::$mod->{type}", $over );
unless ( $tmp[0] ) {
$self->error("Unable to load Auth::$mod->{type}");
return 0;
}
}
# Load UserDB module
unless ( $mod->{for} == 1 ) {
$tmp[1] = $self->p->loadPlugin("::UserDB::$mod->{type}");
$tmp[1] = $self->loadPlugin( "::UserDB::$mod->{type}", $over );
unless ( $tmp[1] ) {
$self->error("Unable to load UserDB::$mod->{type}");
return 0;
}
}
# Store modules as array
$mods{ $mod->{name} } = \@tmp;
}
# Compile expression
eval {
$self->stackSub(
Lemonldap::NG::Common::Combination::Parser->parse(
@ -55,12 +73,8 @@ sub init {
return 1;
}
sub name {
my ( $self, $req, $type ) = @_;
return $req->sessionInfo->{ ( $type eq 'auth' ? '_auth' : '_userDB' ) }
|| 'Combination';
}
# Each first method must call getStack() to get the auth scheme available for
# the current user
sub extractFormInfo {
my ( $self, $req ) = @_;
@ -69,6 +83,7 @@ sub extractFormInfo {
return $self->try( 0, 'extractFormInfo', $req );
}
# Note that UserDB::Combination use the same object.
sub getUser {
return $_[0]->try( 1, 'getUser', $_[1] );
}
@ -98,7 +113,7 @@ sub getDisplayType {
sub getStack {
my ( $self, $req, @steps ) = @_;
return $req->datas->{multiStack} if ( $req->datas->{multiStack} );
my $stack = $req->datas->{multiStack} = $self->stackSub->($req->env);
my $stack = $req->datas->{multiStack} = $self->stackSub->( $req->env );
unless ($stack) {
$self->lmLog( 'No authentication scheme for this user', 'error' );
}
@ -107,6 +122,7 @@ sub getStack {
return $stack;
}
# Main running method: launch the next scheme if the current fails
sub try {
my ( $self, $type, $subname, $req ) = @_;
my ( $nb, $stack ) = ( $req->datas->{multiTry}, $req->datas->{multiStack} );
@ -134,4 +150,34 @@ sub try {
return $res;
}
# try() stores real Auth/UserDB module in sessionInfo
# This method reads them. It is called by getModule()
# (see Main::Run)
sub name {
my ( $self, $req, $type ) = @_;
return $req->sessionInfo->{ ( $type eq 'auth' ? '_auth' : '_userDB' ) }
|| 'Combination';
}
# To avoid "tied" destroy, tied configurations are kept here
our %overC;
# Override portal loadPlugin() to use a wrapped configuration
sub loadPlugin {
my ( $self, $plugin, $over ) = @_;
my $obj;
my $nc;
if($over) {
require Lemonldap::NG::Common::Conf::Wrapper;
tie %$nc, 'Lemonldap::NG::Common::Conf::Wrapper', $self->conf, $over;
$overC{$plugin} = $nc;
}
else {
$nc = $self->conf;
}
return 0
unless ( $obj = $self->p->loadModule( "$plugin", $nc ) );
return $self->p->findEP( $plugin, $obj );
}
1;

View File

@ -142,7 +142,7 @@ sub reloadConf {
my $csp = '';
foreach (qw(default img src style font connect)) {
my $prm = $self->conf->{ 'csp' . ucfirst($_) };
$csp .= "$_-src $prm;" if($prm);
$csp .= "$_-src $prm;" if ($prm);
}
$self->csp($csp);
@ -183,7 +183,7 @@ sub reloadConf {
$self->error("$type is not set");
return $self->fail;
}
$mod = $self->conf->{$type} unless($self->conf->{$type} eq 'Same');
$mod = $self->conf->{$type} unless ( $self->conf->{$type} eq 'Same' );
my $module = '::' . ucfirst($type) . '::' . $mod;
$module =~ s/Authentication/Auth/;
@ -278,6 +278,11 @@ sub loadPlugin {
my $obj;
return 0
unless ( $obj = $self->loadModule("$plugin") );
return $self->findEP( $plugin, $obj );
}
sub findEP {
my ( $self, $plugin, $obj ) = @_;
foreach my $sub (
qw(beforeAuth betweenAuthAndDatas afterDatas forAuthUser beforeLogout))
{
@ -295,7 +300,8 @@ sub loadPlugin {
}
sub loadModule {
my ( $self, $module ) = @_;
my ( $self, $module, $conf ) = @_;
$conf //= $self->conf;
my $obj;
$module = "Lemonldap::NG::Portal$module" if ( $module =~ /^::/ );
@ -305,7 +311,7 @@ sub loadModule {
return 0;
}
eval {
$obj = $module->new( { p => $self, conf => $self->conf } );
$obj = $module->new( { p => $self, conf => $conf } );
$self->lmLog( "Module $module loaded", 'debug' );
};
if ($@) {

View File

@ -136,7 +136,7 @@ sub do {
'WWW-Authenticate' => "SSO " . $self->conf->{portal},
'Access-Control-Allow-Origin' => '*'
],
[]
[qq'{"result":0,"error":$err"}']
];
}
elsif ( $err > 0 ) {

View File

@ -0,0 +1,95 @@
use Test::More;
use strict;
use IO::String;
require 't/test-lib.pm';
my $res;
my $mainTests = 0;
my $client;
eval { unlink 't/userdb.db' };
SKIP: {
eval { require DBI; require DBD::SQLite; };
if ($@) {
skip 'DBD::SQLite not found', $mainTests;
}
my $dbh = DBI->connect("dbi:SQLite:dbname=t/userdb.db");
$dbh->do('CREATE TABLE users (user text,password text,name text)');
$dbh->do("INSERT INTO users VALUES ('dvador','dvador','Test user 1')");
$dbh->do("INSERT INTO users VALUES ('rtyler','rtyler','Test user 1')");
$client = iniCmb('[Dm] or [DB]');
expectCookie( try('dwho') );
expectCookie( try('dvador') );
}
count($mainTests);
clean_sessions();
eval { unlink 't/userdb.db' };
done_testing( count() );
sub try {
my $user = shift;
my $s = "user=$user&password=$user";
my $res;
ok(
$res = $client->_post(
'/', IO::String->new($s),
length => length($s),
custom => { HTTP_X => $user }
),
" Try to connect with login $user"
);
count(1);
return $res;
}
sub iniCmb {
my $expr = shift;
if (
my $res = LLNG::Manager::Test->new(
{
ini => {
logLevel => 'error',
useSafeJail => 1,
authentication => 'Combination',
userDB => 'Same',
combination => $expr,
combModules => [
{
for => 0,
name => 'DB',
type => 'DBI',
},
{
for => 0,
name => 'Dm',
type => 'Demo',
},
],
combOver => {
DB => {
dbiAuthChain => 'dbi:SQLite:dbname=t/userdb.db',
dbiAuthUser => '',
dbiAuthPassword => '',
dbiAuthTable => 'users',
dbiAuthLoginCol => 'user',
dbiAuthPasswordCol => 'password',
dbiAuthPasswordHash => '',
dbiExportedVars => {},
}
},
demoExportedVars => {},
}
}
)
)
{
pass(qq'Expression loaded: "$expr"');
count(1);
return $res;
}
}