# Base package for LLNG portal plugins. It adds somme wrapper to # Lemonldap::NG::Handler::PSGI::Try (base of portal) package Lemonldap::NG::Portal::Main::Plugin; use strict; use Mouse; use HTML::Template; use Lemonldap::NG::Portal::Main::Constants qw( PE_OK PE_INFO PE_ERROR ); our $VERSION = '2.0.14'; extends 'Lemonldap::NG::Common::Module'; sub sendError { my $self = shift; return $self->p->sendError(@_); } sub sendJSONresponse { my $self = shift; return $self->p->sendJSONresponse(@_); } sub addAuthRoute { my $self = shift; return $self->_addRoute( 'addAuthRoute', @_ ); } sub addUnauthRoute { my $self = shift; return $self->_addRoute( 'addUnauthRoute', @_ ); } sub addAuthRouteWithRedirect { my $self = shift; return $self->_addRoute( 'addAuthRouteWithRedirect', @_ ); } sub _addRoute { my ( $self, $type, $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->$type( $word, $subName, $methods, $transform ); return $self; } sub loadTemplate { my $self = shift; return $self->p->loadTemplate(@_); } sub displayTemplate { my ( $self, $req, $template, $params ) = @_; $self->logger->debug("Return $template template"); $req->info( $self->loadTemplate( $req, $template, params => $params ) ); return PE_INFO; } sub createNotification { my ( $self, $req, $uid, $date, $ref, $title, $msg ) = @_; my $notifEngine = $self->p->loadedModules->{ 'Lemonldap::NG::Portal::Plugins::Notifications'}; $self->logger->debug("Loading Notifications plugin..."); return PE_ERROR unless $notifEngine; # Prepare notification my $content = $self->conf->{oldNotifFormat} ? '_title__msg_' : '[{"uid":"_uid_","date":"_date_","title":"_title_","reference":"_ref_","text":"_msg_"}]'; $content =~ s/_uid_/$uid/; $content =~ s/_date_/$date/; $content =~ s/_ref_/$ref/; $content =~ s/_title_/$title/; $content =~ s/_msg_/$msg/; if ( $notifEngine->module->notifObject->newNotification($content) ) { $self->logger->debug("Notification $ref successfully created"); $self->userLogger->notice( "Notification $ref / $date successfully created for $uid"); return PE_OK; } else { $self->logger->debug("Notification $ref NOT created!"); return PE_ERROR; } } sub canUpdateSfa { my ( $self, $req, $action ) = @_; my $user = $req->userData->{ $self->conf->{whatToTrace} }; my $msg = undef; # Test action if ( $action && $action eq 'delete' ) { my $module = lc ref $self; $module = ( $module =~ /2f::register::(\w+)$/ )[0]; $module =~ s/2f//; $self->logger->debug("$user request to delete ${module}2f device"); if ( $self->{conf}->{"${module}2fAuthnLevel"} && $req->userData->{authenticationLevel} < $self->{conf}->{"${module}2fAuthnLevel"} ) { $self->userLogger->warn( "$user request to delete ${module}2f device rejected due to insufficient authentication level!" ); $self->logger->debug( "authLevel: $req->{userData}->{authenticationLevel} < requiredLevel: " . $self->{conf}->{"${module}2fAuthnLevel"} ); $msg = 'notAuthorizedAuthLevel'; } } # Test if impersonation is in progress if ( !$msg && $self->conf->{impersonationRule} ) { $self->logger->debug('Impersonation plugin is enabled'); if ( $req->userData->{"$self->{conf}->{impersonationPrefix}_user"} && $req->userData->{"$self->{conf}->{impersonationPrefix}_user"} ne $req->userData->{_user} ) { $self->userLogger->warn( "Impersonation in progress! $user is not allowed to update 2FA." ); $msg = 'notAuthorized'; } } # Test if contextSwitching is in progress if ( !$msg && $self->conf->{contextSwitchingRule} ) { $self->logger->debug('ContextSwitching plugin is enabled'); if ( $req->userData->{ "$self->{conf}->{contextSwitchingPrefix}_session_id"} && !$self->conf->{contextSwitchingAllowed2fModifications} ) { $self->userLogger->warn( "ContextSwitching in progress! $user is not allowed to update 2FA." ); $msg = 'notAuthorized'; } } $self->logger->debug("$user is allowed to update 2FA") unless $msg; return $msg; } sub addSessionDataToRemember { my ( $self, $newData ) = @_; for my $sessionAttr ( keys %{ $newData || {} } ) { $self->p->pluginSessionDataToRemember->{$sessionAttr} = $newData->{$sessionAttr}; } return; } 1; __END__ =pod =encoding utf8 =head1 NAME Lemonldap::NG::Portal::Main::Plugin - Base class for L modules I<(plugins, authentication modules,...)>. =head1 SYNOPSIS package Lemonldap::NG::Portal::My::Plugin; use Mouse; extends 'Lemonldap::NG::Portal::Main::Plugin'; use constant beforeAuth => 'verifyIP'; sub init { my ($self) = @_; $self->addUnauthRoute( mypath => 'hello', [ 'GET', 'PUT' ] ); $self->addAuthRoute( mypath => 'welcome', [ 'GET', 'PUT' ] ); return 1; } sub verifyIP { my ($self, $req) = @_; return PE_ERROR if($req->address !~ /^10/); return PE_OK; } sub hello { my ($self, $req) = @_; ... return $self->p->sendJSONresponse($req, { hello => 1 }); } sub welcome { my ($self, $req) = @_; ... return $self->p->sendHtml($req, 'template', params => { WELCOME => 1 }); } =head1 DESCRIPTION Lemonldap::NG::Portal::Main::Plugin provides many methods to easily write Lemonldap::NG addons. init() is called for each plugin. If a plugin initialization fails (init() returns 0), the portal responds a 500 status code for each request. =head1 Writing plugins Custom plugins can be inserted in portal by declaring them in C file, section C<[portal]>, key C: [portal] customPlugins = ::My::Plugin1, ::My::Plugin2 Plugins must be valid packages well found in C<@INC>. =head2 Plugin entry points =head3 Entry point based on PATH_INFO Plugins can declare unauthRoutes/authRoutes during initialization (= /path/info). Methods declared in this way must be declared in the plugin class. They will be called with $req argument. $req is the HTTP request. (See L). These methods must return a valid L response. You can also use sendJSONresponse() or sendHtml() methods (see L). Example: sub init { my ($self) = @_; $self->addUnauthRoute( mypath => 'hello', [ 'GET', 'PUT' ] ); $self->addAuthRoute( mypath => 'welcome', [ 'GET', 'PUT' ] ); return 1; } sub hello { my ($self, $req) = @_; ... return $self->p->sendJSONresponse($req, { hello => 1 }); } sub welcome { my ($self, $req) = @_; ... return $self->p->sendHtml($req, 'template', params => { WELLCOME => 1 }); } If you want to get a "protected application" behavior, you can use B. This methods calls B with given arguments and build a "unAuth" route that build a redirection after authentication. =head3 Entry point in auth process A plugin which wants to be inserted in authentication process has to declare constants set with method name to run. Following entry points are available. =over =item C: method called before authentication process =item C: method called after authentication and before setting C provisionning =item C: method called after C provisionning I<(macros, groups,...)>. This entry point is called after 'storeHistory' if login process fails and before 'validSession' if succeeds. =item C: method called when session is validated (after cookie build) =item C: method called when user click on "cancel" during auth process =item C: method called for already authenticated users =item C: method called before logout =back B: methods inserted so must return a PE_* constant. See Lemonldap::NG::Portal::Main::Constants. =head4 Advanced entry points These entry points are not stored in C<$req-Estep> but launched on the fly: =over =item C: hash ref that give methods to call after given main method is called. Example: use constant afterSub => { getUser => 'mysub', } sub mysub { my ( $self ,$req ) = @_; # Do something return PE_OK; } =item C: hash ref that give methods to call instead of given main method. Example: use constant aroundSub => { getUser => 'mysub', }; sub mysub { my ( $self, $sub, $req ) = @_; # Do something before my $ret = $sub->($req); # Do something after return $ret; } Do not launch "getUser" but use the given C<$sub>. This permits multiple plugins to use "aroundSub" in the same time. =item C: hash ref that gives methods to call when a hook is triggered in the LemonLDAP::NG code. Example: use constant hook => { oidcGenerateIDToken => 'addClaimToIDToken' }; sub addClaimToIDToken { my ( $self, $req, $payload, $rp ) = @_; $payload->{"id_token_hook"} = 1; return PE_OK; } =back =head1 LOGGING Logging is provided by $self->logger and $self->userLogger. The following rules must be applied: =over =item logger->debug: technical debugging messages =item logger->info: simple technical information =item logger->notice: technical information that could interest administrators =item logger->warn: technical warning =item logger->error: error that must be reported to administrator =item userLogger->info: simple information about user's action =item userLogger->notice: information that may be registered (auth success,...) =item userLogger->warn: bad action of a user (auth failure). Auth/Combination transform it to "info" when another authentication scheme is available =item userLogger->error: bad action of a user that must be reported, (even if another backend is available with Combination) =back =head1 SEE ALSO L =head2 OTHER POD FILES =over =item Writing an authentication module: L =item Writing a UserDB module: L =item Writing a second factor module: L =item Writing an issuer module: L =item Writing another plugin: L =item Request object: L =item Adding parameters in the manager: L =back =head1 AUTHORS =over =item LemonLDAP::NG team L =back =head1 BUG REPORT Use OW2 system to report bug or ask for features: L =head1 DOWNLOAD Lemonldap::NG is available at L =head1 COPYRIGHT AND LICENSE See COPYING file for details. This library is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see L. =cut