MediaWiki  master
BotPassword.php
Go to the documentation of this file.
1 <?php
29 
34 class BotPassword implements IDBAccessObject {
35 
36  public const APPID_MAXLENGTH = 32;
37 
41  public const PASSWORD_MINLENGTH = 32;
42 
47  public const RESTRICTIONS_MAXLENGTH = 65535;
48 
53  public const GRANTS_MAXLENGTH = 65535;
54 
56  private $isSaved;
57 
59  private $centralId;
60 
62  private $appId;
63 
65  private $token;
66 
68  private $restrictions;
69 
71  private $grants;
72 
74  private $flags;
75 
83  public function __construct( $row, $isSaved, $flags = self::READ_NORMAL ) {
84  $this->isSaved = $isSaved;
85  $this->flags = $flags;
86 
87  $this->centralId = (int)$row->bp_user;
88  $this->appId = $row->bp_app_id;
89  $this->token = $row->bp_token;
90  $this->restrictions = MWRestrictions::newFromJson( $row->bp_restrictions );
91  $this->grants = FormatJson::decode( $row->bp_grants );
92  }
93 
99  public static function getDB( $db ) {
100  return MediaWikiServices::getInstance()
101  ->getBotPasswordStore()
102  ->getDatabase( $db );
103  }
104 
112  public static function newFromUser( UserIdentity $userIdentity, $appId, $flags = self::READ_NORMAL ) {
113  return MediaWikiServices::getInstance()
114  ->getBotPasswordStore()
115  ->getByUser( $userIdentity, (string)$appId, (int)$flags );
116  }
117 
125  public static function newFromCentralId( $centralId, $appId, $flags = self::READ_NORMAL ) {
126  return MediaWikiServices::getInstance()
127  ->getBotPasswordStore()
128  ->getByCentralId( (int)$centralId, (string)$appId, (int)$flags );
129  }
130 
143  public static function newUnsaved( array $data, $flags = self::READ_NORMAL ) {
144  return MediaWikiServices::getInstance()
145  ->getBotPasswordStore()
146  ->newUnsavedBotPassword( $data, (int)$flags );
147  }
148 
153  public function isSaved() {
154  return $this->isSaved;
155  }
156 
161  public function getUserCentralId() {
162  return $this->centralId;
163  }
164 
168  public function getAppId() {
169  return $this->appId;
170  }
171 
175  public function getToken() {
176  return $this->token;
177  }
178 
182  public function getRestrictions() {
183  return $this->restrictions;
184  }
185 
189  public function getGrants() {
190  return $this->grants;
191  }
192 
197  public static function getSeparator() {
198  $userrightsInterwikiDelimiter = MediaWikiServices::getInstance()
199  ->getMainConfig()->get( MainConfigNames::UserrightsInterwikiDelimiter );
200  return $userrightsInterwikiDelimiter;
201  }
202 
206  private function getPassword() {
207  [ $index, $options ] = DBAccessObjectUtils::getDBOptions( $this->flags );
208  $db = self::getDB( $index );
209  $password = $db->selectField(
210  'bot_passwords',
211  'bp_password',
212  [ 'bp_user' => $this->centralId, 'bp_app_id' => $this->appId ],
213  __METHOD__,
214  $options
215  );
216  if ( $password === false ) {
218  }
219 
220  $passwordFactory = MediaWikiServices::getInstance()->getPasswordFactory();
221  try {
222  return $passwordFactory->newFromCiphertext( $password );
223  } catch ( PasswordError $ex ) {
225  }
226  }
227 
233  public function isInvalid() {
234  return $this->getPassword() instanceof InvalidPassword;
235  }
236 
244  public function save( $operation, Password $password = null ) {
245  // Ensure operation is valid
246  if ( $operation !== 'insert' && $operation !== 'update' ) {
247  throw new UnexpectedValueException(
248  "Expected 'insert' or 'update'; got '{$operation}'."
249  );
250  }
251 
252  $store = MediaWikiServices::getInstance()->getBotPasswordStore();
253  if ( $operation === 'insert' ) {
254  $statusValue = $store->insertBotPassword( $this, $password );
255  } else {
256  // Must be update, already checked above
257  $statusValue = $store->updateBotPassword( $this, $password );
258  }
259 
260  if ( $statusValue->isGood() ) {
261  $this->token = $statusValue->getValue();
262  $this->isSaved = true;
263  return Status::newGood();
264  }
265 
266  // Action failed, status will have code botpasswords-insert-failed or
267  // botpasswords-update-failed depending on which action we tried
268  return Status::wrap( $statusValue );
269  }
270 
275  public function delete() {
276  $ok = MediaWikiServices::getInstance()
277  ->getBotPasswordStore()
278  ->deleteBotPassword( $this );
279  if ( $ok ) {
280  $this->token = '**unsaved**';
281  $this->isSaved = false;
282  }
283  return $ok;
284  }
285 
291  public static function invalidateAllPasswordsForUser( $username ) {
292  return MediaWikiServices::getInstance()
293  ->getBotPasswordStore()
294  ->invalidateUserPasswords( (string)$username );
295  }
296 
305  public static function invalidateAllPasswordsForCentralId( $centralId ) {
306  wfDeprecated( __METHOD__, '1.37' );
307 
308  $enableBotPasswords = MediaWikiServices::getInstance()->getMainConfig()
309  ->get( MainConfigNames::EnableBotPasswords );
310 
311  if ( !$enableBotPasswords ) {
312  return false;
313  }
314 
315  $dbw = self::getDB( DB_PRIMARY );
316  $dbw->update(
317  'bot_passwords',
318  [ 'bp_password' => PasswordFactory::newInvalidPassword()->toString() ],
319  [ 'bp_user' => $centralId ],
320  __METHOD__
321  );
322  return (bool)$dbw->affectedRows();
323  }
324 
330  public static function removeAllPasswordsForUser( $username ) {
331  return MediaWikiServices::getInstance()
332  ->getBotPasswordStore()
333  ->removeUserPasswords( (string)$username );
334  }
335 
344  public static function removeAllPasswordsForCentralId( $centralId ) {
345  wfDeprecated( __METHOD__, '1.37' );
346 
347  $enableBotPasswords = MediaWikiServices::getInstance()->getMainConfig()
348  ->get( MainConfigNames::EnableBotPasswords );
349 
350  if ( !$enableBotPasswords ) {
351  return false;
352  }
353 
354  $dbw = self::getDB( DB_PRIMARY );
355  $dbw->delete(
356  'bot_passwords',
357  [ 'bp_user' => $centralId ],
358  __METHOD__
359  );
360  return (bool)$dbw->affectedRows();
361  }
362 
368  public static function generatePassword( $config ) {
370  self::PASSWORD_MINLENGTH, $config->get( MainConfigNames::MinimalPasswordLength ) ) );
371  }
372 
382  public static function canonicalizeLoginData( $username, $password ) {
383  $sep = self::getSeparator();
384  // the strlen check helps minimize the password information obtainable from timing
385  if ( strlen( $password ) >= self::PASSWORD_MINLENGTH && str_contains( $username, $sep ) ) {
386  // the separator is not valid in new usernames but might appear in legacy ones
387  if ( preg_match( '/^[0-9a-w]{' . self::PASSWORD_MINLENGTH . ',}$/', $password ) ) {
388  return [ $username, $password ];
389  }
390  } elseif ( strlen( $password ) > self::PASSWORD_MINLENGTH && str_contains( $password, $sep ) ) {
391  $segments = explode( $sep, $password );
392  $password = array_pop( $segments );
393  $appId = implode( $sep, $segments );
394  if ( preg_match( '/^[0-9a-w]{' . self::PASSWORD_MINLENGTH . ',}$/', $password ) ) {
395  return [ $username . $sep . $appId, $password ];
396  }
397  }
398  return false;
399  }
400 
408  public static function login( $username, $password, WebRequest $request ) {
409  $enableBotPasswords = MediaWikiServices::getInstance()->getMainConfig()
410  ->get( MainConfigNames::EnableBotPasswords );
411  $passwordAttemptThrottle = MediaWikiServices::getInstance()->getMainConfig()
412  ->get( MainConfigNames::PasswordAttemptThrottle );
413  if ( !$enableBotPasswords ) {
414  return Status::newFatal( 'botpasswords-disabled' );
415  }
416 
417  $provider = SessionManager::singleton()->getProvider( BotPasswordSessionProvider::class );
418  if ( !$provider ) {
419  return Status::newFatal( 'botpasswords-no-provider' );
420  }
421 
422  // Split name into name+appId
423  $sep = self::getSeparator();
424  if ( !str_contains( $username, $sep ) ) {
425  return self::loginHook( $username, null, Status::newFatal( 'botpasswords-invalid-name', $sep ) );
426  }
427  [ $name, $appId ] = explode( $sep, $username, 2 );
428 
429  // Find the named user
430  $user = User::newFromName( $name );
431  if ( !$user || $user->isAnon() ) {
432  return self::loginHook( $user ?: $name, null, Status::newFatal( 'nosuchuser', $name ) );
433  }
434 
435  if ( $user->isLocked() ) {
436  return Status::newFatal( 'botpasswords-locked' );
437  }
438 
439  $throttle = null;
440  if ( !empty( $passwordAttemptThrottle ) ) {
441  $throttle = new Throttler( $passwordAttemptThrottle, [
442  'type' => 'botpassword',
444  ] );
445  $result = $throttle->increase( $user->getName(), $request->getIP(), __METHOD__ );
446  if ( $result ) {
447  $msg = wfMessage( 'login-throttled' )->durationParams( $result['wait'] );
448  return self::loginHook( $user, null, Status::newFatal( $msg ) );
449  }
450  }
451 
452  // Get the bot password
453  $bp = self::newFromUser( $user, $appId );
454  if ( !$bp ) {
455  return self::loginHook( $user, $bp,
456  Status::newFatal( 'botpasswords-not-exist', $name, $appId ) );
457  }
458 
459  // Check restrictions
460  $status = $bp->getRestrictions()->check( $request );
461  if ( !$status->isOK() ) {
462  return self::loginHook( $user, $bp, Status::newFatal( 'botpasswords-restriction-failed' ) );
463  }
464 
465  // Check the password
466  $passwordObj = $bp->getPassword();
467  if ( $passwordObj instanceof InvalidPassword ) {
468  return self::loginHook( $user, $bp,
469  Status::newFatal( 'botpasswords-needs-reset', $name, $appId ) );
470  }
471  if ( !$passwordObj->verify( $password ) ) {
472  return self::loginHook( $user, $bp, Status::newFatal( 'wrongpassword' ) );
473  }
474 
475  // Ok! Create the session.
476  if ( $throttle ) {
477  $throttle->clear( $user->getName(), $request->getIP() );
478  }
479  return self::loginHook( $user, $bp,
480  // @phan-suppress-next-line PhanUndeclaredMethod
481  Status::newGood( $provider->newSessionForRequest( $user, $bp, $request ) ) );
482  }
483 
495  private static function loginHook( $user, $bp, Status $status ) {
496  $extraData = [];
497  if ( $user instanceof User ) {
498  $name = $user->getName();
499  if ( $bp ) {
500  $extraData['appId'] = $name . self::getSeparator() . $bp->getAppId();
501  }
502  } else {
503  $name = $user;
504  $user = null;
505  }
506 
507  if ( $status->isGood() ) {
508  $response = AuthenticationResponse::newPass( $name );
509  } else {
510  $response = AuthenticationResponse::newFail( $status->getMessage() );
511  }
512  Hooks::runner()->onAuthManagerLoginAuthenticateAudit( $response, $user, $name, $extraData );
513 
514  return $status;
515  }
516 }
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.
Utility class for bot passwords.
Definition: BotPassword.php:34
isInvalid()
Whether the password is currently invalid.
__construct( $row, $isSaved, $flags=self::READ_NORMAL)
Definition: BotPassword.php:83
static newFromUser(UserIdentity $userIdentity, $appId, $flags=self::READ_NORMAL)
Load a BotPassword from the database.
const APPID_MAXLENGTH
Definition: BotPassword.php:36
static invalidateAllPasswordsForUser( $username)
Invalidate all passwords for a user, by name.
static generatePassword( $config)
Returns a (raw, unhashed) random password string.
const PASSWORD_MINLENGTH
Minimum length for a bot password.
Definition: BotPassword.php:41
getUserCentralId()
Get the central user ID.
static getDB( $db)
Get a database connection for the bot passwords database.
Definition: BotPassword.php:99
const RESTRICTIONS_MAXLENGTH
Maximum length of the json representation of restrictions.
Definition: BotPassword.php:47
static newFromCentralId( $centralId, $appId, $flags=self::READ_NORMAL)
Load a BotPassword from the database.
const GRANTS_MAXLENGTH
Maximum length of the json representation of grants.
Definition: BotPassword.php:53
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.
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.
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",...
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
static decode( $value, $assoc=false)
Decodes a JSON string.
Definition: FormatJson.php:146
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:172
Represents an invalid password hash.
static newFromJson( $json)
This is a value object to hold authentication response data.
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
This serves as the entry point to the MediaWiki session handling system.
static getLocalClusterInstance()
Get the main cluster-local cache object.
Show an error when any operation involving passwords fails to run.
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
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:46
static wrap( $sv)
Succinct helper method to wrap a StatusValue.
Definition: Status.php:64
getMessage( $shortContext=false, $longContext=false, $lang=null)
Get a bullet list of the errors as a Message object.
Definition: Status.php:245
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:71
static newFromName( $name, $validate='valid')
Definition: User.php:592
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
Definition: WebRequest.php:49
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.
Interface for objects representing user identity.
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:36
const DB_PRIMARY
Definition: defines.php:28