lemonldap-ng/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/Combination.pm
2022-02-03 11:20:47 +01:00

331 lines
8.5 KiB
Perl

package Lemonldap::NG::Portal::Auth::Combination;
use strict;
use Mouse;
use Lemonldap::NG::Common::Combination::Parser;
use Lemonldap::NG::Portal::Main::Constants qw(
PE_CONFIRM
PE_ERROR
PE_FIRSTACCESS
PE_FORMEMPTY
PE_PASSWORD_OK
PE_OK
);
use Scalar::Util 'weaken';
our $VERSION = '2.0.14';
extends 'Lemonldap::NG::Portal::Main::Auth';
with 'Lemonldap::NG::Portal::Lib::OverConf';
# PROPERTIES
has stackSub => ( is => 'rw' );
has wrapUserLogger => (
is => 'rw',
lazy => 1,
default => sub {
Lemonldap::NG::Portal::Lib::Combination::UserLogger->new(
$_[0]->userLogger );
}
);
# INITIALIZATION
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 = ( undef, undef );
my $mod = $self->conf->{combModules}->{$key};
unless ( $mod->{type} and defined $mod->{for} ) {
$self->error("Malformed combination module $key");
return 0;
}
# Override parameters
# "for" key can have 3 values:
# 0: this module will be used for Auth and UserDB
# 1: this module will be used for Auth only
# 2: this module will be used for UserDB only
# Load Auth module
if ( $mod->{for} < 2 ) {
$tmp[0] = $self->loadPlugin( "::Auth::$mod->{type}", $mod->{over} );
unless ( $tmp[0] ) {
$self->error("Unable to load Auth::$mod->{type}");
return 0;
}
$tmp[0]->{userLogger} = $self->wrapUserLogger;
weaken $tmp[0]->{userLogger};
}
# Load UserDB module
unless ( $mod->{for} == 1 ) {
$tmp[1] =
$self->loadPlugin( "::UserDB::$mod->{type}", $mod->{over} );
unless ( $tmp[1] ) {
$self->error("Unable to load UserDB::$mod->{type}");
return 0;
}
$tmp[1]->{userLogger} = $self->wrapUserLogger;
weaken $tmp[1]->{userLogger};
}
# Store modules as array
$mods{$key} = \@tmp;
}
# Compile expression
eval {
$self->stackSub(
Lemonldap::NG::Common::Combination::Parser->parse(
\%mods, $self->conf->{combination}
)
);
};
if ($@) {
$self->error("Bad combination: $@");
return 0;
}
return 1;
}
# Each first method must call getStack() to get the auth scheme available for
# the current user
## Auth steps
#############
sub extractFormInfo {
my $self = shift;
return $self->try( 0, 'extractFormInfo', @_ );
}
sub authenticate {
my $self = shift;
return $self->try( 0, 'authenticate', @_ );
}
sub setAuthSessionInfo {
my $self = shift;
return $self->try( 0, 'setAuthSessionInfo', @_ );
}
sub getDisplayType {
my $self = shift;
my ($req) = @_;
return $self->conf->{combinationForms}
if ( $self->conf->{combinationForms} );
my ( $nb, $stack ) = (
$req->data->{dataKeep}->{combinationTry},
$req->data->{combinationStack}
);
my $res = $stack->[$nb]->[0]->( 'getDisplayType', @_ );
return $res;
}
sub authLogout {
my $self = shift;
my ($req) = @_;
$self->getStack( $req, 'extractFormInfo' ) or return PE_ERROR;
# Avoid warning msg at first access
$req->userData->{_combinationTry} ||= 0;
my $sub =
$req->data->{combinationStack}->[ $req->userData->{_combinationTry} ]
->[0];
unless ($sub) {
$self->logger->warn(
"Condition changed between login and logout for "
. $req->user
. ", unable to select good backend" );
return PE_OK;
}
my ( $res, $name ) = $sub->( 'authLogout', @_ );
$self->logger->debug(qq'User disconnected using scheme "$name"');
return $res;
}
sub authFinish {
return PE_OK;
}
sub authForce {
return 0;
}
sub setSecurity {
my $self = shift;
my ($req) = @_;
$self->getStack( $req, 'extractFormInfo' ) or return;
eval {
$req->data->{combinationStack}
->[ $req->data->{dataKeep}->{combinationTry} ]->[0]
->( 'setSecurity', @_ );
};
$self->logger->debug($@) if ($@);
}
## UserDB steps
###############
# Note that UserDB::Combination uses the same object.
sub getUser {
my $self = shift;
return $self->try( 1, 'getUser', @_ );
}
sub findUser {
my $self = shift;
return $self->try( 1, 'findUser', @_ );
}
sub setSessionInfo {
my $self = shift;
return $self->try( 1, 'setSessionInfo', @_ );
}
sub setGroups {
my $self = shift;
return $self->try( 1, 'setGroups', @_ );
}
sub getStack {
my ( $self, $req, @steps ) = @_;
return $req->data->{combinationStack}
if ( $req->data->{combinationStack} );
my $stack = $req->data->{combinationStack} = $self->stackSub->( $req->env );
unless ($stack) {
$self->logger->error('No authentication scheme for this user');
}
@{ $req->data->{combinationSteps} } = ( @steps, @{ $req->steps } );
$req->data->{dataKeep}->{combinationTry} ||= 0;
return $stack;
}
# Main running method: launch the next scheme if the current fails
sub try {
my ( $self, $type, $subname, $req, @args ) = @_;
# Get available authentication schemes for this user if not done
unless ( defined $req->data->{combinationStack} ) {
$self->getStack( $req, $subname ) or return PE_ERROR;
}
my ( $nb, $stack ) = (
$req->data->{dataKeep}->{combinationTry},
$req->data->{combinationStack}
);
# If more than 1 scheme is available
my ( $res, $name );
unless ( ref $stack->[$nb]->[$type] ) {
$self->logger->error(
'Something went wrong in combination, unable to find any auth scheme (try == '
. ( $nb + 1 )
. ')' );
return PE_ERROR;
}
my $stop = 0;
if ( $nb < @$stack - 1 ) {
# TODO: change logLevel for userLog()
( $res, $name ) = $stack->[$nb]->[$type]->( $subname, $req, @args );
# On error, restart authentication with next scheme
unless ( $stop = $self->stop( $stack->[$nb]->[$type], $res ) ) {
$self->logger->info(qq'Scheme "$name" returned $res, trying next');
$req->data->{dataKeep}->{combinationTry}++;
$req->steps( [ @{ $req->data->{combinationSteps} } ] );
$req->continue(1);
return PE_OK;
}
}
else {
( $res, $name ) = $stack->[$nb]->[$type]->( $subname, $req, @args );
}
$req->sessionInfo->{ [ '_auth', '_userDB' ]->[$type] } = $name;
$req->sessionInfo->{_combinationTry} =
$req->data->{dataKeep}->{combinationTry};
if ( $res > 0 ) {
if ($stop) {
$self->userLogger->info(
"Combination stopped by plugin $name (code $res)");
}
elsif ( $res != PE_FIRSTACCESS ) {
$self->userLogger->warn( 'All schemes failed'
. ( $req->user ? ' for user ' . $req->user : '' ) . ' ('
. $req->address
. ')' );
}
}
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';
}
sub stop {
my ( $self, $mod, $res ) = @_;
return 1
if (
$res <= 0 # PE_OK
or $res == PE_CONFIRM
or $res == PE_PASSWORD_OK
# TODO: adding this may generate behavior change
#or $res == PE_FIRSTACCESS
#or $res == PE_FORMEMPTY
);
my ( $ret, $name );
$ret = $mod->( 'can', 'stop' );
if ($ret) {
eval { ( $ret, $name ) = $mod->( 'stop', $res ) };
if ($@) {
$self->logger->error(
"Optional ${name}::stop() method failed: " . $@ );
return 0;
}
}
return $ret;
}
package Lemonldap::NG::Portal::Lib::Combination::UserLogger;
# This logger rewrite "warn" to "notice"
sub new {
my ( $class, $realLogger ) = @_;
return bless { logger => $realLogger }, $class;
}
sub warn {
my ($auth) = caller(0);
$_[0]->{logger}->notice("Combination ($auth): $_[1]");
}
sub AUTOLOAD {
no strict;
return $_[0]->{logger}->$AUTOLOAD( $_[1] )
if ( $AUTOLOAD =~ /^(?:notice|debug|error|info)$/ );
}
1;