Implement login history (LEMONLDAP-389)

This commit is contained in:
François-Xavier Deltombe 2012-01-10 19:54:30 +00:00
parent fae4997976
commit 810865dcbd
11 changed files with 205 additions and 65 deletions

View File

@ -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

View File

@ -124,6 +124,7 @@ sub unserialize {
|samlSPMetaDataOptions
|samlSPMetaDataXML
|samlStorageOptions
|sessionDataToDisplay
|vhostOptions
)$/
and $v ||= {} and not ref($v)

View File

@ -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',

View File

@ -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é',

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 B

View File

@ -23,6 +23,9 @@
<TMPL_IF NAME="ChangePassword">
<li><a href="#password"><span><img src="/skins/common/vcard_edit.png" width="16" height="16" alt="password" /> <lang en="Password" fr="Mot de passe" /></span></a></li>
</TMPL_IF>
<TMPL_IF NAME="LoginHistory">
<li><a href="#loginHistory"><span><img src="/skins/common/calendar.png" width="16" height="16" alt="login history" /> <lang en="Login history" fr="Historique des connexions" /></span></a></li>
</TMPL_IF>
<TMPL_IF NAME="Logout">
<li><a href="#logout"><span><img src="/skins/common/door_out.png" width="16" height="16" alt="logout" /> <lang en="Logout" fr="Se d&eacute;connecter" /></span></a></li>
</TMPL_IF>
@ -154,6 +157,22 @@
<TMPL_INCLUDE NAME="password.tpl">
</TMPL_IF>
<TMPL_IF NAME="LoginHistory">
<div id="loginHistory">
<div class="form">
<TMPL_IF NAME="SUCCESS_LOGIN">
<h3><lang en="Last logins" fr="Derni&egrave;res connexions" /></h3>
<TMPL_VAR NAME="SUCCESS_LOGIN">
</TMPL_IF>
<TMPL_IF NAME="FAILED_LOGIN">
<h3><lang en="Last failed logins" fr="Derni&egrave;res connexions refusées" /></h3>
<TMPL_VAR NAME="FAILED_LOGIN">
</TMPL_IF>
</div>
</div>
</TMPL_IF>
<TMPL_IF NAME="Logout">
<div id="logout">
<p class="text-label">

View File

@ -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 {

View File

@ -19,10 +19,12 @@
<TMPL_IF NAME="ChangePassword">
<li><a href="#password"><span><img src="/skins/common/vcard_edit.png" width="16" height="16" alt="password" /> <lang en="Password" fr="Mot de passe" /></span></a></li>
</TMPL_IF>
<TMPL_IF NAME="Logout">
<TMPL_IF NAME="LoginHistory">
<li><a href="#loginHistory"><span><img src="/skins/common/calendar.png" width="16" height="16" alt="login history" /> <lang en="Login history" fr="Historique des connexions" /></span></a></li>
</TMPL_IF>
<TMPL_IF NAME="Logout">
<li><a href="#logout"><span><img src="/skins/common/door_out.png" width="16" height="16" alt="logout" /> <lang en="Logout" fr="D&eacute;connexion" /></span></a></li>
</TMPL_IF>
</TMPL_LOOP>
</ul>
</TMPL_IF>
@ -145,6 +147,21 @@
<TMPL_INCLUDE NAME="password.tpl">
</TMPL_IF>
<TMPL_IF NAME="LoginHistory">
<div id="loginHistory">
<div class="form">
<TMPL_IF NAME="SUCCESS_LOGIN">
<h3><lang en="Last logins" fr="Derni&egrave;res connexions" /></h3>
<TMPL_VAR NAME="SUCCESS_LOGIN">
</TMPL_IF>
<TMPL_IF NAME="FAILED_LOGIN">
<h3><lang en="Last failed logins" fr="Derni&egrave;res connexions refusées" /></h3>
<TMPL_VAR NAME="FAILED_LOGIN">
</TMPL_IF>
</div>
</div>
</TMPL_IF>
<TMPL_IF NAME="Logout">
<div id="logout">
<h3><lang en="Are you sure ?" fr="&Ecirc;tes vous s&ucirc;r ?" /></h3>
@ -154,7 +171,7 @@
</a>
</div>
</div>
</TMPL_IF>
</TMPL_IF>
</TMPL_LOOP>

View File

@ -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;
}
}

View File

@ -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 = "<h3>$title</h3>";
$tmp .= "<table class=\"info\"><tbody><tr>";
$tmp .= '<th>' . &Lemonldap::NG::Portal::_i18n::msg($_) . '</th>'
foreach ( PM_USER, PM_DATE, PM_IP );
sub mkSessionArray {
my ( $self, $sessions, $title, $displayUser, $displayError ) = @_;
return "" unless (@$sessions);
my $tmp = $title ? "<h3>$title</h3>" : "" ;
$tmp .= "<table class=\"info\"><tbody>";
$tmp .= "<tr>";
$tmp .= "<th>" . &Lemonldap::NG::Portal::_i18n::msg(PM_USER) . "</th>"
if ($displayUser);
$tmp .= "<th>" . &Lemonldap::NG::Portal::_i18n::msg(PM_DATE) . "</th>";
$tmp .= "<th>" . &Lemonldap::NG::Portal::_i18n::msg(PM_IP) . "</th>";
$tmp .= "<th>" . $self->{sessionDataToDisplay}->{$_} . "</th>"
foreach ( keys %{ $self->{sessionDataToDisplay} } );
$tmp .= '<th>' . &Lemonldap::NG::Portal::_i18n::msg(PM_ERROR) . '</th>'
if ($displayError);
$tmp .= '</tr>';
foreach (@datas) {
$tmp .=
"<tr><td>$_->{user}</td><td>"
. "<script>var _date=new Date($_->{time}*1000);document.write(_date.toLocaleString());</script>"
. "</td><td>$_->{ip}</td></tr>";
foreach my $session (@$sessions) {
$tmp .= "<tr>";
$tmp .= "<td>$session->{user}</td>" if ($displayUser);
$tmp .= "<td><script>var _date=new Date($session->{_utime}*1000);document.write(_date.toLocaleString());</script></td>";
$tmp .= "<td>$session->{ipAddr}</td>";
$tmp .= "<td>$session->{$_}</td>"
foreach ( keys %{ $self->{sessionDataToDisplay} } );
$tmp .= "<td>$session->{error}</td>" if ($displayError);
$tmp .= "</tr>";
}
$tmp .= '</tbody></table>';
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()

View File

@ -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&nbsp;?',
'Message d\'erreur',
];
}