From a2ebaf31b17d286ac587f915f1d681a0d34d2988 Mon Sep 17 00:00:00 2001 From: Christophe Maudoux Date: Wed, 19 Jun 2019 18:13:17 +0200 Subject: [PATCH] WIP - AdminImpersonation skeleton (#1783) --- .../Lemonldap/NG/Common/Conf/DefaultValues.pm | 11 +- .../lib/Lemonldap/NG/Manager/Attributes.pm | 4 + .../Lemonldap/NG/Manager/Build/Attributes.pm | 5 + .../lib/Lemonldap/NG/Portal/Main/Menu.pm | 10 + .../lib/Lemonldap/NG/Portal/Main/Plugins.pm | 1 + .../NG/Portal/Plugins/AdminImpersonation.pm | 271 ++++++++++++++++++ .../bootstrap/adminImpersonation.tpl | 38 +++ .../site/templates/bootstrap/menu.tpl | 6 + 8 files changed, 341 insertions(+), 5 deletions(-) create mode 100644 lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/AdminImpersonation.pm create mode 100644 lemonldap-ng-portal/site/templates/bootstrap/adminImpersonation.tpl diff --git a/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/DefaultValues.pm b/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/DefaultValues.pm index 50890410a..86cb98b65 100644 --- a/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/DefaultValues.pm +++ b/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/DefaultValues.pm @@ -5,11 +5,12 @@ our $VERSION = '2.0.5'; sub defaultValues { return { - 'activeTimer' => 1, - 'ADPwdExpireWarning' => 0, - 'ADPwdMaxAge' => 0, - 'apacheAuthnLevel' => 4, - 'applicationList' => { + 'activeTimer' => 1, + 'adminImpersonationRule' => 1, + 'ADPwdExpireWarning' => 0, + 'ADPwdMaxAge' => 0, + 'apacheAuthnLevel' => 4, + 'applicationList' => { 'default' => { 'catname' => 'Default category', 'type' => 'category' diff --git a/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Attributes.pm b/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Attributes.pm index aaa0e4da4..bd0ef1ea0 100644 --- a/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Attributes.pm +++ b/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Attributes.pm @@ -257,6 +257,10 @@ sub attributes { 'default' => 1, 'type' => 'bool' }, + 'adminImpersonationRule' => { + 'default' => 1, + 'type' => 'boolOrExpr' + }, 'ADPwdExpireWarning' => { 'default' => 0, 'type' => 'int' diff --git a/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/Attributes.pm b/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/Attributes.pm index 3a5faa6f2..c391f3155 100644 --- a/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/Attributes.pm +++ b/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/Attributes.pm @@ -486,6 +486,11 @@ sub attributes { documentation => 'Skip session empty values', flags => 'p', }, + adminImpersonationRule => { + type => 'boolOrExpr', + default => 1, + documentation => 'adminImpersonation activation rule', + }, skipRenewConfirmation => { type => 'bool', default => 0, diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Menu.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Menu.pm index 6210b881a..a9d7b13fb 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Menu.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Menu.pm @@ -114,6 +114,16 @@ sub params { $self->p->_sfEngine->display2fRegisters( $req, $req->userData ); $self->logger->debug("Display 2fRegisters link") if $res{sfaManager}; + # Display adminImpersonation link only if allowed + my $impPlugin = $self->p->loadedModules->{ + 'Lemonldap::NG::Portal::Plugins::AdminImpersonation'}; + $res{adminImpersonation} = + $impPlugin + ? $impPlugin->displayAdminImpersonation( $req, $req->userData ) + : ''; + $self->logger->debug("Display AdminImpersonation link") + if $res{adminImpersonation}; + return %res; } diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Plugins.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Plugins.pm index 23386cbd9..906116676 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Plugins.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Plugins.pm @@ -27,6 +27,7 @@ our @pList = ( portalForceAuthn => '::Plugins::ForceAuthn', checkUser => '::Plugins::CheckUser', impersonationRule => '::Plugins::Impersonation', + adminImpersonationRule => '::Plugins::AdminImpersonation', ); ##@method list enabledPlugins diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/AdminImpersonation.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/AdminImpersonation.pm new file mode 100644 index 000000000..9fa65b648 --- /dev/null +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/AdminImpersonation.pm @@ -0,0 +1,271 @@ +package Lemonldap::NG::Portal::Plugins::AdminImpersonation; + +use strict; +use Mouse; +use Lemonldap::NG::Portal::Main::Constants + qw( PE_OK PE_BADCREDENTIALS PE_IMPERSONATION_SERVICE_NOT_ALLOWED PE_MALFORMEDUSER ); + +our $VERSION = '2.0.6'; + +extends + qw(Lemonldap::NG::Portal::Main::Plugin Lemonldap::NG::Portal::Lib::_tokenRule); + +# INITIALIZATION + +has rule => ( is => 'rw', default => sub { 1 } ); +has idRule => ( is => 'rw', default => sub { 1 } ); + +sub hAttr { + $_[0]->{conf}->{impersonationHiddenAttributes} . ' ' + . $_[0]->{conf}->{hiddenAttributes}; +} + +has ott => ( + is => 'rw', + lazy => 1, + default => sub { + my $ott = + $_[0]->{p}->loadModule('Lemonldap::NG::Portal::Lib::OneTimeToken'); + $ott->timeout( $_[0]->{conf}->{formTimeout} ); + return $ott; + } +); + +sub init { + my ($self) = @_; + my $hd = $self->p->HANDLER; + $self->addAuthRoute( impersonate => 'run', ['POST'] ); + $self->addAuthRoute( impersonate => 'display', ['GET'] ); + + # Parse activation rule + $self->logger->debug( + 'AdminImpersonation rule -> ' . $self->conf->{adminImpersonationRule} ); + my $rule = + $hd->buildSub( $hd->substitute( $self->conf->{adminImpersonationRule} ) ); + unless ($rule) { + $self->error( + 'Bad adminImpersonation rule -> ' . $hd->tsv->{jail}->error ); + return 0; + } + $self->rule($rule); + + # Parse identity rule + $self->logger->debug( "Impersonation identity rule -> " + . $self->conf->{impersonationIdRule} ); + $rule = + $hd->buildSub( $hd->substitute( $self->conf->{impersonationIdRule} ) ); + unless ($rule) { + $self->error( + "Bad impersonation identity rule -> " . $hd->tsv->{jail}->error ); + return 0; + } + $self->idRule($rule); + return 1; +} + +# RUNNING METHOD + +sub display { + my ( $self, $req ) = @_; + + # Display form + my $params = { + PORTAL => $self->conf->{portal}, + MAIN_LOGO => $self->conf->{portalMainLogo}, + LANGS => $self->conf->{showLanguages}, + MSG => 'impersonate', + ALERTE => 'alert-danger', + LOGIN => '', + SPOOFID => $self->conf->{adminImpersonationRule}, + TOKEN => ( + $self->ottRule->( $req, {} ) + ? $self->ott->createToken() + : '' + ) + }; + + return $self->p->sendHtml( $req, 'adminImpersonation', params => $params, ); +} + +sub run { + my ( $self, $req ) = @_; + my $statut = PE_OK; + my $spoofId = $req->param('spoofId') || ''; # Impersonation required ? + + unless ($spoofId) { + $self->logger->debug("No impersonation required"); + $req->mustRedirect(1); + return $self->p->do( $req, [ sub { PE_OK } ] ); + } + + unless ( $spoofId =~ /$self->{conf}->{userControl}/o ) { + $self->userLogger->error('Malformed spoofed Id'); + $self->logger->debug( + "AdminImpersonation tried with spoofed Id: $spoofId"); + $spoofId = $req->{user}; + $statut = PE_MALFORMEDUSER; + } + + # Check activation rule + if ($spoofId) { + $self->logger->debug("Spoof Id: $spoofId"); + unless ( $self->rule->( $req, $req->sessionInfo ) ) { + $self->userLogger->error( + 'adminImpersonation service not authorized'); + $spoofId = ''; + $statut = PE_IMPERSONATION_SERVICE_NOT_ALLOWED; + } + } + + # Fill spoof session + my ( $realSession, $spoofSession ) = ( {}, {} ); + $self->logger->debug("Rename real attributes..."); + my $spk = ''; + foreach my $k ( keys %{ $req->{userData} } ) { + if ( $self->{conf}->{impersonationSkipEmptyValues} ) { + next unless defined $req->{userData}->{$k}; + } + $spk = "$self->{conf}->{impersonationPrefix}$k"; + unless ( $self->hAttr =~ /\b$k\b/ + || $k =~ /^(?:_imp|token|_type)\w*\b/ ) + { + $realSession->{$spk} = $req->{userData}->{$k}; + $self->logger->debug("-> Store $k in realSession key: $spk"); + } + $self->logger->debug("Delete $k"); + delete $req->{userData}->{$k}; + } + + $spoofSession = $self->_userData( $req, $spoofId, $realSession ); + if ( $req->error ) { + if ( $req->error == PE_BADCREDENTIALS ) { + $statut = PE_BADCREDENTIALS; + } + else { + return $req->error; + } + } + + # Update spoof session + $self->logger->debug("Populating spoof session..."); + foreach (qw (_auth _userDB _session_id)) { + $self->logger->debug("Processing $_..."); + $spk = "$self->{conf}->{impersonationPrefix}$_"; + $spoofSession->{$_} = $realSession->{$spk}; + } + + # Merging SSO Groups and hGroups & dedup + $spoofSession->{groups} ||= ''; + $spoofSession->{hGroups} ||= {}; + if ( $self->{conf}->{impersonationMergeSSOgroups} ) { + $self->userLogger->warn("MERGING SSO groups and hGroups..."); + my $spg = "$self->{conf}->{impersonationPrefix}groups"; + my $sphg = "$self->{conf}->{impersonationPrefix}hGroups"; + my $separator = $self->{conf}->{multiValuesSeparator}; + + ## GROUPS + my @spoofGrps = split /\Q$separator/, $spoofSession->{groups}; + my @realGrps = split /\Q$separator/, $realSession->{$spg}; + + ## hGROUPS + $realSession->{$sphg} ||= {}; + + # Merge specified groups/hGroups only + unless ( $self->{conf}->{impersonationMergeSSOgroups} eq 1 ) { + my %SSOgroups = map { $_, 1 } split /\Q$separator/, + $self->{conf}->{impersonationMergeSSOgroups}; + + $self->logger->debug("Filtering specified groups/hGroups..."); + @realGrps = grep { exists $SSOgroups{$_} } @realGrps; + my %intersct = + map { + $realSession->{$sphg}->{$_} + ? ( $_, $realSession->{$sphg}->{$_} ) + : () + } keys %SSOgroups; + $realSession->{$sphg} = \%intersct; + } + + $self->logger->debug("Processing groups..."); + @spoofGrps = ( @spoofGrps, @realGrps ); + my %hash = map { $_, 1 } @spoofGrps; + $spoofSession->{groups} = join $separator, sort keys %hash; + + $self->logger->debug("Processing hGroups..."); + $spoofSession->{hGroups} = + { %{ $spoofSession->{hGroups} }, %{ $realSession->{$sphg} } }; + } + + # Main session + $self->p->updateSession( $req, $spoofSession ); + return $self->p->do( $req, [ sub { $statut } ] ); +} + +sub _userData { + my ( $self, $req, $spoofId, $realSession ) = @_; + my $realId = $req->{user}; + $req->{user} = $spoofId; + my $raz = 0; + + # Compute Macros and Groups with real and spoof sessions + $req->{sessionInfo} = {%$realSession}; + + # Search user in database + $req->steps( [ + 'getUser', 'setSessionInfo', + 'setMacros', 'setGroups', + 'setLocalGroups' + ] + ); + if ( my $error = $self->p->process($req) ) { + if ( $error == PE_BADCREDENTIALS ) { + $self->userLogger->warn( + 'Impersonation requested for an unvalid user (' + . $req->{user} + . ")" ); + } + $self->logger->debug("Process returned error: $error"); + $req->error($error); + $raz = 1; + } + + # Check identity rule if Impersonation required + if ( $realId ne $spoofId ) { + unless ( $self->idRule->( $req, $req->sessionInfo ) ) { + $self->userLogger->warn( + 'Impersonation requested for an unvalid user (' + . $req->{user} + . ")" ); + $self->logger->debug('Identity not authorized'); + $raz = 1; + } + } + + # Same real and spoof session - Compute Macros and Groups + if ($raz) { + $req->{sessionInfo} = {}; + $req->{sessionInfo} = {%$realSession}; + $req->{user} = $realId; + $req->steps( [ + 'getUser', 'setSessionInfo', + 'setMacros', 'setGroups', + 'setLocalGroups' + ] + ); + $self->logger->debug('Spoof session equal real session'); + $req->error(PE_BADCREDENTIALS); + if ( my $error = $self->p->process($req) ) { + $self->logger->debug("Process returned error: $error"); + $req->error($error); + } + } + return $req->{sessionInfo}; +} + +sub displayAdminImpersonation { + my ( $self, $req ) = @_; + return $self->rule->( $req, $req->userData ) + || $req->userData->{"$self->{conf}->{impersonationPrefix}_session_id"}; +} + +1; diff --git a/lemonldap-ng-portal/site/templates/bootstrap/adminImpersonation.tpl b/lemonldap-ng-portal/site/templates/bootstrap/adminImpersonation.tpl new file mode 100644 index 000000000..af3dc99c8 --- /dev/null +++ b/lemonldap-ng-portal/site/templates/bootstrap/adminImpersonation.tpl @@ -0,0 +1,38 @@ + + +
+ +
alert">
">
+
+
+ + " /> + + + + + +
+
+ + +
+ + + diff --git a/lemonldap-ng-portal/site/templates/bootstrap/menu.tpl b/lemonldap-ng-portal/site/templates/bootstrap/menu.tpl index 4f2169e6c..d12f05d26 100644 --- a/lemonldap-ng-portal/site/templates/bootstrap/menu.tpl +++ b/lemonldap-ng-portal/site/templates/bootstrap/menu.tpl @@ -67,6 +67,12 @@ sfaManager + + +