Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 264 |
|
0.00% |
0 / 17 |
CRAP | |
0.00% |
0 / 1 |
CentralAuthPrimaryAuthenticationProvider | |
0.00% |
0 / 264 |
|
0.00% |
0 / 17 |
10920 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
2 | |||
getAuthenticationRequests | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
72 | |||
getPasswordAuthenticationRequest | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
beginPrimaryAuthentication | |
0.00% |
0 / 68 |
|
0.00% |
0 / 1 |
462 | |||
postAuthentication | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
72 | |||
testUserCanAuthenticate | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
42 | |||
testUserExists | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
providerAllowsAuthenticationDataChange | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
110 | |||
providerChangeAuthenticationData | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
72 | |||
accountCreationType | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
testUserForCreation | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
182 | |||
getAntiSpoofAuthenticationRequest | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
testForAccountCreation | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
42 | |||
beginPrimaryAccountCreation | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
56 | |||
finishAccountCreation | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
autoCreatedAccount | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
42 | |||
isAutoCreatedByCentralAuth | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 |
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 | * @ingroup Auth |
20 | */ |
21 | |
22 | namespace MediaWiki\Extension\CentralAuth; |
23 | |
24 | use CentralAuthSessionProvider; |
25 | use CentralAuthTokenSessionProvider; |
26 | use LogicException; |
27 | use MediaWiki\Auth\AbstractPasswordPrimaryAuthenticationProvider; |
28 | use MediaWiki\Auth\AuthenticationRequest; |
29 | use MediaWiki\Auth\AuthenticationResponse; |
30 | use MediaWiki\Auth\AuthManager; |
31 | use MediaWiki\Auth\PasswordAuthenticationRequest; |
32 | use MediaWiki\Context\RequestContext; |
33 | use MediaWiki\Deferred\DeferredUpdates; |
34 | use MediaWiki\Extension\AntiSpoof\AntiSpoofAuthenticationRequest; |
35 | use MediaWiki\Extension\CentralAuth\GlobalRename\GlobalRenameRequestStore; |
36 | use MediaWiki\Extension\CentralAuth\User\CentralAuthAntiSpoofManager; |
37 | use MediaWiki\Extension\CentralAuth\User\CentralAuthUser; |
38 | use MediaWiki\Password\InvalidPassword; |
39 | use MediaWiki\User\User; |
40 | use MediaWiki\User\UserIdentityLookup; |
41 | use MediaWiki\User\UserNameUtils; |
42 | use MediaWiki\WikiMap\WikiMap; |
43 | use MWExceptionHandler; |
44 | use Psr\Log\NullLogger; |
45 | use StatusValue; |
46 | use Wikimedia\Rdbms\DBAccessObjectUtils; |
47 | use Wikimedia\Rdbms\IDBAccessObject; |
48 | use Wikimedia\Rdbms\ReadOnlyMode; |
49 | |
50 | /** |
51 | * A primary authentication provider that uses the CentralAuth password. |
52 | */ |
53 | class CentralAuthPrimaryAuthenticationProvider |
54 | extends AbstractPasswordPrimaryAuthenticationProvider |
55 | { |
56 | /** @var string The internal ID of this provider. */ |
57 | public const ID = 'CentralAuthPrimaryAuthenticationProvider'; |
58 | |
59 | private ReadOnlyMode $readOnlyMode; |
60 | private UserIdentityLookup $userIdentityLookup; |
61 | private CentralAuthAntiSpoofManager $caAntiSpoofManager; |
62 | private CentralAuthDatabaseManager $databaseManager; |
63 | private CentralAuthUtilityService $utilityService; |
64 | private GlobalRenameRequestStore $globalRenameRequestStore; |
65 | private SharedDomainUtils $sharedDomainUtils; |
66 | |
67 | /** @var bool Whether to auto-migrate non-merged accounts on login */ |
68 | protected $autoMigrate = null; |
69 | |
70 | /** @var bool Whether to auto-migrate non-global accounts on login */ |
71 | protected $autoMigrateNonGlobalAccounts = null; |
72 | /** @var bool Whether to check for spoofed user names */ |
73 | protected $antiSpoofAccounts = null; |
74 | |
75 | /** |
76 | * @param ReadOnlyMode $readOnlyMode |
77 | * @param UserIdentityLookup $userIdentityLookup |
78 | * @param CentralAuthAntiSpoofManager $caAntiSpoofManager |
79 | * @param CentralAuthDatabaseManager $databaseManager |
80 | * @param CentralAuthUtilityService $utilityService |
81 | * @param GlobalRenameRequestStore $globalRenameRequestStore |
82 | * @param SharedDomainUtils $sharedDomainUtils |
83 | * @param array $params Settings. All are optional, defaulting to the |
84 | * similarly-named $wgCentralAuth* globals. |
85 | * - autoMigrate: If true, attempt to auto-migrate local accounts on other |
86 | * wikis when logging in. |
87 | * - autoMigrateNonGlobalAccounts: If true, attempt to auto-migrate |
88 | * non-global accounts on login. |
89 | * - antiSpoofAccounts: Whether to anti-spoof new accounts. Ignored if the |
90 | * AntiSpoof extension isn't installed or the extension is outdated. |
91 | */ |
92 | public function __construct( |
93 | ReadOnlyMode $readOnlyMode, |
94 | UserIdentityLookup $userIdentityLookup, |
95 | CentralAuthAntiSpoofManager $caAntiSpoofManager, |
96 | CentralAuthDatabaseManager $databaseManager, |
97 | CentralAuthUtilityService $utilityService, |
98 | GlobalRenameRequestStore $globalRenameRequestStore, |
99 | SharedDomainUtils $sharedDomainUtils, |
100 | $params = [] |
101 | ) { |
102 | global $wgCentralAuthAutoMigrate, |
103 | $wgCentralAuthAutoMigrateNonGlobalAccounts, |
104 | $wgCentralAuthStrict, $wgAntiSpoofAccounts; |
105 | |
106 | $this->readOnlyMode = $readOnlyMode; |
107 | $this->userIdentityLookup = $userIdentityLookup; |
108 | $this->caAntiSpoofManager = $caAntiSpoofManager; |
109 | $this->databaseManager = $databaseManager; |
110 | $this->utilityService = $utilityService; |
111 | $this->globalRenameRequestStore = $globalRenameRequestStore; |
112 | $this->sharedDomainUtils = $sharedDomainUtils; |
113 | |
114 | $params += [ |
115 | 'autoMigrate' => $wgCentralAuthAutoMigrate, |
116 | 'autoMigrateNonGlobalAccounts' => $wgCentralAuthAutoMigrateNonGlobalAccounts, |
117 | 'antiSpoofAccounts' => $wgAntiSpoofAccounts, |
118 | 'authoritative' => $wgCentralAuthStrict, |
119 | ]; |
120 | |
121 | parent::__construct( $params ); |
122 | |
123 | $this->autoMigrate = (bool)$params['autoMigrate']; |
124 | $this->autoMigrateNonGlobalAccounts = (bool)$params['autoMigrateNonGlobalAccounts']; |
125 | $this->antiSpoofAccounts = (bool)$params['antiSpoofAccounts']; |
126 | } |
127 | |
128 | /** @inheritDoc */ |
129 | public function getAuthenticationRequests( $action, array $options ) { |
130 | if ( $this->sharedDomainUtils->isSul3Enabled( $this->manager->getRequest() ) |
131 | && !$this->sharedDomainUtils->isSharedDomain() |
132 | && in_array( $action, [ AuthManager::ACTION_LOGIN, AuthManager::ACTION_CREATE ], true ) |
133 | ) { |
134 | return []; |
135 | } |
136 | |
137 | $ret = parent::getAuthenticationRequests( $action, $options ); |
138 | |
139 | if ( $this->antiSpoofAccounts && $action === AuthManager::ACTION_CREATE ) { |
140 | $user = User::newFromName( $options['username'] ) ?: new User(); |
141 | if ( $user->isAllowed( 'override-antispoof' ) ) { |
142 | $ret[] = new AntiSpoofAuthenticationRequest(); |
143 | } |
144 | } |
145 | |
146 | return $ret; |
147 | } |
148 | |
149 | /** |
150 | * @param array $reqs |
151 | * @return PasswordAuthenticationRequest|null |
152 | */ |
153 | private static function getPasswordAuthenticationRequest( array $reqs ) { |
154 | return AuthenticationRequest::getRequestByClass( |
155 | $reqs, PasswordAuthenticationRequest::class |
156 | ); |
157 | } |
158 | |
159 | /** @inheritDoc */ |
160 | public function beginPrimaryAuthentication( array $reqs ) { |
161 | $req = self::getPasswordAuthenticationRequest( $reqs ); |
162 | if ( !$req ) { |
163 | return AuthenticationResponse::newAbstain(); |
164 | } |
165 | |
166 | if ( $req->username === null || $req->password === null ) { |
167 | return AuthenticationResponse::newAbstain(); |
168 | } |
169 | |
170 | $username = $this->userNameUtils->getCanonical( $req->username, UserNameUtils::RIGOR_USABLE ); |
171 | if ( $username === false ) { |
172 | return AuthenticationResponse::newAbstain(); |
173 | } |
174 | |
175 | $status = $this->checkPasswordValidity( $username, $req->password ); |
176 | if ( !$status->isOK() ) { |
177 | return $this->getFatalPasswordErrorResponse( $username, $status ); |
178 | } |
179 | |
180 | if ( $this->sharedDomainUtils->isSul3Enabled( $this->manager->getRequest() ) |
181 | && !$this->sharedDomainUtils->isSharedDomain() |
182 | ) { |
183 | // We are in SUL3 mode on the local domain, we should not have gotten here, |
184 | // it should have been handled by the redirect provider. It is important to |
185 | // prevent authentication as SharedDomainHookHandler might have disabled important checks. |
186 | // But it's relatively easy to get here by accident, if the brittle logic in |
187 | // SharedDomainHookHandler::onAuthManagerFilterProviders fails to disable some provider |
188 | // that generates a password form, so we should fail in some user-comprehensible way. |
189 | MWExceptionHandler::logException( new LogicException( 'Invoked SUL2 provider in SUL3 mode' ) ); |
190 | return AuthenticationResponse::newFail( wfMessage( 'centralauth-login-error-usesul3' ) ); |
191 | } |
192 | |
193 | // First, check normal login |
194 | $centralUser = CentralAuthUser::getInstanceByName( $username ); |
195 | |
196 | $authenticateResult = $centralUser->authenticate( $req->password ); |
197 | |
198 | $pass = $authenticateResult === [ CentralAuthUser::AUTHENTICATE_OK ]; |
199 | |
200 | if ( in_array( CentralAuthUser::AUTHENTICATE_LOCKED, $authenticateResult ) ) { |
201 | if ( !in_array( CentralAuthUser::AUTHENTICATE_BAD_PASSWORD, $authenticateResult ) ) { |
202 | // Because the absence of "bad password" for any code that hooks and receives |
203 | // the returned AuthenticationResponse means either that the password |
204 | // was correct or that the password was not checked, provide "good password" |
205 | // which removes the two possible meanings of no "bad password". |
206 | $authenticateResult[] = CentralAuthUser::AUTHENTICATE_GOOD_PASSWORD; |
207 | } |
208 | return AuthenticationResponse::newFail( |
209 | wfMessage( 'centralauth-login-error-locked' ) |
210 | ->params( wfEscapeWikiText( $centralUser->getName() ) ), |
211 | $authenticateResult |
212 | ); |
213 | } |
214 | |
215 | // If we don't have a central account, see if all local accounts match |
216 | // the password and can be globalized. (bug T72392) |
217 | if ( !$centralUser->exists() ) { |
218 | $this->logger->debug( |
219 | 'no global account for "{username}"', [ 'username' => $username ] ); |
220 | // Confirm using DB_PRIMARY in case of replication lag |
221 | $latestCentralUser = CentralAuthUser::getPrimaryInstanceByName( $username ); |
222 | if ( $this->autoMigrateNonGlobalAccounts && !$latestCentralUser->exists() ) { |
223 | $ok = $latestCentralUser->storeAndMigrate( |
224 | [ $req->password ], |
225 | /* $sendToRC = */ true, |
226 | /* $safe = */ true, |
227 | /* $checkHome = */ true |
228 | ); |
229 | if ( $ok ) { |
230 | $this->logger->debug( |
231 | 'wgCentralAuthAutoMigrateNonGlobalAccounts successful in creating ' . |
232 | 'a global account for "{username}"', |
233 | [ 'username' => $username ] |
234 | ); |
235 | $this->setPasswordResetFlag( $username, $status ); |
236 | return AuthenticationResponse::newPass( $username ); |
237 | } |
238 | } |
239 | return $this->failResponse( $req ); |
240 | } |
241 | |
242 | if ( $pass && $this->autoMigrate ) { |
243 | // If the user passed in the global password, we can identify |
244 | // any remaining local accounts with a matching password |
245 | // and migrate them in transparently. |
246 | // That may or may not include the current wiki. |
247 | $this->logger->debug( 'attempting wgCentralAuthAutoMigrate for "{username}"', [ |
248 | 'username' => $username, |
249 | ] ); |
250 | if ( $centralUser->isAttached() ) { |
251 | // Defer any automatic migration for other wikis |
252 | DeferredUpdates::addCallableUpdate( static function () use ( $username, $req ) { |
253 | $latestCentralUser = CentralAuthUser::getPrimaryInstanceByName( $username ); |
254 | $latestCentralUser->attemptPasswordMigration( $req->password ); |
255 | } ); |
256 | } else { |
257 | // The next steps depend on whether a migration happens for this wiki. |
258 | // Update the $centralUser instance so the checks below reflect any migrations. |
259 | $centralUser = CentralAuthUser::getPrimaryInstanceByName( $username ); |
260 | $centralUser->attemptPasswordMigration( $req->password ); |
261 | } |
262 | } |
263 | |
264 | if ( !$centralUser->isAttached() ) { |
265 | $local = User::newFromName( $username ); |
266 | if ( $local && $local->getId() ) { |
267 | // An unattached local account; central authentication can't |
268 | // be used until this account has been transferred. |
269 | // $wgCentralAuthStrict will determine if local login is allowed. |
270 | $this->logger->debug( 'unattached account for "{username}"', [ |
271 | 'username' => $username, |
272 | ] ); |
273 | return $this->failResponse( $req ); |
274 | } |
275 | } |
276 | |
277 | if ( $pass ) { |
278 | $this->setPasswordResetFlag( $username, $status ); |
279 | return AuthenticationResponse::newPass( $username ); |
280 | } else { |
281 | // We know the central user is attached at this point, so never |
282 | // fall back to other password providers. |
283 | return AuthenticationResponse::newFail( wfMessage( 'wrongpassword' ) ); |
284 | } |
285 | } |
286 | |
287 | /** @inheritDoc */ |
288 | public function postAuthentication( $user, AuthenticationResponse $response ) { |
289 | if ( $response->status === AuthenticationResponse::PASS ) { |
290 | $centralUser = CentralAuthUser::getInstance( $user ); |
291 | if ( $centralUser->exists() && |
292 | $centralUser->isAttached() && |
293 | $centralUser->getEmail() != $user->getEmail() && |
294 | !$this->readOnlyMode->isReadOnly() |
295 | ) { |
296 | DeferredUpdates::addCallableUpdate( static function () use ( $user ) { |
297 | $centralUser = CentralAuthUser::getPrimaryInstance( $user ); |
298 | if ( !$centralUser->exists() || !$centralUser->isAttached() ) { |
299 | // something major changed? |
300 | return; |
301 | } |
302 | |
303 | $user->setEmail( $centralUser->getEmail() ); |
304 | // @TODO: avoid direct User object field access |
305 | $user->mEmailAuthenticated = $centralUser->getEmailAuthenticationTimestamp(); |
306 | $user->saveSettings(); |
307 | } ); |
308 | } |
309 | } |
310 | } |
311 | |
312 | /** @inheritDoc */ |
313 | public function testUserCanAuthenticate( $username ) { |
314 | $username = $this->userNameUtils->getCanonical( $username, UserNameUtils::RIGOR_USABLE ); |
315 | if ( $username === false ) { |
316 | return false; |
317 | } |
318 | |
319 | // Note this omits the case where an unattached local user exists but |
320 | // will be globalized on login thanks to $this->autoMigrate or |
321 | // $this->autoMigrateNonGlobalAccounts. Both are impossible to really |
322 | // test here because they both need cleartext passwords to do their |
323 | // thing. If you have such accounts on your wiki, you should have |
324 | // LocalPasswordPrimaryAuthenticationProvider configured too which |
325 | // will return true for such users. |
326 | |
327 | $centralUser = CentralAuthUser::getInstanceByName( $username ); |
328 | $userIdentity = $this->userIdentityLookup->getUserIdentityByName( $username ); |
329 | return $centralUser->exists() && |
330 | ( $centralUser->isAttached() || !$userIdentity || !$userIdentity->isRegistered() ) && |
331 | !$centralUser->getPasswordObject() instanceof InvalidPassword; |
332 | } |
333 | |
334 | /** @inheritDoc */ |
335 | public function testUserExists( $username, $flags = IDBAccessObject::READ_NORMAL ) { |
336 | $username = $this->userNameUtils->getCanonical( $username, UserNameUtils::RIGOR_USABLE ); |
337 | if ( $username === false ) { |
338 | return false; |
339 | } |
340 | |
341 | $centralUser = CentralAuthUser::getInstanceByName( $username ); |
342 | return $centralUser->exists(); |
343 | } |
344 | |
345 | /** @inheritDoc */ |
346 | public function providerAllowsAuthenticationDataChange( |
347 | AuthenticationRequest $req, $checkData = true |
348 | ) { |
349 | if ( get_class( $req ) === PasswordAuthenticationRequest::class ) { |
350 | if ( !$checkData ) { |
351 | return StatusValue::newGood(); |
352 | } |
353 | |
354 | $username = $this->userNameUtils->getCanonical( $req->username, UserNameUtils::RIGOR_USABLE ); |
355 | if ( $username !== false ) { |
356 | $centralUser = CentralAuthUser::getInstanceByName( $username ); |
357 | $userIdentity = $this->userIdentityLookup |
358 | ->getUserIdentityByName( $username, IDBAccessObject::READ_LATEST ); |
359 | if ( $centralUser->exists() && |
360 | ( $centralUser->isAttached() || |
361 | !$userIdentity || !$userIdentity->isRegistered() ) |
362 | ) { |
363 | $sv = StatusValue::newGood(); |
364 | if ( $req->password !== null ) { |
365 | if ( $req->password !== $req->retype ) { |
366 | $sv->fatal( 'badretype' ); |
367 | } else { |
368 | $sv->merge( $this->checkPasswordValidity( $username, $req->password ) ); |
369 | } |
370 | } |
371 | return $sv; |
372 | } |
373 | } |
374 | } |
375 | |
376 | return StatusValue::newGood( 'ignored' ); |
377 | } |
378 | |
379 | /** @inheritDoc */ |
380 | public function providerChangeAuthenticationData( AuthenticationRequest $req ) { |
381 | $username = $req->username !== null |
382 | ? $this->userNameUtils->getCanonical( $req->username, UserNameUtils::RIGOR_USABLE ) |
383 | : false; |
384 | if ( $username === false ) { |
385 | return; |
386 | } |
387 | |
388 | if ( get_class( $req ) === PasswordAuthenticationRequest::class ) { |
389 | $centralUser = CentralAuthUser::getPrimaryInstanceByName( $username ); |
390 | $userIdentity = $this->userIdentityLookup->getUserIdentityByName( $username, IDBAccessObject::READ_LATEST ); |
391 | if ( $centralUser->exists() && |
392 | ( $centralUser->isAttached() || !$userIdentity || !$userIdentity->isRegistered() ) |
393 | ) { |
394 | $centralUser->setPassword( $req->password ); |
395 | } |
396 | } |
397 | } |
398 | |
399 | public function accountCreationType() { |
400 | return self::TYPE_CREATE; |
401 | } |
402 | |
403 | /** @inheritDoc */ |
404 | public function testUserForCreation( $user, $autocreate, array $options = [] ) { |
405 | global $wgCentralAuthEnableGlobalRenameRequest; |
406 | |
407 | $options += [ 'flags' => IDBAccessObject::READ_NORMAL ]; |
408 | |
409 | $status = parent::testUserForCreation( $user, $autocreate, $options ); |
410 | if ( !$status->isOK() ) { |
411 | return $status; |
412 | } |
413 | |
414 | $centralUser = DBAccessObjectUtils::hasFlags( $options['flags'], IDBAccessObject::READ_LATEST ) |
415 | ? CentralAuthUser::getPrimaryInstance( $user ) |
416 | : CentralAuthUser::getInstance( $user ); |
417 | |
418 | // Rename in progress? |
419 | if ( $centralUser->renameInProgressOn( WikiMap::getCurrentWikiId(), $options['flags'] ) ) { |
420 | $status->fatal( 'centralauth-rename-abortlogin', $user->getName() ); |
421 | return $status; |
422 | } |
423 | |
424 | if ( !$this->isAutoCreatedByCentralAuth( $user, $autocreate ) ) { |
425 | // Prevent creation if the user exists centrally |
426 | if ( $centralUser->exists() ) { |
427 | $status->fatal( 'centralauth-account-exists' ); |
428 | return $status; |
429 | } |
430 | |
431 | // Prevent creation of a new account that would create a global account |
432 | // if it'd steal the global name of existing unattached local accounts |
433 | if ( $centralUser->listUnattached() && $autocreate === false ) { |
434 | $status->fatal( 'centralauth-account-unattached-exists' ); |
435 | return $status; |
436 | } |
437 | |
438 | // Block account creation if name is a pending rename request |
439 | if ( $wgCentralAuthEnableGlobalRenameRequest && |
440 | $this->globalRenameRequestStore->nameHasPendingRequest( $user->getName() ) |
441 | ) { |
442 | $status->fatal( 'centralauth-account-rename-exists' ); |
443 | return $status; |
444 | } |
445 | } |
446 | |
447 | // Check CentralAuthAntiSpoof, if applicable. Assume the user will override if they can. |
448 | if ( $this->antiSpoofAccounts && empty( $options['creating'] ) && |
449 | !RequestContext::getMain()->getAuthority()->isAllowed( 'override-antispoof' ) |
450 | ) { |
451 | $status->merge( $this->caAntiSpoofManager->testNewAccount( |
452 | $user, new User, true, false, new NullLogger |
453 | ) ); |
454 | } |
455 | |
456 | return $status; |
457 | } |
458 | |
459 | /** |
460 | * @param array $reqs |
461 | * @return AntiSpoofAuthenticationRequest|null |
462 | */ |
463 | private static function getAntiSpoofAuthenticationRequest( array $reqs ) { |
464 | return AuthenticationRequest::getRequestByClass( |
465 | $reqs, |
466 | AntiSpoofAuthenticationRequest::class |
467 | ); |
468 | } |
469 | |
470 | /** @inheritDoc */ |
471 | public function testForAccountCreation( $user, $creator, array $reqs ) { |
472 | $req = self::getPasswordAuthenticationRequest( $reqs ); |
473 | |
474 | $ret = StatusValue::newGood(); |
475 | if ( $req && $req->username !== null && $req->password !== null ) { |
476 | if ( $req->password !== $req->retype ) { |
477 | $ret->fatal( 'badretype' ); |
478 | } else { |
479 | $ret->merge( |
480 | $this->checkPasswordValidity( $user->getName(), $req->password ) |
481 | ); |
482 | } |
483 | } |
484 | |
485 | // Check CentralAuthAntiSpoof, if applicable |
486 | $antiSpoofReq = self::getAntiSpoofAuthenticationRequest( $reqs ); |
487 | $ret->merge( $this->caAntiSpoofManager->testNewAccount( |
488 | $user, $creator, $this->antiSpoofAccounts, |
489 | $antiSpoofReq && $antiSpoofReq->ignoreAntiSpoof |
490 | ) ); |
491 | |
492 | return $ret; |
493 | } |
494 | |
495 | /** @inheritDoc */ |
496 | public function beginPrimaryAccountCreation( $user, $creator, array $reqs ) { |
497 | $req = self::getPasswordAuthenticationRequest( $reqs ); |
498 | if ( $req ) { |
499 | if ( $req->username !== null && $req->password !== null ) { |
500 | $centralUser = CentralAuthUser::getPrimaryInstance( $user ); |
501 | if ( $centralUser->exists() ) { |
502 | return AuthenticationResponse::newFail( |
503 | wfMessage( 'centralauth-account-exists' ) |
504 | ); |
505 | } |
506 | if ( $centralUser->listUnattached() ) { |
507 | // $this->testUserForCreation() will already have rejected it if necessary |
508 | return AuthenticationResponse::newAbstain(); |
509 | } |
510 | // Username is unused; set up as a global account |
511 | if ( !$centralUser->register( $req->password, $user->getEmail() ) ) { |
512 | // Wha? |
513 | return AuthenticationResponse::newFail( wfMessage( 'userexists' ) ); |
514 | } |
515 | return AuthenticationResponse::newPass( $user->getName() ); |
516 | } |
517 | } |
518 | return AuthenticationResponse::newAbstain(); |
519 | } |
520 | |
521 | /** @inheritDoc */ |
522 | public function finishAccountCreation( $user, $creator, AuthenticationResponse $response ) { |
523 | $centralUser = CentralAuthUser::getPrimaryInstance( $user ); |
524 | // Do the attach in finishAccountCreation instead of begin because now the user has been |
525 | // added to database and local ID exists (which is needed in attach) |
526 | $centralUser->attach( WikiMap::getCurrentWikiId(), 'new' ); |
527 | $this->databaseManager->getCentralPrimaryDB()->onTransactionCommitOrIdle( |
528 | function () use ( $centralUser ) { |
529 | $this->utilityService->scheduleCreationJobs( $centralUser ); |
530 | }, |
531 | __METHOD__ |
532 | ); |
533 | return null; |
534 | } |
535 | |
536 | /** @inheritDoc */ |
537 | public function autoCreatedAccount( $user, $source ) { |
538 | $centralUser = CentralAuthUser::getPrimaryInstance( $user ); |
539 | if ( !$centralUser->exists() ) { |
540 | // For named accounts, this is a bug. beginPrimaryAccountCreation() should have created |
541 | // the central account. |
542 | // For temp accounts, it is normal. The central account gets created by |
543 | // UserCreationHookHandler, but this method gets called first. |
544 | if ( $user->isNamed() ) { |
545 | $this->logger->warning( |
546 | 'Not centralizing auto-created user {username}, central account doesn\'t exist', |
547 | [ |
548 | 'user' => $user->getName(), |
549 | ] |
550 | ); |
551 | } |
552 | } elseif ( !$this->isAutoCreatedByCentralAuth( $user, $source ) |
553 | && $centralUser->listUnattached() |
554 | ) { |
555 | $this->logger->warning( |
556 | 'Not centralizing auto-created user {username}, unattached accounts exist', |
557 | [ |
558 | 'user' => $user->getName(), |
559 | 'source' => $source, |
560 | ] |
561 | ); |
562 | } else { |
563 | $this->logger->info( |
564 | 'Centralizing auto-created user {username}', |
565 | [ |
566 | 'user' => $user->getName(), |
567 | ] |
568 | ); |
569 | $centralUser->attach( WikiMap::getCurrentWikiId(), 'login' ); |
570 | $centralUser->addLocalName( WikiMap::getCurrentWikiId() ); |
571 | |
572 | if ( $centralUser->getEmail() != $user->getEmail() ) { |
573 | $user->setEmail( $centralUser->getEmail() ); |
574 | $user->mEmailAuthenticated = $centralUser->getEmailAuthenticationTimestamp(); |
575 | } |
576 | } |
577 | } |
578 | |
579 | /** |
580 | * @param User $user |
581 | * @param string $source Autocreation source - the $autocreate parameter passed to |
582 | * testUserForCreation(), or the $source parameter passed to autoCreatedAccount(). |
583 | * @return bool |
584 | */ |
585 | private function isAutoCreatedByCentralAuth( User $user, string $source ): bool { |
586 | if ( $source === AuthManager::AUTOCREATE_SOURCE_SESSION ) { |
587 | // True if the autocreating session provider belongs to CentralAuth. |
588 | // There isn't a clean way to obtain the session, but since we are autocreating |
589 | // from the session, $user should be the session user. |
590 | $sessionProvider = $user->getRequest()->getSession()->getProvider(); |
591 | return $sessionProvider instanceof CentralAuthSessionProvider |
592 | || $sessionProvider instanceof CentralAuthTokenSessionProvider; |
593 | } elseif ( $source ) { |
594 | // True if the autocreating authentication provider belongs to CentralAuth. |
595 | $centralAuthPrimaryProviderIds = [ |
596 | $this->getUniqueId(), |
597 | CentralAuthRedirectingPrimaryAuthenticationProvider::class, |
598 | CentralAuthTemporaryPasswordPrimaryAuthenticationProvider::class, |
599 | ]; |
600 | return in_array( $source, $centralAuthPrimaryProviderIds, true ); |
601 | } else { |
602 | // Not an autocreation at all. |
603 | return false; |
604 | } |
605 | } |
606 | } |