MediaWiki  master
BotPassword.php
Go to the documentation of this file.
1 <?php
28 
33 class BotPassword implements IDBAccessObject {
34 
35  public const APPID_MAXLENGTH = 32;
36 
40  public const PASSWORD_MINLENGTH = 32;
41 
46  public const RESTRICTIONS_MAXLENGTH = 65535;
47 
52  public const GRANTS_MAXLENGTH = 65535;
53 
55  private $isSaved;
56 
58  private $centralId;
59 
61  private $appId;
62 
64  private $token;
65 
67  private $restrictions;
68 
70  private $grants;
71 
73  private $flags;
74 
82  public function __construct( $row, $isSaved, $flags = self::READ_NORMAL ) {
83  $this->isSaved = $isSaved;
84  $this->flags = $flags;
85 
86  $this->centralId = (int)$row->bp_user;
87  $this->appId = $row->bp_app_id;
88  $this->token = $row->bp_token;
89  $this->restrictions = MWRestrictions::newFromJson( $row->bp_restrictions );
90  $this->grants = FormatJson::decode( $row->bp_grants );
91  }
92 
98  public static function getDB( $db ) {
99  return MediaWikiServices::getInstance()
100  ->getBotPasswordStore()
101  ->getDatabase( $db );
102  }
103 
111  public static function newFromUser( UserIdentity $userIdentity, $appId, $flags = self::READ_NORMAL ) {
112  return MediaWikiServices::getInstance()
113  ->getBotPasswordStore()
114  ->getByUser( $userIdentity, (string)$appId, (int)$flags );
115  }
116 
124  public static function newFromCentralId( $centralId, $appId, $flags = self::READ_NORMAL ) {
125  return MediaWikiServices::getInstance()
126  ->getBotPasswordStore()
127  ->getByCentralId( (int)$centralId, (string)$appId, (int)$flags );
128  }
129 
142  public static function newUnsaved( array $data, $flags = self::READ_NORMAL ) {
143  return MediaWikiServices::getInstance()
144  ->getBotPasswordStore()
145  ->newUnsavedBotPassword( $data, (int)$flags );
146  }
147 
152  public function isSaved() {
153  return $this->isSaved;
154  }
155 
160  public function getUserCentralId() {
161  return $this->centralId;
162  }
163 
167  public function getAppId() {
168  return $this->appId;
169  }
170 
174  public function getToken() {
175  return $this->token;
176  }
177 
181  public function getRestrictions() {
182  return $this->restrictions;
183  }
184 
188  public function getGrants() {
189  return $this->grants;
190  }
191 
196  public static function getSeparator() {
199  }
200 
204  private function getPassword() {
205  list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $this->flags );
206  $db = self::getDB( $index );
207  $password = $db->selectField(
208  'bot_passwords',
209  'bp_password',
210  [ 'bp_user' => $this->centralId, 'bp_app_id' => $this->appId ],
211  __METHOD__,
212  $options
213  );
214  if ( $password === false ) {
216  }
217 
218  $passwordFactory = MediaWikiServices::getInstance()->getPasswordFactory();
219  try {
220  return $passwordFactory->newFromCiphertext( $password );
221  } catch ( PasswordError $ex ) {
223  }
224  }
225 
231  public function isInvalid() {
232  return $this->getPassword() instanceof InvalidPassword;
233  }
234 
242  public function save( $operation, Password $password = null ) {
243  // Ensure operation is valid
244  if ( $operation !== 'insert' && $operation !== 'update' ) {
245  throw new UnexpectedValueException(
246  "Expected 'insert' or 'update'; got '{$operation}'."
247  );
248  }
249 
250  $store = MediaWikiServices::getInstance()->getBotPasswordStore();
251  if ( $operation === 'insert' ) {
252  $statusValue = $store->insertBotPassword( $this, $password );
253  } else {
254  // Must be update, already checked above
255  $statusValue = $store->updateBotPassword( $this, $password );
256  }
257 
258  if ( $statusValue->isGood() ) {
259  $this->token = $statusValue->getValue();
260  $this->isSaved = true;
261  return Status::newGood();
262  }
263 
264  // Action failed, status will have code botpasswords-insert-failed or
265  // botpasswords-update-failed depending on which action we tried
266  return Status::wrap( $statusValue );
267  }
268 
273  public function delete() {
274  $ok = MediaWikiServices::getInstance()
275  ->getBotPasswordStore()
276  ->deleteBotPassword( $this );
277  if ( $ok ) {
278  $this->token = '**unsaved**';
279  $this->isSaved = false;
280  }
281  return $ok;
282  }
283 
289  public static function invalidateAllPasswordsForUser( $username ) {
290  return MediaWikiServices::getInstance()
291  ->getBotPasswordStore()
292  ->invalidateUserPasswords( (string)$username );
293  }
294 
303  public static function invalidateAllPasswordsForCentralId( $centralId ) {
304  wfDeprecated( __METHOD__, '1.37' );
305 
306  global $wgEnableBotPasswords;
307 
308  if ( !$wgEnableBotPasswords ) {
309  return false;
310  }
311 
312  $dbw = self::getDB( DB_PRIMARY );
313  $dbw->update(
314  'bot_passwords',
315  [ 'bp_password' => PasswordFactory::newInvalidPassword()->toString() ],
316  [ 'bp_user' => $centralId ],
317  __METHOD__
318  );
319  return (bool)$dbw->affectedRows();
320  }
321 
327  public static function removeAllPasswordsForUser( $username ) {
328  return MediaWikiServices::getInstance()
329  ->getBotPasswordStore()
330  ->removeUserPasswords( (string)$username );
331  }
332 
341  public static function removeAllPasswordsForCentralId( $centralId ) {
342  wfDeprecated( __METHOD__, '1.37' );
343 
344  global $wgEnableBotPasswords;
345 
346  if ( !$wgEnableBotPasswords ) {
347  return false;
348  }
349 
350  $dbw = self::getDB( DB_PRIMARY );
351  $dbw->delete(
352  'bot_passwords',
353  [ 'bp_user' => $centralId ],
354  __METHOD__
355  );
356  return (bool)$dbw->affectedRows();
357  }
358 
364  public static function generatePassword( $config ) {
366  max( self::PASSWORD_MINLENGTH, $config->get( 'MinimalPasswordLength' ) ) );
367  }
368 
378  public static function canonicalizeLoginData( $username, $password ) {
379  $sep = self::getSeparator();
380  // the strlen check helps minimize the password information obtainable from timing
381  if ( strlen( $password ) >= self::PASSWORD_MINLENGTH && strpos( $username, $sep ) !== false ) {
382  // the separator is not valid in new usernames but might appear in legacy ones
383  if ( preg_match( '/^[0-9a-w]{' . self::PASSWORD_MINLENGTH . ',}$/', $password ) ) {
384  return [ $username, $password ];
385  }
386  } elseif ( strlen( $password ) > self::PASSWORD_MINLENGTH && strpos( $password, $sep ) !== false ) {
387  $segments = explode( $sep, $password );
388  $password = array_pop( $segments );
389  $appId = implode( $sep, $segments );
390  if ( preg_match( '/^[0-9a-w]{' . self::PASSWORD_MINLENGTH . ',}$/', $password ) ) {
391  return [ $username . $sep . $appId, $password ];
392  }
393  }
394  return false;
395  }
396 
404  public static function login( $username, $password, WebRequest $request ) {
406 
407  if ( !$wgEnableBotPasswords ) {
408  return Status::newFatal( 'botpasswords-disabled' );
409  }
410 
411  $provider = SessionManager::singleton()->getProvider( BotPasswordSessionProvider::class );
412  if ( !$provider ) {
413  return Status::newFatal( 'botpasswords-no-provider' );
414  }
415 
416  // Split name into name+appId
417  $sep = self::getSeparator();
418  if ( strpos( $username, $sep ) === false ) {
419  return self::loginHook( $username, null, Status::newFatal( 'botpasswords-invalid-name', $sep ) );
420  }
421  list( $name, $appId ) = explode( $sep, $username, 2 );
422 
423  // Find the named user
424  $user = User::newFromName( $name );
425  if ( !$user || $user->isAnon() ) {
426  return self::loginHook( $user ?: $name, null, Status::newFatal( 'nosuchuser', $name ) );
427  }
428 
429  if ( $user->isLocked() ) {
430  return Status::newFatal( 'botpasswords-locked' );
431  }
432 
433  $throttle = null;
434  if ( !empty( $wgPasswordAttemptThrottle ) ) {
435  $throttle = new Throttler( $wgPasswordAttemptThrottle, [
436  'type' => 'botpassword',
438  ] );
439  $result = $throttle->increase( $user->getName(), $request->getIP(), __METHOD__ );
440  if ( $result ) {
441  $msg = wfMessage( 'login-throttled' )->durationParams( $result['wait'] );
442  return self::loginHook( $user, null, Status::newFatal( $msg ) );
443  }
444  }
445 
446  // Get the bot password
447  $bp = self::newFromUser( $user, $appId );
448  if ( !$bp ) {
449  return self::loginHook( $user, $bp,
450  Status::newFatal( 'botpasswords-not-exist', $name, $appId ) );
451  }
452 
453  // Check restrictions
454  $status = $bp->getRestrictions()->check( $request );
455  if ( !$status->isOK() ) {
456  return self::loginHook( $user, $bp, Status::newFatal( 'botpasswords-restriction-failed' ) );
457  }
458 
459  // Check the password
460  $passwordObj = $bp->getPassword();
461  if ( $passwordObj instanceof InvalidPassword ) {
462  return self::loginHook( $user, $bp,
463  Status::newFatal( 'botpasswords-needs-reset', $name, $appId ) );
464  }
465  if ( !$passwordObj->verify( $password ) ) {
466  return self::loginHook( $user, $bp, Status::newFatal( 'wrongpassword' ) );
467  }
468 
469  // Ok! Create the session.
470  if ( $throttle ) {
471  $throttle->clear( $user->getName(), $request->getIP() );
472  }
473  return self::loginHook( $user, $bp,
474  // @phan-suppress-next-line PhanUndeclaredMethod
475  Status::newGood( $provider->newSessionForRequest( $user, $bp, $request ) ) );
476  }
477 
489  private static function loginHook( $user, $bp, Status $status ) {
490  $extraData = [];
491  if ( $user instanceof User ) {
492  $name = $user->getName();
493  if ( $bp ) {
494  $extraData['appId'] = $name . self::getSeparator() . $bp->getAppId();
495  }
496  } else {
497  $name = $user;
498  $user = null;
499  }
500 
501  if ( $status->isGood() ) {
502  $response = AuthenticationResponse::newPass( $name );
503  } else {
504  $response = AuthenticationResponse::newFail( $status->getMessage() );
505  }
506  Hooks::runner()->onAuthManagerLoginAuthenticateAudit( $response, $user, $name, $extraData );
507 
508  return $status;
509  }
510 }
MWRestrictions
A class to check request restrictions expressed as a JSON object.
Definition: MWRestrictions.php:27
BotPassword\getRestrictions
getRestrictions()
Definition: BotPassword.php:181
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:160
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
Defaults to {.
Definition: BotPassword.php:73
BotPassword\canonicalizeLoginData
static canonicalizeLoginData( $username, $password)
There are two ways to login with a bot password: "username@appId", "password" and "username",...
Definition: BotPassword.php:378
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:186
BotPassword
Utility class for bot passwords.
Definition: BotPassword.php:33
BotPassword\newFromUser
static newFromUser(UserIdentity $userIdentity, $appId, $flags=self::READ_NORMAL)
Load a BotPassword from the database.
Definition: BotPassword.php:111
BotPassword\getSeparator
static getSeparator()
Get the separator for combined user name + app ID.
Definition: BotPassword.php:196
Status\getMessage
getMessage( $shortContext=false, $longContext=false, $lang=null)
Get a bullet list of the errors as a Message object.
Definition: Status.php:243
BotPassword\generatePassword
static generatePassword( $config)
Returns a (raw, unhashed) random password string.
Definition: BotPassword.php:364
BotPassword\$grants
string[] $grants
Definition: BotPassword.php:70
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:231
User\newFromName
static newFromName( $name, $validate='valid')
Definition: User.php:602
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1182
BotPassword\getAppId
getAppId()
Definition: BotPassword.php:167
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:174
BotPassword\getDB
static getDB( $db)
Get a database connection for the bot passwords database.
Definition: BotPassword.php:98
BotPassword\invalidateAllPasswordsForCentralId
static invalidateAllPasswordsForCentralId( $centralId)
Invalidate all passwords for a user, by central ID.
Definition: BotPassword.php:303
BotPassword\APPID_MAXLENGTH
const APPID_MAXLENGTH
Definition: BotPassword.php:35
BotPassword\$isSaved
bool $isSaved
Definition: BotPassword.php:55
IDBAccessObject
Interface for database access objects.
Definition: IDBAccessObject.php:57
BotPassword\__construct
__construct( $row, $isSaved, $flags=self::READ_NORMAL)
Definition: BotPassword.php:82
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:39
BotPassword\save
save( $operation, Password $password=null)
Save the BotPassword to the database.
Definition: BotPassword.php:242
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:64
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
BotPassword\isSaved
isSaved()
Indicate whether this is known to be saved.
Definition: BotPassword.php:152
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:996
BotPassword\newUnsaved
static newUnsaved(array $data, $flags=self::READ_NORMAL)
Create an unsaved BotPassword.
Definition: BotPassword.php:142
BotPassword\$appId
string $appId
Definition: BotPassword.php:61
Status\wrap
static wrap( $sv)
Succinct helper method to wrap a StatusValue.
Definition: Status.php:62
BotPassword\RESTRICTIONS_MAXLENGTH
const RESTRICTIONS_MAXLENGTH
Maximum length of the json representation of restrictions.
Definition: BotPassword.php:46
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:5774
BotPassword\PASSWORD_MINLENGTH
const PASSWORD_MINLENGTH
Minimum length for a bot password.
Definition: BotPassword.php:40
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:204
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
DB_PRIMARY
const DB_PRIMARY
Definition: defines.php:27
BotPassword\invalidateAllPasswordsForUser
static invalidateAllPasswordsForUser( $username)
Invalidate all passwords for a user, by name.
Definition: BotPassword.php:289
BotPassword\$centralId
int $centralId
Definition: BotPassword.php:58
MediaWiki\Session\SessionManager
This serves as the entry point to the MediaWiki session handling system.
Definition: SessionManager.php:83
Hooks\runner
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:173
$wgPasswordAttemptThrottle
$wgPasswordAttemptThrottle
Limit password attempts to X attempts per Y seconds per IP per account.
Definition: DefaultSettings.php:6661
BotPassword\newFromCentralId
static newFromCentralId( $centralId, $appId, $flags=self::READ_NORMAL)
Load a BotPassword from the database.
Definition: BotPassword.php:124
BotPassword\login
static login( $username, $password, WebRequest $request)
Try to log the user in.
Definition: BotPassword.php:404
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:1261
BotPassword\getGrants
getGrants()
Definition: BotPassword.php:188
PasswordFactory\newInvalidPassword
static newInvalidPassword()
Create an InvalidPassword.
Definition: PasswordFactory.php:242
BotPassword\loginHook
static loginHook( $user, $bp, Status $status)
Call AuthManagerLoginAuthenticateAudit.
Definition: BotPassword.php:489
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:52
BotPassword\removeAllPasswordsForUser
static removeAllPasswordsForUser( $username)
Remove all passwords for a user, by name.
Definition: BotPassword.php:327
$wgEnableBotPasswords
bool $wgEnableBotPasswords
Whether to enable bot passwords.
Definition: DefaultSettings.php:6838
Password
Represents a password hash for use in authentication.
Definition: Password.php:61
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:68
BotPassword\removeAllPasswordsForCentralId
static removeAllPasswordsForCentralId( $centralId)
Remove all passwords for a user, by central ID.
Definition: BotPassword.php:341
BotPassword\$restrictions
MWRestrictions $restrictions
Definition: BotPassword.php:67