Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
97.90% |
140 / 143 |
|
85.00% |
17 / 20 |
CRAP | |
0.00% |
0 / 1 |
UserAuthority | |
97.90% |
140 / 143 |
|
85.00% |
17 / 20 |
51 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
setUseLimitCache | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getUser | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isAllowed | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isAllowedAny | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
isAllowedAll | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
probablyCan | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
definitelyCan | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
isDefinitelyAllowed | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
authorizeAction | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
authorizeRead | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
authorizeWrite | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
internalAllowed | |
100.00% |
31 / 31 |
|
100.00% |
1 / 1 |
11 | |||
internalCan | |
100.00% |
27 / 27 |
|
100.00% |
1 / 1 |
9 | |||
limit | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
8 | |||
getBlock | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
5 | |||
getApplicableBlock | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
isRegistered | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isTemp | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isNamed | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | namespace MediaWiki\Permissions; |
22 | |
23 | use IDBAccessObject; |
24 | use InvalidArgumentException; |
25 | use MediaWiki\Block\Block; |
26 | use MediaWiki\Block\BlockErrorFormatter; |
27 | use MediaWiki\Context\IContextSource; |
28 | use MediaWiki\Linker\LinkTarget; |
29 | use MediaWiki\Page\PageIdentity; |
30 | use MediaWiki\Request\WebRequest; |
31 | use MediaWiki\Title\TitleValue; |
32 | use MediaWiki\User\User; |
33 | use MediaWiki\User\UserIdentity; |
34 | use Wikimedia\Assert\Assert; |
35 | use Wikimedia\DebugInfo\DebugInfoTrait; |
36 | |
37 | /** |
38 | * Represents the authority of a given User. For anonymous visitors, this will typically |
39 | * allow only basic permissions. For logged in users, permissions are generally based on group |
40 | * membership, but may be adjusted based on things like IP range blocks, OAuth grants, or |
41 | * rate limits. |
42 | * |
43 | * @note This is intended as an intermediate step towards an implementation of Authority that |
44 | * contains much of the logic currently in PermissionManager, and is based directly on |
45 | * WebRequest and Session, rather than a User object. However, for now, code that needs an |
46 | * Authority that reflects the current user and web request should use a User object directly. |
47 | * |
48 | * @unstable |
49 | * @since 1.36 |
50 | */ |
51 | class UserAuthority implements Authority { |
52 | |
53 | use DebugInfoTrait; |
54 | |
55 | /** |
56 | * @var PermissionManager |
57 | * @noVarDump |
58 | */ |
59 | private $permissionManager; |
60 | |
61 | /** |
62 | * @var RateLimiter |
63 | * @noVarDump |
64 | */ |
65 | private $rateLimiter; |
66 | |
67 | /** |
68 | * @var User |
69 | * @noVarDump |
70 | */ |
71 | private $actor; |
72 | |
73 | /** |
74 | * Local cache for user block information. False is used to indicate that there is no block, |
75 | * while null indicates that we don't know and have to check. |
76 | * @var Block|false|null |
77 | */ |
78 | private $userBlock = null; |
79 | |
80 | /** |
81 | * Cache for the outcomes of rate limit checks. |
82 | * We cache the outcomes primarily so we don't bump the counter multiple times |
83 | * per request. |
84 | * @var array<string,array> Map of actions to [ int, bool ] pairs. |
85 | * The first element is the increment performed so far (typically 1). |
86 | * The second element is the cached outcome of the check (whether the limit was reached) |
87 | */ |
88 | private $limitCache = []; |
89 | |
90 | /** |
91 | * Whether the limit cache should be used. Generally, the limit cache should be used in web |
92 | * requests, since we don't want to bump the same limit more than once per request. It |
93 | * should not be used during testing, so limits can easily be tested without knowledge |
94 | * about the caching mechanism. |
95 | * |
96 | * @var bool |
97 | */ |
98 | private bool $useLimitCache; |
99 | |
100 | private WebRequest $request; |
101 | private IContextSource $uiContext; |
102 | private BlockErrorFormatter $blockErrorFormatter; |
103 | |
104 | /** |
105 | * @param User $user |
106 | * @param WebRequest $request |
107 | * @param IContextSource $uiContext |
108 | * @param PermissionManager $permissionManager |
109 | * @param RateLimiter $rateLimiter |
110 | * @param BlockErrorFormatter $blockErrorFormatter |
111 | */ |
112 | public function __construct( |
113 | User $user, |
114 | WebRequest $request, |
115 | IContextSource $uiContext, |
116 | PermissionManager $permissionManager, |
117 | RateLimiter $rateLimiter, |
118 | BlockErrorFormatter $blockErrorFormatter |
119 | ) { |
120 | $this->actor = $user; |
121 | $this->request = $request; |
122 | $this->uiContext = $uiContext; |
123 | $this->permissionManager = $permissionManager; |
124 | $this->rateLimiter = $rateLimiter; |
125 | $this->blockErrorFormatter = $blockErrorFormatter; |
126 | $this->useLimitCache = !defined( 'MW_PHPUNIT_TEST' ); |
127 | } |
128 | |
129 | /** |
130 | * @internal |
131 | * @param bool $useLimitCache |
132 | */ |
133 | public function setUseLimitCache( bool $useLimitCache ) { |
134 | $this->useLimitCache = $useLimitCache; |
135 | } |
136 | |
137 | /** @inheritDoc */ |
138 | public function getUser(): UserIdentity { |
139 | return $this->actor; |
140 | } |
141 | |
142 | /** @inheritDoc */ |
143 | public function isAllowed( string $permission, PermissionStatus $status = null ): bool { |
144 | return $this->internalAllowed( $permission, $status, false, null ); |
145 | } |
146 | |
147 | /** @inheritDoc */ |
148 | public function isAllowedAny( ...$permissions ): bool { |
149 | if ( !$permissions ) { |
150 | throw new InvalidArgumentException( 'At least one permission must be specified' ); |
151 | } |
152 | |
153 | return $this->permissionManager->userHasAnyRight( $this->actor, ...$permissions ); |
154 | } |
155 | |
156 | /** @inheritDoc */ |
157 | public function isAllowedAll( ...$permissions ): bool { |
158 | if ( !$permissions ) { |
159 | throw new InvalidArgumentException( 'At least one permission must be specified' ); |
160 | } |
161 | |
162 | return $this->permissionManager->userHasAllRights( $this->actor, ...$permissions ); |
163 | } |
164 | |
165 | /** @inheritDoc */ |
166 | public function probablyCan( |
167 | string $action, |
168 | PageIdentity $target, |
169 | PermissionStatus $status = null |
170 | ): bool { |
171 | return $this->internalCan( |
172 | PermissionManager::RIGOR_QUICK, |
173 | $action, |
174 | $target, |
175 | $status, |
176 | false // do not check the rate limit |
177 | ); |
178 | } |
179 | |
180 | /** @inheritDoc */ |
181 | public function definitelyCan( |
182 | string $action, |
183 | PageIdentity $target, |
184 | PermissionStatus $status = null |
185 | ): bool { |
186 | // Note that we do not use RIGOR_SECURE to avoid hitting the primary |
187 | // database for read operations. RIGOR_FULL performs the same checks, |
188 | // but is subject to replication lag. |
189 | return $this->internalCan( |
190 | PermissionManager::RIGOR_FULL, |
191 | $action, |
192 | $target, |
193 | $status, |
194 | 0 // only check the rate limit, don't count it as a hit |
195 | ); |
196 | } |
197 | |
198 | /** @inheritDoc */ |
199 | public function isDefinitelyAllowed( string $action, PermissionStatus $status = null ): bool { |
200 | $userBlock = $this->getApplicableBlock( PermissionManager::RIGOR_FULL, $action ); |
201 | return $this->internalAllowed( $action, $status, 0, $userBlock ); |
202 | } |
203 | |
204 | /** @inheritDoc */ |
205 | public function authorizeAction( |
206 | string $action, |
207 | PermissionStatus $status = null |
208 | ): bool { |
209 | // Any side-effects can be added here. |
210 | |
211 | $userBlock = $this->getApplicableBlock( PermissionManager::RIGOR_SECURE, $action ); |
212 | |
213 | return $this->internalAllowed( |
214 | $action, |
215 | $status, |
216 | 1, |
217 | $userBlock |
218 | ); |
219 | } |
220 | |
221 | /** @inheritDoc */ |
222 | public function authorizeRead( |
223 | string $action, |
224 | PageIdentity $target, |
225 | PermissionStatus $status = null |
226 | ): bool { |
227 | // Any side-effects can be added here. |
228 | |
229 | // Note that we do not use RIGOR_SECURE to avoid hitting the primary |
230 | // database for read operations. RIGOR_FULL performs the same checks, |
231 | // but is subject to replication lag. |
232 | return $this->internalCan( |
233 | PermissionManager::RIGOR_FULL, |
234 | $action, |
235 | $target, |
236 | $status, |
237 | 1 // count a hit towards the rate limit |
238 | ); |
239 | } |
240 | |
241 | /** @inheritDoc */ |
242 | public function authorizeWrite( |
243 | string $action, |
244 | PageIdentity $target, |
245 | PermissionStatus $status = null |
246 | ): bool { |
247 | // Any side-effects can be added here. |
248 | |
249 | // Note that we need to use RIGOR_SECURE here to ensure that we do not |
250 | // miss a user block or page protection due to replication lag. |
251 | return $this->internalCan( |
252 | PermissionManager::RIGOR_SECURE, |
253 | $action, |
254 | $target, |
255 | $status, |
256 | 1 // count a hit towards the rate limit |
257 | ); |
258 | } |
259 | |
260 | /** |
261 | * Check whether the user is allowed to perform the action, taking into account |
262 | * the user's block status as well as any rate limits. |
263 | * |
264 | * @param string $action |
265 | * @param PermissionStatus|null $status |
266 | * @param int|false $limitRate False means no check, 0 means check only, |
267 | * and 1 means check and increment |
268 | * @param ?Block $userBlock |
269 | * |
270 | * @return bool |
271 | */ |
272 | private function internalAllowed( |
273 | string $action, |
274 | ?PermissionStatus $status, |
275 | $limitRate, |
276 | ?Block $userBlock |
277 | ): bool { |
278 | if ( $status ) { |
279 | Assert::precondition( |
280 | $status->isGood(), |
281 | 'The PermissionStatus passed as $status parameter must still be good' |
282 | ); |
283 | } |
284 | |
285 | if ( !$this->permissionManager->userHasRight( $this->actor, $action ) ) { |
286 | if ( !$status ) { |
287 | return false; |
288 | } |
289 | |
290 | $status->setPermission( $action ); |
291 | $status->merge( |
292 | $this->permissionManager->newFatalPermissionDeniedStatus( |
293 | $action, |
294 | $this->uiContext |
295 | ) |
296 | ); |
297 | } |
298 | |
299 | if ( $userBlock ) { |
300 | if ( !$status ) { |
301 | return false; |
302 | } |
303 | |
304 | $messages = $this->blockErrorFormatter->getMessages( |
305 | $userBlock, |
306 | $this->actor, |
307 | $this->request->getIP() |
308 | ); |
309 | |
310 | $status->setPermission( $action ); |
311 | foreach ( $messages as $message ) { |
312 | $status->fatal( $message ); |
313 | } |
314 | } |
315 | |
316 | // Check and bump the rate limit. |
317 | if ( $limitRate !== false ) { |
318 | $isLimited = $this->limit( $action, $limitRate, $status ); |
319 | if ( $isLimited && !$status ) { |
320 | return false; |
321 | } |
322 | } |
323 | |
324 | return !$status || $status->isOK(); |
325 | } |
326 | |
327 | /** |
328 | * @param string $rigor |
329 | * @param string $action |
330 | * @param PageIdentity $target |
331 | * @param ?PermissionStatus $status |
332 | * @param int|false $limitRate False means no check, 0 means check only, |
333 | * a non-zero values means check and increment |
334 | * |
335 | * @return bool |
336 | */ |
337 | private function internalCan( |
338 | string $rigor, |
339 | string $action, |
340 | PageIdentity $target, |
341 | ?PermissionStatus $status, |
342 | $limitRate |
343 | ): bool { |
344 | // Check and bump the rate limit. |
345 | if ( $limitRate !== false ) { |
346 | $isLimited = $this->limit( $action, $limitRate, $status ); |
347 | if ( $isLimited && !$status ) { |
348 | // bail early if we don't have a status object |
349 | return false; |
350 | } |
351 | } |
352 | |
353 | if ( !( $target instanceof LinkTarget ) ) { |
354 | // TODO: PermissionManager should accept PageIdentity! |
355 | $target = TitleValue::newFromPage( $target ); |
356 | } |
357 | |
358 | if ( $status ) { |
359 | $status->setPermission( $action ); |
360 | |
361 | $errors = $this->permissionManager->getPermissionErrors( |
362 | $action, |
363 | $this->actor, |
364 | $target, |
365 | $rigor |
366 | ); |
367 | |
368 | foreach ( $errors as $err ) { |
369 | $status->fatal( wfMessage( ...$err ) ); |
370 | |
371 | // HACK: Detect whether the permission was denied because the user is blocked. |
372 | // A similar hack exists in ApiBase::$blockMsgMap. |
373 | // When permission checking logic is moved out of PermissionManager, |
374 | // we can record the block info directly when first checking the block, |
375 | // rather than doing that here. |
376 | if ( strpos( $err[0], 'blockedtext' ) !== false ) { |
377 | $block = $this->getBlock(); |
378 | |
379 | if ( $block ) { |
380 | $status->setBlock( $block ); |
381 | } |
382 | } |
383 | } |
384 | |
385 | return $status->isOK(); |
386 | } else { |
387 | // allow PermissionManager to short-circuit |
388 | return $this->permissionManager->userCan( |
389 | $action, |
390 | $this->actor, |
391 | $target, |
392 | $rigor |
393 | ); |
394 | } |
395 | } |
396 | |
397 | /** |
398 | * Check whether a rate limit has been exceeded for the given action. |
399 | * |
400 | * @see RateLimiter::limit |
401 | * @internal For use by User::pingLimiter only. |
402 | * |
403 | * @param string $action |
404 | * @param int $incrBy |
405 | * @param PermissionStatus|null $status |
406 | * |
407 | * @return bool |
408 | */ |
409 | public function limit( string $action, int $incrBy, ?PermissionStatus $status ): bool { |
410 | $isLimited = null; |
411 | |
412 | if ( $this->useLimitCache && isset( $this->limitCache[ $action ] ) ) { |
413 | // subtract the increment that was already applied earlier |
414 | $incrRemaining = $incrBy - $this->limitCache[ $action ][ 0 ]; |
415 | |
416 | // if no increment is left to apply, return the cached outcome |
417 | if ( $incrRemaining < 1 ) { |
418 | $isLimited = $this->limitCache[ $action ][ 1 ]; |
419 | } |
420 | } else { |
421 | $incrRemaining = $incrBy; |
422 | } |
423 | |
424 | if ( $isLimited === null ) { |
425 | // NOTE: Avoid toRateLimitSubject() if possible, for performance |
426 | if ( $this->rateLimiter->isLimitable( $action ) ) { |
427 | $isLimited = $this->rateLimiter->limit( |
428 | $this->actor->toRateLimitSubject(), |
429 | $action, |
430 | $incrRemaining |
431 | ); |
432 | } else { |
433 | $isLimited = false; |
434 | } |
435 | |
436 | // Cache the outcome, so we don't bump the counter twice during the same request. |
437 | $this->limitCache[ $action ] = [ $incrBy, $isLimited ]; |
438 | } |
439 | |
440 | if ( $isLimited && $status ) { |
441 | $status->setRateLimitExceeded(); |
442 | } |
443 | |
444 | return $isLimited; |
445 | } |
446 | |
447 | /** @inheritDoc */ |
448 | public function getBlock( int $freshness = IDBAccessObject::READ_NORMAL ): ?Block { |
449 | // Cache block info, so we don't have to fetch it again unnecessarily. |
450 | if ( $this->userBlock === null || $freshness === IDBAccessObject::READ_LATEST ) { |
451 | $this->userBlock = $this->actor->getBlock( $freshness ); |
452 | |
453 | // if we got null back, remember this as "false" |
454 | $this->userBlock = $this->userBlock ?: false; |
455 | } |
456 | |
457 | // if we remembered "false", return null |
458 | return $this->userBlock ?: null; |
459 | } |
460 | |
461 | private function getApplicableBlock( |
462 | string $rigor, |
463 | string $action, |
464 | ?PageIdentity $target = null |
465 | ): ?Block { |
466 | // NOTE: We follow the parameter order of internalCan here. |
467 | // It doesn't match the one in PermissionManager. |
468 | return $this->permissionManager->getApplicableBlock( |
469 | $action, |
470 | $this->actor, |
471 | $rigor, |
472 | $target, |
473 | $this->request |
474 | ); |
475 | } |
476 | |
477 | public function isRegistered(): bool { |
478 | return $this->actor->isRegistered(); |
479 | } |
480 | |
481 | public function isTemp(): bool { |
482 | return $this->actor->isTemp(); |
483 | } |
484 | |
485 | public function isNamed(): bool { |
486 | return $this->actor->isNamed(); |
487 | } |
488 | } |