MediaWiki  master
BotPassword.php
Go to the documentation of this file.
1 <?php
27 
32 class BotPassword implements IDBAccessObject {
33 
34  public const APPID_MAXLENGTH = 32;
35 
39  public const PASSWORD_MINLENGTH = 32;
40 
45  public const RESTRICTIONS_MAXLENGTH = 65535;
46 
51  public const GRANTS_MAXLENGTH = 65535;
52 
54  private $isSaved;
55 
57  private $centralId;
58 
60  private $appId;
61 
63  private $token;
64 
66  private $restrictions;
67 
69  private $grants;
70 
72  private $flags = self::READ_NORMAL;
73 
79  private function __construct( $row, $isSaved, $flags = self::READ_NORMAL ) {
80  $this->isSaved = $isSaved;
81  $this->flags = $flags;
82 
83  $this->centralId = (int)$row->bp_user;
84  $this->appId = $row->bp_app_id;
85  $this->token = $row->bp_token;
86  $this->restrictions = MWRestrictions::newFromJson( $row->bp_restrictions );
87  $this->grants = FormatJson::decode( $row->bp_grants );
88  }
89 
95  public static function getDB( $db ) {
97 
98  $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
100  ? $lbFactory->getExternalLB( $wgBotPasswordsCluster )
101  : $lbFactory->getMainLB( $wgBotPasswordsDatabase );
102  return $lb->getConnectionRef( $db, [], $wgBotPasswordsDatabase );
103  }
104 
112  public static function newFromUser( User $user, $appId, $flags = self::READ_NORMAL ) {
113  $centralId = CentralIdLookup::factory()->centralIdFromLocalUser(
115  );
117  }
118 
126  public static function newFromCentralId( $centralId, $appId, $flags = self::READ_NORMAL ) {
127  global $wgEnableBotPasswords;
128 
129  if ( !$wgEnableBotPasswords ) {
130  return null;
131  }
132 
133  list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
134  $db = self::getDB( $index );
135  $row = $db->selectRow(
136  'bot_passwords',
137  [ 'bp_user', 'bp_app_id', 'bp_token', 'bp_restrictions', 'bp_grants' ],
138  [ 'bp_user' => $centralId, 'bp_app_id' => $appId ],
139  __METHOD__,
140  $options
141  );
142  return $row ? new self( $row, true, $flags ) : null;
143  }
144 
157  public static function newUnsaved( array $data, $flags = self::READ_NORMAL ) {
158  if ( isset( $data['user'] ) && ( !$data['user'] instanceof User ) ) {
159  return null;
160  }
161 
162  $row = (object)[
163  'bp_user' => 0,
164  'bp_app_id' => isset( $data['appId'] ) ? trim( $data['appId'] ) : '',
165  'bp_token' => '**unsaved**',
166  'bp_restrictions' => $data['restrictions'] ?? MWRestrictions::newDefault(),
167  'bp_grants' => $data['grants'] ?? [],
168  ];
169 
170  if (
171  $row->bp_app_id === '' ||
172  strlen( $row->bp_app_id ) > self::APPID_MAXLENGTH ||
173  !$row->bp_restrictions instanceof MWRestrictions ||
174  !is_array( $row->bp_grants )
175  ) {
176  return null;
177  }
178 
179  $row->bp_restrictions = $row->bp_restrictions->toJson();
180  $row->bp_grants = FormatJson::encode( $row->bp_grants );
181 
182  if ( isset( $data['user'] ) ) {
183  // Must be a User object, already checked above
184  $row->bp_user = CentralIdLookup::factory()->centralIdFromLocalUser(
185  $data['user'], CentralIdLookup::AUDIENCE_RAW, $flags
186  );
187  } elseif ( isset( $data['username'] ) ) {
188  $row->bp_user = CentralIdLookup::factory()->centralIdFromName(
189  $data['username'], CentralIdLookup::AUDIENCE_RAW, $flags
190  );
191  } elseif ( isset( $data['centralId'] ) ) {
192  $row->bp_user = $data['centralId'];
193  }
194  if ( !$row->bp_user ) {
195  return null;
196  }
197 
198  return new self( $row, false, $flags );
199  }
200 
205  public function isSaved() {
206  return $this->isSaved;
207  }
208 
213  public function getUserCentralId() {
214  return $this->centralId;
215  }
216 
220  public function getAppId() {
221  return $this->appId;
222  }
223 
227  public function getToken() {
228  return $this->token;
229  }
230 
234  public function getRestrictions() {
235  return $this->restrictions;
236  }
237 
241  public function getGrants() {
242  return $this->grants;
243  }
244 
249  public static function getSeparator() {
252  }
253 
257  private function getPassword() {
258  list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $this->flags );
259  $db = self::getDB( $index );
260  $password = $db->selectField(
261  'bot_passwords',
262  'bp_password',
263  [ 'bp_user' => $this->centralId, 'bp_app_id' => $this->appId ],
264  __METHOD__,
265  $options
266  );
267  if ( $password === false ) {
269  }
270 
271  $passwordFactory = MediaWikiServices::getInstance()->getPasswordFactory();
272  try {
273  return $passwordFactory->newFromCiphertext( $password );
274  } catch ( PasswordError $ex ) {
276  }
277  }
278 
284  public function isInvalid() {
285  return $this->getPassword() instanceof InvalidPassword;
286  }
287 
295  public function save( $operation, Password $password = null ) {
296  // Ensure operation is valid
297  if ( $operation !== 'insert' && $operation !== 'update' ) {
298  throw new UnexpectedValueException(
299  "Expected 'insert' or 'update'; got '{$operation}'."
300  );
301  }
302 
303  $conds = [
304  'bp_user' => $this->centralId,
305  'bp_app_id' => $this->appId,
306  ];
307 
308  $res = Status::newGood();
309 
310  $restrictions = $this->restrictions->toJson();
311 
312  if ( strlen( $restrictions ) > self::RESTRICTIONS_MAXLENGTH ) {
313  $res->fatal( 'botpasswords-toolong-restrictions' );
314  }
315 
316  $grants = FormatJson::encode( $this->grants );
317 
318  if ( strlen( $grants ) > self::GRANTS_MAXLENGTH ) {
319  $res->fatal( 'botpasswords-toolong-grants' );
320  }
321 
322  if ( !$res->isGood() ) {
323  return $res;
324  }
325 
326  $fields = [
328  'bp_restrictions' => $restrictions,
329  'bp_grants' => $grants,
330  ];
331 
332  if ( $password !== null ) {
333  $fields['bp_password'] = $password->toString();
334  } elseif ( $operation === 'insert' ) {
335  $fields['bp_password'] = PasswordFactory::newInvalidPassword()->toString();
336  }
337 
338  $dbw = self::getDB( DB_MASTER );
339 
340  if ( $operation === 'insert' ) {
341  $dbw->insert( 'bot_passwords', $fields + $conds, __METHOD__, [ 'IGNORE' ] );
342  } else {
343  // Must be update, already checked above
344  $dbw->update( 'bot_passwords', $fields, $conds, __METHOD__ );
345  }
346 
347  $ok = (bool)$dbw->affectedRows();
348  if ( $ok ) {
349  $this->token = $dbw->selectField( 'bot_passwords', 'bp_token', $conds, __METHOD__ );
350  $this->isSaved = true;
351 
352  return $res;
353  }
354 
355  // Messages: botpasswords-insert-failed, botpasswords-update-failed
356  return Status::newFatal( "botpasswords-{$operation}-failed", $this->appId );
357  }
358 
363  public function delete() {
364  $dbw = self::getDB( DB_MASTER );
365  $dbw->delete(
366  'bot_passwords',
367  [
368  'bp_user' => $this->centralId,
369  'bp_app_id' => $this->appId,
370  ],
371  __METHOD__
372  );
373  $ok = (bool)$dbw->affectedRows();
374  if ( $ok ) {
375  $this->token = '**unsaved**';
376  $this->isSaved = false;
377  }
378  return $ok;
379  }
380 
386  public static function invalidateAllPasswordsForUser( $username ) {
387  $centralId = CentralIdLookup::factory()->centralIdFromName(
388  $username, CentralIdLookup::AUDIENCE_RAW, CentralIdLookup::READ_LATEST
389  );
391  }
392 
401  public static function invalidateAllPasswordsForCentralId( $centralId ) {
402  global $wgEnableBotPasswords;
403 
404  if ( !$wgEnableBotPasswords ) {
405  return false;
406  }
407 
408  $dbw = self::getDB( DB_MASTER );
409  $dbw->update(
410  'bot_passwords',
411  [ 'bp_password' => PasswordFactory::newInvalidPassword()->toString() ],
412  [ 'bp_user' => $centralId ],
413  __METHOD__
414  );
415  return (bool)$dbw->affectedRows();
416  }
417 
423  public static function removeAllPasswordsForUser( $username ) {
424  $centralId = CentralIdLookup::factory()->centralIdFromName(
425  $username, CentralIdLookup::AUDIENCE_RAW, CentralIdLookup::READ_LATEST
426  );
428  }
429 
438  public static function removeAllPasswordsForCentralId( $centralId ) {
439  global $wgEnableBotPasswords;
440 
441  if ( !$wgEnableBotPasswords ) {
442  return false;
443  }
444 
445  $dbw = self::getDB( DB_MASTER );
446  $dbw->delete(
447  'bot_passwords',
448  [ 'bp_user' => $centralId ],
449  __METHOD__
450  );
451  return (bool)$dbw->affectedRows();
452  }
453 
459  public static function generatePassword( $config ) {
461  max( self::PASSWORD_MINLENGTH, $config->get( 'MinimalPasswordLength' ) ) );
462  }
463 
473  public static function canonicalizeLoginData( $username, $password ) {
474  $sep = self::getSeparator();
475  // the strlen check helps minimize the password information obtainable from timing
476  if ( strlen( $password ) >= self::PASSWORD_MINLENGTH && strpos( $username, $sep ) !== false ) {
477  // the separator is not valid in new usernames but might appear in legacy ones
478  if ( preg_match( '/^[0-9a-w]{' . self::PASSWORD_MINLENGTH . ',}$/', $password ) ) {
479  return [ $username, $password ];
480  }
481  } elseif ( strlen( $password ) > self::PASSWORD_MINLENGTH && strpos( $password, $sep ) !== false ) {
482  $segments = explode( $sep, $password );
483  $password = array_pop( $segments );
484  $appId = implode( $sep, $segments );
485  if ( preg_match( '/^[0-9a-w]{' . self::PASSWORD_MINLENGTH . ',}$/', $password ) ) {
486  return [ $username . $sep . $appId, $password ];
487  }
488  }
489  return false;
490  }
491 
499  public static function login( $username, $password, WebRequest $request ) {
501 
502  if ( !$wgEnableBotPasswords ) {
503  return Status::newFatal( 'botpasswords-disabled' );
504  }
505 
506  $provider = SessionManager::singleton()->getProvider( BotPasswordSessionProvider::class );
507  if ( !$provider ) {
508  return Status::newFatal( 'botpasswords-no-provider' );
509  }
510 
511  // Split name into name+appId
512  $sep = self::getSeparator();
513  if ( strpos( $username, $sep ) === false ) {
514  return self::loginHook( $username, null, Status::newFatal( 'botpasswords-invalid-name', $sep ) );
515  }
516  list( $name, $appId ) = explode( $sep, $username, 2 );
517 
518  // Find the named user
519  $user = User::newFromName( $name );
520  if ( !$user || $user->isAnon() ) {
521  return self::loginHook( $user ?: $name, null, Status::newFatal( 'nosuchuser', $name ) );
522  }
523 
524  if ( $user->isLocked() ) {
525  return Status::newFatal( 'botpasswords-locked' );
526  }
527 
528  $throttle = null;
529  if ( !empty( $wgPasswordAttemptThrottle ) ) {
530  $throttle = new Throttler( $wgPasswordAttemptThrottle, [
531  'type' => 'botpassword',
533  ] );
534  $result = $throttle->increase( $user->getName(), $request->getIP(), __METHOD__ );
535  if ( $result ) {
536  $msg = wfMessage( 'login-throttled' )->durationParams( $result['wait'] );
537  return self::loginHook( $user, null, Status::newFatal( $msg ) );
538  }
539  }
540 
541  // Get the bot password
542  $bp = self::newFromUser( $user, $appId );
543  if ( !$bp ) {
544  return self::loginHook( $user, $bp,
545  Status::newFatal( 'botpasswords-not-exist', $name, $appId ) );
546  }
547 
548  // Check restrictions
549  $status = $bp->getRestrictions()->check( $request );
550  if ( !$status->isOK() ) {
551  return self::loginHook( $user, $bp, Status::newFatal( 'botpasswords-restriction-failed' ) );
552  }
553 
554  // Check the password
555  $passwordObj = $bp->getPassword();
556  if ( $passwordObj instanceof InvalidPassword ) {
557  return self::loginHook( $user, $bp,
558  Status::newFatal( 'botpasswords-needs-reset', $name, $appId ) );
559  }
560  if ( !$passwordObj->verify( $password ) ) {
561  return self::loginHook( $user, $bp, Status::newFatal( 'wrongpassword' ) );
562  }
563 
564  // Ok! Create the session.
565  if ( $throttle ) {
566  $throttle->clear( $user->getName(), $request->getIP() );
567  }
568  return self::loginHook( $user, $bp,
569  // @phan-suppress-next-line PhanUndeclaredMethod
570  Status::newGood( $provider->newSessionForRequest( $user, $bp, $request ) ) );
571  }
572 
584  private static function loginHook( $user, $bp, Status $status ) {
585  $extraData = [];
586  if ( $user instanceof User ) {
587  $name = $user->getName();
588  if ( $bp ) {
589  $extraData['appId'] = $name . self::getSeparator() . $bp->getAppId();
590  }
591  } else {
592  $name = $user;
593  $user = null;
594  }
595 
596  if ( $status->isGood() ) {
597  $response = AuthenticationResponse::newPass( $name );
598  } else {
599  $response = AuthenticationResponse::newFail( $status->getMessage() );
600  }
601  Hooks::runner()->onAuthManagerLoginAuthenticateAudit( $response, $user, $name, $extraData );
602 
603  return $status;
604  }
605 }
MWRestrictions
A class to check request restrictions expressed as a JSON object.
Definition: MWRestrictions.php:27
BotPassword\getRestrictions
getRestrictions()
Definition: BotPassword.php:234
StatusValue\newFatal
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
BotPassword\getUserCentralId
getUserCentralId()
Get the central user ID.
Definition: BotPassword.php:213
$wgBotPasswordsDatabase
string bool $wgBotPasswordsDatabase
Database name for the bot_passwords table.
Definition: DefaultSettings.php:6422
ObjectCache\getLocalClusterInstance
static getLocalClusterInstance()
Get the main cluster-local cache object.
Definition: ObjectCache.php:272
MediaWiki\Session\Session\BotPasswordSessionProvider
Session provider for bot passwords.
Definition: BotPasswordSessionProvider.php:34
BotPassword\$flags
int $flags
Definition: BotPassword.php:72
BotPassword\canonicalizeLoginData
static canonicalizeLoginData( $username, $password)
There are two ways to login with a bot password: "username@appId", "password" and "username",...
Definition: BotPassword.php:473
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:166
BotPassword
Utility class for bot passwords.
Definition: BotPassword.php:32
BotPassword\getSeparator
static getSeparator()
Get the separator for combined user name + app ID.
Definition: BotPassword.php:249
Status\getMessage
getMessage( $shortContext=false, $longContext=false, $lang=null)
Get a bullet list of the errors as a Message object.
Definition: Status.php:243
$wgBotPasswordsCluster
string bool $wgBotPasswordsCluster
Cluster for the bot_passwords table If false, the normal cluster will be used.
Definition: DefaultSettings.php:6412
BotPassword\generatePassword
static generatePassword( $config)
Returns a (raw, unhashed) random password string.
Definition: BotPassword.php:459
BotPassword\$grants
string[] $grants
Definition: BotPassword.php:69
PasswordError
Show an error when any operation involving passwords fails to run.
Definition: PasswordError.php:29
BotPassword\isInvalid
isInvalid()
Whether the password is currently invalid.
Definition: BotPassword.php:284
User\newFromName
static newFromName( $name, $validate='valid')
Definition: User.php:548
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1230
BotPassword\getAppId
getAppId()
Definition: BotPassword.php:220
MediaWiki\Auth\Throttler
Definition: Throttler.php:37
DBAccessObjectUtils\getDBOptions
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
Definition: DBAccessObjectUtils.php:52
InvalidPassword
Represents an invalid password hash.
Definition: InvalidPassword.php:34
BotPassword\getToken
getToken()
Definition: BotPassword.php:227
BotPassword\getDB
static getDB( $db)
Get a database connection for the bot passwords database.
Definition: BotPassword.php:95
BotPassword\invalidateAllPasswordsForCentralId
static invalidateAllPasswordsForCentralId( $centralId)
Invalidate all passwords for a user, by central ID.
Definition: BotPassword.php:401
BotPassword\APPID_MAXLENGTH
const APPID_MAXLENGTH
Definition: BotPassword.php:34
$res
$res
Definition: testCompression.php:57
BotPassword\$isSaved
bool $isSaved
Definition: BotPassword.php:54
IDBAccessObject
Interface for database access objects.
Definition: IDBAccessObject.php:57
BotPassword\__construct
__construct( $row, $isSaved, $flags=self::READ_NORMAL)
Definition: BotPassword.php:79
BotPassword\save
save( $operation, Password $password=null)
Save the BotPassword to the database.
Definition: BotPassword.php:295
Wikimedia\Rdbms\IDatabase
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
Status
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:44
BotPassword\$token
string $token
Definition: BotPassword.php:63
StatusValue\isGood
isGood()
Returns whether the operation completed and didn't have any error or warnings.
Definition: StatusValue.php:122
FormatJson\decode
static decode( $value, $assoc=false)
Decodes a JSON string.
Definition: FormatJson.php:174
FormatJson\encode
static encode( $value, $pretty=false, $escaping=0)
Returns the JSON representation of a value.
Definition: FormatJson.php:115
BotPassword\isSaved
isSaved()
Indicate whether this is known to be saved.
Definition: BotPassword.php:205
BotPassword\newUnsaved
static newUnsaved(array $data, $flags=self::READ_NORMAL)
Create an unsaved BotPassword.
Definition: BotPassword.php:157
BotPassword\$appId
string $appId
Definition: BotPassword.php:60
MWRestrictions\toJson
toJson( $pretty=false)
Return the restrictions as a JSON string.
Definition: MWRestrictions.php:114
BotPassword\RESTRICTIONS_MAXLENGTH
const RESTRICTIONS_MAXLENGTH
Maximum length of the json representation of restrictions.
Definition: BotPassword.php:45
MediaWiki\Auth\AuthenticationResponse
This is a value object to hold authentication response data.
Definition: AuthenticationResponse.php:37
$wgUserrightsInterwikiDelimiter
$wgUserrightsInterwikiDelimiter
Character used as a delimiter when testing for interwiki userrights (In Special:UserRights,...
Definition: DefaultSettings.php:5361
BotPassword\PASSWORD_MINLENGTH
const PASSWORD_MINLENGTH
Minimum length for a bot password.
Definition: BotPassword.php:39
User\TOKEN_LENGTH
const TOKEN_LENGTH
Number of characters required for the user_token field.
Definition: User.php:62
DB_MASTER
const DB_MASTER
Definition: defines.php:26
MWRestrictions\newDefault
static newDefault()
Definition: MWRestrictions.php:44
BotPassword\newFromUser
static newFromUser(User $user, $appId, $flags=self::READ_NORMAL)
Load a BotPassword from the database.
Definition: BotPassword.php:112
PasswordFactory\generateRandomPasswordString
static generateRandomPasswordString(int $minLength=10)
Generate a random string suitable for a password.
Definition: PasswordFactory.php:226
BotPassword\getPassword
getPassword()
Definition: BotPassword.php:257
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
BotPassword\invalidateAllPasswordsForUser
static invalidateAllPasswordsForUser( $username)
Invalidate all passwords for a user, by name.
Definition: BotPassword.php:386
BotPassword\$centralId
int $centralId
Definition: BotPassword.php:57
MediaWiki\Session\SessionManager
This serves as the entry point to the MediaWiki session handling system.
Definition: SessionManager.php:53
Hooks\runner
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:172
$wgPasswordAttemptThrottle
$wgPasswordAttemptThrottle
Limit password attempts to X attempts per Y seconds per IP per account.
Definition: DefaultSettings.php:6230
MWCryptRand\generateHex
static generateHex( $chars)
Generate a run of cryptographically random data and return it in hexadecimal string format.
Definition: MWCryptRand.php:36
BotPassword\newFromCentralId
static newFromCentralId( $centralId, $appId, $flags=self::READ_NORMAL)
Load a BotPassword from the database.
Definition: BotPassword.php:126
BotPassword\login
static login( $username, $password, WebRequest $request)
Try to log the user in.
Definition: BotPassword.php:499
WebRequest
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
Definition: WebRequest.php:42
WebRequest\getIP
getIP()
Work out the IP address based on various globals For trusted proxies, use the XFF client IP (first of...
Definition: WebRequest.php:1270
BotPassword\getGrants
getGrants()
Definition: BotPassword.php:241
PasswordFactory\newInvalidPassword
static newInvalidPassword()
Create an InvalidPassword.
Definition: PasswordFactory.php:242
BotPassword\loginHook
static loginHook( $user, $bp, Status $status)
Call AuthManagerLoginAuthenticateAudit.
Definition: BotPassword.php:584
MWRestrictions\newFromJson
static newFromJson( $json)
Definition: MWRestrictions.php:62
BotPassword\GRANTS_MAXLENGTH
const GRANTS_MAXLENGTH
Maximum length of the json representation of grants.
Definition: BotPassword.php:51
BotPassword\removeAllPasswordsForUser
static removeAllPasswordsForUser( $username)
Remove all passwords for a user, by name.
Definition: BotPassword.php:423
$wgEnableBotPasswords
bool $wgEnableBotPasswords
Whether to enable bot passwords.
Definition: DefaultSettings.php:6405
CentralIdLookup\AUDIENCE_RAW
const AUDIENCE_RAW
Definition: CentralIdLookup.php:34
Password
Represents a password hash for use in authentication.
Definition: Password.php:61
CentralIdLookup\factory
static factory( $providerId=null)
Fetch a CentralIdLookup.
Definition: CentralIdLookup.php:47
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:56
BotPassword\removeAllPasswordsForCentralId
static removeAllPasswordsForCentralId( $centralId)
Remove all passwords for a user, by central ID.
Definition: BotPassword.php:438
BotPassword\$restrictions
MWRestrictions $restrictions
Definition: BotPassword.php:66