MediaWiki  master
BotPasswordStore.php
Go to the documentation of this file.
1 <?php
23 namespace MediaWiki\User;
24 
25 use BotPassword;
26 use CentralIdLookup;
28 use FormatJson;
29 use IDBAccessObject;
32 use MWCryptRand;
33 use MWRestrictions;
34 use Password;
35 use PasswordFactory;
36 use StatusValue;
37 use User;
40 
46 
50  public const CONSTRUCTOR_OPTIONS = [
54  ];
55 
57  private $options;
58 
60  private $lbFactory;
61 
63  private $centralIdLookup;
64 
70  public function __construct(
71  ServiceOptions $options,
72  CentralIdLookup $centralIdLookup,
73  LBFactory $lbFactory
74  ) {
75  $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
76  $this->options = $options;
77  $this->centralIdLookup = $centralIdLookup;
78  $this->lbFactory = $lbFactory;
79  }
80 
87  public function getDatabase( int $db ): IDatabase {
88  if ( $this->options->get( MainConfigNames::BotPasswordsCluster ) ) {
89  $loadBalancer = $this->lbFactory->getExternalLB(
90  $this->options->get( MainConfigNames::BotPasswordsCluster )
91  );
92  } else {
93  $loadBalancer = $this->lbFactory->getMainLB(
94  $this->options->get( MainConfigNames::BotPasswordsDatabase )
95  );
96  }
97  return $loadBalancer->getConnectionRef(
98  $db,
99  [],
100  $this->options->get( MainConfigNames::BotPasswordsDatabase )
101  );
102  }
103 
111  public function getByUser(
112  UserIdentity $userIdentity,
113  string $appId,
114  int $flags = self::READ_NORMAL
115  ): ?BotPassword {
116  if ( !$this->options->get( MainConfigNames::EnableBotPasswords ) ) {
117  return null;
118  }
119 
120  $centralId = $this->centralIdLookup->centralIdFromLocalUser(
121  $userIdentity,
123  $flags
124  );
125  return $centralId ? $this->getByCentralId( $centralId, $appId, $flags ) : null;
126  }
127 
135  public function getByCentralId(
136  int $centralId,
137  string $appId,
138  int $flags = self::READ_NORMAL
139  ): ?BotPassword {
140  if ( !$this->options->get( MainConfigNames::EnableBotPasswords ) ) {
141  return null;
142  }
143 
144  [ $index, $options ] = DBAccessObjectUtils::getDBOptions( $flags );
145  $db = $this->getDatabase( $index );
146  $row = $db->selectRow(
147  'bot_passwords',
148  [ 'bp_user', 'bp_app_id', 'bp_token', 'bp_restrictions', 'bp_grants' ],
149  [ 'bp_user' => $centralId, 'bp_app_id' => $appId ],
150  __METHOD__,
151  $options
152  );
153  return $row ? new BotPassword( $row, true, $flags ) : null;
154  }
155 
168  public function newUnsavedBotPassword(
169  array $data,
170  int $flags = self::READ_NORMAL
171  ): ?BotPassword {
172  if ( isset( $data['user'] ) && ( !$data['user'] instanceof UserIdentity ) ) {
173  return null;
174  }
175 
176  $row = (object)[
177  'bp_user' => 0,
178  'bp_app_id' => trim( $data['appId'] ?? '' ),
179  'bp_token' => '**unsaved**',
180  'bp_restrictions' => $data['restrictions'] ?? MWRestrictions::newDefault(),
181  'bp_grants' => $data['grants'] ?? [],
182  ];
183 
184  if (
185  $row->bp_app_id === '' ||
186  strlen( $row->bp_app_id ) > BotPassword::APPID_MAXLENGTH ||
187  !$row->bp_restrictions instanceof MWRestrictions ||
188  !is_array( $row->bp_grants )
189  ) {
190  return null;
191  }
192 
193  $row->bp_restrictions = $row->bp_restrictions->toJson();
194  $row->bp_grants = FormatJson::encode( $row->bp_grants );
195 
196  if ( isset( $data['user'] ) ) {
197  // Must be a UserIdentity object, already checked above
198  $row->bp_user = $this->centralIdLookup->centralIdFromLocalUser(
199  $data['user'],
201  $flags
202  );
203  } elseif ( isset( $data['username'] ) ) {
204  $row->bp_user = $this->centralIdLookup->centralIdFromName(
205  $data['username'],
207  $flags
208  );
209  } elseif ( isset( $data['centralId'] ) ) {
210  $row->bp_user = $data['centralId'];
211  }
212  if ( !$row->bp_user ) {
213  return null;
214  }
215 
216  return new BotPassword( $row, false, $flags );
217  }
218 
228  public function insertBotPassword(
229  BotPassword $botPassword,
230  Password $password = null
231  ): StatusValue {
232  $res = $this->validateBotPassword( $botPassword );
233  if ( !$res->isGood() ) {
234  return $res;
235  }
236 
237  if ( $password === null ) {
239  }
240  $fields = [
241  'bp_user' => $botPassword->getUserCentralId(),
242  'bp_app_id' => $botPassword->getAppId(),
244  'bp_restrictions' => $botPassword->getRestrictions()->toJson(),
245  'bp_grants' => FormatJson::encode( $botPassword->getGrants() ),
246  'bp_password' => $password->toString(),
247  ];
248 
249  $dbw = $this->getDatabase( DB_PRIMARY );
250  $dbw->insert(
251  'bot_passwords',
252  $fields,
253  __METHOD__,
254  [ 'IGNORE' ]
255  );
256 
257  $ok = (bool)$dbw->affectedRows();
258  if ( $ok ) {
259  $token = $dbw->selectField(
260  'bot_passwords',
261  'bp_token',
262  [
263  'bp_user' => $botPassword->getUserCentralId(),
264  'bp_app_id' => $botPassword->getAppId(),
265  ],
266  __METHOD__
267  );
268  return StatusValue::newGood( $token );
269  }
270  return StatusValue::newFatal( 'botpasswords-insert-failed', $botPassword->getAppId() );
271  }
272 
282  public function updateBotPassword(
283  BotPassword $botPassword,
284  Password $password = null
285  ): StatusValue {
286  $res = $this->validateBotPassword( $botPassword );
287  if ( !$res->isGood() ) {
288  return $res;
289  }
290 
291  $conds = [
292  'bp_user' => $botPassword->getUserCentralId(),
293  'bp_app_id' => $botPassword->getAppId(),
294  ];
295  $fields = [
297  'bp_restrictions' => $botPassword->getRestrictions()->toJson(),
298  'bp_grants' => FormatJson::encode( $botPassword->getGrants() ),
299  ];
300  if ( $password !== null ) {
301  $fields['bp_password'] = $password->toString();
302  }
303 
304  $dbw = $this->getDatabase( DB_PRIMARY );
305  $dbw->update(
306  'bot_passwords',
307  $fields,
308  $conds,
309  __METHOD__
310  );
311 
312  $ok = (bool)$dbw->affectedRows();
313  if ( $ok ) {
314  $token = $dbw->selectField(
315  'bot_passwords',
316  'bp_token',
317  $conds,
318  __METHOD__
319  );
320  return StatusValue::newGood( $token );
321  }
322  return StatusValue::newFatal( 'botpasswords-update-failed', $botPassword->getAppId() );
323  }
324 
332  private function validateBotPassword( BotPassword $botPassword ): StatusValue {
333  $res = StatusValue::newGood();
334 
335  $restrictions = $botPassword->getRestrictions()->toJson();
336  if ( strlen( $restrictions ) > BotPassword::RESTRICTIONS_MAXLENGTH ) {
337  $res->fatal( 'botpasswords-toolong-restrictions' );
338  }
339 
340  $grants = FormatJson::encode( $botPassword->getGrants() );
341  if ( strlen( $grants ) > BotPassword::GRANTS_MAXLENGTH ) {
342  $res->fatal( 'botpasswords-toolong-grants' );
343  }
344 
345  return $res;
346  }
347 
354  public function deleteBotPassword( BotPassword $botPassword ): bool {
355  $dbw = $this->getDatabase( DB_PRIMARY );
356  $dbw->delete(
357  'bot_passwords',
358  [
359  'bp_user' => $botPassword->getUserCentralId(),
360  'bp_app_id' => $botPassword->getAppId(),
361  ],
362  __METHOD__
363  );
364 
365  return (bool)$dbw->affectedRows();
366  }
367 
373  public function invalidateUserPasswords( string $username ): bool {
374  if ( !$this->options->get( MainConfigNames::EnableBotPasswords ) ) {
375  return false;
376  }
377 
378  $centralId = $this->centralIdLookup->centralIdFromName(
379  $username,
381  CentralIdLookup::READ_LATEST
382  );
383  if ( !$centralId ) {
384  return false;
385  }
386 
387  $dbw = $this->getDatabase( DB_PRIMARY );
388  $dbw->update(
389  'bot_passwords',
390  [ 'bp_password' => PasswordFactory::newInvalidPassword()->toString() ],
391  [ 'bp_user' => $centralId ],
392  __METHOD__
393  );
394  return (bool)$dbw->affectedRows();
395  }
396 
402  public function removeUserPasswords( string $username ): bool {
403  if ( !$this->options->get( MainConfigNames::EnableBotPasswords ) ) {
404  return false;
405  }
406 
407  $centralId = $this->centralIdLookup->centralIdFromName(
408  $username,
410  CentralIdLookup::READ_LATEST
411  );
412  if ( !$centralId ) {
413  return false;
414  }
415 
416  $dbw = $this->getDatabase( DB_PRIMARY );
417  $dbw->delete(
418  'bot_passwords',
419  [ 'bp_user' => $centralId ],
420  __METHOD__
421  );
422  return (bool)$dbw->affectedRows();
423  }
424 
425 }
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
Definition: WebStart.php:82
Utility class for bot passwords.
Definition: BotPassword.php:34
const APPID_MAXLENGTH
Definition: BotPassword.php:36
getUserCentralId()
Get the central user ID.
const RESTRICTIONS_MAXLENGTH
Maximum length of the json representation of restrictions.
Definition: BotPassword.php:47
const GRANTS_MAXLENGTH
Maximum length of the json representation of grants.
Definition: BotPassword.php:53
The CentralIdLookup service allows for connecting local users with cluster-wide IDs.
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:26
static encode( $value, $pretty=false, $escaping=0)
Returns the JSON representation of a value.
Definition: FormatJson.php:96
static generateHex( $chars)
Generate a run of cryptographically random data and return it in hexadecimal string format.
Definition: MWCryptRand.php:36
A class to check request restrictions expressed as a JSON object.
static newDefault()
A class for passing options to services.
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
A class containing constants representing the names of configuration variables.
const BotPasswordsDatabase
Name constant for the BotPasswordsDatabase setting, for use with Config::get()
const EnableBotPasswords
Name constant for the EnableBotPasswords setting, for use with Config::get()
const BotPasswordsCluster
Name constant for the BotPasswordsCluster setting, for use with Config::get()
getByUser(UserIdentity $userIdentity, string $appId, int $flags=self::READ_NORMAL)
Load a BotPassword from the database based on a UserIdentity object.
deleteBotPassword(BotPassword $botPassword)
Delete an existing BotPassword in the database.
newUnsavedBotPassword(array $data, int $flags=self::READ_NORMAL)
Create an unsaved BotPassword.
invalidateUserPasswords(string $username)
Invalidate all passwords for a user, by name.
removeUserPasswords(string $username)
Remove all passwords for a user, by name.
getDatabase(int $db)
Get a database connection for the bot passwords database.
__construct(ServiceOptions $options, CentralIdLookup $centralIdLookup, LBFactory $lbFactory)
getByCentralId(int $centralId, string $appId, int $flags=self::READ_NORMAL)
Load a BotPassword from the database.
insertBotPassword(BotPassword $botPassword, Password $password=null)
Save the new BotPassword to the database.
updateBotPassword(BotPassword $botPassword, Password $password=null)
Update an existing BotPassword in the database.
Factory class for creating and checking Password objects.
static newInvalidPassword()
Create an InvalidPassword.
Represents a password hash for use in authentication.
Definition: Password.php:61
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: StatusValue.php:46
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:73
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:85
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:70
const TOKEN_LENGTH
Number of characters required for the user_token field.
Definition: User.php:89
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:40
const DB_PRIMARY
Definition: defines.php:28