MediaWiki  master
BotPassword.php
Go to the documentation of this file.
1 <?php
25 
30 class BotPassword implements IDBAccessObject {
31 
32  public const APPID_MAXLENGTH = 32;
33 
37  public const PASSWORD_MINLENGTH = 32;
38 
40  private $isSaved;
41 
43  private $centralId;
44 
46  private $appId;
47 
49  private $token;
50 
52  private $restrictions;
53 
55  private $grants;
56 
58  private $flags = self::READ_NORMAL;
59 
65  protected function __construct( $row, $isSaved, $flags = self::READ_NORMAL ) {
66  $this->isSaved = $isSaved;
67  $this->flags = $flags;
68 
69  $this->centralId = (int)$row->bp_user;
70  $this->appId = $row->bp_app_id;
71  $this->token = $row->bp_token;
72  $this->restrictions = MWRestrictions::newFromJson( $row->bp_restrictions );
73  $this->grants = FormatJson::decode( $row->bp_grants );
74  }
75 
81  public static function getDB( $db ) {
83 
84  $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
86  ? $lbFactory->getExternalLB( $wgBotPasswordsCluster )
87  : $lbFactory->getMainLB( $wgBotPasswordsDatabase );
88  return $lb->getConnectionRef( $db, [], $wgBotPasswordsDatabase );
89  }
90 
98  public static function newFromUser( User $user, $appId, $flags = self::READ_NORMAL ) {
99  $centralId = CentralIdLookup::factory()->centralIdFromLocalUser(
101  );
103  }
104 
112  public static function newFromCentralId( $centralId, $appId, $flags = self::READ_NORMAL ) {
113  global $wgEnableBotPasswords;
114 
115  if ( !$wgEnableBotPasswords ) {
116  return null;
117  }
118 
119  list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
120  $db = self::getDB( $index );
121  $row = $db->selectRow(
122  'bot_passwords',
123  [ 'bp_user', 'bp_app_id', 'bp_token', 'bp_restrictions', 'bp_grants' ],
124  [ 'bp_user' => $centralId, 'bp_app_id' => $appId ],
125  __METHOD__,
126  $options
127  );
128  return $row ? new self( $row, true, $flags ) : null;
129  }
130 
143  public static function newUnsaved( array $data, $flags = self::READ_NORMAL ) {
144  $row = (object)[
145  'bp_user' => 0,
146  'bp_app_id' => isset( $data['appId'] ) ? trim( $data['appId'] ) : '',
147  'bp_token' => '**unsaved**',
148  'bp_restrictions' => $data['restrictions'] ?? MWRestrictions::newDefault(),
149  'bp_grants' => $data['grants'] ?? [],
150  ];
151 
152  if (
153  $row->bp_app_id === '' || strlen( $row->bp_app_id ) > self::APPID_MAXLENGTH ||
154  !$row->bp_restrictions instanceof MWRestrictions ||
155  !is_array( $row->bp_grants )
156  ) {
157  return null;
158  }
159 
160  $row->bp_restrictions = $row->bp_restrictions->toJson();
161  $row->bp_grants = FormatJson::encode( $row->bp_grants );
162 
163  if ( isset( $data['user'] ) ) {
164  if ( !$data['user'] instanceof User ) {
165  return null;
166  }
167  $row->bp_user = CentralIdLookup::factory()->centralIdFromLocalUser(
168  $data['user'], CentralIdLookup::AUDIENCE_RAW, $flags
169  );
170  } elseif ( isset( $data['username'] ) ) {
171  $row->bp_user = CentralIdLookup::factory()->centralIdFromName(
172  $data['username'], CentralIdLookup::AUDIENCE_RAW, $flags
173  );
174  } elseif ( isset( $data['centralId'] ) ) {
175  $row->bp_user = $data['centralId'];
176  }
177  if ( !$row->bp_user ) {
178  return null;
179  }
180 
181  return new self( $row, false, $flags );
182  }
183 
188  public function isSaved() {
189  return $this->isSaved;
190  }
191 
196  public function getUserCentralId() {
197  return $this->centralId;
198  }
199 
204  public function getAppId() {
205  return $this->appId;
206  }
207 
212  public function getToken() {
213  return $this->token;
214  }
215 
220  public function getRestrictions() {
221  return $this->restrictions;
222  }
223 
228  public function getGrants() {
229  return $this->grants;
230  }
231 
236  public static function getSeparator() {
239  }
240 
245  protected function getPassword() {
246  list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $this->flags );
247  $db = self::getDB( $index );
248  $password = $db->selectField(
249  'bot_passwords',
250  'bp_password',
251  [ 'bp_user' => $this->centralId, 'bp_app_id' => $this->appId ],
252  __METHOD__,
253  $options
254  );
255  if ( $password === false ) {
257  }
258 
259  $passwordFactory = MediaWikiServices::getInstance()->getPasswordFactory();
260  try {
261  return $passwordFactory->newFromCiphertext( $password );
262  } catch ( PasswordError $ex ) {
264  }
265  }
266 
272  public function isInvalid() {
273  return $this->getPassword() instanceof InvalidPassword;
274  }
275 
282  public function save( $operation, Password $password = null ) {
283  $conds = [
284  'bp_user' => $this->centralId,
285  'bp_app_id' => $this->appId,
286  ];
287  $fields = [
289  'bp_restrictions' => $this->restrictions->toJson(),
290  'bp_grants' => FormatJson::encode( $this->grants ),
291  ];
292 
293  if ( $password !== null ) {
294  $fields['bp_password'] = $password->toString();
295  } elseif ( $operation === 'insert' ) {
296  $fields['bp_password'] = PasswordFactory::newInvalidPassword()->toString();
297  }
298 
299  $dbw = self::getDB( DB_MASTER );
300  switch ( $operation ) {
301  case 'insert':
302  $dbw->insert( 'bot_passwords', $fields + $conds, __METHOD__, [ 'IGNORE' ] );
303  break;
304 
305  case 'update':
306  $dbw->update( 'bot_passwords', $fields, $conds, __METHOD__ );
307  break;
308 
309  default:
310  return false;
311  }
312  $ok = (bool)$dbw->affectedRows();
313  if ( $ok ) {
314  $this->token = $dbw->selectField( 'bot_passwords', 'bp_token', $conds, __METHOD__ );
315  $this->isSaved = true;
316  }
317  return $ok;
318  }
319 
324  public function delete() {
325  $conds = [
326  'bp_user' => $this->centralId,
327  'bp_app_id' => $this->appId,
328  ];
329  $dbw = self::getDB( DB_MASTER );
330  $dbw->delete( 'bot_passwords', $conds, __METHOD__ );
331  $ok = (bool)$dbw->affectedRows();
332  if ( $ok ) {
333  $this->token = '**unsaved**';
334  $this->isSaved = false;
335  }
336  return $ok;
337  }
338 
344  public static function invalidateAllPasswordsForUser( $username ) {
345  $centralId = CentralIdLookup::factory()->centralIdFromName(
346  $username, CentralIdLookup::AUDIENCE_RAW, CentralIdLookup::READ_LATEST
347  );
349  }
350 
356  public static function invalidateAllPasswordsForCentralId( $centralId ) {
357  global $wgEnableBotPasswords;
358 
359  if ( !$wgEnableBotPasswords ) {
360  return false;
361  }
362 
363  $dbw = self::getDB( DB_MASTER );
364  $dbw->update(
365  'bot_passwords',
366  [ 'bp_password' => PasswordFactory::newInvalidPassword()->toString() ],
367  [ 'bp_user' => $centralId ],
368  __METHOD__
369  );
370  return (bool)$dbw->affectedRows();
371  }
372 
378  public static function removeAllPasswordsForUser( $username ) {
379  $centralId = CentralIdLookup::factory()->centralIdFromName(
380  $username, CentralIdLookup::AUDIENCE_RAW, CentralIdLookup::READ_LATEST
381  );
383  }
384 
390  public static function removeAllPasswordsForCentralId( $centralId ) {
391  global $wgEnableBotPasswords;
392 
393  if ( !$wgEnableBotPasswords ) {
394  return false;
395  }
396 
397  $dbw = self::getDB( DB_MASTER );
398  $dbw->delete(
399  'bot_passwords',
400  [ 'bp_user' => $centralId ],
401  __METHOD__
402  );
403  return (bool)$dbw->affectedRows();
404  }
405 
411  public static function generatePassword( $config ) {
413  max( self::PASSWORD_MINLENGTH, $config->get( 'MinimalPasswordLength' ) ) );
414  }
415 
425  public static function canonicalizeLoginData( $username, $password ) {
426  $sep = self::getSeparator();
427  // the strlen check helps minimize the password information obtainable from timing
428  if ( strlen( $password ) >= self::PASSWORD_MINLENGTH && strpos( $username, $sep ) !== false ) {
429  // the separator is not valid in new usernames but might appear in legacy ones
430  if ( preg_match( '/^[0-9a-w]{' . self::PASSWORD_MINLENGTH . ',}$/', $password ) ) {
431  return [ $username, $password ];
432  }
433  } elseif ( strlen( $password ) > self::PASSWORD_MINLENGTH && strpos( $password, $sep ) !== false ) {
434  $segments = explode( $sep, $password );
435  $password = array_pop( $segments );
436  $appId = implode( $sep, $segments );
437  if ( preg_match( '/^[0-9a-w]{' . self::PASSWORD_MINLENGTH . ',}$/', $password ) ) {
438  return [ $username . $sep . $appId, $password ];
439  }
440  }
441  return false;
442  }
443 
451  public static function login( $username, $password, WebRequest $request ) {
453 
454  if ( !$wgEnableBotPasswords ) {
455  return Status::newFatal( 'botpasswords-disabled' );
456  }
457 
459  $provider = $manager->getProvider( BotPasswordSessionProvider::class );
460  if ( !$provider ) {
461  return Status::newFatal( 'botpasswords-no-provider' );
462  }
463 
464  // Split name into name+appId
465  $sep = self::getSeparator();
466  if ( strpos( $username, $sep ) === false ) {
467  return self::loginHook( $username, null, Status::newFatal( 'botpasswords-invalid-name', $sep ) );
468  }
469  list( $name, $appId ) = explode( $sep, $username, 2 );
470 
471  // Find the named user
472  $user = User::newFromName( $name );
473  if ( !$user || $user->isAnon() ) {
474  return self::loginHook( $user ?: $name, null, Status::newFatal( 'nosuchuser', $name ) );
475  }
476 
477  if ( $user->isLocked() ) {
478  return Status::newFatal( 'botpasswords-locked' );
479  }
480 
481  $throttle = null;
482  if ( !empty( $wgPasswordAttemptThrottle ) ) {
484  'type' => 'botpassword',
486  ] );
487  $result = $throttle->increase( $user->getName(), $request->getIP(), __METHOD__ );
488  if ( $result ) {
489  $msg = wfMessage( 'login-throttled' )->durationParams( $result['wait'] );
490  return self::loginHook( $user, null, Status::newFatal( $msg ) );
491  }
492  }
493 
494  // Get the bot password
495  $bp = self::newFromUser( $user, $appId );
496  if ( !$bp ) {
497  return self::loginHook( $user, $bp,
498  Status::newFatal( 'botpasswords-not-exist', $name, $appId ) );
499  }
500 
501  // Check restrictions
502  $status = $bp->getRestrictions()->check( $request );
503  if ( !$status->isOK() ) {
504  return self::loginHook( $user, $bp, Status::newFatal( 'botpasswords-restriction-failed' ) );
505  }
506 
507  // Check the password
508  $passwordObj = $bp->getPassword();
509  if ( $passwordObj instanceof InvalidPassword ) {
510  return self::loginHook( $user, $bp,
511  Status::newFatal( 'botpasswords-needs-reset', $name, $appId ) );
512  }
513  if ( !$passwordObj->verify( $password ) ) {
514  return self::loginHook( $user, $bp, Status::newFatal( 'wrongpassword' ) );
515  }
516 
517  // Ok! Create the session.
518  if ( $throttle ) {
519  $throttle->clear( $user->getName(), $request->getIP() );
520  }
521  return self::loginHook( $user, $bp,
522  // @phan-suppress-next-line PhanUndeclaredMethod
523  Status::newGood( $provider->newSessionForRequest( $user, $bp, $request ) ) );
524  }
525 
537  private static function loginHook( $user, $bp, Status $status ) {
538  $extraData = [];
539  if ( $user instanceof User ) {
540  $name = $user->getName();
541  if ( $bp ) {
542  $extraData['appId'] = $name . self::getSeparator() . $bp->getAppId();
543  }
544  } else {
545  $name = $user;
546  $user = null;
547  }
548 
549  if ( $status->isGood() ) {
550  $response = AuthenticationResponse::newPass( $name );
551  } else {
552  $response = AuthenticationResponse::newFail( $status->getMessage() );
553  }
554  Hooks::runner()->onAuthManagerLoginAuthenticateAudit( $response, $user, $name, $extraData );
555 
556  return $status;
557  }
558 }
MWRestrictions
A class to check request restrictions expressed as a JSON object.
Definition: MWRestrictions.php:27
BotPassword\getRestrictions
getRestrictions()
Get the restrictions.
Definition: BotPassword.php:220
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:196
$wgBotPasswordsDatabase
string bool $wgBotPasswordsDatabase
Database name for the bot_passwords table.
Definition: DefaultSettings.php:6411
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:58
BotPassword\canonicalizeLoginData
static canonicalizeLoginData( $username, $password)
There are two ways to login with a bot password: "username@appId", "password" and "username",...
Definition: BotPassword.php:425
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:163
BotPassword
Utility class for bot passwords.
Definition: BotPassword.php:30
BotPassword\getSeparator
static getSeparator()
Get the separator for combined user name + app ID.
Definition: BotPassword.php:236
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:6401
BotPassword\generatePassword
static generatePassword( $config)
Returns a (raw, unhashed) random password string.
Definition: BotPassword.php:411
BotPassword\$grants
string[] $grants
Definition: BotPassword.php:55
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:272
User\newFromName
static newFromName( $name, $validate='valid')
Definition: User.php:545
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1220
BotPassword\getAppId
getAppId()
Get the app ID.
Definition: BotPassword.php:204
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()
Get the token.
Definition: BotPassword.php:212
BotPassword\getDB
static getDB( $db)
Get a database connection for the bot passwords database.
Definition: BotPassword.php:81
BotPassword\invalidateAllPasswordsForCentralId
static invalidateAllPasswordsForCentralId( $centralId)
Invalidate all passwords for a user, by central ID.
Definition: BotPassword.php:356
BotPassword\APPID_MAXLENGTH
const APPID_MAXLENGTH
Definition: BotPassword.php:32
BotPassword\$isSaved
bool $isSaved
Definition: BotPassword.php:40
IDBAccessObject
Interface for database access objects.
Definition: IDBAccessObject.php:57
BotPassword\__construct
__construct( $row, $isSaved, $flags=self::READ_NORMAL)
Definition: BotPassword.php:65
BotPassword\save
save( $operation, Password $password=null)
Save the BotPassword to the database.
Definition: BotPassword.php:282
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:49
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:188
BotPassword\newUnsaved
static newUnsaved(array $data, $flags=self::READ_NORMAL)
Create an unsaved BotPassword.
Definition: BotPassword.php:143
BotPassword\$appId
string $appId
Definition: BotPassword.php:46
MediaWiki\Auth\AuthenticationResponse
This is a value object to hold authentication response data.
Definition: AuthenticationResponse.php:37
MediaWiki\Session\SessionManager\singleton
static singleton()
Get the global SessionManager.
Definition: SessionManager.php:100
$wgUserrightsInterwikiDelimiter
$wgUserrightsInterwikiDelimiter
Character used as a delimiter when testing for interwiki userrights (In Special:UserRights,...
Definition: DefaultSettings.php:5349
BotPassword\PASSWORD_MINLENGTH
const PASSWORD_MINLENGTH
Minimum length for a bot password.
Definition: BotPassword.php:37
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:98
PasswordFactory\generateRandomPasswordString
static generateRandomPasswordString(int $minLength=10)
Generate a random string suitable for a password.
Definition: PasswordFactory.php:226
BotPassword\getPassword
getPassword()
Get the password.
Definition: BotPassword.php:245
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:344
BotPassword\$centralId
int $centralId
Definition: BotPassword.php:43
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:6219
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:112
BotPassword\login
static login( $username, $password, WebRequest $request)
Try to log the user in.
Definition: BotPassword.php:451
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:1271
BotPassword\getGrants
getGrants()
Get the grants.
Definition: BotPassword.php:228
PasswordFactory\newInvalidPassword
static newInvalidPassword()
Create an InvalidPassword.
Definition: PasswordFactory.php:242
BotPassword\loginHook
static loginHook( $user, $bp, Status $status)
Call AuthManagerLoginAuthenticateAudit.
Definition: BotPassword.php:537
MWRestrictions\newFromJson
static newFromJson( $json)
Definition: MWRestrictions.php:62
BotPassword\removeAllPasswordsForUser
static removeAllPasswordsForUser( $username)
Remove all passwords for a user, by name.
Definition: BotPassword.php:378
$wgEnableBotPasswords
bool $wgEnableBotPasswords
Whether to enable bot passwords.
Definition: DefaultSettings.php:6394
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:390
BotPassword\$restrictions
MWRestrictions $restrictions
Definition: BotPassword.php:52