Merge branch 'feature-password-change-combination-714' into 'v2.0'

Add Password::Combination

See merge request lemonldap-ng/lemonldap-ng!174
This commit is contained in:
Maxime Besson 2021-01-05 18:35:57 +01:00
commit 402a39a176
9 changed files with 460 additions and 19 deletions

View File

@ -1,11 +1,11 @@
Combination of authentication schemes
=====================================
============== ===== ========
============== ===== ==================
Authentication Users Password
============== ===== ========
✔ ✔
============== ===== ========
============== ===== ==================
✔ ✔ ✔ (since *2.0.10*)
============== ===== ==================
Presentation
------------
@ -184,6 +184,47 @@ lemonldap-ng.ini. Example:
[portal]
combinationForms = standardform, openidform
Password management
-------------------
.. versionadded:: 2.0.10
Not all configurations of the Combination module allow password management.
If your combination looks like this ::
[Kerberos, LDAP] or [LDAP]
Then you can simply set ``LDAP`` as the password module, and password changes
and reset will work as expected.
If your combination looks like this ::
[LDAP1] or [LDAP2]
Then you can configure the ``Combination`` password module to automatically
send password changes to the LDAP server which was used during authentication.
This module also enables password reset.
.. note::
You can set the ``_cmbPasswordDB`` session variable to manually select which
backend will be called when changing the password. This is useful when using
SASL delegation
Limitations
~~~~~~~~~~~
* When using password reset with a combination of 2 or more LDAP servers, you
need to make sure that there is no duplication of email addresses between all
your servers. If an email exists in more than one server, the password will
be reset on the first LDAP server that contains this email address
* Combinations using the ``and`` boolean expression will not cause passwords to
be changed in both backends for now
* Forcing the user to reset their password on next login is not currently
supported by the combination module
Known problems
--------------

View File

@ -156,13 +156,13 @@ Official Backends Authenticat
:doc:`Custom modules<authcustom>` |new| ✔ ✔ ✔
==================================================================== =============================================== ======== ========
==================================================================== ================================================= ======== ========
==================================================================== ================================================= ======== =========================
Combo Backends Authentication Users Password
==================================================================== ================================================= ======== ========
==================================================================== ================================================= ======== =========================
:doc:`Choice by users<authchoice>` ✔ ✔ ✔
:doc:`Combination of auth schemes<authcombination>` |new| ✔ ✔
:doc:`Combination of auth schemes<authcombination>` |new| ✔ ✔ ✔ (since *2.0.10*)
:doc:`Multiple backends stack<authmulti>` |deprecated| *Replaced by* :doc:`Combination<authcombination>`
==================================================================== ================================================= ======== ========
==================================================================== ================================================= ======== =========================
==================================================================== =============================================== ======== ========
Obsolete Backends Authentication Users Password

View File

@ -2563,6 +2563,10 @@ m[^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{1,5})?|ldap(?:s|\+tls)?://\w[\w\-\.]*(?:
'k' => 'Null',
'v' => 'None'
},
{
'k' => 'Combination',
'v' => 'combineMods'
},
{
'k' => 'Custom',
'v' => 'customModule'

View File

@ -3100,14 +3100,15 @@ sub attributes {
passwordDB => {
type => 'select',
select => [
{ k => 'AD', v => 'Active Directory' },
{ k => 'Choice', v => 'authChoice' },
{ k => 'DBI', v => 'Database (DBI)' },
{ k => 'Demo', v => 'Demonstration' },
{ k => 'LDAP', v => 'LDAP' },
{ k => 'REST', v => 'REST' },
{ k => 'Null', v => 'None' },
{ k => 'Custom', v => 'customModule' },
{ k => 'AD', v => 'Active Directory' },
{ k => 'Choice', v => 'authChoice' },
{ k => 'DBI', v => 'Database (DBI)' },
{ k => 'Demo', v => 'Demonstration' },
{ k => 'LDAP', v => 'LDAP' },
{ k => 'REST', v => 'REST' },
{ k => 'Null', v => 'None' },
{ k => 'Combination', v => 'combineMods' },
{ k => 'Custom', v => 'customModule' },
],
default => 'Demo',
documentation => 'Password module',

File diff suppressed because one or more lines are too long

View File

@ -95,6 +95,7 @@ lib/Lemonldap/NG/Portal/Main/SecondFactor.pm
lib/Lemonldap/NG/Portal/Password/AD.pm
lib/Lemonldap/NG/Portal/Password/Base.pm
lib/Lemonldap/NG/Portal/Password/Choice.pm
lib/Lemonldap/NG/Portal/Password/Combination.pm
lib/Lemonldap/NG/Portal/Password/Custom.pm
lib/Lemonldap/NG/Portal/Password/DBI.pm
lib/Lemonldap/NG/Portal/Password/Demo.pm
@ -578,11 +579,12 @@ t/35-REST-sessions-with-REST-server.t
t/35-SOAP-config-backend.t
t/35-SOAP-sessions-with-SOAP-server.t
t/36-Combination-Kerberos-or-Demo.t
t/36-Combination-Password.t
t/36-Combination.t
t/36-Combination-with-Choice.t
t/36-Combination-with-over.t
t/36-Combination-with-token.t
t/36-Combination-with-TOTP.t
t/36-Combination.t
t/37-CAS-App-to-SAML-IdP-POST-with-WAYF.t
t/37-CAS-App-to-SAML-IdP-POST.t
t/37-Issuer-Timeout.t
@ -616,11 +618,12 @@ t/42-Register-LDAP.t
t/42-Register-Security.t
t/43-MailPasswordReset-Choice.t
t/43-MailPasswordReset-Combination-LDAP.t
t/43-MailPasswordReset-Combination.t
t/43-MailPasswordReset-DBI.t
t/43-MailPasswordReset-LDAP.t
t/43-MailPasswordReset.t
t/43-MailPasswordReset-with-captcha.t
t/43-MailPasswordReset-with-token.t
t/43-MailPasswordReset.t
t/44-CertificateResetByMail-Demo.t
t/44-CertificateResetByMail-LDAP.t
t/50-IssuerGet.t

View File

@ -0,0 +1,74 @@
package Lemonldap::NG::Portal::Password::Combination;
our $VERSION = '2.0.10';
use strict;
use Mouse;
use Lemonldap::NG::Portal::Main::Constants qw(PE_OK PE_ERROR PE_FIRSTACCESS);
extends qw(
Lemonldap::NG::Portal::Password::Base
);
with 'Lemonldap::NG::Portal::Lib::OverConf';
has 'mods' => ( is => 'rw', isa => 'HashRef', default => sub { {} } );
sub init {
my $self = shift;
# Check if expression exists
unless ( $self->conf->{combination} ) {
$self->error('No combination found');
return 0;
}
# Load all declared modules
my %mods;
foreach my $key ( keys %{ $self->conf->{combModules} } ) {
my $tmp;
my $mod = $self->conf->{combModules}->{$key};
unless ( $mod->{type} and defined $mod->{for} ) {
$self->error("Malformed combination module $key");
return 0;
}
# Only load modules used for UserDB
unless ( $mod->{for} == 1 ) {
$tmp =
$self->loadModule( "::Password::$mod->{type}", $mod->{over} );
unless ($tmp) {
$self->notice("Unable to load Password::$mod->{type}");
next;
}
}
# Store modules as array
$self->mods->{$key} = $tmp;
}
return $self->SUPER::init;
}
sub delegate {
my ( $self, $req, $name, @args ) = @_;
# The user might want to override which password DB is used with a macro
# This is useful when using SASL delegation in OpenLDAP
my $userDB = $req->sessionInfo->{_cmbPasswordDB} || $req->sessionInfo->{_userDB};
unless ( $self->mods->{$userDB} ) {
$self->logger->error("No Password module available for $userDB");
return PE_ERROR;
}
return $self->mods->{$userDB}->$name( $req, @args );
}
sub confirm {
my ( $self, $req, @args ) = @_;
return $self->delegate( $req, "confirm", @args );
}
sub modifyPassword {
my ( $self, $req, @args ) = @_;
return $self->delegate( $req, "modifyPassword", @args );
}
1;

View File

@ -0,0 +1,130 @@
use Test::More;
use strict;
use IO::String;
require 't/test-lib.pm';
my $res;
my $maintests = 0;
my $client;
SKIP: {
eval { require DBI; require DBD::SQLite; };
if ($@) {
skip 'DBD::SQLite not found', $maintests;
}
$client = iniCmb();
# as dwho: login, change password, logout, login again
my $id = expectCookie( try('jkirk') );
expectPwChanged( pwchange( $id, "jkirk", "kobayashi" ) );
expectReject( try('jkirk') );
expectCookie( try( 'jkirk', 'kobayashi' ) );
# as dvador: login, change password, logout, login again
$id = expectCookie( try('dvador') );
expectPwChanged( pwchange( $id, "dvador", "darkside" ) );
expectReject( try('dvador') );
expectCookie( try( 'dvador', 'darkside' ) );
}
count($maintests);
clean_sessions();
done_testing( count() );
sub expectPwChanged {
my $res = shift;
my $j = expectJSON($res);
is( $j->{error}, 35, "PE_PASSWORD_OK" );
count(1);
}
sub try {
my $user = shift;
my $password = shift || $user;
my $s = "user=$user&password=$password";
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 pwchange {
my $id = shift;
my $old = shift;
my $new = shift;
my $s = "oldpassword=$old&newpassword=$new&confirmpassword=$new";
my $res;
ok(
$res = $client->_post(
'/', IO::String->new($s),
length => length($s),
cookie => "lemonldap=$id",
),
" Try to change password"
);
count(1);
return $res;
}
sub iniCmb {
my $userdb = tempdb();
my $dbh = DBI->connect("dbi:SQLite:dbname=$userdb");
$dbh->do('CREATE TABLE wars (user text,password text,name text)');
$dbh->do("INSERT INTO wars VALUES ('dvador','dvador','Anakin Skywalker')");
$dbh->do('CREATE TABLE trek (user text,password text,name text)');
$dbh->do("INSERT INTO trek VALUES ('jkirk','jkirk','James Tiberius Kirk')");
&Lemonldap::NG::Handler::Main::cfgNum( 0, 0 );
if (
my $res = LLNG::Manager::Test->new( {
ini => {
logLevel => 'error',
useSafeJail => 1,
authentication => 'Combination',
userDB => 'Same',
passwordDB => 'Combination',
restSessionServer => 1,
combination => '[Wars] or [Trek]',
combModules => {
Wars => {
for => 0,
type => 'DBI',
over => {
dbiAuthTable => 'wars',
}
},
Trek => {
for => 0,
type => 'DBI',
over => {
dbiAuthTable => 'trek',
}
},
},
dbiAuthChain => "dbi:SQLite:dbname=$userdb",
dbiAuthUser => '',
dbiAuthPassword => '',
dbiAuthLoginCol => 'user',
dbiAuthPasswordCol => 'password',
dbiAuthPasswordHash => '',
dbiExportedVars => { dbi => 'user' },
demoExportedVars => { demo => 'uid' },
}
}
)
)
{
return $res;
}
}

View File

@ -0,0 +1,188 @@
use Test::More;
use strict;
use IO::String;
BEGIN {
eval {
require 't/test-lib.pm';
require 't/smtp.pm';
};
}
my ( $res, $user, $pwd, $client );
SKIP: {
eval
'require Email::Sender::Simple;use GD::SecurityImage;use Image::Magick; require DBI; require DBD::SQLite;';
if ($@) {
skip 'Missing dependencies', 0;
}
$client = iniCmb();
# As dvador
# Check first password
expectCookie( try( 'dvador', 'dvador' ) );
# Get mail reset code
my $query = getMailQuery('dvador@wars.star');
# Set new password
expectPortalError( updatePassword( $query, "skywalker" ),
46, "Password update successful" );
# Check that new password works
expectCookie( try( 'dvador', 'skywalker' ) );
# As jkirk
# Check first password
expectCookie( try( 'jkirk', 'jkirk' ) );
# Get mail reset code
my $query = getMailQuery('jkirk@trek.star');
# Set new password
expectPortalError( updatePassword( $query, "kobayashi" ),
46, "Password update successful" );
# Check that new password works
expectCookie( try( 'jkirk', 'kobayashi' ) );
}
count(0);
clean_sessions();
done_testing( count() );
sub updatePassword {
my $query = shift;
my $newpassword = shift;
my $res;
ok(
$res = $client->_get(
'/resetpwd',
query => $query,
accept => 'text/html'
),
'Post mail token received by mail'
);
count(1);
( my $host, my $url, $query ) = expectForm( $res, '#', undef, 'token' );
ok( $res->[2]->[0] =~ /newpassword/s, ' Ask for a new password' );
count(1);
$query .= "&newpassword=$newpassword&confirmpassword=$newpassword";
# Post new password
ok(
$res = $client->_post(
'/resetpwd', IO::String->new($query),
length => length($query),
accept => 'text/html'
),
'Post new password'
);
count(1);
return $res;
}
sub getMailQuery {
my $mail = shift;
my $query = buildForm( { mail => $mail } );
ok(
$res = $client->_post(
'/resetpwd', IO::String->new($query),
length => length($query),
accept => 'text/html',
cookie => 'llnglanguage=fr',
),
'Post mail'
);
count(1);
ok( mail() =~ m#a href="http://auth.example.com/resetpwd\?(.*?)"#,
'Found link in mail' );
count(1);
return $1;
}
sub iniCmb {
my $userdb = tempdb();
my $dbh = DBI->connect("dbi:SQLite:dbname=$userdb");
$dbh->do(
'CREATE TABLE wars (user text,password text,email text, name text)');
$dbh->do(
"INSERT INTO wars VALUES ('dvador','dvador','dvador\@wars.star', 'Anakin Skywalker')"
);
$dbh->do(
'CREATE TABLE trek (user text,password text,email text, name text)');
$dbh->do(
"INSERT INTO trek VALUES ('jkirk','jkirk','jkirk\@trek.star', 'James Tiberius Kirk')"
);
&Lemonldap::NG::Handler::Main::cfgNum( 0, 0 );
if (
my $res = LLNG::Manager::Test->new( {
ini => {
logLevel => 'error',
useSafeJail => 1,
authentication => 'Combination',
userDB => 'Same',
passwordDB => 'Combination',
restSessionServer => 1,
portalDisplayResetPassword => 1,
requireToken => 0,
combination => '[Wars] or [Trek]',
combModules => {
Wars => {
for => 0,
type => 'DBI',
over => {
dbiAuthTable => 'wars',
}
},
Trek => {
for => 0,
type => 'DBI',
over => {
dbiAuthTable => 'trek',
}
},
},
dbiAuthChain => "dbi:SQLite:dbname=$userdb",
dbiAuthUser => '',
dbiAuthPassword => '',
dbiAuthLoginCol => 'user',
dbiAuthPasswordCol => 'password',
dbiMailCol => 'email',
dbiAuthPasswordHash => '',
dbiExportedVars => { cn => 'name', mail => 'email' },
captcha_mail_enabled => 0,
}
}
)
)
{
return $res;
}
}
sub try {
my $user = shift;
my $password = shift || $user;
my $s = "user=$user&password=$password";
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;
}