feat(core) Clean expiration handling

Moved userinfo related functions as methods in userinfo
Moved ppolicy account expiration check to expired_status method
Split ppolicy fetch in a method so that it can be used by expiration
 warning detection later.

issue #6001
parent 5971fa40
......@@ -106,35 +106,33 @@ if (session::global_is_set('plugin_index')) {
$plist->gen_menu();
/* check if we are using account expiration */
$smarty->assign("hideMenus", FALSE);
if ($config->get_cfg_value("handleExpiredAccounts") == "TRUE") {
$expired = $ui->expired_status();
if (($expired == POSIX_WARN_ABOUT_EXPIRATION) && !session::is_set('POSIX_WARN_ABOUT_EXPIRATION__DONE')) {
@DEBUG(DEBUG_TRACE, __LINE__, __FUNCTION__, __FILE__, $expired, 'This user account ('.$ui->uid.') is about to expire');
// The users password is about to xpire soon, display a warning message.
logging::log('security', 'fusiondirectory', '', [], 'password for user "'.$ui->uid.'" is about to expire');
msg_dialog::display(_('Password change'), _('Your password is about to expire, please change your password!'), INFO_DIALOG);
session::set('POSIX_WARN_ABOUT_EXPIRATION__DONE', TRUE);
} elseif ($expired == POSIX_FORCE_PASSWORD_CHANGE) {
@DEBUG(DEBUG_TRACE, __LINE__, __FUNCTION__, __FILE__, $expired, "This user account expired");
// The password is expired, we are now going to enforce a new one from the user.
// Hide the FusionDirectory menus to avoid leaving the enforced password change dialog.
$smarty->assign("hideMenus", TRUE);
$plug = (isset($_GET['plug'])) ? $_GET['plug'] : NULL;
// Search for the 'user' class and set its id as active plug.
foreach ($plist->dirlist as $key => $value) {
if ($value == 'user') {
if (!isset($_GET['plug']) || ($_GET['plug'] != $key)) {
$_GET['plug'] = $key;
msg_dialog::display(_('Warning'), _('Your password has expired, please set a new one.'), WARNING_DIALOG);
}
break;
$smarty->assign('hideMenus', FALSE);
/* check user expiration status */
$expired = $ui->expired_status();
if (($expired == POSIX_WARN_ABOUT_EXPIRATION) && !session::is_set('POSIX_WARN_ABOUT_EXPIRATION__DONE')) {
@DEBUG(DEBUG_TRACE, __LINE__, __FUNCTION__, __FILE__, $expired, 'This user account ('.$ui->uid.') is about to expire');
// The users password is about to expire soon, display a warning message.
logging::log('security', 'fusiondirectory', '', [], 'password for user "'.$ui->uid.'" is about to expire');
msg_dialog::display(_('Password change'), _('Your password is about to expire, please change your password!'), INFO_DIALOG);
session::set('POSIX_WARN_ABOUT_EXPIRATION__DONE', TRUE);
} elseif ($expired == POSIX_FORCE_PASSWORD_CHANGE) {
@DEBUG(DEBUG_TRACE, __LINE__, __FUNCTION__, __FILE__, $expired, 'This user account expired');
// The password is expired, we are now going to enforce a new one from the user.
// Hide the FusionDirectory menus to avoid leaving the enforced password change dialog.
$smarty->assign('hideMenus', TRUE);
$plug = (isset($_GET['plug'])) ? $_GET['plug'] : NULL;
// Search for the 'user' class and set its id as active plug.
foreach ($plist->dirlist as $key => $value) {
if ($value == 'user') {
if (!isset($_GET['plug']) || ($_GET['plug'] != $key)) {
$_GET['plug'] = $key;
msg_dialog::display(_('Warning'), _('Your password has expired, please set a new one.'), WARNING_DIALOG);
}
break;
}
}
}
......
<?php
/*
This code is part of FusionDirectory (http://www.fusiondirectory.org/)
Copyright (C) 2003-2010 Cajus Pollmeier
Copyright (C) 2011-2016 FusionDirectory
Copyright (C) 2011-2019 FusionDirectory
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
......@@ -24,6 +25,13 @@
* Source code for the class userinfo
*/
/* Define shadow states */
define('POSIX_ACCOUNT_EXPIRED', 1);
define('POSIX_WARN_ABOUT_EXPIRATION', 2);
define('POSIX_FORCE_PASSWORD_CHANGE', 4);
define('POSIX_DISALLOW_PASSWORD_CHANGE', 8);
define('PPOLICY_ACCOUNT_EXPIRED', 16);
/*!
* \brief Class userinfo
* This class contains all informations and functions
......@@ -890,12 +898,26 @@ class userinfo
function expired_status ()
{
global $config;
// Skip this for the admin account, we do not want to lock him out.
if ($this->is_user_admin()) {
return 0;
}
$ldap = $config->get_ldap_link();
if (class_available('ppolicyAccount')) {
$ldap->cd($config->current['BASE']);
$ldap->search('(objectClass=*)', [], 'one');
if (!$ldap->success()) {
return PPOLICY_ACCOUNT_EXPIRED;
}
}
if ($config->get_cfg_value('handleExpiredAccounts') != 'TRUE') {
return 0;
}
$ldap->cd($config->current['BASE']);
$ldap->cat($this->dn);
$attrs = $ldap->fetch();
......@@ -1078,4 +1100,114 @@ class userinfo
{
return preg_replace('/[\/\-,.#:;]/', '_', $name);
}
/*!
* \brief Get user from LDAP directory
*
* Search the user by login or other fields authorized by the configuration
*
* \param string $username The username or email to check
*
* \return userinfo instance on SUCCESS, FALSE if not found, string error on error
*/
public static function getLdapUser (string $username)
{
global $config;
/* look through the entire ldap */
$ldap = $config->get_ldap_link();
if (!$ldap->success()) {
msg_dialog::display(_('LDAP error'),
msgPool::ldaperror($ldap->get_error(), '', LDAP_AUTH),
FATAL_ERROR_DIALOG);
exit();
}
$allowed_attributes = ['uid','mail'];
$verify_attr = [];
$tmp = explode(',', $config->get_cfg_value('loginAttribute'));
foreach ($tmp as $attr) {
if (in_array($attr, $allowed_attributes)) {
$verify_attr[] = $attr;
}
}
if (count($verify_attr) == 0) {
$verify_attr = ['uid'];
}
$tmp = $verify_attr;
$tmp[] = 'uid';
$filter = '';
foreach ($verify_attr as $attr) {
$filter .= '('.$attr.'='.$username.')';
}
$filter = '(&(|'.$filter.')(objectClass=inetOrgPerson))';
$ldap->cd($config->current['BASE']);
$ldap->search($filter, $tmp);
/* get results, only a count of 1 is valid */
if ($ldap->count() == 0) {
/* user not found */
return FALSE;
} elseif ($ldap->count() != 1) {
/* found more than one matching id */
return _('Login (uid) is not unique inside the LDAP tree. Please contact your administrator.');
}
/* LDAP schema is not case sensitive. Perform additional check. */
$attrs = $ldap->fetch();
$success = FALSE;
foreach ($verify_attr as $attr) {
if (isset($attrs[$attr][0]) && $attrs[$attr][0] == $username) {
$success = TRUE;
}
}
$ldap->disconnect();
if (!$success) {
return FALSE;
}
return new userinfo($attrs['dn']);
}
/*!
* \brief Verify user login against LDAP directory
*
* Checks if the specified username is in the LDAP and verifies if the
* password is correct by binding to the LDAP with the given credentials.
*
* \param string $username The username to check
*
* \param string $password The password to check
*
* \return TRUE on SUCCESS, NULL or FALSE on error
*/
public static function loginUser (string $username, string $password)
{
global $config;
$ui = static::getLdapUser($username);
if ($ui === FALSE) {
return NULL;
} elseif (is_string($ui)) {
msg_dialog::display(_('Internal error'), $ui, FATAL_ERROR_DIALOG);
return NULL;
}
/* password check, bind as user with supplied password */
$ldapObj = new LDAP($ui->dn, $password, $config->current['SERVER'],
isset($config->current['LDAPFOLLOWREFERRALS']) && ($config->current['LDAPFOLLOWREFERRALS'] == 'TRUE'),
isset($config->current['LDAPTLS']) && ($config->current['LDAPTLS'] == 'TRUE')
);
$ldap = new ldapMultiplexer($ldapObj);
if (!$ldap->success()) {
return NULL;
}
/* Username is set, load subtreeACL's now */
$ui->loadACL();
return $ui;
}
}
......@@ -45,12 +45,6 @@ define('DEBUG_SI', 256); /*! Debug level for communication with Argonaut *
define('DEBUG_MAIL', 512); /*! Debug level for all about mail (mailAccounts, imap, sieve etc.) */
define('DEBUG_FAI', 1024); /* FAI (incomplete) */
/* Define shadow states */
define('POSIX_ACCOUNT_EXPIRED', 1);
define('POSIX_WARN_ABOUT_EXPIRATION', 2);
define('POSIX_FORCE_PASSWORD_CHANGE', 4);
define('POSIX_DISALLOW_PASSWORD_CHANGE', 8);
/* Rewrite german 'umlauts' and spanish 'accents'
to get better results */
$REWRITE = [ "ä" => "ae",
......@@ -394,130 +388,6 @@ function ldap_init ($server, $base, $binddn = '', $pass = '')
return $ldap;
}
/*!
* \brief Get user from LDAP directory
*
* Search the user by login or other fields authorized by the configuration
*
* \param string $username The username or email to check
*
* \return userinfo instance on SUCCESS, FALSE if not found, string error on error
*/
function ldap_get_user ($username)
{
global $config;
/* look through the entire ldap */
$ldap = $config->get_ldap_link();
if (!$ldap->success()) {
msg_dialog::display(_('LDAP error'),
msgPool::ldaperror($ldap->get_error(), '', LDAP_AUTH),
FATAL_ERROR_DIALOG);
exit();
}
$allowed_attributes = ['uid','mail'];
$verify_attr = [];
$tmp = explode(',', $config->get_cfg_value('loginAttribute'));
foreach ($tmp as $attr) {
if (in_array($attr, $allowed_attributes)) {
$verify_attr[] = $attr;
}
}
if (count($verify_attr) == 0) {
$verify_attr = ['uid'];
}
$tmp = $verify_attr;
$tmp[] = 'uid';
$filter = '';
foreach ($verify_attr as $attr) {
$filter .= '('.$attr.'='.$username.')';
}
$filter = '(&(|'.$filter.')(objectClass=inetOrgPerson))';
$ldap->cd($config->current['BASE']);
$ldap->search($filter, $tmp);
/* get results, only a count of 1 is valid */
if ($ldap->count() == 0) {
/* user not found */
return FALSE;
} elseif ($ldap->count() != 1) {
/* found more than one matching id */
return _('Login (uid) is not unique inside the LDAP tree. Please contact your administrator.');
}
/* LDAP schema is not case sensitive. Perform additional check. */
$attrs = $ldap->fetch();
$success = FALSE;
foreach ($verify_attr as $attr) {
if (isset($attrs[$attr][0]) && $attrs[$attr][0] == $username) {
$success = TRUE;
}
}
$ldap->disconnect();
if (!$success) {
return FALSE;
}
return new userinfo($attrs['dn']);
}
/*!
* \brief Verify user login against LDAP directory
*
* Checks if the specified username is in the LDAP and verifies if the
* password is correct by binding to the LDAP with the given credentials.
*
* \param string $username The username to check
*
* \param string $password The password to check
*
* \return TRUE on SUCCESS, NULL or FALSE on error
*/
function ldap_login_user ($username, $password)
{
global $config;
$ui = ldap_get_user($username);
if ($ui === FALSE) {
return NULL;
} elseif (is_string($ui)) {
msg_dialog::display(_('Internal error'), $ui, FATAL_ERROR_DIALOG);
return NULL;
}
/* password check, bind as user with supplied password */
$ldapObj = new LDAP($ui->dn, $password, $config->current['SERVER'],
isset($config->current['LDAPFOLLOWREFERRALS']) &&
$config->current['LDAPFOLLOWREFERRALS'] == 'TRUE',
isset($config->current['LDAPTLS'])
&& $config->current['LDAPTLS'] == 'TRUE'
);
$ldap = new ldapMultiplexer($ldapObj);
if (!$ldap->success()) {
return NULL;
}
if (class_available('ppolicyAccount')) {
$ldap->cd($config->current['BASE']);
$ldap->search('(objectClass=*)', [], 'one');
if (!$ldap->success()) {
msg_dialog::display(
_('Authentication error'),
_('It seems your user password has expired. Please use <a href="recovery.php">password recovery</a> to change it.'),
ERROR_DIALOG
);
return NULL;
}
}
/* Username is set, load subtreeACL's now */
$ui->loadACL();
return $ui;
}
/*!
* \brief Add a lock for object(s)
*
......
......@@ -59,7 +59,7 @@ class LoginCAS extends LoginMethod
phpCAS::forceAuthentication();
static::$username = phpCAS::getUser();
$ui = ldap_get_user(static::$username);
$ui = userinfo::getLdapUser(static::$username);
if ($ui === FALSE) {
msg_dialog::display(
......
......@@ -55,7 +55,7 @@ class LoginHTTPHeader extends LoginMethod
exit();
}
$ui = ldap_get_user(static::$username);
$ui = userinfo::getLdapUser(static::$username);
if ($ui === FALSE) {
msg_dialog::display(
......
......@@ -95,7 +95,7 @@ class LoginMethod
{
global $ui, $config, $message, $smarty;
/* Login as user, initialize user ACL's */
$ui = ldap_login_user(static::$username, static::$password);
$ui = userinfo::loginUser(static::$username, static::$password);
if ($ui === NULL) {
if (isset($_SERVER['REMOTE_ADDR'])) {
logging::log('security', 'login', '', [], 'Authentication failed for user "'.static::$username.'" [from '.$_SERVER['REMOTE_ADDR'].']');
......@@ -132,16 +132,26 @@ class LoginMethod
$config->checkLdapConfig();
/* Check account expiration */
if ($config->get_cfg_value('handleExpiredAccounts') == 'TRUE') {
$expired = $ui->expired_status();
$expired = $ui->expired_status();
if ($expired == PPOLICY_ACCOUNT_EXPIRED) {
msg_dialog::display(
_('Authentication error'),
_('It seems your user password has expired. Please use <a href="recovery.php">password recovery</a> to change it.'),
ERROR_DIALOG
);
logging::log('security', 'login', '', [], 'Account for user "'.static::$username.'" has expired (ppolicy)');
$message = _('Password expired');
$smarty->assign('focusfield', 'username');
return FALSE;
}
if ($expired == POSIX_ACCOUNT_EXPIRED) {
logging::log('security', 'login', '', [], 'Account for user "'.static::$username.'" has expired');
$message = _('Account locked. Please contact your system administrator!');
$smarty->assign('focusfield', 'username');
return FALSE;
}
if ($expired == POSIX_ACCOUNT_EXPIRED) {
logging::log('security', 'login', '', [], 'Account for user "'.static::$username.'" has expired');
$message = _('Account locked. Please contact your system administrator!');
$smarty->assign('focusfield', 'username');
return FALSE;
}
return TRUE;
}
......
......@@ -29,11 +29,11 @@ require_once("variables.inc");
function html_trace ($errstr = "")
{
static $hideArgs = [
'ldap_init' => [3],
'ldap_login_user' => [1],
'change_password' => [1],
'cred_decrypt' => [0,1],
'LDAP/__construct' => [1],
'ldap_init' => [3],
'userinfo/loginUser' => [1],
'change_password' => [1],
'cred_decrypt' => [0,1],
'LDAP/__construct' => [1],
];
if (!function_exists('debug_backtrace')) {
return ['', ''];
......
......@@ -397,23 +397,12 @@ class user extends simplePlugin
$addAttrs['passwordClear'] = $this->attributesAccess['userPassword']->getClear();
}
static function reportPasswordProblems ($user, $new_password, $repeated_password, $current_password = NULL)
static function fetchPpolicy (string $userdn): array
{
global $config, $ui;
/* Should we check different characters in new password */
$check_differ = ($config->get_cfg_value('passwordMinDiffer') != '');
$differ = $config->get_cfg_value('passwordMinDiffer', 0);
if ($current_password === NULL) {
$check_differ = FALSE;
}
/* Enable length check ? */
$check_length = ($config->get_cfg_value('passwordMinLength') != '');
$length = $config->get_cfg_value('passwordMinLength', 0);
global $config;
$ldap = $config->get_ldap_link();
$ldap->cat($user, ['pwdPolicySubentry', 'pwdHistory', 'pwdChangedTime', 'userPassword']);
$ldap->cat($userdn, ['pwdPolicySubentry', 'pwdHistory', 'pwdChangedTime', 'userPassword']);
$attrs = $ldap->fetch();
$ppolicydn = '';
if (isset($attrs['pwdPolicySubentry'][0])) {
......@@ -424,12 +413,41 @@ class user extends simplePlugin
$ppolicydn = 'cn='.$ppolicydn.','.get_ou('ppolicyRDN').$config->current['BASE'];
}
}
$policy = NULL;
if (!empty($ppolicydn)) {
$ldap->cat($ppolicydn, ['pwdAllowUserChange', 'pwdMinLength', 'pwdMinAge', 'pwdSafeModify']);
$policy = $ldap->fetch();
if (!$policy) {
return sprintf(_('Ppolicy "%s" could not be found in the LDAP!'), $ppolicydn);
throw new NonExistingLdapNodeException(sprintf(_('Ppolicy "%s" could not be found in the LDAP!'), $ppolicydn));
}
}
return [$policy, $attrs];
}
static function reportPasswordProblems ($user, $new_password, $repeated_password, $current_password = NULL)
{
global $config, $ui;
/* Should we check different characters in new password */
$check_differ = ($config->get_cfg_value('passwordMinDiffer') != '');
$differ = $config->get_cfg_value('passwordMinDiffer', 0);
if ($current_password === NULL) {
$check_differ = FALSE;
}
/* Enable length check ? */
$check_length = ($config->get_cfg_value('passwordMinLength') != '');
$length = $config->get_cfg_value('passwordMinLength', 0);
try {
list($policy, $attrs) = static::fetchPpolicy($user);
} catch (NonExistingLdapNodeException $e) {
return $e->getMessage();
}
if (isset($policy)) {
if (isset($policy['pwdAllowUserChange'][0]) && ($policy['pwdAllowUserChange'][0] == 'FALSE') && ($ui->dn == $user)) {
return _('You are not allowed to change your own password');
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment