Merge branch 'manager-plugins' into 'v2.0'

Change Manager framework to plugins

See merge request lemonldap-ng/lemonldap-ng!124
This commit is contained in:
Xavier Guimard 2020-02-04 21:04:21 +01:00
commit 663a7759f8
12 changed files with 199 additions and 86 deletions

View File

@ -24,6 +24,9 @@ extends 'Lemonldap::NG::Common::Conf::AccessLib',
has csp => ( is => 'rw' );
has loadedPlugins => ( is => 'rw', default => sub { [] } );
has hLoadedPlugins => ( is => 'rw', default => sub { {} } );
## @method boolean init($args)
# Launch initialization method
#
@ -52,32 +55,50 @@ sub init {
return 0;
}
my $conf = $self->confAcc->getConf;
$conf->{$_} = $args->{$_} foreach(keys %$args);
$self->{enabledModules} ||= "conf, sessions, notifications, 2ndFA, api";
my @links;
my @enabledModules =
map { push @links, $_; "Lemonldap::NG::Manager::" . ucfirst($_) }
map {
my @res = ( "Lemonldap::NG::Manager::" . ucfirst($_) );
if ( my $tmp = $self->loadPlugin( @res, $conf ) ) {
$self->logger->debug("Plugin $_ loaded");
push @links, $_;
push @{ $self->loadedPlugins }, $tmp;
$self->hLoadedPlugins->{$_} = $tmp;
}
else {
$self->logger->error("Unable to load $_, skipping");
@res = ();
}
(@res);
}
split( /[,\s]+/, $self->{enabledModules} );
extends 'Lemonldap::NG::Handler::PSGI::Router', @enabledModules;
my @working;
my $conf = $self->confAcc->getConf;
unless (@enabledModules) {
$self->logger->error('No plugins loaded, aborting');
return 0;
}
unless ($conf) {
require Lemonldap::NG::Manager::Conf::Zero;
$conf = Lemonldap::NG::Manager::Conf::Zero::zeroConf();
}
for ( my $i = 0 ; $i < @enabledModules ; $i++ ) {
my $mod = $enabledModules[$i];
no strict 'refs';
if ( &{"${mod}::addRoutes"}( $self, $conf ) ) {
$self->logger->debug("Module $mod enabled");
push @working, $mod;
}
else {
$links[$i] = undef;
$self->logger->error(
"Module $mod can not be enabled: " . $self->error );
}
}
return 0 unless (@working);
# TODO: -> loadPlugin
#for ( my $i = 0 ; $i < @enabledModules ; $i++ ) {
# my $mod = $enabledModules[$i];
# no strict 'refs';
# if ( &{"${mod}::addRoutes"}( $self, $conf ) ) {
# $self->logger->debug("Module $mod enabled");
# push @working, $mod;
# }
# else {
# $links[$i] = undef;
# $self->logger->error(
# "Module $mod can not be enabled: " . $self->error );
# }
#}
$self->addRoute( links => 'links', ['GET'] );
$self->addRoute( 'psgi.js' => 'sendJs', ['GET'] );
@ -88,13 +109,14 @@ sub init {
);
# Avoid restricted users to access configuration by default route
my $defaultMod = $self->{defaultModule} || 'conf';
my $defaultMod = $self->{defaultModule} =
$self->{defaultModule} || $enabledModules[0];
$self->logger->debug("Default module -> $defaultMod");
my ($index) =
grep { $working[$_] =~ /::$defaultMod$/i } ( 0 .. $#working );
$index //= $#working;
grep { $enabledModules[$_] eq $defaultMod } ( 0 .. $#enabledModules );
$index //= 0;
$self->logger->debug("Default index -> $index");
$self->defaultRoute( $working[$index]->defaultRoute );
$self->defaultRoute( $self->loadedPlugins->[$index]->defaultRoute );
# Find out more glyphicones at https://www.w3schools.com/icons/bootstrap_icons_glyphicons.asp
my $linksIcons = {
@ -110,7 +132,7 @@ sub init {
next unless ( defined $links[$i] );
push @{ $self->links },
{
target => $enabledModules[$i]->defaultRoute,
target => $self->loadedPlugins->[$i]->defaultRoute,
title => $links[$i],
icon => $linksIcons->{ $links[$i] }
};
@ -141,13 +163,14 @@ sub init {
sub tplParams {
my ( $self, $req ) = @_;
my $res = $self->brwRule->( $req, $req->{userData} ) || 0;
my $res = eval { $self->hLoadedPlugins->{viewer}->brwRule->( $req, $req->{userData} ) } || 0;
return ( VERSION => $VERSION, ALLOWBROWSER => $res );
}
sub javascript {
my ( $self, $req ) = @_;
my $res = $self->diffRule->( $req, $req->{userData} ) || 0;
my $res = eval { $self->hLoadedPlugins->{viewer}->diffRule->( $req, $req->{userData} )} || 0;
print STDERR $@ if $@;
my $impPrefix = $self->{impersonationPrefix};
my $ttl = $self->{timeout} || 72000;
@ -176,6 +199,32 @@ sub sendHtml {
return $res;
}
sub loadPlugin {
my ( $self, $plugin, $conf ) = @_;
unless ($plugin) {
require Carp;
Carp::confess('Calling loadPugin without arg !');
}
my $obj;
$plugin = "Lemonldap::NG::Manager$plugin" if ( $plugin =~ /^::/ );
eval "require $plugin";
if ($@) {
$self->logger->error("$plugin load error: $@");
return 0;
}
eval {
$obj = $plugin->new( { p => $self, conf => $conf } );
$self->logger->debug("Module $plugin loaded");
};
if ($@) {
$self->error("Unable to build $plugin object: $@");
return 0;
}
( $obj and $obj->init($conf) ) or return 0;
return $obj;
}
1;
__END__

View File

@ -13,7 +13,8 @@ use Lemonldap::NG::Common::Conf::ReConstants;
use feature 'state';
extends 'Lemonldap::NG::Common::Conf::AccessLib',
extends 'Lemonldap::NG::Manager::Plugin',
'Lemonldap::NG::Common::Conf::AccessLib',
'Lemonldap::NG::Common::Session::REST';
our $VERSION = '2.0.2';
@ -24,7 +25,7 @@ our $VERSION = '2.0.2';
use constant defaultRoute => '2ndfa.html';
sub addRoutes {
sub init {
my ( $self, $conf ) = @_;
# Remote Procedure are defined in Lemonldap::NG::Common::Session::REST
@ -46,6 +47,7 @@ sub addRoutes {
$self->{multiValuesSeparator} ||= '; ';
$self->{hiddenAttributes} //= "_password";
$self->{TOTPCheck} = $self->{U2FCheck} = $self->{UBKCheck} = '1';
return 1;
}
###################

View File

@ -5,7 +5,8 @@ use 5.10.0;
use utf8;
use Mouse;
extends 'Lemonldap::NG::Common::Conf::RESTServer',
extends 'Lemonldap::NG::Manager::Plugin',
'Lemonldap::NG::Common::Conf::RESTServer',
'Lemonldap::NG::Common::Session::REST';
use Lemonldap::NG::Manager::Api::2F;
@ -20,7 +21,7 @@ our $VERSION = '2.0.7';
use constant defaultRoute => 'api.html';
sub addRoutes {
sub init {
my ( $self, $conf ) = @_;
# HTML template
@ -148,6 +149,7 @@ sub addRoutes {
$self->{multiValuesSeparator} ||= '; ';
$self->{hiddenAttributes} //= "_password";
$self->{TOTPCheck} = $self->{U2FCheck} = $self->{UBKCheck} = '1';
return 1;
}
1;

View File

@ -76,7 +76,7 @@ sub set {
}
}
require Clone;
my $new = Clone::clone( $self->mgr->currentConf );
my $new = Clone::clone( $self->mgr->hLoadedPlugins->{conf}->currentConf );
foreach my $key ( keys %pairs ) {
$self->_setKey( $new, $key, $pairs{$key} );
}
@ -101,7 +101,7 @@ sub addKey {
push @list, [ $root, $newKey, $value ];
}
require Clone;
my $new = Clone::clone( $self->mgr->currentConf );
my $new = Clone::clone( $self->mgr->hLoadedPlugins->{conf}->currentConf );
foreach my $el (@list) {
my @path = split $sep, $el->[0];
if ( $#path == 0 ) {
@ -142,7 +142,7 @@ sub delKey {
push @list, [ $root, $key ];
}
require Clone;
my $new = Clone::clone( $self->mgr->currentConf );
my $new = Clone::clone( $self->mgr->hLoadedPlugins->{conf}->currentConf );
foreach my $el (@list) {
my @path = split $sep, $el->[0];
if ( $#path == 0 ) {
@ -237,7 +237,7 @@ sub _getKey {
warn "Malformed key $base";
return ();
}
my $value = $self->mgr->getConfKey( $self->req, $base, noCache => 1 );
my $value = $self->mgr->hLoadedPlugins->{conf}->getConfKey( $self->req, $base, noCache => 1 );
if ( $self->req->error ) {
die $self->req->error;
}
@ -273,7 +273,7 @@ sub _save {
require Lemonldap::NG::Manager::Conf::Parser;
my $parser = Lemonldap::NG::Manager::Conf::Parser->new( {
newConf => $new,
refConf => $self->mgr->currentConf,
refConf => $self->mgr->hLoadedPlugins->{conf}->currentConf,
req => $self->req
}
);
@ -310,7 +310,7 @@ sub _save {
"CLI: Configuration $s has been saved by $new->{cfgAuthor}");
$self->logger->info("CLI: Configuration $s saved");
print STDERR "Saved under number $s\n";
$parser->{status} = [ $self->mgr->applyConf($new) ];
$parser->{status} = [ $self->mgr->hLoadedPlugins->{conf}->applyConf($new) ];
}
else {
$self->logger->error("CLI: Configuration not saved!");

View File

@ -17,7 +17,8 @@ use URI::URL;
use feature 'state';
extends 'Lemonldap::NG::Common::Conf::RESTServer';
extends 'Lemonldap::NG::Manager::Plugin',
'Lemonldap::NG::Common::Conf::RESTServer';
our $VERSION = '2.0.7';
@ -28,35 +29,10 @@ our $VERSION = '2.0.7';
use constant defaultRoute => 'manager.html';
has ua => ( is => 'rw' );
has diffRule => ( is => 'rw', default => sub { 0 } );
has brwRule => ( is => 'rw', default => sub { 0 } );
sub addRoutes {
sub init {
my ( $self, $conf ) = @_;
$self->ua( Lemonldap::NG::Common::UserAgent->new($conf) );
my $hd = "Lemonldap::NG::Handler::PSGI::Main";
# Parse Diff activation rule
$self->logger->debug(
"Diff activation rule -> " . ( $self->{viewerAllowDiff} // 0 ) );
my $rule = $hd->buildSub( $hd->substitute( $self->{viewerAllowDiff} ) );
unless ($rule) {
$self->error(
"Bad Diff activation rule -> " . $hd->tsv->{jail}->error );
return 0;
}
$self->diffRule($rule);
# Parse Browser activation rule
$self->logger->debug(
"Browser activation rule -> " . ( $self->{viewerAllowBrowser} // 0 ) );
$rule = $hd->buildSub( $hd->substitute( $self->{viewerAllowBrowser} ) );
unless ($rule) {
$self->error(
"Bad Browser activation rule -> " . $hd->tsv->{jail}->error );
return 0;
}
$self->brwRule($rule);
# HTML template
$self->addRoute( 'manager.html', undef, ['GET'] )
@ -95,6 +71,7 @@ sub addRoutes {
# Url loader
->addRoute( 'prx', undef, ['POST'] );
return 1;
}
# 35 - New RSA key pair on demand
@ -268,7 +245,7 @@ sub newConf {
}
}
if ( $res->{result} ) {
if ( $self->{demoMode} ) {
if ( $self->p->{demoMode} ) {
$res->{message} = '__demoModeOn__';
}
else {
@ -279,7 +256,7 @@ sub newConf {
unless ( @{ $parser->{needConfirmation} } && !$args{force} );
if ( $s > 0 ) {
$self->userLogger->notice(
'User ' . $self->userId($req) . " has stored conf $s" );
'User ' . $self->p->userId($req) . " has stored conf $s" );
$res->{result} = 1;
$res->{cfgNum} = $s;
if ( my $status = $self->applyConf( $parser->newConf ) ) {
@ -291,7 +268,7 @@ sub newConf {
else {
$self->userLogger->notice(
'Saving attempt rejected, asking for confirmation to '
. $self->userId($req) );
. $self->p->userId($req) );
$res->{result} = 0;
if ( $s == CONFIG_WAS_CHANGED ) {
$res->{needConfirm} = 1;
@ -324,7 +301,7 @@ sub newRawConf {
}
my $res = {};
if ( $self->{demoMode} ) {
if ( $self->p->{demoMode} ) {
$res->{message} = '__demoModeOn__';
}
else {
@ -333,14 +310,14 @@ sub newRawConf {
my $s = $self->confAcc->saveConf( $new, force => 1 );
if ( $s > 0 ) {
$self->userLogger->notice(
'User ' . $self->userId($req) . " has stored (raw) conf $s" );
'User ' . $self->p->userId($req) . " has stored (raw) conf $s" );
$res->{result} = 1;
$res->{cfgNum} = $s;
}
else {
$self->userLogger->notice(
'Raw saving attempt rejected, asking for confirmation to '
. $self->userId($req) );
. $self->p->userId($req) );
$res->{result} = 0;
$res->{needConfirm} = 1 if ( $s == CONFIG_WAS_CHANGED );
$res->{message} .= '__needConfirmation__';
@ -359,7 +336,7 @@ sub applyConf {
my $status;
# 1 Apply conf locally
$self->api->checkConf();
$self->p->api->checkConf();
# Get apply section values
my %reloadUrls =

View File

@ -1159,8 +1159,6 @@ sub _unitTest {
if ( $key =~ /^(?:$simpleHashKeys|$doubleHashKeys)$/o
or $attr->{type} =~ /Container$/ )
{
my $keyMsg = $attr->{keyMsgFail} // $type->{keyMsgFail};
my $msg = $attr->{msgFail} // $type->{msgFail};
$res = 0
unless (
$self->_execTest( {

View File

@ -13,7 +13,8 @@ require Lemonldap::NG::Common::Notifications;
use feature 'state';
extends 'Lemonldap::NG::Common::Conf::AccessLib';
extends 'Lemonldap::NG::Manager::Plugin',
'Lemonldap::NG::Common::Conf::AccessLib';
our $VERSION = '2.0.7';
@ -26,7 +27,7 @@ has notifFormat => ( is => 'rw' );
use constant defaultRoute => 'notifications.html';
sub addRoutes {
sub init {
my ( $self, $conf ) = @_;
if ( $conf->{oldNotifFormat} ) {
@ -74,6 +75,7 @@ sub addRoutes {
{ done => { ':notificationId' => 'deleteDoneNotification' } },
['DELETE']
);
return 1;
}
sub setNotifAccess {

View File

@ -0,0 +1,51 @@
package Lemonldap::NG::Manager::Plugin;
use strict;
use Mouse;
our $VERSION = '2.0.6';
extends 'Lemonldap::NG::Common::Module';
has _confAcc => (
is => 'rw',
lazy => 1,
default => sub { return $_[0]->p->{_confAcc} },
);
sub sendError {
my $self = shift;
return $self->p->sendError(@_);
}
sub sendJSONresponse {
my $self = shift;
return $self->p->sendJSONresponse(@_);
}
sub addRoute {
my ( $self, $word, $subName, $methods, $transform ) = @_;
$transform //= sub {
my ($sub) = @_;
if ( ref $sub ) {
return sub {
shift;
return $sub->( $self, @_ );
}
}
else {
return sub {
shift;
return $self->$sub(@_);
}
}
};
$self->p->addRoute( $word, $subName, $methods, $transform );
return $self;
}
sub loadTemplate {
my $self = shift;
return $self->p->loadTemplate(@_);
}
1;

View File

@ -14,7 +14,8 @@ use Lemonldap::NG::Common::IPv6;
use feature 'state';
extends 'Lemonldap::NG::Common::Conf::AccessLib',
extends 'Lemonldap::NG::Manager::Plugin',
'Lemonldap::NG::Common::Conf::AccessLib',
'Lemonldap::NG::Common::Session::REST';
our $VERSION = '2.0.4';
@ -25,7 +26,7 @@ our $VERSION = '2.0.4';
use constant defaultRoute => 'sessions.html';
sub addRoutes {
sub init {
my ( $self, $conf ) = @_;
# HTML template
@ -55,6 +56,7 @@ sub addRoutes {
$self->{multiValuesSeparator} ||= '; ';
$self->{impersonationPrefix} = $conf->{impersonationPrefix} || 'real_';
$self->{hiddenAttributes} //= "_password";
return 1;
}
#######################
@ -98,7 +100,7 @@ sub sessions {
or return $self->sendError( $req, undef, 400 );
my $params = $req->parameters();
my $type = delete $params->{sessionType};
$type = $type eq 'global' ? 'SSO' : ucfirst($type);
$type = $type eq 'global' ? 'SSO' : ucfirst($type);
$type = $type eq 'Offline' ? 'OIDCI' : ucfirst($type);
my $res;

View File

@ -11,6 +11,9 @@ use feature 'state';
extends 'Lemonldap::NG::Manager::Conf';
has diffRule => ( is => 'rw', default => sub { 0 } );
has brwRule => ( is => 'rw', default => sub { 0 } );
our $VERSION = '2.0.6';
#############################
@ -21,11 +24,36 @@ use constant defaultRoute => 'viewer.html';
has ua => ( is => 'rw' );
sub addRoutes {
sub init {
my ( $self, $conf ) = @_;
$self->ua( Lemonldap::NG::Common::UserAgent->new($conf) );
my $hd = "Lemonldap::NG::Handler::PSGI::Main";
my $hiddenKeys = $self->{viewerHiddenKeys} || '';
# Parse Diff activation rule
$conf->{viewerAllowDiff} //= 0;
$self->logger->debug(
"Diff activation rule -> " . ( $conf->{viewerAllowDiff} ) );
my $rule = $hd->buildSub( $hd->substitute( $conf->{viewerAllowDiff} ) );
unless ($rule) {
$self->logger->error(
"Bad Diff activation rule -> " . $hd->tsv->{jail}->error );
return 0;
}
$self->diffRule($rule);
# Parse Browser activation rule
$conf->{viewerAllowBrowser} //= 0;
$self->logger->debug(
"Browser activation rule -> " . ( $conf->{viewerAllowBrowser} ) );
$rule = $hd->buildSub( $hd->substitute( $conf->{viewerAllowBrowser} ) );
unless ($rule) {
$self->logger->error(
"Bad Browser activation rule -> " . $hd->tsv->{jail}->error );
return 0;
}
$self->brwRule($rule);
my $hiddenKeys = $conf->{viewerHiddenKeys} || '';
my @enabledKeys = ();
my @keys = qw(virtualHosts samlIDPMetaDataNodes samlSPMetaDataNodes
applicationList oidcOPMetaDataNodes oidcRPMetaDataNodes
@ -67,6 +95,7 @@ sub addRoutes {
# Other keys
->addRoute( view => { ':cfgNum' => { '*' => 'viewKey' } }, ['GET'] );
return 1;
}
sub getConfByNum {

View File

@ -25,7 +25,7 @@ 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($resBody);
ok(
@{ $resBody->{details}->{__warnings__} } == 2,
$resBody->{details}->{__warnings__} and @{ $resBody->{details}->{__warnings__} } == 2,
'JSON response contains 2 warnings'
) or print STDERR Dumper($resBody);
@ -38,7 +38,7 @@ foreach my $i ( 0 .. 1 ) {
}
ok(
@{ $resBody->{details}->{__changes__} } == 24,
$resBody->{details}->{__changes__} and @{ $resBody->{details}->{__changes__} } == 24,
'JSON response contains 24 changes'
) or print STDERR Dumper($resBody);
ok(

View File

@ -9,6 +9,9 @@ require 't/test-lib.pm';
my $struct = 't/jsonfiles/70-diff.json';
# Remove new conf
unlink 't/conf/lmConf-2.json';
sub body {
return IO::File->new( $struct, 'r' );
}
@ -23,13 +26,13 @@ $res = &client->jsonResponse('/view/1/portalDisplayLogout');
ok( $res->{value} eq '_Hidden_', 'Key is hidden' )
or explain( $res, 'value => "_Hidden_"' );
$res = &client->jsonResponse('/view/1/samlIDPMetaDataNodes');
ok( $res->{value} eq '_Hidden_', 'Key is hidden' )
ok( ref($res) eq 'HASH' and $res->{value} eq '_Hidden_', 'Key is hidden' )
or explain( $res, 'value => "_Hidden_"' );
count(2);
# Try to display latest conf
$res = &client->jsonResponse('/view/latest');
ok( $res->{cfgNum} eq '1', 'Latest conf loaded' );
ok( $res->{cfgNum} eq '1', 'Latest conf loaded' ) or explain($res,"cfgNum => 1");
count(1);
ok(
@ -57,9 +60,7 @@ $res = &client->jsonResponse('/view/diff/1/2');
# ok( $res->[1]->{captcha_login_enabled} eq '1', 'Key found' );
ok( $res->[1]->{captcha_mail_enabled} eq '0', 'Key found' );
ok( 6 == keys %{ $res->[1] }, 'Right number of keys found' )
or print STDERR Dumper($res);
count(2);
count(1);
# Try to display previous conf
$res = &client->jsonResponse('/view/1');
@ -68,7 +69,7 @@ ok( $res->{cfgNum} eq '1', 'Browser is allowed' )
count(1);
# Remove new conf
`rm -rf t/conf/lmConf-2.json`;
unlink 't/conf/lmConf-2.json';
done_testing( count() );