Merge "Various dependency injection cleanups to LoadBalancer"
[lhc/web/wiklou.git] / includes / auth / AuthManager.php
index b8c536e..51efe56 100644 (file)
@@ -37,8 +37,46 @@ use WebRequest;
  * In the future, it may also serve as the entry point to the authorization
  * system.
  *
+ * If you are looking at this because you are working on an extension that creates its own
+ * login or signup page, then 1) you really shouldn't do that, 2) if you feel you absolutely
+ * have to, subclass AuthManagerSpecialPage or build it on the client side using the clientlogin
+ * or the createaccount API. Trying to call this class directly will very likely end up in
+ * security vulnerabilities or broken UX in edge cases.
+ *
+ * If you are working on an extension that needs to integrate with the authentication system
+ * (e.g. by providing a new login method, or doing extra permission checks), you'll probably
+ * need to write an AuthenticationProvider.
+ *
+ * If you want to create a "reserved" user programmatically, User::newSystemUser() might be what
+ * you are looking for. If you want to change user data, use User::changeAuthenticationData().
+ * Code that is related to some SessionProvider or PrimaryAuthenticationProvider can
+ * create a (non-reserved) user by calling AuthManager::autoCreateUser(); it is then the provider's
+ * responsibility to ensure that the user can authenticate somehow (see especially
+ * PrimaryAuthenticationProvider::autoCreatedAccount()).
+ * If you are writing code that is not associated with such a provider and needs to create accounts
+ * programmatically for real users, you should rethink your architecture. There is no good way to
+ * do that as such code has no knowledge of what authentication methods are enabled on the wiki and
+ * cannot provide any means for users to access the accounts it would create.
+ *
+ * The two main control flows when using this class are as follows:
+ * * Login, user creation or account linking code will call getAuthenticationRequests(), populate
+ *   the requests with data (by using them to build a HTMLForm and have the user fill it, or by
+ *   exposing a form specification via the API, so that the client can build it), and pass them to
+ *   the appropriate begin* method. That will return either a success/failure response, or more
+ *   requests to fill (either by building a form or by redirecting the user to some external
+ *   provider which will send the data back), in which case they need to be submitted to the
+ *   appropriate continue* method and that step has to be repeated until the response is a success
+ *   or failure response. AuthManager will use the session to maintain internal state during the
+ *   process.
+ * * Code doing an authentication data change will call getAuthenticationRequests(), select
+ *   a single request, populate it, and pass it to allowsAuthenticationDataChange() and then
+ *   changeAuthenticationData(). If the data change is user-initiated, the whole process needs
+ *   to be preceded by a call to securitySensitiveOperationStatus() and aborted if that returns
+ *   a non-OK status.
+ *
  * @ingroup Auth
  * @since 1.27
+ * @see https://www.mediawiki.org/wiki/Manual:SessionManager_and_AuthManager
  */
 class AuthManager implements LoggerAwareInterface {
        /** Log in with an existing (not necessarily local) user */
@@ -568,6 +606,7 @@ class AuthManager implements LoggerAwareInterface {
 
                        $user = User::newFromName( $res->username, 'usable' );
                        if ( !$user ) {
+                               $provider = $this->getAuthenticationProvider( $state['primary'] );
                                throw new \DomainException(
                                        get_class( $provider ) . " returned an invalid username: {$res->username}"
                                );
@@ -643,6 +682,7 @@ class AuthManager implements LoggerAwareInterface {
                        $this->logger->info( 'Login for {user} succeeded', [
                                'user' => $user->getName(),
                        ] );
+                       /** @var RememberMeAuthenticationRequest $req */
                        $req = AuthenticationRequest::getRequestByClass(
                                $beginReqs, RememberMeAuthenticationRequest::class
                        );
@@ -737,7 +777,10 @@ class AuthManager implements LoggerAwareInterface {
        /**
         * Determine whether a username can authenticate
         *
-        * @param string $username
+        * This is mainly for internal purposes and only takes authentication data into account,
+        * not things like blocks that can change without the authentication system being aware.
+        *
+        * @param string $username MediaWiki username
         * @return bool
         */
        public function userCanAuthenticate( $username ) {
@@ -832,6 +875,9 @@ class AuthManager implements LoggerAwareInterface {
         * If $req was returned for AuthManager::ACTION_REMOVE, using $req should
         * no longer result in a successful login.
         *
+        * This method should only be called if allowsAuthenticationDataChange( $req, true )
+        * returned success.
+        *
         * @param AuthenticationRequest $req
         */
        public function changeAuthenticationData( AuthenticationRequest $req ) {
@@ -871,7 +917,7 @@ class AuthManager implements LoggerAwareInterface {
 
        /**
         * Determine whether a particular account can be created
-        * @param string $username
+        * @param string $username MediaWiki username
         * @param array $options
         *  - flags: (int) Bitfield of User:READ_* constants, default User::READ_NORMAL
         *  - creating: (bool) For internal use only. Never specify this.
@@ -1354,7 +1400,7 @@ class AuthManager implements LoggerAwareInterface {
                                        'creator' => $creator->getName(),
                                ] );
                                $status = $user->addToDatabase();
-                               if ( !$status->isOk() ) {
+                               if ( !$status->isOK() ) {
                                        // @codeCoverageIgnoreStart
                                        $ret = AuthenticationResponse::newFail( $status->getMessage() );
                                        $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
@@ -1385,6 +1431,7 @@ class AuthManager implements LoggerAwareInterface {
                                        );
                                        $logEntry->setPerformer( $isAnon ? $user : $creator );
                                        $logEntry->setTarget( $user->getUserPage() );
+                                       /** @var CreationReasonAuthenticationRequest $req */
                                        $req = AuthenticationRequest::getRequestByClass(
                                                $state['reqs'], CreationReasonAuthenticationRequest::class
                                        );
@@ -1474,6 +1521,13 @@ class AuthManager implements LoggerAwareInterface {
 
        /**
         * Auto-create an account, and log into that account
+        *
+        * PrimaryAuthenticationProviders can invoke this method by returning a PASS from
+        * beginPrimaryAuthentication/continuePrimaryAuthentication with the username of a
+        * non-existing user. SessionProviders can invoke it by returning a SessionInfo with
+        * the username of a non-existing user from provideSessionInfo(). Calling this method
+        * explicitly (e.g. from a maintenance script) is also fine.
+        *
         * @param User $user User to auto-create
         * @param string $source What caused the auto-creation? This must be the ID
         *  of a PrimaryAuthenticationProvider or the constant self::AUTOCREATE_SOURCE_SESSION.
@@ -1489,7 +1543,7 @@ class AuthManager implements LoggerAwareInterface {
 
                $username = $user->getName();
 
-               // Try the local user from the slave DB
+               // Try the local user from the replica DB
                $localId = User::idFromName( $username );
                $flags = User::READ_NORMAL;
 
@@ -1550,7 +1604,7 @@ class AuthManager implements LoggerAwareInterface {
                        $this->logger->debug( __METHOD__ . ': name "{username}" is not creatable', [
                                'username' => $username,
                        ] );
-                       $session->set( 'AuthManager::AutoCreateBlacklist', 'noname', 600 );
+                       $session->set( 'AuthManager::AutoCreateBlacklist', 'noname' );
                        $user->setId( 0 );
                        $user->loadFromId();
                        return Status::newFatal( 'noname' );
@@ -1563,7 +1617,7 @@ class AuthManager implements LoggerAwareInterface {
                                'username' => $username,
                                'ip' => $anon->getName(),
                        ] );
-                       $session->set( 'AuthManager::AutoCreateBlacklist', 'authmanager-autocreate-noperm', 600 );
+                       $session->set( 'AuthManager::AutoCreateBlacklist', 'authmanager-autocreate-noperm' );
                        $session->persist();
                        $user->setId( 0 );
                        $user->loadFromId();
@@ -1598,7 +1652,7 @@ class AuthManager implements LoggerAwareInterface {
                                        'username' => $username,
                                        'reason' => $ret->getWikiText( null, null, 'en' ),
                                ] );
-                               $session->set( 'AuthManager::AutoCreateBlacklist', $status, 600 );
+                               $session->set( 'AuthManager::AutoCreateBlacklist', $status );
                                $user->setId( 0 );
                                $user->loadFromId();
                                return $ret;
@@ -1627,15 +1681,13 @@ class AuthManager implements LoggerAwareInterface {
                $trxProfiler->setSilenced( true );
                try {
                        $status = $user->addToDatabase();
-                       if ( !$status->isOk() ) {
-                               // double-check for a race condition (T70012)
-                               $localId = User::idFromName( $username, User::READ_LATEST );
-                               if ( $localId ) {
+                       if ( !$status->isOK() ) {
+                               // Double-check for a race condition (T70012). We make use of the fact that when
+                               // addToDatabase fails due to the user already existing, the user object gets loaded.
+                               if ( $user->getId() ) {
                                        $this->logger->info( __METHOD__ . ': {username} already exists locally (race)', [
                                                'username' => $username,
                                        ] );
-                                       $user->setId( $localId );
-                                       $user->loadFromId( User::READ_LATEST );
                                        if ( $login ) {
                                                $this->setSessionDataForUser( $user );
                                        }
@@ -1688,13 +1740,9 @@ class AuthManager implements LoggerAwareInterface {
                        $logEntry->setParameters( [
                                '4::userid' => $user->getId(),
                        ] );
-                       $logid = $logEntry->insert();
+                       $logEntry->insert();
                }
 
-               // Commit database changes, so even if something else later blows up
-               // the newly-created user doesn't get lost.
-               wfGetLBFactory()->commitMasterChanges( __METHOD__ );
-
                $trxProfiler->setSilenced( false );
 
                if ( $login ) {
@@ -2314,6 +2362,7 @@ class AuthManager implements LoggerAwareInterface {
        }
 
        /**
+        * Log the user in
         * @param User $user
         * @param bool|null $remember
         */
@@ -2378,6 +2427,7 @@ class AuthManager implements LoggerAwareInterface {
 
        /**
         * Reset the internal caching for unit testing
+        * @protected Unit tests only
         */
        public static function resetCache() {
                if ( !defined( 'MW_PHPUNIT_TEST' ) ) {