From 810865dcbd3c84b2fd6f8289afb7ac673bec44d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Xavier=20Deltombe?= Date: Tue, 10 Jan 2012 19:54:30 +0000 Subject: [PATCH] Implement login history (LEMONLDAP-389) --- modules/lemonldap-ng-common/lemonldap-ng.ini | 1 + .../Lemonldap/NG/Common/Conf/Serializer.pm | 1 + .../lib/Lemonldap/NG/Manager/_Struct.pm | 27 ++- .../lib/Lemonldap/NG/Manager/_i18n.pm | 12 +- .../example/skins/common/calendar.png | Bin 0 -> 572 bytes .../example/skins/impact/menu.tpl | 19 ++ .../example/skins/pastel/css/styles.css | 8 +- .../example/skins/pastel/menu.tpl | 23 ++- .../lib/Lemonldap/NG/Portal/Menu.pm | 14 +- .../lib/Lemonldap/NG/Portal/Simple.pm | 162 ++++++++++++------ .../lib/Lemonldap/NG/Portal/_i18n.pm | 3 + 11 files changed, 205 insertions(+), 65 deletions(-) create mode 100644 modules/lemonldap-ng-portal/example/skins/common/calendar.png diff --git a/modules/lemonldap-ng-common/lemonldap-ng.ini b/modules/lemonldap-ng-common/lemonldap-ng.ini index c618e7bac..89ca3971c 100644 --- a/modules/lemonldap-ng-common/lemonldap-ng.ini +++ b/modules/lemonldap-ng-common/lemonldap-ng.ini @@ -96,6 +96,7 @@ localStorageOptions={ 'namespace' => 'MyNamespace', 'default_expires_in' => 600, ;portalDisplayResetPassword = 1 ;portalDisplayChangePassword = 1 ;portalDisplayAppslist = 1 +;portalDisplayLoginHistory = 1 # Allow password autocompletion (passwords stored in user web browsers) ;portalAutocomplete = 1 # Require the old password when changing password diff --git a/modules/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/Serializer.pm b/modules/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/Serializer.pm index 40bf1fe64..f1d314983 100644 --- a/modules/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/Serializer.pm +++ b/modules/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/Serializer.pm @@ -124,6 +124,7 @@ sub unserialize { |samlSPMetaDataOptions |samlSPMetaDataXML |samlStorageOptions + |sessionDataToDisplay |vhostOptions )$/ and $v ||= {} and not ref($v) diff --git a/modules/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/_Struct.pm b/modules/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/_Struct.pm index 0650a2083..2661ad490 100644 --- a/modules/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/_Struct.pm +++ b/modules/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/_Struct.pm @@ -267,7 +267,7 @@ sub struct { _help => 'menu', portalModules => { _nodes => [ - qw(portalDisplayLogout portalDisplayChangePassword portalDisplayAppslist) + qw(portalDisplayLogout portalDisplayChangePassword portalDisplayAppslist portalDisplayLoginHistory) ], portalDisplayLogout => 'text:/portalDisplayLogout:menu:boolOrPerlExpr', @@ -275,6 +275,8 @@ sub struct { 'text:/portalDisplayChangePassword:menu:boolOrPerlExpr', portalDisplayAppslist => 'text:/portalDisplayAppslist:menu:boolOrPerlExpr', + portalDisplayLoginHistory => + 'text:/portalDisplayLoginHistory:menu:boolOrPerlExpr', }, applicationList => { _nodes => [ @@ -287,7 +289,7 @@ sub struct { portalCustomization => { _nodes => [ - qw(portalSkin portalAutocomplete portalUserAttr portalOpenLinkInNewWindow portalAntiFrame passwordManagement) + qw(portalSkin portalAutocomplete portalUserAttr portalOpenLinkInNewWindow portalAntiFrame passwordManagement loginDisplay) ], _help => 'portalcustom', @@ -310,6 +312,17 @@ sub struct { hideOldPassword => 'bool:/hideOldPassword', mailOnPasswordChange => 'bool:/mailOnPasswordChange', }, + + loginDisplay => { + _nodes => [qw(successLoginNumber failedLoginNumber cn:sessionDataToDisplay)], + _help => 'loginDisplay', + successLoginNumber => 'int:/successLoginNumber', + failedLoginNumber => 'int:/failedLoginNumber', + sessionDataToDisplay => { + _nodes => ['hash:/sessionDataToDisplay:loginDisplay:btext'], + _js => 'hashRoot', + }, + }, }, }, @@ -1338,6 +1351,7 @@ sub testStruct { test => qr/^[a-zA-Z][\w-]*$/, msgFail => 'Bad attribute name', }, + failedLoginNumber => $integer, globalStorage => { test => qr/^[\w:]+$/, msgFail => 'Bad module name', @@ -1513,6 +1527,7 @@ sub testStruct { msgFail => 'Bad portal value', }, portalAutocomplete => $boolean, + portalDisplayLoginHistory => { test => $perlExpr, }, portalDisplayAppslist => { test => $perlExpr, }, portalDisplayChangePassword => { test => $perlExpr, }, portalDisplayLogout => { test => $perlExpr, }, @@ -1544,11 +1559,16 @@ sub testStruct { test => qr/^(?:0|1|2)$/, msgFail => 'securedCookie must be 0, 1 or 2', }, + sessionDataToDisplay => { + keyTest => qr/^[\w-]+$/, + keyMsgFail => 'Invalid session data', + }, singleSession => $boolean, singleIP => $boolean, singleUserByIP => $boolean, Soap => $boolean, storePassword => $boolean, + successLoginNumber => $integer, syslog => { test => qw/^(?:auth|authpriv|daemon|local\d|user)?$/, msgFail => @@ -1852,6 +1872,7 @@ sub defaultConf { cda => '0', cookieName => 'lemonldap', domain => 'example.com', + failedLoginNumber => '5', globalStorage => 'Apache::Session::File', hideOldPassword => '0', httpOnly => '1', @@ -1893,6 +1914,7 @@ sub defaultConf { portal => 'http://auth.example.com', portalSkin => 'pastel', portalUserAttr => '_user', + portalDisplayLoginHistory => '1', portalDisplayAppslist => '1', portalDisplayChangePassword => '$_auth eq "LDAP" or $_auth eq "DBI"', portalDisplayLogout => '1', @@ -1914,6 +1936,7 @@ sub defaultConf { Soap => '1', SSLRequired => '0', storePassword => '0', + successLoginNumber => '5', syslog => '', timeout => '72000', timeoutActivity => '0', diff --git a/modules/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/_i18n.pm b/modules/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/_i18n.pm index 7d731e15f..9b9eea092 100644 --- a/modules/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/_i18n.pm +++ b/modules/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/_i18n.pm @@ -131,6 +131,7 @@ sub en { error => 'Error', exportedAttr => 'SOAP exported attributes', exportedVars => 'Exported Variables', + failedLoginNumber => 'Number of registered failed logins', generalParameters => 'General Parameters', globalStorage => 'Apache::Session module', globalStorageOptions => 'Apache::Session module parameters', @@ -186,6 +187,7 @@ sub en { ldapTimeout => 'Timeout', ldapUsePasswordResetAttribute => 'Use reset attribute', ldapVersion => 'Version', + loginDisplay => 'Login display', logoutServices => 'Logout forward', logParams => 'Logs', macros => 'Macros', @@ -257,6 +259,7 @@ sub en { portalCustomization => 'Customization', portalDisplayAppslist => 'Applications list', portalDisplayChangePassword => 'Password change', + portalDisplayLoginHistory => 'Login History', portalDisplayLogout => 'Logout', portalDisplayResetPassword => 'Reset password', portalForceAuthn => 'Force authentication', @@ -293,6 +296,7 @@ sub en { security => 'Security', session => 'session', sessions => 'sessions', + sessionDataToDisplay => 'Session data to display', sessionDeleted => 'The session was deleted', sessionParams => 'Sessions', sessionStartedAt => 'Session started on', @@ -319,6 +323,7 @@ sub en { SSLVar => 'Extracted certificate field', startTime => 'Creation date', storePassword => 'Store user password in session datas', + successLoginNumber => 'Number of registered logins', sympaHandler => 'Sympa', sympaMailKey => 'Mail session key', sympaSecret => 'Shared secret', @@ -569,6 +574,7 @@ sub fr { error => 'Erreur', exportedAttr => 'Attributs exportés par le portail (SOAP)', exportedVars => 'Attributs à exporter', + failedLoginNumber => 'Nombre d\'échecs de connexion mémorisés', generalParameters => 'Paramètres généraux', globalStorage => 'Module Apache::Session', globalStorageOptions => 'Paramètres du module Apache::Session', @@ -625,6 +631,7 @@ sub fr { ldapUsePasswordResetAttribute => 'Utiliser l\'attribut de réinitialisation', ldapVersion => 'Version', + loginDisplay => 'Affichage des connexions', logoutServices => 'Transfert de la déconnexion', logParams => 'Journalisation', macros => 'Macros', @@ -700,6 +707,7 @@ sub fr { portalCustomization => 'Personnalisation', portalDisplayAppslist => 'Liste des applications', portalDisplayChangePassword => 'Changement de mot de passe', + portalDisplayLoginHistory => 'Historique des connexions', portalDisplayLogout => 'Déconnexion', portalDisplayResetPassword => 'Réinitialisation de mot de passe', portalForceAuthn => 'Authentication forcée', @@ -737,6 +745,7 @@ sub fr { security => 'Sécurité', session => 'session', sessions => 'sessions', + sessionDataToDisplay => 'Données de session à afficher', sessionDeleted => 'La session a été supprimée', sessionParams => 'Sessions', sessionStartedAt => 'Session démarrée le ', @@ -762,7 +771,8 @@ sub fr { SSLRequire => 'SSL Requis', SSLVar => 'Champ extrait du certificat', startTime => 'Date de création', - storePassword => "Stocke le mot de passe de l'utilisateur en session", + storePassword => "Stocke le mot de passe de l'utilisateur en session", + successLoginNumber => 'Nombre de connexions mémorisées', sympaHandler => 'Sympa', sympaMailKey => 'Clé de session pour le mail', sympaSecret => 'Secret partagé', diff --git a/modules/lemonldap-ng-portal/example/skins/common/calendar.png b/modules/lemonldap-ng-portal/example/skins/common/calendar.png new file mode 100644 index 0000000000000000000000000000000000000000..9740f76ee6c1dee706f6fe724bda1208ef445a47 GIT binary patch literal 572 zcmV-C0>k}@P)Ff}Nlc zJ+V-1gdjvrBToYb_&=5JJoz%#Q=n#p0PUsg;0_wW6hzWF z6d(Z#0TH%uy+T9>YJjQ_4o41w#S|V(NC8qn46yPY0ExQO163;l8LV%BTT)_{lxVdB zLqhH!O}vfXt+G~AtirG?JdRPE!p4;(u<%+06sOr5$!l?%`w$ZJisblGeq3lk?M znK(IF&Lv~|%q&@!^#Q8MB|waLDd+L)A8Bgth=QmR)CiKix0+Qk`lsOTj2++4X|3MA z_x{3e%gn9zko@i4h1Ih`tu0ZJsAx1qji_-~z~
  • password
  • + +
  • login history
  • +
  • logout
  • @@ -154,6 +157,22 @@ + +
    +
    + +

    + +
    + +

    + +
    +
    +
    +
    + +

    diff --git a/modules/lemonldap-ng-portal/example/skins/pastel/css/styles.css b/modules/lemonldap-ng-portal/example/skins/pastel/css/styles.css index e7b6cb88e..0ff6d8160 100644 --- a/modules/lemonldap-ng-portal/example/skins/pastel/css/styles.css +++ b/modules/lemonldap-ng-portal/example/skins/pastel/css/styles.css @@ -134,7 +134,7 @@ margin:0px 200px -20px 200px; -webkit-border-radius:10px 10px 10px 10px; } -form { +form, div.form { display:block; overflow:visible; padding:0; @@ -149,13 +149,13 @@ color:#336699; -webkit-border-radius:10px 10px 10px 10px; } -form.login, form.password { +form.login, form.password, div.form { margin:40px 200px; } -form table { +form table, div.form table { border:0; -margin:0 auto; +margin: 0 auto 10px; } form th { diff --git a/modules/lemonldap-ng-portal/example/skins/pastel/menu.tpl b/modules/lemonldap-ng-portal/example/skins/pastel/menu.tpl index 050701776..6339b20c7 100644 --- a/modules/lemonldap-ng-portal/example/skins/pastel/menu.tpl +++ b/modules/lemonldap-ng-portal/example/skins/pastel/menu.tpl @@ -19,10 +19,12 @@

  • password
  • - + +
  • login history
  • +
    +
  • logout
  • -
    @@ -145,6 +147,21 @@ + +
    +
    + +

    + +
    + +

    + +
    +
    +
    +
    +

    @@ -154,7 +171,7 @@
    -
    + diff --git a/modules/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Menu.pm b/modules/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Menu.pm index 7198020e7..c26bdaca5 100644 --- a/modules/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Menu.pm +++ b/modules/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Menu.pm @@ -23,7 +23,7 @@ sub menuInit { $self->{apps}->{imgpath} ||= '/apps/'; # Modules to display - $self->{menuModules} ||= "Appslist ChangePassword Logout"; + $self->{menuModules} ||= "Appslist ChangePassword LoginHistory Logout"; $self->{menuDisplayModules} = $self->displayModules(); # Extract password from POST data @@ -101,6 +101,18 @@ sub displayModules { my $moduleHash = { $module => 1 }; $moduleHash->{'APPSLIST_LOOP'} = $self->appslist() if ( $module eq 'Appslist' ); + if ( $module eq 'LoginHistory' ) { + $moduleHash->{'SUCCESS_LOGIN'} + = $self->mkSessionArray( + $self->{sessionInfo}->{loginHistory}->{successLogin}, + "", 0, 0 + ); + $moduleHash->{'FAILED_LOGIN'} + = $self->mkSessionArray( + $self->{sessionInfo}->{loginHistory}->{failedLogin}, + "", 0, 1 + ); + } push @$displayModules, $moduleHash; } } diff --git a/modules/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Simple.pm b/modules/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Simple.pm index df3831ee7..1e27d9c74 100644 --- a/modules/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Simple.pm +++ b/modules/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Simple.pm @@ -2032,12 +2032,15 @@ sub sendPasswordMail { } ##@apmethod int authenticate() -# Call authenticate() in Auth* module and call userNotice(). +# Call authenticate() in Auth* module, and registerLogin() +# if authentication failed, userNotice() if it succeeded. #@return Lemonldap::NG::Portal constant sub authenticate { my $self = shift; - my $tmp; - return $tmp if ( $tmp = $self->SUPER::authenticate() ); + if ( my $errorCode = $self->SUPER::authenticate() ) { + $self->registerLogin( $errorCode ) ; + return $errorCode ; + } # Log good authentication my $user = $self->{sessionInfo}->{ $self->{whatToTrace} }; @@ -2049,6 +2052,37 @@ sub authenticate { PE_OK; } +##@apmethod registerLogin +# Store current login in login history +# @param $errorCode Code returned by authenticate() +sub registerLogin { + my ( $self, $errorCode ) = @_; + + if ($self->{portalDisplayLoginHistory}) { + my $history = $self->{sessionInfo}->{loginHistory} ||= {}; + + my $type = ( $errorCode ? "failed" : "success")."Login"; + $history->{$type} ||= []; + $self->lmLog("Current login saved into $type", "debug"); + + # Gather current login's parameters + my $login = $self->_sumUpSession( $self->{sessionInfo}, 1 ); + $login->{error} = $self->error(undef, $errorCode) + if ($errorCode); + + # Add current login into history + unshift @{$history->{$type}}, $login; + + # Forget oldest logins + splice @{$history->{$type}}, $self->{$type."Number"}; + + # Save into persistent session + $self->updatePersistentSession({ + loginHistory => $history, + }); + } +} + ##@apmethod int removeOther() # check singleSession or singleIP parameters, and remove other sessions if needed #@return Lemonldap::NG::Portal constant @@ -2073,21 +2107,11 @@ sub removeOther { and $self->{sessionInfo}->{ipAddr} ne $h->{ipAddr} ) ) { - push @{ $self->{deleted} }, - { - time => $h->{_utime}, - ip => $h->{ipAddr}, - user => $h->{ $self->{whatToTrace} }, - }; + push @{ $self->{deleted} }, $self->_sumUpSession($h); $self->_deleteSession( $h, 1 ); } else { - push @{ $self->{otherSessions} }, - { - time => $h->{_utime}, - ip => $h->{ipAddr}, - user => $h->{ $self->{whatToTrace} }, - }; + push @{ $self->{otherSessions} }, $self->_sumUpSession($h); } } } @@ -2101,48 +2125,76 @@ sub removeOther { unless ( $self->{sessionInfo}->{ $self->{whatToTrace} } eq $h->{ $self->{whatToTrace} } ) { - push @{ $self->{deleted} }, - { - time => $h->{_utime}, - ip => $h->{ipAddr}, - user => $h->{ $self->{whatToTrace} }, - }; + push @{ $self->{deleted} }, $self->_sumUpSession($h); $self->_deleteSession( $h, 1 ); } } } $self->info( - $self->_mkDateIpArray( + $self->mkSessionArray( + $self->{deleted}, &Lemonldap::NG::Portal::_i18n::msg(PM_SESSIONS_DELETED), - @{ $self->{deleted} } + 1 ) ) if ( $self->{notifyDeleted} and @{ $self->{deleted} } ); $self->info( - $self->_mkDateIpArray( + $self->mkSessionArray( + $self->{otherSessions}, &Lemonldap::NG::Portal::_i18n::msg(PM_OTHER_SESSIONS), - @{ $self->{otherSessions} } ) - . $self->_mkRemoveOtherLink() + 1 + ) . $self->_mkRemoveOtherLink() ) if ( $self->{notifyOther} and @{ $self->{otherSessions} } ); PE_OK; } -##@method private string _mkDateIpArray(string title,array datas) -# Build the HTML array to display sessions deleted or found by removeOther() +##@method private hashref _sumUpSession(hashref session) +# put main session data into a hash ref +# @param hashref $session The session to sum up +# @return hashref +sub _sumUpSession { + my ($self, $session, $withoutUser) = @_ ; + my $res = $withoutUser ? {} : + { user => $session->{ $self->{whatToTrace} } }; + $res->{$_} = $session->{$_} + foreach ("_utime", "ipAddr", keys %{ $self->{sessionDataToDisplay} }); + return $res; +} + +##@method private string mkSessionArray(string title,array datas) +# Build an HTML array to display sessions +# @param $sessions Array ref of hash ref containing sessions datas # @param $title Title of the array -# @param @datas Array of hash ref containing sessions datas +# @param $displayUser To display "User" column +# @param $displaError To display "Error" column # @return HTML string -sub _mkDateIpArray { - my ( $self, $title, @datas ) = @_; - my $tmp = "

    $title

    "; - $tmp .= ""; - $tmp .= '' - foreach ( PM_USER, PM_DATE, PM_IP ); +sub mkSessionArray { + my ( $self, $sessions, $title, $displayUser, $displayError ) = @_; + + return "" unless (@$sessions); + + my $tmp = $title ? "

    $title

    " : "" ; + $tmp .= "
    ' . &Lemonldap::NG::Portal::_i18n::msg($_) . '
    "; + + $tmp .= ""; + $tmp .= "" + if ($displayUser); + $tmp .= ""; + $tmp .= ""; + $tmp .= "" + foreach ( keys %{ $self->{sessionDataToDisplay} } ); + $tmp .= '' + if ($displayError); $tmp .= ''; - foreach (@datas) { - $tmp .= - ""; + + foreach my $session (@$sessions) { + $tmp .= ""; + $tmp .= "" if ($displayUser); + $tmp .= ""; + $tmp .= ""; + $tmp .= "" + foreach ( keys %{ $self->{sessionDataToDisplay} } ); + $tmp .= "" if ($displayError); + $tmp .= ""; } $tmp .= '
    " . &Lemonldap::NG::Portal::_i18n::msg(PM_USER) . "" . &Lemonldap::NG::Portal::_i18n::msg(PM_DATE) . "" . &Lemonldap::NG::Portal::_i18n::msg(PM_IP) . "" . $self->{sessionDataToDisplay}->{$_} . "' . &Lemonldap::NG::Portal::_i18n::msg(PM_ERROR) . '
    $_->{user}" - . "" - . "$_->{ip}
    $session->{user}$session->{ipAddr}$session->{$_}$session->{error}
    '; return $tmp; @@ -2169,22 +2221,24 @@ sub _mkRemoveOtherLink { sub grantSession { my ($self) = @_; - # Return PE_OK if no grantSessionRule - return PE_OK unless defined $self->{grantSessionRule}; - - # Eval grantSessionRule - my $grantSessionRule = $self->{grantSessionRule}; - - unless ( $self->safe->reval($grantSessionRule) ) { - $self->lmLog( - "User " . $self->{user} . " was not granted to open session", - 'error' ); - return PE_SESSIONNOTGRANTED; + my $errorCode = PE_OK; + if (defined $self->{grantSessionRule}) { + # Eval grantSessionRule + my $grantSessionRule = $self->{grantSessionRule}; + + unless ( $self->safe->reval($grantSessionRule) ) { + $self->lmLog( + "User " . $self->{user} . " was not granted to open session", + 'error' ); + $self->registerLogin( ); + $errorCode = PE_SESSIONNOTGRANTED; + } else { + $self->lmLog( "Session granted for " . $self->{user}, 'notice' ); + } } - $self->lmLog( "Session granted for " . $self->{user}, 'notice' ); - - PE_OK; + $self->registerLogin( $errorCode ); + return $errorCode; } ##@apmethod int store() diff --git a/modules/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/_i18n.pm b/modules/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/_i18n.pm index 141c7b4c6..cb4933e2d 100644 --- a/modules/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/_i18n.pm +++ b/modules/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/_i18n.pm @@ -408,6 +408,7 @@ sub error_ro { # * PM_OPENID_RPNS 18 # * PM_OPENID_PA 19 # * PM_OPENID_AP 20 +# * PM_ERROR_MSG 21 sub msg_en { use utf8; @@ -433,6 +434,7 @@ sub msg_en { 'Parameter %s requested for federation isn\'t available', 'Data usage policy is available at', 'Do you agree to provide the following parameters?', + 'Error Message', ]; } @@ -460,6 +462,7 @@ sub msg_fr { 'Le paramètre %s exigé pour la fédération n\'est pas disponible', 'La politique d\'utilisation des données est disponible ici', 'Consentez-vous à communiquer les paramètres suivants ?', + 'Message d\'erreur', ]; }