Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 264
0.00% covered (danger)
0.00%
0 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
CentralAuthPrimaryAuthenticationProvider
0.00% covered (danger)
0.00%
0 / 264
0.00% covered (danger)
0.00%
0 / 17
10920
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
2
 getAuthenticationRequests
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
72
 getPasswordAuthenticationRequest
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 beginPrimaryAuthentication
0.00% covered (danger)
0.00%
0 / 68
0.00% covered (danger)
0.00%
0 / 1
462
 postAuthentication
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
72
 testUserCanAuthenticate
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
42
 testUserExists
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 providerAllowsAuthenticationDataChange
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
110
 providerChangeAuthenticationData
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
72
 accountCreationType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 testUserForCreation
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
182
 getAntiSpoofAuthenticationRequest
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 testForAccountCreation
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
42
 beginPrimaryAccountCreation
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
56
 finishAccountCreation
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 autoCreatedAccount
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
42
 isAutoCreatedByCentralAuth
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
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
22namespace MediaWiki\Extension\CentralAuth;
23
24use CentralAuthSessionProvider;
25use CentralAuthTokenSessionProvider;
26use LogicException;
27use MediaWiki\Auth\AbstractPasswordPrimaryAuthenticationProvider;
28use MediaWiki\Auth\AuthenticationRequest;
29use MediaWiki\Auth\AuthenticationResponse;
30use MediaWiki\Auth\AuthManager;
31use MediaWiki\Auth\PasswordAuthenticationRequest;
32use MediaWiki\Context\RequestContext;
33use MediaWiki\Deferred\DeferredUpdates;
34use MediaWiki\Extension\AntiSpoof\AntiSpoofAuthenticationRequest;
35use MediaWiki\Extension\CentralAuth\GlobalRename\GlobalRenameRequestStore;
36use MediaWiki\Extension\CentralAuth\User\CentralAuthAntiSpoofManager;
37use MediaWiki\Extension\CentralAuth\User\CentralAuthUser;
38use MediaWiki\Password\InvalidPassword;
39use MediaWiki\User\User;
40use MediaWiki\User\UserIdentityLookup;
41use MediaWiki\User\UserNameUtils;
42use MediaWiki\WikiMap\WikiMap;
43use MWExceptionHandler;
44use Psr\Log\NullLogger;
45use StatusValue;
46use Wikimedia\Rdbms\DBAccessObjectUtils;
47use Wikimedia\Rdbms\IDBAccessObject;
48use Wikimedia\Rdbms\ReadOnlyMode;
49
50/**
51 * A primary authentication provider that uses the CentralAuth password.
52 */
53class 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}