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:
commit
402a39a176
|
@ -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
|
||||
--------------
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
@ -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
|
||||
|
|
|
@ -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;
|
130
lemonldap-ng-portal/t/36-Combination-Password.t
Normal file
130
lemonldap-ng-portal/t/36-Combination-Password.t
Normal 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;
|
||||
}
|
||||
}
|
188
lemonldap-ng-portal/t/43-MailPasswordReset-Combination.t
Normal file
188
lemonldap-ng-portal/t/43-MailPasswordReset-Combination.t
Normal 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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user