Skip to content

Commit

Permalink
Merge branch 'w51_MDL-21342_m25_lockout' of git://github.com/skodak/m…
Browse files Browse the repository at this point in the history
…oodle
  • Loading branch information
Sam Hemelryk committed Jan 7, 2013
2 parents 17e5c6e + c4844bf commit 70601b9
Show file tree
Hide file tree
Showing 14 changed files with 549 additions and 50 deletions.
5 changes: 5 additions & 0 deletions admin/settings/security.php
Expand Up @@ -59,6 +59,11 @@
$temp->add(new admin_setting_configcheckbox('cronclionly', new lang_string('cronclionly', 'admin'), new lang_string('configcronclionly', 'admin'), 0));
$temp->add(new admin_setting_configpasswordunmask('cronremotepassword', new lang_string('cronremotepassword', 'admin'), new lang_string('configcronremotepassword', 'admin'), ''));

$options = array(0=>get_string('no'), 3=>3, 5=>5, 7=>7, 10=>10, 20=>20, 30=>30, 50=>50, 100=>100);
$temp->add(new admin_setting_configselect('lockoutthreshold', new lang_string('lockoutthreshold', 'admin'), new lang_string('lockoutthreshold_desc', 'admin'), 0, $options));
$temp->add(new admin_setting_configduration('lockoutwindow', new lang_string('lockoutwindow', 'admin'), new lang_string('lockoutwindow_desc', 'admin'), 60*30));
$temp->add(new admin_setting_configduration('lockoutduration', new lang_string('lockoutduration', 'admin'), new lang_string('lockoutduration_desc', 'admin'), 60*30));

$temp->add(new admin_setting_configcheckbox('passwordpolicy', new lang_string('passwordpolicy', 'admin'), new lang_string('configpasswordpolicy', 'admin'), 1));
$temp->add(new admin_setting_configtext('minpasswordlength', new lang_string('minpasswordlength', 'admin'), new lang_string('configminpasswordlength', 'admin'), 8, PARAM_INT));
$temp->add(new admin_setting_configtext('minpassworddigits', new lang_string('minpassworddigits', 'admin'), new lang_string('configminpassworddigits', 'admin'), 1, PARAM_INT));
Expand Down
14 changes: 14 additions & 0 deletions admin/user.php
Expand Up @@ -2,6 +2,7 @@

require_once('../config.php');
require_once($CFG->libdir.'/adminlib.php');
require_once($CFG->libdir.'/authlib.php');
require_once($CFG->dirroot.'/user/filters/lib.php');

$delete = optional_param('delete', 0, PARAM_INT);
Expand All @@ -16,6 +17,7 @@
$acl = optional_param('acl', '0', PARAM_INT); // id of user to tweak mnet ACL (requires $access)
$suspend = optional_param('suspend', 0, PARAM_INT);
$unsuspend = optional_param('unsuspend', 0, PARAM_INT);
$unlock = optional_param('unlock', 0, PARAM_INT);

admin_externalpage_setup('editusers');

Expand All @@ -32,6 +34,7 @@
$strshowallusers = get_string('showallusers');
$strsuspend = get_string('suspenduser', 'admin');
$strunsuspend = get_string('unsuspenduser', 'admin');
$strunlock = get_string('unlockaccount', 'admin');
$strconfirm = get_string('confirm');

if (empty($CFG->loginhttps)) {
Expand Down Expand Up @@ -143,6 +146,14 @@
}
}
redirect($returnurl);

} else if ($unlock and confirm_sesskey()) {
require_capability('moodle/user:update', $sitecontext);

if ($user = $DB->get_record('user', array('id'=>$unlock, 'mnethostid'=>$CFG->mnet_localhost_id, 'deleted'=>0))) {
login_unlock_account($user);
}
redirect($returnurl);
}

// create the user filter form
Expand Down Expand Up @@ -303,6 +314,9 @@
}
}

if (login_is_lockedout($user)) {
$buttons[] = html_writer::link(new moodle_url($returnurl, array('unlock'=>$user->id, 'sesskey'=>sesskey())), html_writer::empty_tag('img', array('src'=>$OUTPUT->pix_url('t/unlock'), 'alt'=>$strunlock, 'class'=>'iconsmall')), array('title'=>$strunlock));
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion auth/ldap/auth.php
Expand Up @@ -1643,7 +1643,7 @@ function ntlmsso_finish() {
$username = $cf[$key];
// Here we want to trigger the whole authentication machinery
// to make sure no step is bypassed...
$user = authenticate_user_login($username, $key);
$user = authenticate_user_login($username, $key, false);
if ($user) {
add_to_log(SITEID, 'user', 'login', "view.php?id=$USER->id&course=".SITEID,
$user->id, 0, $user->id);
Expand Down
23 changes: 23 additions & 0 deletions lang/en/admin.php
Expand Up @@ -625,6 +625,28 @@
$string['location'] = 'Location';
$string['locationsettings'] = 'Location settings';
$string['locked'] = 'locked';
$string['lockoutduration'] = 'Account lockout duration';
$string['lockoutduration_desc'] = 'Locked out account is automatically unlocked after this duration.';
$string['lockoutemailbody'] = 'Your account with username {$a->username} on server \'{$a->sitename}\'
was locked out after multiple invalid login attempts.
To unlock the account immediately go to the following address
{$a->link}
In most mail programs, this should appear as a blue link
which you can just click on. If that doesn\'t work,
then cut and paste the address into the address
line at the top of your web browser window.
If you need help, please contact the site administrator,
{$a->admin}';
$string['lockoutemailsubject'] = 'Your account on {$a} was locked out';
$string['lockouterrorunlock'] = 'Invalid account unlock information supplied.';
$string['lockoutthreshold'] = 'Account lockout threshold';
$string['lockoutthreshold_desc'] = 'Select number of failed login attempts that result in account lockout. This feature may be abused in denial of service attacks.';
$string['lockoutwindow'] = 'Account lockout observation window';
$string['lockoutwindow_desc'] = 'Observation time for lockout threshold, if there are no failed attempts the threshold counter is reset after this time.';
$string['log'] = 'Logs';
$string['logguests'] = 'Log guest access';
$string['logguests_help'] = 'This setting enables logging of actions by guest account and not logged in users. High profile sites may want to disable this logging for performance reasons. It is recommended to keep this setting enabled on production sites.';
Expand Down Expand Up @@ -989,6 +1011,7 @@
$string['unicoderecommended'] = 'Storing all your data in Unicode (UTF-8) is recommended. New installations should be performed into databases that have their default character set as Unicode. If you are upgrading, you should perform the UTF-8 migration process (see the Admin page).';
$string['unicoderequired'] = 'It is required that you store all your data in Unicode format (UTF-8). New installations must be performed into databases that have their default character set as Unicode. If you are upgrading, you should perform the UTF-8 migration process (see the Admin page).';
$string['uninstallplugin'] = 'Uninstall';
$string['unlockaccount'] = 'Unlock account';
$string['unsettheme'] = 'Unset theme';
$string['unsupported'] = 'Unsupported';
$string['unsuspenduser'] = 'Activate user account';
Expand Down
1 change: 0 additions & 1 deletion lang/en/moodle.php
Expand Up @@ -649,7 +649,6 @@
$string['errorcreatingactivity'] = 'Unable to create an instance of activity \'{$a}\'';
$string['errorfiletoobig'] = 'The file was bigger than the limit of {$a} bytes';
$string['errornouploadrepo'] = 'There is no upload repository enabled for this site';
$string['errortoomanylogins'] = 'Sorry, you have exceeded the allowed number of login attempts. Restart your browser.';
$string['errorwhenconfirming'] = 'You are not confirmed yet because an error occurred. If you clicked on a link in an email to get here, make sure that the line in your email wasn\'t broken or wrapped. You may have to use cut and paste to reconstruct the link properly.';
$string['everybody'] = 'Everybody';
$string['executeat'] = 'Execute at';
Expand Down
191 changes: 191 additions & 0 deletions lib/authlib.php
Expand Up @@ -61,6 +61,22 @@
define('AUTH_REMOVEUSER_SUSPEND', 1);
define('AUTH_REMOVEUSER_FULLDELETE', 2);

/** Login attempt successful. */
define('AUTH_LOGIN_OK', 0);

/** Can not login because user does not exist. */
define('AUTH_LOGIN_NOUSER', 1);

/** Can not login because user is suspended. */
define('AUTH_LOGIN_SUSPENDED', 2);

/** Can not login, most probably password did not match. */
define('AUTH_LOGIN_FAILED', 3);

/** Can not login because user is locked out. */
define('AUTH_LOGIN_LOCKOUT', 4);


/**
* Abstract authentication plugin.
*
Expand Down Expand Up @@ -507,3 +523,178 @@ function loginpage_idp_list($wantsurl) {
}

}

/**
* Verify if user is locked out.
*
* @param stdClass $user
* @return bool true if user locked out
*/
function login_is_lockedout($user) {
global $CFG;

if ($user->mnethostid != $CFG->mnet_localhost_id) {
return false;
}
if (isguestuser($user)) {
return false;
}

if (empty($CFG->lockoutthreshold)) {
// Lockout not enabled.
return false;
}

if (get_user_preferences('login_lockout_ignored', 0, $user)) {
// This preference may be used for accounts that must not be locked out.
return false;
}

$locked = get_user_preferences('login_lockout', 0, $user);
if (!$locked) {
return false;
}

if (empty($CFG->lockoutduration)) {
// Locked out forever.
return true;
}

if (time() - $locked < $CFG->lockoutduration) {
return true;
}

login_unlock_account($user);

return false;
}

/**
* To be called after valid user login.
* @param stdClass $user
*/
function login_attempt_valid($user) {
global $CFG;

if ($user->mnethostid != $CFG->mnet_localhost_id) {
return;
}
if (isguestuser($user)) {
return;
}

// Always unlock here, there might be some race conditions or leftovers when switching threshold.
login_unlock_account($user);
}

/**
* To be called after failed user login.
* @param stdClass $user
*/
function login_attempt_failed($user) {
global $CFG;

if ($user->mnethostid != $CFG->mnet_localhost_id) {
return;
}
if (isguestuser($user)) {
return;
}

if (empty($CFG->lockoutthreshold)) {
// No threshold means no lockout.
// Always unlock here, there might be some race conditions or leftovers when switching threshold.
login_unlock_account($user);
return;
}

$count = get_user_preferences('login_failed_count', 0, $user);
$last = get_user_preferences('login_failed_last', 0, $user);

if (!empty($CFG->lockoutwindow) and time() - $last > $CFG->lockoutwindow) {
$count = 0;
}

$count = $count+1;

set_user_preference('login_failed_count', $count, $user);
set_user_preference('login_failed_last', time(), $user);

if ($count >= $CFG->lockoutthreshold) {
login_lock_account($user);
}
}

/**
* Lockout user and send notification email.
*
* @param stdClass $user
*/
function login_lock_account($user) {
global $CFG, $SESSION;

if ($user->mnethostid != $CFG->mnet_localhost_id) {
return;
}
if (isguestuser($user)) {
return;
}

if (get_user_preferences('login_lockout_ignored', 0, $user)) {
// This user can not be locked out.
return;
}

$alreadylockedout = get_user_preferences('login_lockout', 0, $user);

set_user_preference('login_lockout', time(), $user);

if ($alreadylockedout == 0) {
$secret = random_string(15);
set_user_preference('login_lockout_secret', $secret, $user);

// Some nasty hackery to get strings and dates localised for target user.
$sessionlang = isset($SESSION->lang) ? $SESSION->lang : null;
if (get_string_manager()->translation_exists($user->lang, false)) {
$SESSION->lang = $user->lang;
moodle_setlocale();
}

$site = get_site();
$supportuser = generate_email_supportuser();

$data = new stdClass();
$data->firstname = $user->firstname;
$data->lastname = $user->lastname;
$data->username = $user->username;
$data->sitename = format_string($site->fullname);
$data->link = $CFG->wwwroot.'/login/unlock_account.php?u='.$user->id.'&s='.$secret;
$data->admin = generate_email_signoff();

$message = get_string('lockoutemailbody', 'admin', $data);
$subject = get_string('lockoutemailsubject', 'admin', format_string($site->fullname));

if ($message) {
// Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
email_to_user($user, $supportuser, $subject, $message);
}

if ($SESSION->lang !== $sessionlang) {
$SESSION->lang = $sessionlang;
moodle_setlocale();
}
}
}

/**
* Unlock user account and reset timers.
*
* @param stdClass $user
*/
function login_unlock_account($user) {
unset_user_preference('login_lockout', $user);
unset_user_preference('login_failed_count', $user);
unset_user_preference('login_failed_last', $user);

// Note: do not clear the lockout secret because user might click on the link repeatedly.
}
19 changes: 19 additions & 0 deletions lib/deprecatedlib.php
Expand Up @@ -30,6 +30,25 @@

defined('MOODLE_INTERNAL') || die();

/**
* Not used any more, the account lockout handling is now
* part of authenticate_user_login().
* @deprecated
*/
function update_login_count() {
// TODO: delete function in Moodle 2.6
debugging('update_login_count() is deprecated, all calls need to be removed');
}

/**
* Not used any more, replaced by proper account lockout.
* @deprecated
*/
function reset_login_count() {
// TODO: delete function in Moodle 2.6
debugging('reset_login_count() is deprecated, all calls need to be removed');
}

/**
* Unsupported session id rewriting.
* @deprecated
Expand Down

0 comments on commit 70601b9

Please sign in to comment.