jenkins-bot has submitted this change and it was merged. (
https://gerrit.wikimedia.org/r/360787 )
Change subject: Move expensive processing into job queue
......................................................................
Move expensive processing into job queue
Bug: T167731
Depends-On: I618840fafd22d9b6471eb470ef0414e354aa17f5
Change-Id: I1fcd15f523828141e8fadee9a8ad824eacefc0f9
---
M extension.json
A includes/DeferredChecksJob.php
M includes/Hooks.php
M includes/LoginNotify.php
4 files changed, 249 insertions(+), 75 deletions(-)
Approvals:
Niharika29: Looks good to me, approved
jenkins-bot: Verified
diff --git a/extension.json b/extension.json
index b7f69ba..84334b1 100644
--- a/extension.json
+++ b/extension.json
@@ -25,8 +25,9 @@
]
},
"AutoloadClasses": {
- "LoginNotify\\LoginNotify": "includes/LoginNotify.php",
+ "LoginNotify\\DeferredChecksJob":
"includes/DeferredChecksJob.php",
"LoginNotify\\Hooks": "includes/Hooks.php",
+ "LoginNotify\\LoginNotify": "includes/LoginNotify.php",
"LoginNotify\\PresentationModel":
"includes/PresentationModel.php"
},
"Hooks": {
@@ -37,7 +38,7 @@
"LoginNotify\\Hooks::onEchoGetBundleRules"
],
"LoginAuthenticateAudit": [
- "LoginNotifyHooks::onLoginAuthenticateAudit"
+ "LoginNotify\\Hooks::onLoginAuthenticateAudit"
],
"AuthManagerLoginAuthenticateAudit": [
"LoginNotify\\Hooks::onAuthManagerLoginAuthenticateAudit"
@@ -55,6 +56,9 @@
"LoginNotify\\Hooks::onLocalUserCreated"
]
},
+ "JobClasses": {
+ "LoginNotifyChecks": "LoginNotify\\DeferredChecksJob"
+ },
"config": {
"@docLoginNotifyAttemptsKnownIP": "The number of failed login
attempts to permit from a known IP before a notification is triggered.",
"LoginNotifyAttemptsKnownIP": 5,
diff --git a/includes/DeferredChecksJob.php b/includes/DeferredChecksJob.php
new file mode 100644
index 0000000..05096eb
--- /dev/null
+++ b/includes/DeferredChecksJob.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace LoginNotify;
+
+use Exception;
+use Job;
+use Title;
+use User;
+
+/**
+ * Class DeferredChecksJob
+ * @package LoginNotify
+ */
+class DeferredChecksJob extends Job {
+ const TYPE_LOGIN_FAILED = 'failed';
+ const TYPE_LOGIN_SUCCESS = 'success';
+
+ /**
+ * @param Title $title
+ * @param array|bool $params
+ */
+ public function __construct( Title $title, $params = false ) {
+ parent::__construct( 'LoginNotifyChecks', $title, $params );
+ }
+
+ /**
+ * Run the job
+ * @return bool Success
+ */
+ public function run() {
+ $checkType = $this->params['checkType'];
+ $userId = $this->params['userId'];
+ $user = User::newFromId( $userId );
+ if ( !$user ) {
+ throw new Exception( "Can't find user for user id=" .
print_r( $userId, true ) );
+ }
+ if ( !isset( $this->params['subnet'] ) || !is_string(
$this->params['subnet'] ) ) {
+ throw new Exception( __CLASS__
+ . " expected to receive a string parameter
'subnet', got "
+ . print_r( $this->params['subnet'], true )
+ );
+ }
+ $subnet = $this->params['resultSoFar'];
+ if ( !isset( $this->params['resultSoFar'] ) || !is_string(
$this->params['resultSoFar'] ) ) {
+ throw new Exception( __CLASS__
+ . " expected to receive a string parameter
'resultSoFar', got "
+ . print_r( $this->params['resultSoFar'], true )
+ );
+ }
+ $resultSoFar = $this->params['resultSoFar'];
+
+ $loginNotify = new LoginNotify();
+
+ switch ( $checkType ) {
+ case self::TYPE_LOGIN_FAILED:
+ $loginNotify->recordFailureDeferred( $user,
$subnet, $resultSoFar );
+ break;
+ case self::TYPE_LOGIN_SUCCESS:
+ $loginNotify->sendSuccessNoticeDeferred( $user,
$subnet, $resultSoFar );
+ break;
+ default:
+ throw new Exception( 'Unknown check type ' .
print_r( $checkType, true ) );
+ }
+
+ return true;
+ }
+}
diff --git a/includes/Hooks.php b/includes/Hooks.php
index 82a9cce..9685022 100644
--- a/includes/Hooks.php
+++ b/includes/Hooks.php
@@ -50,6 +50,14 @@
'category' => 'login-fail',
'group' => 'negative',
'presentation-model' => PresentationModel::class,
+ // fixme, what does this actually do?
+ 'title-message' => 'loginnotify-login-fail',
+ 'title-params' => [],
+ // FIXME Should count be a parameter
+ 'email-subject-params' => [ 'agent', 'count' ],
+ 'email-body-batch-params' => [ 'agent', 'count' ],
+ // FIXME is it ok not to set batch email messages, since
+ // we have immediate flag?
'icon' => 'LoginNotify-user-avatar',
'immediate' => true,
];
diff --git a/includes/LoginNotify.php b/includes/LoginNotify.php
index bb911e5..b0cd393 100644
--- a/includes/LoginNotify.php
+++ b/includes/LoginNotify.php
@@ -12,6 +12,8 @@
use CentralAuthUser;
use Config;
use EchoEvent;
+use JobQueueGroup;
+use JobSpecification;
use MediaWiki\MediaWikiServices;
use WebRequest;
use Wikimedia\Rdbms\Database;
@@ -34,7 +36,14 @@
class LoginNotify implements LoggerAwareInterface {
const COOKIE_NAME = 'loginnotify_prevlogins';
- const NO_INFO_AVAILABLE = 2;
+
+ // The following 3 constants specify outcomes of user search
+ /** User's system is known to us */
+ const USER_KNOWN = 'known';
+ /** User's system is new for us, based on our data */
+ const USER_NOT_KNOWN = 'not known';
+ /** We don't have data to confirm or deny this is a known system */
+ const USER_NO_INFO = 'no info';
/** @var BagOStuff */
private $cache;
@@ -111,39 +120,51 @@
}
/**
- * Is the current computer known to be used by the given user
+ * Is the current computer known to be used by the current user (fast
checks)
+ * To be used for checks that are fast enough to be run at the moment
the user logs in.
*
* @param User $user User in question
+ * @param WebRequest $request
+ * @return string One of USER_* constants
+ */
+ private function isKnownSystemFast( User $user, WebRequest $request ) {
+ $result = $this->userIsInCookie( $user, $request );
+ if ( $result === self::USER_KNOWN ) {
+ return $result;
+ }
+
+ $result = $this->mergeResults( $result, $this->userIsInCache(
$user, $request ) );
+
+ return $result;
+ }
+
+ /**
+ * Is the current computer known to be used by the current user (slow
checks)
+ * These checks are slow enough to be run via the job queue
+ *
+ * @param User $user User in question
+ * @param string $subnet User's current subnet
+ * @param string $resultSoFar Value returned by isKnownSystemFast() or
null if
+ * not available.
* @return bool true if the user has used this computer before
*/
- private function isFromKnownIP( User $user ) {
- $cookieResult = $this->isUserInCookie( $user );
- if ( $cookieResult === true ) {
- // User has cookie
+ private function isKnownSystemSlow( User $user, $subnet, $resultSoFar =
null ) {
+ $result = $this->checkUserAllWikis( $user, $subnet );
+ if ( $result === self::USER_KNOWN ) {
return true;
}
- $cacheResult = $this->isUserInCache( $user );
- if ( $cacheResult === true ) {
- return true;
- }
-
- $cuResult = $this->isUserInCheckUser( $user );
- if ( $cuResult === true ) {
- return true;
+ if ( $resultSoFar !== null ) {
+ $result = $this->mergeResults( $result, $resultSoFar );
}
// If we have no check user data for the user, and there was
// no cookie supplied, just pass the user in, since we don't
have
// enough info to determine if from known ip.
// FIXME: Does this make sense
- if (
- $cuResult === self::NO_INFO_AVAILABLE &&
- $cookieResult === self::NO_INFO_AVAILABLE &&
- $cacheResult === self::NO_INFO_AVAILABLE
- ) {
+ if ( $result === self::USER_NO_INFO ) {
// We have to be careful here. Whether $cookieResult is
- // self::NO_INFO_AVAILABLE, is under control of the
attacker.
+ // self::USER_NO_INFO, is under control of the attacker.
// If checking CheckUser is disabled, then we should not
// hit this branch.
@@ -160,17 +181,18 @@
/**
* Check if we cached this user's ip address from last login.
*
- * @param User $user User in question.
- * @return Mixed true, false or self::NO_INFO_AVAILABLE.
+ * @param User $user User in question
+ * @param WebRequest $request
+ * @return string One of USER_* constants
*/
- private function isUserInCache( User $user ) {
- $ipPrefix = $this->getIPNetwork( $user->getRequest()->getIP() );
+ private function userIsInCache( User $user, WebRequest $request ) {
+ $ipPrefix = $this->getIPNetwork( $request->getIP() );
$key = $this->getKey( $user, 'prevSubnet' );
$res = $this->cache->get( $key );
if ( $res !== false ) {
- return $res === $ipPrefix;
+ return $res === $ipPrefix ? self::USER_KNOWN :
self::USER_NOT_KNOWN;
}
- return self::NO_INFO_AVAILABLE;
+ return self::USER_NO_INFO;
}
/**
@@ -179,29 +201,29 @@
* If CentralAuth is installed, this will check not only the current
wiki,
* but also the ten wikis where user has most edits on.
*
- * @param User $user User in question.
- * @return Mixed true, false or self::NO_INFO_AVAILABLE.
+ * @param User $user User in question
+ * @param string $subnet User's current subnet
+ * @return string One of USER_* constants
*/
- private function isUserInCheckUser( User $user ) {
+ private function checkUserAllWikis( User $user, $subnet ) {
if ( !$this->config->get( 'LoginNotifyCheckKnownIPs' )
|| !class_exists( 'CheckUser' )
) {
- // Check user checks disabled.
- // Note: Its important this be false and not
self::NO_INFO_AVAILABLE.
- return false;
+ // Checkuser checks disabled.
+ // Note: It's important this be USER_NOT_KNOWN and not
USER_NO_INFO.
+ return self::USER_NOT_KNOWN;
}
-
- $haveAnyInfo = false;
- $prefix = $this->getIPNetwork( $user->getRequest()->getIP() );
$dbr = wfGetDB( DB_SLAVE );
- $localResult = $this->isUserInCheckUserQuery( $user->getId(),
$prefix, $dbr );
- if ( $localResult ) {
- return true;
+ $result = $this->checkUserOneWiki( $user->getId(), $subnet,
$dbr );
+ if ( $result === self::USER_KNOWN ) {
+ return $result;
}
- if ( !$haveAnyInfo ) {
- $haveAnyInfo = $this->isUserInCheckUserAnyInfo(
$user->getId(), $dbr );
+ if ( $result === self::USER_NO_INFO
+ && $this->userHasCheckUserData( $user->getId(), $dbr )
+ ) {
+ $result = self::USER_NOT_KNOWN;
}
// Also check checkuser table on the top ten wikis where this
user has
@@ -244,28 +266,27 @@
}
// FIXME The case where there are no CU
entries for
// this user.
- $res = $this->isUserInCheckUserQuery(
+ $res = $this->checkUserOneWiki(
$localInfo['id'],
- $prefix,
+ $subnet,
$dbrLocal
);
if ( $res ) {
$lb->reuseConnection( $dbrLocal
);
- return true;
+ return self::USER_KNOWN;
}
- if ( !$haveAnyInfo ) {
- $haveAnyInfo =
$this->isUserInCheckUserAnyInfo( $user->getId(), $dbr );
+ if ( $result === self::USER_NO_INFO
+ &&
$this->userHasCheckUserData( $user->getId(), $dbr )
+ ) {
+ $result = self::USER_NOT_KNOWN;
}
$lb->reuseConnection( $dbrLocal );
$count++;
}
}
}
- if ( !$haveAnyInfo ) {
- return self::NO_INFO_AVAILABLE;
- }
- return false;
+ return $result;
}
/**
@@ -277,9 +298,9 @@
* @param int $userId User id number (Not necessarily for the local
wiki)
* @param string $ipFragment Prefix to match against cuc_ip (from
$this->getIPNetwork())
* @param Database $dbr A database connection (possibly foreign)
- * @return bool If $ipFragment is in check user db
+ * @return string One of USER_* constants
*/
- private function isUserInCheckUserQuery( $userId, $ipFragment, Database
$dbr ) {
+ private function checkUserOneWiki( $userId, $ipFragment, Database $dbr
) {
// For some unknown reason, the index is on
// (cuc_user, cuc_ip, cuc_timestamp), instead of
// cuc_ip_hex which would be ideal.
@@ -297,7 +318,7 @@
],
__METHOD__
);
- return $IPHasBeenUsedBefore;
+ return $IPHasBeenUsedBefore ? self::USER_KNOWN :
self::USER_NO_INFO;
}
/**
@@ -311,9 +332,9 @@
* @todo FIXME Does this behaviour make sense, esp. with cookie check?
* @param int $userId User id number (possibly on foreign wiki)
* @param Database $dbr DB connection (possibly to foreign wiki)
- * @return bool|mixed
+ * @return bool
*/
- private function isUserInCheckUserAnyInfo( $userId, Database $dbr ) {
+ private function userHasCheckUserData( $userId, Database $dbr ) {
// Verify that we actually have IP info for
// this user.
// @todo: Should this instead be if we have a
@@ -402,19 +423,38 @@
}
/**
+ * Merges results of various isKnownSystem*() checks
+ *
+ * @param string $x One of USER_* constants
+ * @param string $y One of USER_* constants
+ * @return string
+ */
+ private function mergeResults( $x, $y ) {
+ if ( $x === self::USER_KNOWN || $y === self::USER_KNOWN ) {
+ return self::USER_KNOWN;
+ }
+ if ( $x === self::USER_NOT_KNOWN || $y === self::USER_NOT_KNOWN
) {
+ return self::USER_NOT_KNOWN;
+ }
+ return self::USER_NO_INFO;
+ }
+
+ /**
* Check if a certain user is in the cookie.
*
* @param User $user User in question
- * @return bool|integer Either true, false, or self::NO_INFO_AVAILABLE.
+ * @param WebRequest $request
+ * @return string One of USER_* constants
*/
- private function isUserInCookie( User $user ) {
- $cookie = $this->getPrevLoginCookie( $user->getRequest() );
+ private function userIsInCookie( User $user, WebRequest $request ) {
+ $cookie = $this->getPrevLoginCookie( $request );
+
+ // FIXME, does this really make sense?
if ( $cookie === '' ) {
- // FIXME, does this really make sense?
- return self::NO_INFO_AVAILABLE;
+ return self::USER_NO_INFO;
}
list( $userKnown, ) = $this->checkAndGenerateCookie( $user,
$cookie );
- return $userKnown;
+ return $userKnown ? self::USER_KNOWN : self::USER_NOT_KNOWN;
}
/**
@@ -529,7 +569,7 @@
* @param string $username Username,
* @param int|bool $year int [Optional] Year. Default to current year
* @param string|bool $salt [Optional] Salt (expected to be base-36
encoded)
- * @return String A record for the cookie
+ * @return string A record for the cookie
*/
private function generateUserCookieRecord( $username, $year = false,
$salt = false ) {
if ( $year === false ) {
@@ -570,7 +610,7 @@
*
* @param User $user
*/
- private function incNewIP( User $user ) {
+ private function recordLoginFailureFromUnknownSystem( User $user ) {
$key = $this->getKey( $user, 'new' );
$count = $this->checkAndIncKey(
$key,
@@ -582,14 +622,14 @@
}
}
- /*
+ /**
* Increment hit counters for a failed login from a known computer.
*
* If a sufficient number of hits have accumulated, send an echo notice.
*
* @param User $user
*/
- private function incKnownIP( User $user ) {
+ private function recordLoginFailureFromKnownSystem( User $user ) {
$key = $this->getKey( $user, 'known' );
$count = $this->checkAndIncKey(
$key,
@@ -661,27 +701,82 @@
/**
* On login failure, record failure and maybe send notice
*
- * @param User $user The user whose account was attempted to log into.
+ * @param User $user User in question
*/
public function recordFailure( User $user ) {
- $fromKnownIP = $this->isFromKnownIP( $user );
- if ( $fromKnownIP ) {
- $this->incKnownIP( $user );
+ $known = $this->isKnownSystemFast( $user, $user->getRequest() );
+ if ( $known === self::USER_KNOWN ) {
+ $this->recordLoginFailureFromKnownSystem( $user );
} else {
- $this->incNewIP( $user );
+ $this->createJob( DeferredChecksJob::TYPE_LOGIN_FAILED,
+ $user, $user->getRequest(), $known
+ );
}
}
/**
- * Send a notice on successful login from an unknown IP.
+ * Asynchronous part of recordFailure(), to be called from
DeferredChecksJob
+ *
+ * @param User $user User in question
+ * @param string $subnet User's current subnet
+ * @param string $resultSoFar Value returned by isKnownSystemFast()
+ */
+ public function recordFailureDeferred( User $user, $subnet,
$resultSoFar ) {
+ $isKnown = $this->isKnownSystemSlow( $user, $subnet,
$resultSoFar );
+ if ( !$isKnown ) {
+ $this->recordLoginFailureFromUnknownSystem( $user );
+ }
+ }
+
+ /**
+ * Send a notice on successful login from an unknown IP
*
* @param User $user User account in question.
*/
public function sendSuccessNotice( User $user ) {
- if ( $this->config->get( 'LoginNotifyEnableOnSuccess' )
- && !$this->isFromKnownIP( $user )
- ) {
+ if ( !$this->config->get( 'LoginNotifyEnableOnSuccess' ) ) {
+ return;
+ }
+ $result = $this->isKnownSystemFast( $user, $user->getRequest()
);
+ if ( $result !== self::USER_KNOWN ) {
+ $this->createJob( DeferredChecksJob::TYPE_LOGIN_SUCCESS,
+ $user, $user->getRequest(), $result
+ );
+ }
+ }
+
+ /**
+ * Asynchronous part of sendSuccessNotice(), to be called from
DeferredChecksJob
+ *
+ * @param User $user User in question
+ * @param string $subnet User's current subnet
+ * @param string $resultSoFar Value returned by isKnownSystemFast()
+ */
+ public function sendSuccessNoticeDeferred( User $user, $subnet,
$resultSoFar ) {
+ $isKnown = $this->isKnownSystemSlow( $user, $subnet,
$resultSoFar );
+ if ( !$isKnown ) {
$this->sendNotice( $user, 'login-success' );
}
}
+
+ /**
+ * Creates and enqueues a job to do asynchronous processing of user
login success/failure
+ *
+ * @param string $type Job type, one of DeferredChecksJob::TYPE_*
constants
+ * @param User $user User in question
+ * @param WebRequest $request
+ * @param string $resultSoFar Value returned by isKnownSystemFast()
+ */
+ private function createJob( $type, User $user, WebRequest $request,
$resultSoFar ) {
+ $subnet = $this->getIPNetwork( $request->getIP() );
+ $job = new JobSpecification( 'LoginNotifyChecks',
+ [
+ 'checkType' => $type,
+ 'userId' => $user->getId(),
+ 'subnet' => $subnet,
+ 'resultSoFar' => $resultSoFar,
+ ]
+ );
+ JobQueueGroup::singleton()->push( $job );
+ }
}
--
To view, visit https://gerrit.wikimedia.org/r/360787
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I1fcd15f523828141e8fadee9a8ad824eacefc0f9
Gerrit-PatchSet: 8
Gerrit-Project: mediawiki/extensions/LoginNotify
Gerrit-Branch: master
Gerrit-Owner: MaxSem <[email protected]>
Gerrit-Reviewer: Aaron Schulz <[email protected]>
Gerrit-Reviewer: Kaldari <[email protected]>
Gerrit-Reviewer: Legoktm <[email protected]>
Gerrit-Reviewer: MaxSem <[email protected]>
Gerrit-Reviewer: Niharika29 <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits