MediaWiki  master
BotPassword.php
Go to the documentation of this file.
1 <?php
21 namespace MediaWiki\User;
22 
24 use FormatJson;
25 use IDBAccessObject;
26 use InvalidPassword;
37 use MWRestrictions;
38 use ObjectCache;
39 use Password;
40 use PasswordError;
41 use PasswordFactory;
42 use stdClass;
43 use UnexpectedValueException;
45 
50 class BotPassword implements IDBAccessObject {
51 
52  public const APPID_MAXLENGTH = 32;
53 
57  public const PASSWORD_MINLENGTH = 32;
58 
63  public const RESTRICTIONS_MAXLENGTH = 65535;
64 
69  public const GRANTS_MAXLENGTH = 65535;
70 
72  private $isSaved;
73 
75  private $centralId;
76 
78  private $appId;
79 
81  private $token;
82 
84  private $restrictions;
85 
87  private $grants;
88 
90  private $flags;
91 
99  public function __construct( $row, $isSaved, $flags = self::READ_NORMAL ) {
100  $this->isSaved = $isSaved;
101  $this->flags = $flags;
102 
103  $this->centralId = (int)$row->bp_user;
104  $this->appId = $row->bp_app_id;
105  $this->token = $row->bp_token;
106  $this->restrictions = MWRestrictions::newFromJson( $row->bp_restrictions );
107  $this->grants = FormatJson::decode( $row->bp_grants );
108  }
109 
115  public static function getDB( $db ) {
117  ->getBotPasswordStore()
118  ->getDatabase( $db );
119  }
120 
128  public static function newFromUser( UserIdentity $userIdentity, $appId, $flags = self::READ_NORMAL ) {
130  ->getBotPasswordStore()
131  ->getByUser( $userIdentity, (string)$appId, (int)$flags );
132  }
133 
141  public static function newFromCentralId( $centralId, $appId, $flags = self::READ_NORMAL ) {
143  ->getBotPasswordStore()
144  ->getByCentralId( (int)$centralId, (string)$appId, (int)$flags );
145  }
146 
159  public static function newUnsaved( array $data, $flags = self::READ_NORMAL ) {
161  ->getBotPasswordStore()
162  ->newUnsavedBotPassword( $data, (int)$flags );
163  }
164 
169  public function isSaved() {
170  return $this->isSaved;
171  }
172 
177  public function getUserCentralId() {
178  return $this->centralId;
179  }
180 
184  public function getAppId() {
185  return $this->appId;
186  }
187 
191  public function getToken() {
192  return $this->token;
193  }
194 
198  public function getRestrictions() {
199  return $this->restrictions;
200  }
201 
205  public function getGrants() {
206  return $this->grants;
207  }
208 
213  public static function getSeparator() {
214  $userrightsInterwikiDelimiter = MediaWikiServices::getInstance()
215  ->getMainConfig()->get( MainConfigNames::UserrightsInterwikiDelimiter );
216  return $userrightsInterwikiDelimiter;
217  }
218 
222  private function getPassword() {
223  [ $index, $options ] = DBAccessObjectUtils::getDBOptions( $this->flags );
224  $db = self::getDB( $index );
225  $password = $db->newSelectQueryBuilder()
226  ->select( 'bp_password' )
227  ->from( 'bot_passwords' )
228  ->where( [ 'bp_user' => $this->centralId, 'bp_app_id' => $this->appId ] )
229  ->options( $options )
230  ->caller( __METHOD__ )->fetchField();
231  if ( $password === false ) {
233  }
234 
235  $passwordFactory = MediaWikiServices::getInstance()->getPasswordFactory();
236  try {
237  return $passwordFactory->newFromCiphertext( $password );
238  } catch ( PasswordError $ex ) {
240  }
241  }
242 
248  public function isInvalid() {
249  return $this->getPassword() instanceof InvalidPassword;
250  }
251 
259  public function save( $operation, Password $password = null ) {
260  // Ensure operation is valid
261  if ( $operation !== 'insert' && $operation !== 'update' ) {
262  throw new UnexpectedValueException(
263  "Expected 'insert' or 'update'; got '{$operation}'."
264  );
265  }
266 
267  $store = MediaWikiServices::getInstance()->getBotPasswordStore();
268  if ( $operation === 'insert' ) {
269  $statusValue = $store->insertBotPassword( $this, $password );
270  } else {
271  // Must be update, already checked above
272  $statusValue = $store->updateBotPassword( $this, $password );
273  }
274 
275  if ( $statusValue->isGood() ) {
276  $this->token = $statusValue->getValue();
277  $this->isSaved = true;
278  return Status::newGood();
279  }
280 
281  // Action failed, status will have code botpasswords-insert-failed or
282  // botpasswords-update-failed depending on which action we tried
283  return Status::wrap( $statusValue );
284  }
285 
290  public function delete() {
292  ->getBotPasswordStore()
293  ->deleteBotPassword( $this );
294  if ( $ok ) {
295  $this->token = '**unsaved**';
296  $this->isSaved = false;
297  }
298  return $ok;
299  }
300 
306  public static function invalidateAllPasswordsForUser( $username ) {
308  ->getBotPasswordStore()
309  ->invalidateUserPasswords( (string)$username );
310  }
311 
320  public static function invalidateAllPasswordsForCentralId( $centralId ) {
321  wfDeprecated( __METHOD__, '1.37' );
322 
323  $enableBotPasswords = MediaWikiServices::getInstance()->getMainConfig()
325 
326  if ( !$enableBotPasswords ) {
327  return false;
328  }
329 
330  $dbw = self::getDB( DB_PRIMARY );
331  $dbw->newUpdateQueryBuilder()
332  ->update( 'bot_passwords' )
333  ->set( [ 'bp_password' => PasswordFactory::newInvalidPassword()->toString() ] )
334  ->where( [ 'bp_user' => $centralId ] )
335  ->caller( __METHOD__ )->execute();
336  return (bool)$dbw->affectedRows();
337  }
338 
344  public static function removeAllPasswordsForUser( $username ) {
346  ->getBotPasswordStore()
347  ->removeUserPasswords( (string)$username );
348  }
349 
358  public static function removeAllPasswordsForCentralId( $centralId ) {
359  wfDeprecated( __METHOD__, '1.37' );
360 
361  $enableBotPasswords = MediaWikiServices::getInstance()->getMainConfig()
363 
364  if ( !$enableBotPasswords ) {
365  return false;
366  }
367 
368  $dbw = self::getDB( DB_PRIMARY );
369  $dbw->newDeleteQueryBuilder()
370  ->deleteFrom( 'bot_passwords' )
371  ->where( [ 'bp_user' => $centralId ] )
372  ->caller( __METHOD__ )->execute();
373  return (bool)$dbw->affectedRows();
374  }
375 
381  public static function generatePassword( $config ) {
383  self::PASSWORD_MINLENGTH, $config->get( MainConfigNames::MinimalPasswordLength ) ) );
384  }
385 
395  public static function canonicalizeLoginData( $username, $password ) {
396  $sep = self::getSeparator();
397  // the strlen check helps minimize the password information obtainable from timing
398  if ( strlen( $password ) >= self::PASSWORD_MINLENGTH && str_contains( $username, $sep ) ) {
399  // the separator is not valid in new usernames but might appear in legacy ones
400  if ( preg_match( '/^[0-9a-w]{' . self::PASSWORD_MINLENGTH . ',}$/', $password ) ) {
401  return [ $username, $password ];
402  }
403  } elseif ( strlen( $password ) > self::PASSWORD_MINLENGTH && str_contains( $password, $sep ) ) {
404  $segments = explode( $sep, $password );
405  $password = array_pop( $segments );
406  $appId = implode( $sep, $segments );
407  if ( preg_match( '/^[0-9a-w]{' . self::PASSWORD_MINLENGTH . ',}$/', $password ) ) {
408  return [ $username . $sep . $appId, $password ];
409  }
410  }
411  return false;
412  }
413 
421  public static function login( $username, $password, WebRequest $request ) {
422  $enableBotPasswords = MediaWikiServices::getInstance()->getMainConfig()
424  $passwordAttemptThrottle = MediaWikiServices::getInstance()->getMainConfig()
426  if ( !$enableBotPasswords ) {
427  return Status::newFatal( 'botpasswords-disabled' );
428  }
429 
430  $provider = SessionManager::singleton()->getProvider( BotPasswordSessionProvider::class );
431  if ( !$provider ) {
432  return Status::newFatal( 'botpasswords-no-provider' );
433  }
434 
435  // Split name into name+appId
436  $sep = self::getSeparator();
437  if ( !str_contains( $username, $sep ) ) {
438  return self::loginHook( $username, null, Status::newFatal( 'botpasswords-invalid-name', $sep ) );
439  }
440  [ $name, $appId ] = explode( $sep, $username, 2 );
441 
442  // Find the named user
443  $user = User::newFromName( $name );
444  if ( !$user || $user->isAnon() ) {
445  return self::loginHook( $user ?: $name, null, Status::newFatal( 'nosuchuser', $name ) );
446  }
447 
448  if ( $user->isLocked() ) {
449  return Status::newFatal( 'botpasswords-locked' );
450  }
451 
452  $throttle = null;
453  if ( $passwordAttemptThrottle ) {
454  $throttle = new Throttler( $passwordAttemptThrottle, [
455  'type' => 'botpassword',
457  ] );
458  $result = $throttle->increase( $user->getName(), $request->getIP(), __METHOD__ );
459  if ( $result ) {
460  $msg = wfMessage( 'login-throttled' )->durationParams( $result['wait'] );
461  return self::loginHook( $user, null, Status::newFatal( $msg ) );
462  }
463  }
464 
465  // Get the bot password
466  $bp = self::newFromUser( $user, $appId );
467  if ( !$bp ) {
468  return self::loginHook( $user, $bp,
469  Status::newFatal( 'botpasswords-not-exist', $name, $appId ) );
470  }
471 
472  // Check restrictions
473  $status = $bp->getRestrictions()->check( $request );
474  if ( !$status->isOK() ) {
475  return self::loginHook( $user, $bp, Status::newFatal( 'botpasswords-restriction-failed' ) );
476  }
477 
478  // Check the password
479  $passwordObj = $bp->getPassword();
480  if ( $passwordObj instanceof InvalidPassword ) {
481  return self::loginHook( $user, $bp,
482  Status::newFatal( 'botpasswords-needs-reset', $name, $appId ) );
483  }
484  if ( !$passwordObj->verify( $password ) ) {
485  return self::loginHook( $user, $bp, Status::newFatal( 'wrongpassword' ) );
486  }
487 
488  // Ok! Create the session.
489  if ( $throttle ) {
490  $throttle->clear( $user->getName(), $request->getIP() );
491  }
492  return self::loginHook( $user, $bp,
493  // @phan-suppress-next-line PhanUndeclaredMethod
494  Status::newGood( $provider->newSessionForRequest( $user, $bp, $request ) ) );
495  }
496 
508  private static function loginHook( $user, $bp, Status $status ) {
509  $extraData = [];
510  if ( $user instanceof User ) {
511  $name = $user->getName();
512  if ( $bp ) {
513  $extraData['appId'] = $name . self::getSeparator() . $bp->getAppId();
514  }
515  } else {
516  $name = $user;
517  $user = null;
518  }
519 
520  if ( $status->isGood() ) {
521  $response = AuthenticationResponse::newPass( $name );
522  } else {
523  $response = AuthenticationResponse::newFail( $status->getMessage() );
524  }
525  ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
526  ->onAuthManagerLoginAuthenticateAudit( $response, $user, $name, $extraData );
527 
528  return $status;
529  }
530 }
531 
536 class_alias( BotPassword::class, 'BotPassword' );
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
Helper class for DAO classes.
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
JSON formatter wrapper class.
Definition: FormatJson.php:28
static decode( $value, $assoc=false)
Decodes a JSON string.
Definition: FormatJson.php:148
Represents an invalid password hash.
A class to check request restrictions expressed as a JSON object.
static newFromJson( $json)
This is a value object to hold authentication response data.
static newFail(Message $msg, array $failReasons=[])
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:568
A class containing constants representing the names of configuration variables.
const MinimalPasswordLength
Name constant for the MinimalPasswordLength setting, for use with Config::get()
const PasswordAttemptThrottle
Name constant for the PasswordAttemptThrottle setting, for use with Config::get()
const UserrightsInterwikiDelimiter
Name constant for the UserrightsInterwikiDelimiter setting, for use with Config::get()
const EnableBotPasswords
Name constant for the EnableBotPasswords setting, for use with Config::get()
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
Definition: WebRequest.php:50
getIP()
Work out the IP address based on various globals For trusted proxies, use the XFF client IP (first of...
This serves as the entry point to the MediaWiki session handling system.
static singleton()
Get the global SessionManager.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:58
getMessage( $shortContext=false, $longContext=false, $lang=null)
Get a bullet list of the errors as a Message object.
Definition: Status.php:260
static wrap( $sv)
Succinct helper method to wrap a StatusValue.
Definition: Status.php:76
Utility class for bot passwords.
Definition: BotPassword.php:50
const RESTRICTIONS_MAXLENGTH
Maximum length of the json representation of restrictions.
Definition: BotPassword.php:63
static removeAllPasswordsForUser( $username)
Remove all passwords for a user, by name.
static generatePassword( $config)
Returns a (raw, unhashed) random password string.
static invalidateAllPasswordsForUser( $username)
Invalidate all passwords for a user, by name.
static newFromUser(UserIdentity $userIdentity, $appId, $flags=self::READ_NORMAL)
Load a BotPassword from the database.
static getSeparator()
Get the separator for combined user name + app ID.
isSaved()
Indicate whether this is known to be saved.
static canonicalizeLoginData( $username, $password)
There are two ways to login with a bot password: "username@appId", "password" and "username",...
const PASSWORD_MINLENGTH
Minimum length for a bot password.
Definition: BotPassword.php:57
static removeAllPasswordsForCentralId( $centralId)
Remove all passwords for a user, by central ID.
static login( $username, $password, WebRequest $request)
Try to log the user in.
__construct( $row, $isSaved, $flags=self::READ_NORMAL)
Definition: BotPassword.php:99
static newFromCentralId( $centralId, $appId, $flags=self::READ_NORMAL)
Load a BotPassword from the database.
isInvalid()
Whether the password is currently invalid.
static invalidateAllPasswordsForCentralId( $centralId)
Invalidate all passwords for a user, by central ID.
save( $operation, Password $password=null)
Save the BotPassword to the database.
const GRANTS_MAXLENGTH
Maximum length of the json representation of grants.
Definition: BotPassword.php:69
getUserCentralId()
Get the central user ID.
static newUnsaved(array $data, $flags=self::READ_NORMAL)
Create an unsaved BotPassword.
static getDB( $db)
Get a database connection for the bot passwords database.
internal since 1.36
Definition: User.php:98
static newFromName( $name, $validate='valid')
Definition: User.php:600
Functions to get cache objects.
Definition: ObjectCache.php:67
static getLocalClusterInstance()
Get the main cluster-local cache object.
Show an error when any operation involving passwords fails to run.
Factory class for creating and checking Password objects.
static generateRandomPasswordString(int $minLength=10)
Generate a random string suitable for a password.
static newInvalidPassword()
Create an InvalidPassword.
Represents a password hash for use in authentication.
Definition: Password.php:61
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:73
isOK()
Returns whether the operation completed.
isGood()
Returns whether the operation completed and didn't have any error or warnings.
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:85
Interface for database access objects.
Interface for configuration instances.
Definition: Config.php:32
Interface for objects representing user identity.
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:36
Utility class for bot passwords.
Definition: ActorCache.php:21
const DB_PRIMARY
Definition: defines.php:28