]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blobdiff - includes/session/SessionProvider.php
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / includes / session / SessionProvider.php
diff --git a/includes/session/SessionProvider.php b/includes/session/SessionProvider.php
new file mode 100644 (file)
index 0000000..ba075e0
--- /dev/null
@@ -0,0 +1,533 @@
+<?php
+/**
+ * MediaWiki session provider base class
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Session
+ */
+
+namespace MediaWiki\Session;
+
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use Config;
+use Language;
+use User;
+use WebRequest;
+
+/**
+ * A SessionProvider provides SessionInfo and support for Session
+ *
+ * A SessionProvider is responsible for taking a WebRequest and determining
+ * the authenticated session that it's a part of. It does this by returning an
+ * SessionInfo object with basic information about the session it thinks is
+ * associated with the request, namely the session ID and possibly the
+ * authenticated user the session belongs to.
+ *
+ * The SessionProvider also provides for updating the WebResponse with
+ * information necessary to provide the client with data that the client will
+ * send with later requests, and for populating the Vary and Key headers with
+ * the data necessary to correctly vary the cache on these client requests.
+ *
+ * An important part of the latter is indicating whether it even *can* tell the
+ * client to include such data in future requests, via the persistsSessionId()
+ * and canChangeUser() methods. The cases are (in order of decreasing
+ * commonness):
+ *  - Cannot persist ID, no changing User: The request identifies and
+ *    authenticates a particular local user, and the client cannot be
+ *    instructed to include an arbitrary session ID with future requests. For
+ *    example, OAuth or SSL certificate auth.
+ *  - Can persist ID and can change User: The client can be instructed to
+ *    return at least one piece of arbitrary data, that being the session ID.
+ *    The user identity might also be given to the client, otherwise it's saved
+ *    in the session data. For example, cookie-based sessions.
+ *  - Can persist ID but no changing User: The request uniquely identifies and
+ *    authenticates a local user, and the client can be instructed to return an
+ *    arbitrary session ID with future requests. For example, HTTP Digest
+ *    authentication might somehow use the 'opaque' field as a session ID
+ *    (although getting MediaWiki to return 401 responses without breaking
+ *    other stuff might be a challenge).
+ *  - Cannot persist ID but can change User: I can't think of a way this
+ *    would make sense.
+ *
+ * Note that many methods that are technically "cannot persist ID" could be
+ * turned into "can persist ID but not change User" using a session cookie,
+ * as implemented by ImmutableSessionProviderWithCookie. If doing so, different
+ * session cookie names should be used for different providers to avoid
+ * collisions.
+ *
+ * @ingroup Session
+ * @since 1.27
+ * @see https://www.mediawiki.org/wiki/Manual:SessionManager_and_AuthManager
+ */
+abstract class SessionProvider implements SessionProviderInterface, LoggerAwareInterface {
+
+       /** @var LoggerInterface */
+       protected $logger;
+
+       /** @var Config */
+       protected $config;
+
+       /** @var SessionManager */
+       protected $manager;
+
+       /** @var int Session priority. Used for the default newSessionInfo(), but
+        * could be used by subclasses too.
+        */
+       protected $priority;
+
+       /**
+        * @note To fully initialize a SessionProvider, the setLogger(),
+        *  setConfig(), and setManager() methods must be called (and should be
+        *  called in that order). Failure to do so is liable to cause things to
+        *  fail unexpectedly.
+        */
+       public function __construct() {
+               $this->priority = SessionInfo::MIN_PRIORITY + 10;
+       }
+
+       public function setLogger( LoggerInterface $logger ) {
+               $this->logger = $logger;
+       }
+
+       /**
+        * Set configuration
+        * @param Config $config
+        */
+       public function setConfig( Config $config ) {
+               $this->config = $config;
+       }
+
+       /**
+        * Set the session manager
+        * @param SessionManager $manager
+        */
+       public function setManager( SessionManager $manager ) {
+               $this->manager = $manager;
+       }
+
+       /**
+        * Get the session manager
+        * @return SessionManager
+        */
+       public function getManager() {
+               return $this->manager;
+       }
+
+       /**
+        * Provide session info for a request
+        *
+        * If no session exists for the request, return null. Otherwise return an
+        * SessionInfo object identifying the session.
+        *
+        * If multiple SessionProviders provide sessions, the one with highest
+        * priority wins. In case of a tie, an exception is thrown.
+        * SessionProviders are encouraged to make priorities user-configurable
+        * unless only max-priority makes sense.
+        *
+        * @warning This will be called early in the MediaWiki setup process,
+        *  before $wgUser, $wgLang, $wgOut, $wgParser, $wgTitle, and corresponding
+        *  pieces of the main RequestContext are set up! If you try to use these,
+        *  things *will* break.
+        * @note The SessionProvider must not attempt to auto-create users.
+        *  MediaWiki will do this later (when it's safe) if the chosen session has
+        *  a user with a valid name but no ID.
+        * @protected For use by \MediaWiki\Session\SessionManager only
+        * @param WebRequest $request
+        * @return SessionInfo|null
+        */
+       abstract public function provideSessionInfo( WebRequest $request );
+
+       /**
+        * Provide session info for a new, empty session
+        *
+        * Return null if such a session cannot be created. This base
+        * implementation assumes that it only makes sense if a session ID can be
+        * persisted and changing users is allowed.
+        *
+        * @protected For use by \MediaWiki\Session\SessionManager only
+        * @param string|null $id ID to force for the new session
+        * @return SessionInfo|null
+        *  If non-null, must return true for $info->isIdSafe(); pass true for
+        *  $data['idIsSafe'] to ensure this.
+        */
+       public function newSessionInfo( $id = null ) {
+               if ( $this->canChangeUser() && $this->persistsSessionId() ) {
+                       return new SessionInfo( $this->priority, [
+                               'id' => $id,
+                               'provider' => $this,
+                               'persisted' => false,
+                               'idIsSafe' => true,
+                       ] );
+               }
+               return null;
+       }
+
+       /**
+        * Merge saved session provider metadata
+        *
+        * This method will be used to compare the metadata returned by
+        * provideSessionInfo() with the saved metadata (which has been returned by
+        * provideSessionInfo() the last time the session was saved), and merge the two
+        * into the new saved metadata, or abort if the current request is not a valid
+        * continuation of the session.
+        *
+        * The default implementation checks that anything in both arrays is
+        * identical, then returns $providedMetadata.
+        *
+        * @protected For use by \MediaWiki\Session\SessionManager only
+        * @param array $savedMetadata Saved provider metadata
+        * @param array $providedMetadata Provided provider metadata (from the SessionInfo)
+        * @return array Resulting metadata
+        * @throws MetadataMergeException If the metadata cannot be merged.
+        *  Such exceptions will be handled by SessionManager and are a safe way of rejecting
+        *  a suspicious or incompatible session. The provider is expected to write an
+        *  appropriate message to its logger.
+        */
+       public function mergeMetadata( array $savedMetadata, array $providedMetadata ) {
+               foreach ( $providedMetadata as $k => $v ) {
+                       if ( array_key_exists( $k, $savedMetadata ) && $savedMetadata[$k] !== $v ) {
+                               $e = new MetadataMergeException( "Key \"$k\" changed" );
+                               $e->setContext( [
+                                       'old_value' => $savedMetadata[$k],
+                                       'new_value' => $v,
+                               ] );
+                               throw $e;
+                       }
+               }
+               return $providedMetadata;
+       }
+
+       /**
+        * Validate a loaded SessionInfo and refresh provider metadata
+        *
+        * This is similar in purpose to the 'SessionCheckInfo' hook, and also
+        * allows for updating the provider metadata. On failure, the provider is
+        * expected to write an appropriate message to its logger.
+        *
+        * @protected For use by \MediaWiki\Session\SessionManager only
+        * @param SessionInfo $info Any changes by mergeMetadata() will already be reflected here.
+        * @param WebRequest $request
+        * @param array|null &$metadata Provider metadata, may be altered.
+        * @return bool Return false to reject the SessionInfo after all.
+        */
+       public function refreshSessionInfo( SessionInfo $info, WebRequest $request, &$metadata ) {
+               return true;
+       }
+
+       /**
+        * Indicate whether self::persistSession() can save arbitrary session IDs
+        *
+        * If false, any session passed to self::persistSession() will have an ID
+        * that was originally provided by self::provideSessionInfo().
+        *
+        * If true, the provider may be passed sessions with arbitrary session IDs,
+        * and will be expected to manipulate the request in such a way that future
+        * requests will cause self::provideSessionInfo() to provide a SessionInfo
+        * with that ID.
+        *
+        * For example, a session provider for OAuth would function by matching the
+        * OAuth headers to a particular user, and then would use self::hashToSessionId()
+        * to turn the user and OAuth client ID (and maybe also the user token and
+        * client secret) into a session ID, and therefore can't easily assign that
+        * user+client a different ID. Similarly, a session provider for SSL client
+        * certificates would function by matching the certificate to a particular
+        * user, and then would use self::hashToSessionId() to turn the user and
+        * certificate fingerprint into a session ID, and therefore can't easily
+        * assign a different ID either. On the other hand, a provider that saves
+        * the session ID into a cookie can easily just set the cookie to a
+        * different value.
+        *
+        * @protected For use by \MediaWiki\Session\SessionBackend only
+        * @return bool
+        */
+       abstract public function persistsSessionId();
+
+       /**
+        * Indicate whether the user associated with the request can be changed
+        *
+        * If false, any session passed to self::persistSession() will have a user
+        * that was originally provided by self::provideSessionInfo(). Further,
+        * self::provideSessionInfo() may only provide sessions that have a user
+        * already set.
+        *
+        * If true, the provider may be passed sessions with arbitrary users, and
+        * will be expected to manipulate the request in such a way that future
+        * requests will cause self::provideSessionInfo() to provide a SessionInfo
+        * with that ID. This can be as simple as not passing any 'userInfo' into
+        * SessionInfo's constructor, in which case SessionInfo will load the user
+        * from the saved session's metadata.
+        *
+        * For example, a session provider for OAuth or SSL client certificates
+        * would function by matching the OAuth headers or certificate to a
+        * particular user, and thus would return false here since it can't
+        * arbitrarily assign those OAuth credentials or that certificate to a
+        * different user. A session provider that shoves information into cookies,
+        * on the other hand, could easily do so.
+        *
+        * @protected For use by \MediaWiki\Session\SessionBackend only
+        * @return bool
+        */
+       abstract public function canChangeUser();
+
+       /**
+        * Returns the duration (in seconds) for which users will be remembered when
+        * Session::setRememberUser() is set. Null means setting the remember flag will
+        * have no effect (and endpoints should not offer that option).
+        * @return int|null
+        */
+       public function getRememberUserDuration() {
+               return null;
+       }
+
+       /**
+        * Notification that the session ID was reset
+        *
+        * No need to persist here, persistSession() will be called if appropriate.
+        *
+        * @protected For use by \MediaWiki\Session\SessionBackend only
+        * @param SessionBackend $session Session to persist
+        * @param string $oldId Old session ID
+        * @codeCoverageIgnore
+        */
+       public function sessionIdWasReset( SessionBackend $session, $oldId ) {
+       }
+
+       /**
+        * Persist a session into a request/response
+        *
+        * For example, you might set cookies for the session's ID, user ID, user
+        * name, and user token on the passed request.
+        *
+        * To correctly persist a user independently of the session ID, the
+        * provider should persist both the user ID (or name, but preferably the
+        * ID) and the user token. When reading the data from the request, it
+        * should construct a User object from the ID/name and then verify that the
+        * User object's token matches the token included in the request. Should
+        * the tokens not match, an anonymous user *must* be passed to
+        * SessionInfo::__construct().
+        *
+        * When persisting a user independently of the session ID,
+        * $session->shouldRememberUser() should be checked first. If this returns
+        * false, the user token *must not* be saved to cookies. The user name
+        * and/or ID may be persisted, and should be used to construct an
+        * unverified UserInfo to pass to SessionInfo::__construct().
+        *
+        * A backend that cannot persist sesison ID or user info should implement
+        * this as a no-op.
+        *
+        * @protected For use by \MediaWiki\Session\SessionBackend only
+        * @param SessionBackend $session Session to persist
+        * @param WebRequest $request Request into which to persist the session
+        */
+       abstract public function persistSession( SessionBackend $session, WebRequest $request );
+
+       /**
+        * Remove any persisted session from a request/response
+        *
+        * For example, blank and expire any cookies set by self::persistSession().
+        *
+        * A backend that cannot persist sesison ID or user info should implement
+        * this as a no-op.
+        *
+        * @protected For use by \MediaWiki\Session\SessionManager only
+        * @param WebRequest $request Request from which to remove any session data
+        */
+       abstract public function unpersistSession( WebRequest $request );
+
+       /**
+        * Prevent future sessions for the user
+        *
+        * If the provider is capable of returning a SessionInfo with a verified
+        * UserInfo for the named user in some manner other than by validating
+        * against $user->getToken(), steps must be taken to prevent that from
+        * occurring in the future. This might add the username to a blacklist, or
+        * it might just delete whatever authentication credentials would allow
+        * such a session in the first place (e.g. remove all OAuth grants or
+        * delete record of the SSL client certificate).
+        *
+        * The intention is that the named account will never again be usable for
+        * normal login (i.e. there is no way to undo the prevention of access).
+        *
+        * Note that the passed user name might not exist locally (i.e.
+        * User::idFromName( $username ) === 0); the name should still be
+        * prevented, if applicable.
+        *
+        * @protected For use by \MediaWiki\Session\SessionManager only
+        * @param string $username
+        */
+       public function preventSessionsForUser( $username ) {
+               if ( !$this->canChangeUser() ) {
+                       throw new \BadMethodCallException(
+                               __METHOD__ . ' must be implmented when canChangeUser() is false'
+                       );
+               }
+       }
+
+       /**
+        * Invalidate existing sessions for a user
+        *
+        * If the provider has its own equivalent of CookieSessionProvider's Token
+        * cookie (and doesn't use User::getToken() to implement it), it should
+        * reset whatever token it does use here.
+        *
+        * @protected For use by \MediaWiki\Session\SessionManager only
+        * @param User $user
+        */
+       public function invalidateSessionsForUser( User $user ) {
+       }
+
+       /**
+        * Return the HTTP headers that need varying on.
+        *
+        * The return value is such that someone could theoretically do this:
+        * @code
+        * foreach ( $provider->getVaryHeaders() as $header => $options ) {
+        *   $outputPage->addVaryHeader( $header, $options );
+        * }
+        * @endcode
+        *
+        * @protected For use by \MediaWiki\Session\SessionManager only
+        * @return array
+        */
+       public function getVaryHeaders() {
+               return [];
+       }
+
+       /**
+        * Return the list of cookies that need varying on.
+        * @protected For use by \MediaWiki\Session\SessionManager only
+        * @return string[]
+        */
+       public function getVaryCookies() {
+               return [];
+       }
+
+       /**
+        * Get a suggested username for the login form
+        * @protected For use by \MediaWiki\Session\SessionBackend only
+        * @param WebRequest $request
+        * @return string|null
+        */
+       public function suggestLoginUsername( WebRequest $request ) {
+               return null;
+       }
+
+       /**
+        * Fetch the rights allowed the user when the specified session is active.
+        *
+        * This is mainly meant for allowing the user to restrict access to the account
+        * by certain methods; you probably want to use this with MWGrants. The returned
+        * rights will be intersected with the user's actual rights.
+        *
+        * @param SessionBackend $backend
+        * @return null|string[] Allowed user rights, or null to allow all.
+        */
+       public function getAllowedUserRights( SessionBackend $backend ) {
+               if ( $backend->getProvider() !== $this ) {
+                       // Not that this should ever happen...
+                       throw new \InvalidArgumentException( 'Backend\'s provider isn\'t $this' );
+               }
+
+               return null;
+       }
+
+       /**
+        * @note Only override this if it makes sense to instantiate multiple
+        *  instances of the provider. Value returned must be unique across
+        *  configured providers. If you override this, you'll likely need to
+        *  override self::describeMessage() as well.
+        * @return string
+        */
+       public function __toString() {
+               return static::class;
+       }
+
+       /**
+        * Return a Message identifying this session type
+        *
+        * This default implementation takes the class name, lowercases it,
+        * replaces backslashes with dashes, and prefixes 'sessionprovider-' to
+        * determine the message key. For example, MediaWiki\Session\CookieSessionProvider
+        * produces 'sessionprovider-mediawiki-session-cookiesessionprovider'.
+        *
+        * @note If self::__toString() is overridden, this will likely need to be
+        *  overridden as well.
+        * @warning This will be called early during MediaWiki startup. Do not
+        *  use $wgUser, $wgLang, $wgOut, $wgParser, or their equivalents via
+        *  RequestContext from this method!
+        * @return \Message
+        */
+       protected function describeMessage() {
+               return wfMessage(
+                       'sessionprovider-' . str_replace( '\\', '-', strtolower( static::class ) )
+               );
+       }
+
+       public function describe( Language $lang ) {
+               $msg = $this->describeMessage();
+               $msg->inLanguage( $lang );
+               if ( $msg->isDisabled() ) {
+                       $msg = wfMessage( 'sessionprovider-generic', (string)$this )->inLanguage( $lang );
+               }
+               return $msg->plain();
+       }
+
+       public function whyNoSession() {
+               return null;
+       }
+
+       /**
+        * Hash data as a session ID
+        *
+        * Generally this will only be used when self::persistsSessionId() is false and
+        * the provider has to base the session ID on the verified user's identity
+        * or other static data. The SessionInfo should then typically have the
+        * 'forceUse' flag set to avoid persistent session failure if validation of
+        * the stored data fails.
+        *
+        * @param string $data
+        * @param string|null $key Defaults to $this->config->get( 'SecretKey' )
+        * @return string
+        */
+       final protected function hashToSessionId( $data, $key = null ) {
+               if ( !is_string( $data ) ) {
+                       throw new \InvalidArgumentException(
+                               '$data must be a string, ' . gettype( $data ) . ' was passed'
+                       );
+               }
+               if ( $key !== null && !is_string( $key ) ) {
+                       throw new \InvalidArgumentException(
+                               '$key must be a string or null, ' . gettype( $key ) . ' was passed'
+                       );
+               }
+
+               $hash = \MWCryptHash::hmac( "$this\n$data", $key ?: $this->config->get( 'SecretKey' ), false );
+               if ( strlen( $hash ) < 32 ) {
+                       // Should never happen, even md5 is 128 bits
+                       // @codeCoverageIgnoreStart
+                       throw new \UnexpectedValueException( 'Hash fuction returned less than 128 bits' );
+                       // @codeCoverageIgnoreEnd
+               }
+               if ( strlen( $hash ) >= 40 ) {
+                       $hash = \Wikimedia\base_convert( $hash, 16, 32, 32 );
+               }
+               return substr( $hash, -32 );
+       }
+
+}