From 140118e3e8da984fdafe7dbb4fa68fee97039b8a Mon Sep 17 00:00:00 2001 From: Rajesh Taneja Date: Wed, 29 Jan 2014 13:27:50 +0800 Subject: [PATCH] MDL-40911 events: replaced add_to_log with events for login failed --- lang/en/auth.php | 1 + lib/classes/event/user_login_failed.php | 109 ++++++++++++++++++++++++ lib/moodlelib.php | 62 +++++++++++--- lib/tests/authlib_test.php | 71 ++++++++++++++- 4 files changed, 230 insertions(+), 13 deletions(-) create mode 100644 lib/classes/event/user_login_failed.php diff --git a/lang/en/auth.php b/lang/en/auth.php index 6e8a439115a..1d73f7edc81 100644 --- a/lang/en/auth.php +++ b/lang/en/auth.php @@ -83,6 +83,7 @@ $string['errorminpasswordupper'] = 'Passwords must have at least {$a} upper case $string['errorpasswordupdate'] = 'Error updating password, password not changed'; $string['event_user_loggedin'] = 'User has logged in'; $string['eventuserloggedinas'] = 'User logged in as another user'; +$string['eventuserloginfailed'] = 'User login failed'; $string['forcechangepassword'] = 'Force change password'; $string['forcechangepasswordfirst_help'] = 'Force users to change password on their first login to Moodle.'; $string['forcechangepassword_help'] = 'Force users to change password on their next login to Moodle.'; diff --git a/lib/classes/event/user_login_failed.php b/lib/classes/event/user_login_failed.php new file mode 100644 index 00000000000..6533a105fbc --- /dev/null +++ b/lib/classes/event/user_login_failed.php @@ -0,0 +1,109 @@ +. + +/** + * User login failed event. + * + * @package core + * @copyright 2014 Rajesh Taneja + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * User login failed event class. + * + * @property-read array $other { + * Extra information about event. + * + * @type string username name of user. + * @type int reason failure reason. + * } + * + * @package core + * @copyright 2014 Rajesh Taneja + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_login_failed extends \core\event\base { + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->context = \context_system::instance(); + $this->data['crud'] = 'r'; + $this->data['edulevel'] = self::LEVEL_OTHER; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventuserloginfailed', 'auth'); + } + + /** + * Returns non-localised event description with id's for admin use only. + * + * @return string + */ + public function get_description() { + return 'Login failed for user "' . $this->other['username'] . '" for reason id: ' . $this->other['reason']; + } + + /** + * Get URL related to the action. + * + * @return \moodle_url + */ + public function get_url() { + if (isset($this->data['userid'])) { + return new \moodle_url('/user/profile.php', array('id' => $this->data['userid'])); + } else { + return null; + } + } + + /** + * Return legacy data for add_to_log(). + * + * @return array + */ + protected function get_legacy_logdata() { + return array(SITEID, 'login', 'error', 'index.php', $this->other['username']); + } + + /** + * Custom validation. + * + * @throws \coding_exception when validation does not pass. + * @return void + */ + protected function validate_data() { + if (!isset($this->other['reason'])) { + throw new \coding_exception("other['reason'] has to be specified."); + } else if (!isset($this->other['username'])) { + throw new \coding_exception("other['username'] has to be specified."); + } + } + +} diff --git a/lib/moodlelib.php b/lib/moodlelib.php index 312ac4e8502..ece4e4cf5b2 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -4335,16 +4335,24 @@ function authenticate_user_login($username, $password, $ignorelockout=false, &$f // Use manual if auth not set. $auth = empty($user->auth) ? 'manual' : $user->auth; if (!empty($user->suspended)) { - add_to_log(SITEID, 'login', 'error', 'index.php', $username); - error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']); $failurereason = AUTH_LOGIN_SUSPENDED; + + // Trigger login failed event. + $event = \core\event\user_login_failed::create(array('userid' => $user->id, + 'other' => array('username' => $username, 'reason' => $failurereason))); + $event->trigger(); + error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']); return false; } if ($auth=='nologin' or !is_enabled_auth($auth)) { - add_to_log(SITEID, 'login', 'error', 'index.php', $username); - error_log('[client '.getremoteaddr()."] $CFG->wwwroot Disabled Login: $username ".$_SERVER['HTTP_USER_AGENT']); // Legacy way to suspend user. $failurereason = AUTH_LOGIN_SUSPENDED; + + // Trigger login failed event. + $event = \core\event\user_login_failed::create(array('userid' => $user->id, + 'other' => array('username' => $username, 'reason' => $failurereason))); + $event->trigger(); + error_log('[client '.getremoteaddr()."] $CFG->wwwroot Disabled Login: $username ".$_SERVER['HTTP_USER_AGENT']); return false; } $auths = array($auth); @@ -4352,16 +4360,27 @@ function authenticate_user_login($username, $password, $ignorelockout=false, &$f } else { // Check if there's a deleted record (cheaply), this should not happen because we mangle usernames in delete_user(). if ($DB->get_field('user', 'id', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'deleted' => 1))) { - error_log('[client '.getremoteaddr()."] $CFG->wwwroot Deleted Login: $username ".$_SERVER['HTTP_USER_AGENT']); $failurereason = AUTH_LOGIN_NOUSER; + + // Trigger login failed event. + $event = \core\event\user_login_failed::create(array('other' => array('username' => $username, + 'reason' => $failurereason))); + $event->trigger(); + error_log('[client '.getremoteaddr()."] $CFG->wwwroot Deleted Login: $username ".$_SERVER['HTTP_USER_AGENT']); return false; } // Do not try to authenticate non-existent accounts when user creation is not disabled. if (!empty($CFG->authpreventaccountcreation)) { - add_to_log(SITEID, 'login', 'error', 'index.php', $username); - error_log('[client '.getremoteaddr()."] $CFG->wwwroot Unknown user, can not create new accounts: $username ".$_SERVER['HTTP_USER_AGENT']); $failurereason = AUTH_LOGIN_NOUSER; + + // Trigger login failed event. + $event = \core\event\user_login_failed::create(array('other' => array('username' => $username, + 'reason' => $failurereason))); + $event->trigger(); + + error_log('[client '.getremoteaddr()."] $CFG->wwwroot Unknown user, can not create new accounts: $username ". + $_SERVER['HTTP_USER_AGENT']); return false; } @@ -4377,9 +4396,14 @@ function authenticate_user_login($username, $password, $ignorelockout=false, &$f } else if ($user->id) { // Verify login lockout after other ways that may prevent user login. if (login_is_lockedout($user)) { - add_to_log(SITEID, 'login', 'error', 'index.php', $username); - error_log('[client '.getremoteaddr()."] $CFG->wwwroot Login lockout: $username ".$_SERVER['HTTP_USER_AGENT']); $failurereason = AUTH_LOGIN_LOCKOUT; + + // Trigger login failed event. + $event = \core\event\user_login_failed::create(array('userid' => $user->id, + 'other' => array('username' => $username, 'reason' => $failurereason))); + $event->trigger(); + + error_log('[client '.getremoteaddr()."] $CFG->wwwroot Login lockout: $username ".$_SERVER['HTTP_USER_AGENT']); return false; } } else { @@ -4425,14 +4449,21 @@ function authenticate_user_login($username, $password, $ignorelockout=false, &$f if (empty($user->id)) { $failurereason = AUTH_LOGIN_NOUSER; + // Trigger login failed event. + $event = \core\event\user_login_failed::create(array('other' => array('username' => $username, + 'reason' => $failurereason))); + $event->trigger(); return false; } if (!empty($user->suspended)) { // Just in case some auth plugin suspended account. - add_to_log(SITEID, 'login', 'error', 'index.php', $username); - error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']); $failurereason = AUTH_LOGIN_SUSPENDED; + // Trigger login failed event. + $event = \core\event\user_login_failed::create(array('userid' => $user->id, + 'other' => array('username' => $username, 'reason' => $failurereason))); + $event->trigger(); + error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']); return false; } @@ -4442,7 +4473,6 @@ function authenticate_user_login($username, $password, $ignorelockout=false, &$f } // Failed if all the plugins have failed. - add_to_log(SITEID, 'login', 'error', 'index.php', $username); if (debugging('', DEBUG_ALL)) { error_log('[client '.getremoteaddr()."] $CFG->wwwroot Failed Login: $username ".$_SERVER['HTTP_USER_AGENT']); } @@ -4450,8 +4480,16 @@ function authenticate_user_login($username, $password, $ignorelockout=false, &$f if ($user->id) { login_attempt_failed($user); $failurereason = AUTH_LOGIN_FAILED; + // Trigger login failed event. + $event = \core\event\user_login_failed::create(array('userid' => $user->id, + 'other' => array('username' => $username, 'reason' => $failurereason))); + $event->trigger(); } else { $failurereason = AUTH_LOGIN_NOUSER; + // Trigger login failed event. + $event = \core\event\user_login_failed::create(array('other' => array('username' => $username, + 'reason' => $failurereason))); + $event->trigger(); } return false; diff --git a/lib/tests/authlib_test.php b/lib/tests/authlib_test.php index be7666f4fb6..2a1a05ef393 100644 --- a/lib/tests/authlib_test.php +++ b/lib/tests/authlib_test.php @@ -133,35 +133,104 @@ class core_authlib_testcase extends advanced_testcase { $user1 = $this->getDataGenerator()->create_user(array('username'=>'username1', 'password'=>'password1')); $user2 = $this->getDataGenerator()->create_user(array('username'=>'username2', 'password'=>'password2', 'suspended'=>1)); $user3 = $this->getDataGenerator()->create_user(array('username'=>'username3', 'password'=>'password3', 'auth'=>'nologin')); - + // Capture events. + $sink = $this->redirectEvents(); $result = authenticate_user_login('username1', 'password1'); + $events = $sink->get_events(); + $sink->close(); + + // No event is triggred. + $this->assertEmpty($events); $this->assertInstanceOf('stdClass', $result); $this->assertEquals($user1->id, $result->id); $reason = null; + // Capture event. + $sink = $this->redirectEvents(); $result = authenticate_user_login('username1', 'password1', false, $reason); + $events = $sink->get_events(); + $sink->close(); + + // No event is triggred. + $this->assertEmpty($events); $this->assertInstanceOf('stdClass', $result); $this->assertEquals(AUTH_LOGIN_OK, $reason); $reason = null; + // Capture failed login event. + $sink = $this->redirectEvents(); $result = authenticate_user_login('username1', 'nopass', false, $reason); + $events = $sink->get_events(); + $sink->close(); + $event = array_pop($events); + $this->assertFalse($result); $this->assertEquals(AUTH_LOGIN_FAILED, $reason); + // Test Event. + $this->assertInstanceOf('\core\event\user_login_failed', $event); + $expectedlogdata = array(SITEID, 'login', 'error', 'index.php', 'username1'); + $this->assertEventLegacyLogData($expectedlogdata, $event); + $eventdata = $event->get_data(); + $this->assertSame($eventdata['other']['username'], 'username1'); + $this->assertSame($eventdata['other']['reason'], AUTH_LOGIN_FAILED); + $this->assertEventContextNotUsed($event); $reason = null; + // Capture failed login event. + $sink = $this->redirectEvents(); $result = authenticate_user_login('username2', 'password2', false, $reason); + $events = $sink->get_events(); + $sink->close(); + $event = array_pop($events); + $this->assertFalse($result); $this->assertEquals(AUTH_LOGIN_SUSPENDED, $reason); + // Test Event. + $this->assertInstanceOf('\core\event\user_login_failed', $event); + $expectedlogdata = array(SITEID, 'login', 'error', 'index.php', 'username2'); + $this->assertEventLegacyLogData($expectedlogdata, $event); + $eventdata = $event->get_data(); + $this->assertSame($eventdata['other']['username'], 'username2'); + $this->assertSame($eventdata['other']['reason'], AUTH_LOGIN_SUSPENDED); + $this->assertEventContextNotUsed($event); $reason = null; + // Capture failed login event. + $sink = $this->redirectEvents(); $result = authenticate_user_login('username3', 'password3', false, $reason); + $events = $sink->get_events(); + $sink->close(); + $event = array_pop($events); + $this->assertFalse($result); $this->assertEquals(AUTH_LOGIN_SUSPENDED, $reason); + // Test Event. + $this->assertInstanceOf('\core\event\user_login_failed', $event); + $expectedlogdata = array(SITEID, 'login', 'error', 'index.php', 'username3'); + $this->assertEventLegacyLogData($expectedlogdata, $event); + $eventdata = $event->get_data(); + $this->assertSame($eventdata['other']['username'], 'username3'); + $this->assertSame($eventdata['other']['reason'], AUTH_LOGIN_SUSPENDED); + $this->assertEventContextNotUsed($event); $reason = null; + // Capture failed login event. + $sink = $this->redirectEvents(); $result = authenticate_user_login('username4', 'password3', false, $reason); + $events = $sink->get_events(); + $sink->close(); + $event = array_pop($events); + $this->assertFalse($result); $this->assertEquals(AUTH_LOGIN_NOUSER, $reason); + // Test Event. + $this->assertInstanceOf('\core\event\user_login_failed', $event); + $expectedlogdata = array(SITEID, 'login', 'error', 'index.php', 'username4'); + $this->assertEventLegacyLogData($expectedlogdata, $event); + $eventdata = $event->get_data(); + $this->assertSame($eventdata['other']['username'], 'username4'); + $this->assertSame($eventdata['other']['reason'], AUTH_LOGIN_NOUSER); + $this->assertEventContextNotUsed($event); set_config('lockoutthreshold', 3);