MediaWiki REL1_34
BotPassword.php
Go to the documentation of this file.
1<?php
25
30class BotPassword implements IDBAccessObject {
31
32 const APPID_MAXLENGTH = 32;
33
35 private $isSaved;
36
38 private $centralId;
39
41 private $appId;
42
44 private $token;
45
48
50 private $grants;
51
53 private $flags = self::READ_NORMAL;
54
60 protected function __construct( $row, $isSaved, $flags = self::READ_NORMAL ) {
61 $this->isSaved = $isSaved;
62 $this->flags = $flags;
63
64 $this->centralId = (int)$row->bp_user;
65 $this->appId = $row->bp_app_id;
66 $this->token = $row->bp_token;
67 $this->restrictions = MWRestrictions::newFromJson( $row->bp_restrictions );
68 $this->grants = FormatJson::decode( $row->bp_grants );
69 }
70
76 public static function getDB( $db ) {
78
79 $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
81 ? $lbFactory->getExternalLB( $wgBotPasswordsCluster )
82 : $lbFactory->getMainLB( $wgBotPasswordsDatabase );
83 return $lb->getConnectionRef( $db, [], $wgBotPasswordsDatabase );
84 }
85
93 public static function newFromUser( User $user, $appId, $flags = self::READ_NORMAL ) {
94 $centralId = CentralIdLookup::factory()->centralIdFromLocalUser(
95 $user, CentralIdLookup::AUDIENCE_RAW, $flags
96 );
97 return $centralId ? self::newFromCentralId( $centralId, $appId, $flags ) : null;
98 }
99
107 public static function newFromCentralId( $centralId, $appId, $flags = self::READ_NORMAL ) {
109
110 if ( !$wgEnableBotPasswords ) {
111 return null;
112 }
113
114 list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
115 $db = self::getDB( $index );
116 $row = $db->selectRow(
117 'bot_passwords',
118 [ 'bp_user', 'bp_app_id', 'bp_token', 'bp_restrictions', 'bp_grants' ],
119 [ 'bp_user' => $centralId, 'bp_app_id' => $appId ],
120 __METHOD__,
121 $options
122 );
123 return $row ? new self( $row, true, $flags ) : null;
124 }
125
138 public static function newUnsaved( array $data, $flags = self::READ_NORMAL ) {
139 $row = (object)[
140 'bp_user' => 0,
141 'bp_app_id' => isset( $data['appId'] ) ? trim( $data['appId'] ) : '',
142 'bp_token' => '**unsaved**',
143 'bp_restrictions' => $data['restrictions'] ?? MWRestrictions::newDefault(),
144 'bp_grants' => $data['grants'] ?? [],
145 ];
146
147 if (
148 $row->bp_app_id === '' || strlen( $row->bp_app_id ) > self::APPID_MAXLENGTH ||
149 !$row->bp_restrictions instanceof MWRestrictions ||
150 !is_array( $row->bp_grants )
151 ) {
152 return null;
153 }
154
155 $row->bp_restrictions = $row->bp_restrictions->toJson();
156 $row->bp_grants = FormatJson::encode( $row->bp_grants );
157
158 if ( isset( $data['user'] ) ) {
159 if ( !$data['user'] instanceof User ) {
160 return null;
161 }
162 $row->bp_user = CentralIdLookup::factory()->centralIdFromLocalUser(
163 $data['user'], CentralIdLookup::AUDIENCE_RAW, $flags
164 );
165 } elseif ( isset( $data['username'] ) ) {
166 $row->bp_user = CentralIdLookup::factory()->centralIdFromName(
167 $data['username'], CentralIdLookup::AUDIENCE_RAW, $flags
168 );
169 } elseif ( isset( $data['centralId'] ) ) {
170 $row->bp_user = $data['centralId'];
171 }
172 if ( !$row->bp_user ) {
173 return null;
174 }
175
176 return new self( $row, false, $flags );
177 }
178
183 public function isSaved() {
184 return $this->isSaved;
185 }
186
191 public function getUserCentralId() {
192 return $this->centralId;
193 }
194
199 public function getAppId() {
200 return $this->appId;
201 }
202
207 public function getToken() {
208 return $this->token;
209 }
210
215 public function getRestrictions() {
216 return $this->restrictions;
217 }
218
223 public function getGrants() {
224 return $this->grants;
225 }
226
231 public static function getSeparator() {
234 }
235
240 protected function getPassword() {
241 list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $this->flags );
242 $db = self::getDB( $index );
243 $password = $db->selectField(
244 'bot_passwords',
245 'bp_password',
246 [ 'bp_user' => $this->centralId, 'bp_app_id' => $this->appId ],
247 __METHOD__,
248 $options
249 );
250 if ( $password === false ) {
251 return PasswordFactory::newInvalidPassword();
252 }
253
254 $passwordFactory = MediaWikiServices::getInstance()->getPasswordFactory();
255 try {
256 return $passwordFactory->newFromCiphertext( $password );
257 } catch ( PasswordError $ex ) {
258 return PasswordFactory::newInvalidPassword();
259 }
260 }
261
267 public function isInvalid() {
268 return $this->getPassword() instanceof InvalidPassword;
269 }
270
277 public function save( $operation, Password $password = null ) {
278 $conds = [
279 'bp_user' => $this->centralId,
280 'bp_app_id' => $this->appId,
281 ];
282 $fields = [
283 'bp_token' => MWCryptRand::generateHex( User::TOKEN_LENGTH ),
284 'bp_restrictions' => $this->restrictions->toJson(),
285 'bp_grants' => FormatJson::encode( $this->grants ),
286 ];
287
288 if ( $password !== null ) {
289 $fields['bp_password'] = $password->toString();
290 } elseif ( $operation === 'insert' ) {
291 $fields['bp_password'] = PasswordFactory::newInvalidPassword()->toString();
292 }
293
294 $dbw = self::getDB( DB_MASTER );
295 switch ( $operation ) {
296 case 'insert':
297 $dbw->insert( 'bot_passwords', $fields + $conds, __METHOD__, [ 'IGNORE' ] );
298 break;
299
300 case 'update':
301 $dbw->update( 'bot_passwords', $fields, $conds, __METHOD__ );
302 break;
303
304 default:
305 return false;
306 }
307 $ok = (bool)$dbw->affectedRows();
308 if ( $ok ) {
309 $this->token = $dbw->selectField( 'bot_passwords', 'bp_token', $conds, __METHOD__ );
310 $this->isSaved = true;
311 }
312 return $ok;
313 }
314
319 public function delete() {
320 $conds = [
321 'bp_user' => $this->centralId,
322 'bp_app_id' => $this->appId,
323 ];
324 $dbw = self::getDB( DB_MASTER );
325 $dbw->delete( 'bot_passwords', $conds, __METHOD__ );
326 $ok = (bool)$dbw->affectedRows();
327 if ( $ok ) {
328 $this->token = '**unsaved**';
329 $this->isSaved = false;
330 }
331 return $ok;
332 }
333
339 public static function invalidateAllPasswordsForUser( $username ) {
340 $centralId = CentralIdLookup::factory()->centralIdFromName(
341 $username, CentralIdLookup::AUDIENCE_RAW, CentralIdLookup::READ_LATEST
342 );
343 return $centralId && self::invalidateAllPasswordsForCentralId( $centralId );
344 }
345
351 public static function invalidateAllPasswordsForCentralId( $centralId ) {
353
354 if ( !$wgEnableBotPasswords ) {
355 return false;
356 }
357
358 $dbw = self::getDB( DB_MASTER );
359 $dbw->update(
360 'bot_passwords',
361 [ 'bp_password' => PasswordFactory::newInvalidPassword()->toString() ],
362 [ 'bp_user' => $centralId ],
363 __METHOD__
364 );
365 return (bool)$dbw->affectedRows();
366 }
367
373 public static function removeAllPasswordsForUser( $username ) {
374 $centralId = CentralIdLookup::factory()->centralIdFromName(
375 $username, CentralIdLookup::AUDIENCE_RAW, CentralIdLookup::READ_LATEST
376 );
377 return $centralId && self::removeAllPasswordsForCentralId( $centralId );
378 }
379
385 public static function removeAllPasswordsForCentralId( $centralId ) {
387
388 if ( !$wgEnableBotPasswords ) {
389 return false;
390 }
391
392 $dbw = self::getDB( DB_MASTER );
393 $dbw->delete(
394 'bot_passwords',
395 [ 'bp_user' => $centralId ],
396 __METHOD__
397 );
398 return (bool)$dbw->affectedRows();
399 }
400
406 public static function generatePassword( $config ) {
407 return PasswordFactory::generateRandomPasswordString(
408 max( 32, $config->get( 'MinimalPasswordLength' ) ) );
409 }
410
420 public static function canonicalizeLoginData( $username, $password ) {
421 $sep = self::getSeparator();
422 // the strlen check helps minimize the password information obtainable from timing
423 if ( strlen( $password ) >= 32 && strpos( $username, $sep ) !== false ) {
424 // the separator is not valid in new usernames but might appear in legacy ones
425 if ( preg_match( '/^[0-9a-w]{32,}$/', $password ) ) {
426 return [ $username, $password ];
427 }
428 } elseif ( strlen( $password ) > 32 && strpos( $password, $sep ) !== false ) {
429 $segments = explode( $sep, $password );
430 $password = array_pop( $segments );
431 $appId = implode( $sep, $segments );
432 if ( preg_match( '/^[0-9a-w]{32,}$/', $password ) ) {
433 return [ $username . $sep . $appId, $password ];
434 }
435 }
436 return false;
437 }
438
446 public static function login( $username, $password, WebRequest $request ) {
448
449 if ( !$wgEnableBotPasswords ) {
450 return Status::newFatal( 'botpasswords-disabled' );
451 }
452
453 $manager = MediaWiki\Session\SessionManager::singleton();
454 $provider = $manager->getProvider( BotPasswordSessionProvider::class );
455 if ( !$provider ) {
456 return Status::newFatal( 'botpasswords-no-provider' );
457 }
458
459 // Split name into name+appId
460 $sep = self::getSeparator();
461 if ( strpos( $username, $sep ) === false ) {
462 return self::loginHook( $username, null, Status::newFatal( 'botpasswords-invalid-name', $sep ) );
463 }
464 list( $name, $appId ) = explode( $sep, $username, 2 );
465
466 // Find the named user
467 $user = User::newFromName( $name );
468 if ( !$user || $user->isAnon() ) {
469 return self::loginHook( $user ?: $name, null, Status::newFatal( 'nosuchuser', $name ) );
470 }
471
472 if ( $user->isLocked() ) {
473 return Status::newFatal( 'botpasswords-locked' );
474 }
475
476 $throttle = null;
477 if ( !empty( $wgPasswordAttemptThrottle ) ) {
479 'type' => 'botpassword',
480 'cache' => ObjectCache::getLocalClusterInstance(),
481 ] );
482 $result = $throttle->increase( $user->getName(), $request->getIP(), __METHOD__ );
483 if ( $result ) {
484 $msg = wfMessage( 'login-throttled' )->durationParams( $result['wait'] );
485 return self::loginHook( $user, null, Status::newFatal( $msg ) );
486 }
487 }
488
489 // Get the bot password
490 $bp = self::newFromUser( $user, $appId );
491 if ( !$bp ) {
492 return self::loginHook( $user, $bp,
493 Status::newFatal( 'botpasswords-not-exist', $name, $appId ) );
494 }
495
496 // Check restrictions
497 $status = $bp->getRestrictions()->check( $request );
498 if ( !$status->isOK() ) {
499 return self::loginHook( $user, $bp, Status::newFatal( 'botpasswords-restriction-failed' ) );
500 }
501
502 // Check the password
503 $passwordObj = $bp->getPassword();
504 if ( $passwordObj instanceof InvalidPassword ) {
505 return self::loginHook( $user, $bp,
506 Status::newFatal( 'botpasswords-needs-reset', $name, $appId ) );
507 }
508 if ( !$passwordObj->verify( $password ) ) {
509 return self::loginHook( $user, $bp, Status::newFatal( 'wrongpassword' ) );
510 }
511
512 // Ok! Create the session.
513 if ( $throttle ) {
514 $throttle->clear( $user->getName(), $request->getIP() );
515 }
516 return self::loginHook( $user, $bp,
517 // @phan-suppress-next-line PhanUndeclaredMethod
518 Status::newGood( $provider->newSessionForRequest( $user, $bp, $request ) ) );
519 }
520
532 private static function loginHook( $user, $bp, Status $status ) {
533 $extraData = [];
534 if ( $user instanceof User ) {
535 $name = $user->getName();
536 if ( $bp ) {
537 $extraData['appId'] = $name . self::getSeparator() . $bp->getAppId();
538 }
539 } else {
540 $name = $user;
541 $user = null;
542 }
543
544 if ( $status->isGood() ) {
545 $response = AuthenticationResponse::newPass( $name );
546 } else {
547 $response = AuthenticationResponse::newFail( $status->getMessage() );
548 }
549 Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $response, $user, $name, $extraData ] );
550
551 return $status;
552 }
553}
getDB()
$wgUserrightsInterwikiDelimiter
Character used as a delimiter when testing for interwiki userrights (In Special:UserRights,...
bool $wgEnableBotPasswords
Whether to enable bot passwords.
$wgPasswordAttemptThrottle
Limit password attempts to X attempts per Y seconds per IP per account.
string bool $wgBotPasswordsDatabase
Database name for the bot_passwords table.
string bool $wgBotPasswordsCluster
Cluster for the bot_passwords table If false, the normal cluster will be used.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Utility class for bot passwords.
static newFromUser(User $user, $appId, $flags=self::READ_NORMAL)
Load a BotPassword from the database.
isInvalid()
Whether the password is currently invalid.
__construct( $row, $isSaved, $flags=self::READ_NORMAL)
const APPID_MAXLENGTH
static invalidateAllPasswordsForUser( $username)
Invalidate all passwords for a user, by name.
static loginHook( $user, $bp, Status $status)
Call AuthManagerLoginAuthenticateAudit.
getRestrictions()
Get the restrictions.
MWRestrictions $restrictions
static generatePassword( $config)
Returns a (raw, unhashed) random password string.
getPassword()
Get the password.
getUserCentralId()
Get the central user ID.
getGrants()
Get the grants.
string[] $grants
string $token
static getDB( $db)
Get a database connection for the bot passwords database.
getAppId()
Get the app ID.
static newFromCentralId( $centralId, $appId, $flags=self::READ_NORMAL)
Load a BotPassword from the database.
static login( $username, $password, WebRequest $request)
Try to log the user in.
static invalidateAllPasswordsForCentralId( $centralId)
Invalidate all passwords for a user, by central ID.
isSaved()
Indicate whether this is known to be saved.
string $appId
static removeAllPasswordsForUser( $username)
Remove all passwords for a user, by name.
static removeAllPasswordsForCentralId( $centralId)
Remove all passwords for a user, by central ID.
static newUnsaved(array $data, $flags=self::READ_NORMAL)
Create an unsaved BotPassword.
getToken()
Get the token.
static getSeparator()
Get the separator for combined user name + app ID.
save( $operation, Password $password=null)
Save the BotPassword to the database.
static canonicalizeLoginData( $username, $password)
There are two ways to login with a bot password: "username@appId", "password" and "username",...
Represents an invalid password hash.
static generateHex( $chars)
Generate a run of cryptographically random data and return it in hexadecimal string format.
A class to check request restrictions expressed as a JSON object.
static newFromJson( $json)
This is a value object to hold authentication response data.
MediaWikiServices is the service locator for the application scope of MediaWiki.
Show an error when any operation involving passwords fails to run.
Represents a password hash for use in authentication.
Definition Password.php:61
isGood()
Returns whether the operation completed and didn't have any error or warnings.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:40
getMessage( $shortContext=false, $longContext=false, $lang=null)
Get a bullet list of the errors as a Message object.
Definition Status.php:232
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:51
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
getIP()
Work out the IP address based on various globals For trusted proxies, use the XFF client IP (first of...
Interface for database access objects.
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
const DB_MASTER
Definition defines.php:26