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() {
197  $userrightsInterwikiDelimiter = MediaWikiServices::getInstance()
198  ->getMainConfig()->get( 'UserrightsInterwikiDelimiter' );
199  return $userrightsInterwikiDelimiter;
200  }
201 
205  private function getPassword() {
206  list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $this->flags );
207  $db = self::getDB( $index );
208  $password = $db->selectField(
209  'bot_passwords',
210  'bp_password',
211  [ 'bp_user' => $this->centralId, 'bp_app_id' => $this->appId ],
212  __METHOD__,
213  $options
214  );
215  if ( $password === false ) {
217  }
218 
219  $passwordFactory = MediaWikiServices::getInstance()->getPasswordFactory();
220  try {
221  return $passwordFactory->newFromCiphertext( $password );
222  } catch ( PasswordError $ex ) {
224  }
225  }
226 
232  public function isInvalid() {
233  return $this->getPassword() instanceof InvalidPassword;
234  }
235 
243  public function save( $operation, Password $password = null ) {
244  // Ensure operation is valid
245  if ( $operation !== 'insert' && $operation !== 'update' ) {
246  throw new UnexpectedValueException(
247  "Expected 'insert' or 'update'; got '{$operation}'."
248  );
249  }
250 
251  $store = MediaWikiServices::getInstance()->getBotPasswordStore();
252  if ( $operation === 'insert' ) {
253  $statusValue = $store->insertBotPassword( $this, $password );
254  } else {
255  // Must be update, already checked above
256  $statusValue = $store->updateBotPassword( $this, $password );
257  }
258 
259  if ( $statusValue->isGood() ) {
260  $this->token = $statusValue->getValue();
261  $this->isSaved = true;
262  return Status::newGood();
263  }
264 
265  // Action failed, status will have code botpasswords-insert-failed or
266  // botpasswords-update-failed depending on which action we tried
267  return Status::wrap( $statusValue );
268  }
269 
274  public function delete() {
275  $ok = MediaWikiServices::getInstance()
276  ->getBotPasswordStore()
277  ->deleteBotPassword( $this );
278  if ( $ok ) {
279  $this->token = '**unsaved**';
280  $this->isSaved = false;
281  }
282  return $ok;
283  }
284 
290  public static function invalidateAllPasswordsForUser( $username ) {
291  return MediaWikiServices::getInstance()
292  ->getBotPasswordStore()
293  ->invalidateUserPasswords( (string)$username );
294  }
295 
304  public static function invalidateAllPasswordsForCentralId( $centralId ) {
305  wfDeprecated( __METHOD__, '1.37' );
306 
307  $enableBotPasswords = MediaWikiServices::getInstance()->getMainConfig()->get( 'EnableBotPasswords' );
308 
309  if ( !$enableBotPasswords ) {
310  return false;
311  }
312 
313  $dbw = self::getDB( DB_PRIMARY );
314  $dbw->update(
315  'bot_passwords',
316  [ 'bp_password' => PasswordFactory::newInvalidPassword()->toString() ],
317  [ 'bp_user' => $centralId ],
318  __METHOD__
319  );
320  return (bool)$dbw->affectedRows();
321  }
322 
328  public static function removeAllPasswordsForUser( $username ) {
329  return MediaWikiServices::getInstance()
330  ->getBotPasswordStore()
331  ->removeUserPasswords( (string)$username );
332  }
333 
342  public static function removeAllPasswordsForCentralId( $centralId ) {
343  wfDeprecated( __METHOD__, '1.37' );
344 
345  $enableBotPasswords = MediaWikiServices::getInstance()->getMainConfig()->get( 'EnableBotPasswords' );
346 
347  if ( !$enableBotPasswords ) {
348  return false;
349  }
350 
351  $dbw = self::getDB( DB_PRIMARY );
352  $dbw->delete(
353  'bot_passwords',
354  [ 'bp_user' => $centralId ],
355  __METHOD__
356  );
357  return (bool)$dbw->affectedRows();
358  }
359 
365  public static function generatePassword( $config ) {
367  max( self::PASSWORD_MINLENGTH, $config->get( 'MinimalPasswordLength' ) ) );
368  }
369 
379  public static function canonicalizeLoginData( $username, $password ) {
380  $sep = self::getSeparator();
381  // the strlen check helps minimize the password information obtainable from timing
382  if ( strlen( $password ) >= self::PASSWORD_MINLENGTH && strpos( $username, $sep ) !== false ) {
383  // the separator is not valid in new usernames but might appear in legacy ones
384  if ( preg_match( '/^[0-9a-w]{' . self::PASSWORD_MINLENGTH . ',}$/', $password ) ) {
385  return [ $username, $password ];
386  }
387  } elseif ( strlen( $password ) > self::PASSWORD_MINLENGTH && strpos( $password, $sep ) !== false ) {
388  $segments = explode( $sep, $password );
389  $password = array_pop( $segments );
390  $appId = implode( $sep, $segments );
391  if ( preg_match( '/^[0-9a-w]{' . self::PASSWORD_MINLENGTH . ',}$/', $password ) ) {
392  return [ $username . $sep . $appId, $password ];
393  }
394  }
395  return false;
396  }
397 
405  public static function login( $username, $password, WebRequest $request ) {
406  $enableBotPasswords = MediaWikiServices::getInstance()->getMainConfig()->get( 'EnableBotPasswords' );
407  $passwordAttemptThrottle = MediaWikiServices::getInstance()->getMainConfig()->get( 'PasswordAttemptThrottle' );
408  if ( !$enableBotPasswords ) {
409  return Status::newFatal( 'botpasswords-disabled' );
410  }
411 
412  $provider = SessionManager::singleton()->getProvider( BotPasswordSessionProvider::class );
413  if ( !$provider ) {
414  return Status::newFatal( 'botpasswords-no-provider' );
415  }
416 
417  // Split name into name+appId
418  $sep = self::getSeparator();
419  if ( strpos( $username, $sep ) === false ) {
420  return self::loginHook( $username, null, Status::newFatal( 'botpasswords-invalid-name', $sep ) );
421  }
422  list( $name, $appId ) = explode( $sep, $username, 2 );
423 
424  // Find the named user
425  $user = User::newFromName( $name );
426  if ( !$user || $user->isAnon() ) {
427  return self::loginHook( $user ?: $name, null, Status::newFatal( 'nosuchuser', $name ) );
428  }
429 
430  if ( $user->isLocked() ) {
431  return Status::newFatal( 'botpasswords-locked' );
432  }
433 
434  $throttle = null;
435  if ( !empty( $passwordAttemptThrottle ) ) {
436  $throttle = new Throttler( $passwordAttemptThrottle, [
437  'type' => 'botpassword',
439  ] );
440  $result = $throttle->increase( $user->getName(), $request->getIP(), __METHOD__ );
441  if ( $result ) {
442  $msg = wfMessage( 'login-throttled' )->durationParams( $result['wait'] );
443  return self::loginHook( $user, null, Status::newFatal( $msg ) );
444  }
445  }
446 
447  // Get the bot password
448  $bp = self::newFromUser( $user, $appId );
449  if ( !$bp ) {
450  return self::loginHook( $user, $bp,
451  Status::newFatal( 'botpasswords-not-exist', $name, $appId ) );
452  }
453 
454  // Check restrictions
455  $status = $bp->getRestrictions()->check( $request );
456  if ( !$status->isOK() ) {
457  return self::loginHook( $user, $bp, Status::newFatal( 'botpasswords-restriction-failed' ) );
458  }
459 
460  // Check the password
461  $passwordObj = $bp->getPassword();
462  if ( $passwordObj instanceof InvalidPassword ) {
463  return self::loginHook( $user, $bp,
464  Status::newFatal( 'botpasswords-needs-reset', $name, $appId ) );
465  }
466  if ( !$passwordObj->verify( $password ) ) {
467  return self::loginHook( $user, $bp, Status::newFatal( 'wrongpassword' ) );
468  }
469 
470  // Ok! Create the session.
471  if ( $throttle ) {
472  $throttle->clear( $user->getName(), $request->getIP() );
473  }
474  return self::loginHook( $user, $bp,
475  // @phan-suppress-next-line PhanUndeclaredMethod
476  Status::newGood( $provider->newSessionForRequest( $user, $bp, $request ) ) );
477  }
478 
490  private static function loginHook( $user, $bp, Status $status ) {
491  $extraData = [];
492  if ( $user instanceof User ) {
493  $name = $user->getName();
494  if ( $bp ) {
495  $extraData['appId'] = $name . self::getSeparator() . $bp->getAppId();
496  }
497  } else {
498  $name = $user;
499  $user = null;
500  }
501 
502  if ( $status->isGood() ) {
503  $response = AuthenticationResponse::newPass( $name );
504  } else {
505  $response = AuthenticationResponse::newFail( $status->getMessage() );
506  }
507  Hooks::runner()->onAuthManagerLoginAuthenticateAudit( $response, $user, $name, $extraData );
508 
509  return $status;
510  }
511 }
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:273
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:379
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:203
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:365
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:232
User\newFromName
static newFromName( $name, $validate='valid')
Definition: User.php:595
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1167
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:304
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:243
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:146
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 a deprecated feature was used.
Definition: GlobalFunctions.php:997
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
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:205
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:290
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
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:405
WebRequest
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
Definition: WebRequest.php:43
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:1272
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:490
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:328
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:67
BotPassword\removeAllPasswordsForCentralId
static removeAllPasswordsForCentralId( $centralId)
Remove all passwords for a user, by central ID.
Definition: BotPassword.php:342
BotPassword\$restrictions
MWRestrictions $restrictions
Definition: BotPassword.php:67