MediaWiki 1.41.2
BotPasswordStore.php
Go to the documentation of this file.
1<?php
23namespace MediaWiki\User;
24
26use FormatJson;
31use MWCryptRand;
33use Password;
35use StatusValue;
38
44
48 public const CONSTRUCTOR_OPTIONS = [
52 ];
53
54 private ServiceOptions $options;
55 private LBFactory $lbFactory;
56 private CentralIdLookup $centralIdLookup;
57
63 public function __construct(
64 ServiceOptions $options,
65 CentralIdLookup $centralIdLookup,
66 LBFactory $lbFactory
67 ) {
68 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
69 $this->options = $options;
70 $this->centralIdLookup = $centralIdLookup;
71 $this->lbFactory = $lbFactory;
72 }
73
80 public function getDatabase( int $db ): IDatabase {
81 if ( $this->options->get( MainConfigNames::BotPasswordsCluster ) ) {
82 $loadBalancer = $this->lbFactory->getExternalLB(
83 $this->options->get( MainConfigNames::BotPasswordsCluster )
84 );
85 } else {
86 $loadBalancer = $this->lbFactory->getMainLB(
87 $this->options->get( MainConfigNames::BotPasswordsDatabase )
88 );
89 }
90 return $loadBalancer->getConnectionRef(
91 $db,
92 [],
93 $this->options->get( MainConfigNames::BotPasswordsDatabase )
94 );
95 }
96
104 public function getByUser(
105 UserIdentity $userIdentity,
106 string $appId,
107 int $flags = self::READ_NORMAL
108 ): ?BotPassword {
109 if ( !$this->options->get( MainConfigNames::EnableBotPasswords ) ) {
110 return null;
111 }
112
113 $centralId = $this->centralIdLookup->centralIdFromLocalUser(
114 $userIdentity,
115 CentralIdLookup::AUDIENCE_RAW,
116 $flags
117 );
118 return $centralId ? $this->getByCentralId( $centralId, $appId, $flags ) : null;
119 }
120
128 public function getByCentralId(
129 int $centralId,
130 string $appId,
131 int $flags = self::READ_NORMAL
132 ): ?BotPassword {
133 if ( !$this->options->get( MainConfigNames::EnableBotPasswords ) ) {
134 return null;
135 }
136
137 [ $index, $options ] = DBAccessObjectUtils::getDBOptions( $flags );
138 $db = $this->getDatabase( $index );
139 $row = $db->newSelectQueryBuilder()
140 ->select( [ 'bp_user', 'bp_app_id', 'bp_token', 'bp_restrictions', 'bp_grants' ] )
141 ->from( 'bot_passwords' )
142 ->where( [ 'bp_user' => $centralId, 'bp_app_id' => $appId ] )
143 ->options( $options )
144 ->caller( __METHOD__ )->fetchRow();
145 return $row ? new BotPassword( $row, true, $flags ) : null;
146 }
147
160 public function newUnsavedBotPassword(
161 array $data,
162 int $flags = self::READ_NORMAL
163 ): ?BotPassword {
164 if ( isset( $data['user'] ) && ( !$data['user'] instanceof UserIdentity ) ) {
165 return null;
166 }
167
168 $row = (object)[
169 'bp_user' => 0,
170 'bp_app_id' => trim( $data['appId'] ?? '' ),
171 'bp_token' => '**unsaved**',
172 'bp_restrictions' => $data['restrictions'] ?? MWRestrictions::newDefault(),
173 'bp_grants' => $data['grants'] ?? [],
174 ];
175
176 if (
177 $row->bp_app_id === '' ||
178 strlen( $row->bp_app_id ) > BotPassword::APPID_MAXLENGTH ||
179 !$row->bp_restrictions instanceof MWRestrictions ||
180 !is_array( $row->bp_grants )
181 ) {
182 return null;
183 }
184
185 $row->bp_restrictions = $row->bp_restrictions->toJson();
186 $row->bp_grants = FormatJson::encode( $row->bp_grants );
187
188 if ( isset( $data['user'] ) ) {
189 // Must be a UserIdentity object, already checked above
190 $row->bp_user = $this->centralIdLookup->centralIdFromLocalUser(
191 $data['user'],
192 CentralIdLookup::AUDIENCE_RAW,
193 $flags
194 );
195 } elseif ( isset( $data['username'] ) ) {
196 $row->bp_user = $this->centralIdLookup->centralIdFromName(
197 $data['username'],
198 CentralIdLookup::AUDIENCE_RAW,
199 $flags
200 );
201 } elseif ( isset( $data['centralId'] ) ) {
202 $row->bp_user = $data['centralId'];
203 }
204 if ( !$row->bp_user ) {
205 return null;
206 }
207
208 return new BotPassword( $row, false, $flags );
209 }
210
220 public function insertBotPassword(
221 BotPassword $botPassword,
222 Password $password = null
223 ): StatusValue {
224 $res = $this->validateBotPassword( $botPassword );
225 if ( !$res->isGood() ) {
226 return $res;
227 }
228
229 if ( $password === null ) {
230 $password = PasswordFactory::newInvalidPassword();
231 }
232
233 $dbw = $this->getDatabase( DB_PRIMARY );
234 $dbw->newInsertQueryBuilder()
235 ->insertInto( 'bot_passwords' )
236 ->ignore()
237 ->row( [
238 'bp_user' => $botPassword->getUserCentralId(),
239 'bp_app_id' => $botPassword->getAppId(),
240 'bp_token' => MWCryptRand::generateHex( User::TOKEN_LENGTH ),
241 'bp_restrictions' => $botPassword->getRestrictions()->toJson(),
242 'bp_grants' => FormatJson::encode( $botPassword->getGrants() ),
243 'bp_password' => $password->toString(),
244 ] )
245 ->caller( __METHOD__ )->execute();
246
247 $ok = (bool)$dbw->affectedRows();
248 if ( $ok ) {
249 $token = $dbw->newSelectQueryBuilder()
250 ->select( 'bp_token' )
251 ->from( 'bot_passwords' )
252 ->where( [ 'bp_user' => $botPassword->getUserCentralId(), 'bp_app_id' => $botPassword->getAppId(), ] )
253 ->caller( __METHOD__ )->fetchField();
254 return StatusValue::newGood( $token );
255 }
256 return StatusValue::newFatal( 'botpasswords-insert-failed', $botPassword->getAppId() );
257 }
258
268 public function updateBotPassword(
269 BotPassword $botPassword,
270 Password $password = null
271 ): StatusValue {
272 $res = $this->validateBotPassword( $botPassword );
273 if ( !$res->isGood() ) {
274 return $res;
275 }
276
277 $conds = [
278 'bp_user' => $botPassword->getUserCentralId(),
279 'bp_app_id' => $botPassword->getAppId(),
280 ];
281 $fields = [
282 'bp_token' => MWCryptRand::generateHex( User::TOKEN_LENGTH ),
283 'bp_restrictions' => $botPassword->getRestrictions()->toJson(),
284 'bp_grants' => FormatJson::encode( $botPassword->getGrants() ),
285 ];
286 if ( $password !== null ) {
287 $fields['bp_password'] = $password->toString();
288 }
289
290 $dbw = $this->getDatabase( DB_PRIMARY );
291 $dbw->newUpdateQueryBuilder()
292 ->update( 'bot_passwords' )
293 ->set( $fields )
294 ->where( $conds )
295 ->caller( __METHOD__ )->execute();
296
297 $ok = (bool)$dbw->affectedRows();
298 if ( $ok ) {
299 $token = $dbw->newSelectQueryBuilder()
300 ->select( 'bp_token' )
301 ->from( 'bot_passwords' )
302 ->where( $conds )
303 ->caller( __METHOD__ )->fetchField();
304 return StatusValue::newGood( $token );
305 }
306 return StatusValue::newFatal( 'botpasswords-update-failed', $botPassword->getAppId() );
307 }
308
316 private function validateBotPassword( BotPassword $botPassword ): StatusValue {
317 $res = StatusValue::newGood();
318
319 $restrictions = $botPassword->getRestrictions()->toJson();
320 if ( strlen( $restrictions ) > BotPassword::RESTRICTIONS_MAXLENGTH ) {
321 $res->fatal( 'botpasswords-toolong-restrictions' );
322 }
323
324 $grants = FormatJson::encode( $botPassword->getGrants() );
325 if ( strlen( $grants ) > BotPassword::GRANTS_MAXLENGTH ) {
326 $res->fatal( 'botpasswords-toolong-grants' );
327 }
328
329 return $res;
330 }
331
338 public function deleteBotPassword( BotPassword $botPassword ): bool {
339 $dbw = $this->getDatabase( DB_PRIMARY );
340 $dbw->newDeleteQueryBuilder()
341 ->deleteFrom( 'bot_passwords' )
342 ->where( [ 'bp_user' => $botPassword->getUserCentralId() ] )
343 ->andWhere( [ 'bp_app_id' => $botPassword->getAppId() ] )
344 ->caller( __METHOD__ )->execute();
345
346 return (bool)$dbw->affectedRows();
347 }
348
354 public function invalidateUserPasswords( string $username ): bool {
355 if ( !$this->options->get( MainConfigNames::EnableBotPasswords ) ) {
356 return false;
357 }
358
359 $centralId = $this->centralIdLookup->centralIdFromName(
360 $username,
361 CentralIdLookup::AUDIENCE_RAW,
362 CentralIdLookup::READ_LATEST
363 );
364 if ( !$centralId ) {
365 return false;
366 }
367
368 $dbw = $this->getDatabase( DB_PRIMARY );
369 $dbw->newUpdateQueryBuilder()
370 ->update( 'bot_passwords' )
371 ->set( [ 'bp_password' => PasswordFactory::newInvalidPassword()->toString() ] )
372 ->where( [ 'bp_user' => $centralId ] )
373 ->caller( __METHOD__ )->execute();
374 return (bool)$dbw->affectedRows();
375 }
376
382 public function removeUserPasswords( string $username ): bool {
383 if ( !$this->options->get( MainConfigNames::EnableBotPasswords ) ) {
384 return false;
385 }
386
387 $centralId = $this->centralIdLookup->centralIdFromName(
388 $username,
389 CentralIdLookup::AUDIENCE_RAW,
390 CentralIdLookup::READ_LATEST
391 );
392 if ( !$centralId ) {
393 return false;
394 }
395
396 $dbw = $this->getDatabase( DB_PRIMARY );
397 $dbw->newDeleteQueryBuilder()
398 ->deleteFrom( 'bot_passwords' )
399 ->where( [ 'bp_user' => $centralId ] )
400 ->caller( __METHOD__ )->execute();
401 return (bool)$dbw->affectedRows();
402 }
403
404}
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:88
Helper class for DAO classes.
JSON formatter wrapper class.
static encode( $value, $pretty=false, $escaping=0)
Returns the JSON representation of a value.
A class to check request restrictions expressed as a JSON object.
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.
Utility class for bot passwords.
getUserCentralId()
Get the central user ID.
The CentralIdLookup service allows for connecting local users with cluster-wide IDs.
Factory class for creating and checking Password objects.
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.
fatal( $message,... $parameters)
Add an error and set OK to false, indicating that the operation as a whole was fatal.
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
Utility class for bot passwords.
const DB_PRIMARY
Definition defines.php:28