domainList() returns * more than one domain, this must be a PasswordDomainAuthenticationRequest. */ public function __construct( AuthPlugin $auth, $requestType = null ) { parent::__construct(); if ( $auth instanceof AuthManagerAuthPlugin ) { throw new \InvalidArgumentException( 'Trying to wrap AuthManagerAuthPlugin in AuthPluginPrimaryAuthenticationProvider ' . 'makes no sense.' ); } $need = count( $auth->domainList() ) > 1 ? PasswordDomainAuthenticationRequest::class : PasswordAuthenticationRequest::class; if ( $requestType === null ) { $requestType = $need; } elseif ( $requestType !== $need && !is_subclass_of( $requestType, $need ) ) { throw new \InvalidArgumentException( "$requestType is not a $need" ); } $this->auth = $auth; $this->requestType = $requestType; $this->hasDomain = ( $requestType === PasswordDomainAuthenticationRequest::class || is_subclass_of( $requestType, PasswordDomainAuthenticationRequest::class ) ); $this->authoritative = $auth->strict(); // Registering hooks from core is unusual, but is needed here to be // able to call the AuthPlugin methods those hooks replace. \Hooks::register( 'UserSaveSettings', [ $this, 'onUserSaveSettings' ] ); \Hooks::register( 'UserGroupsChanged', [ $this, 'onUserGroupsChanged' ] ); \Hooks::register( 'UserLoggedIn', [ $this, 'onUserLoggedIn' ] ); \Hooks::register( 'LocalUserCreated', [ $this, 'onLocalUserCreated' ] ); } /** * Create an appropriate AuthenticationRequest * @return PasswordAuthenticationRequest */ protected function makeAuthReq() { $class = $this->requestType; if ( $this->hasDomain ) { return new $class( $this->auth->domainList() ); } else { return new $class(); } } /** * Call $this->auth->setDomain() * @param PasswordAuthenticationRequest $req */ protected function setDomain( $req ) { if ( $this->hasDomain ) { $domain = $req->domain; } else { // Just grab the first one. $domainList = $this->auth->domainList(); $domain = reset( $domainList ); } // Special:UserLogin does this. Strange. if ( !$this->auth->validDomain( $domain ) ) { $domain = $this->auth->getDomain(); } $this->auth->setDomain( $domain ); } /** * Hook function to call AuthPlugin::updateExternalDB() * @param User $user * @codeCoverageIgnore */ public function onUserSaveSettings( $user ) { // No way to know the domain, just hope the provider handles that. $this->auth->updateExternalDB( $user ); } /** * Hook function to call AuthPlugin::updateExternalDBGroups() * @param User $user * @param array $added * @param array $removed */ public function onUserGroupsChanged( $user, $added, $removed ) { // No way to know the domain, just hope the provider handles that. $this->auth->updateExternalDBGroups( $user, $added, $removed ); } /** * Hook function to call AuthPlugin::updateUser() * @param User $user */ public function onUserLoggedIn( $user ) { $hookUser = $user; // No way to know the domain, just hope the provider handles that. $this->auth->updateUser( $hookUser ); if ( $hookUser !== $user ) { throw new \UnexpectedValueException( get_class( $this->auth ) . '::updateUser() tried to replace $user!' ); } } /** * Hook function to call AuthPlugin::initUser() * @param User $user * @param bool $autocreated */ public function onLocalUserCreated( $user, $autocreated ) { // For $autocreated, see self::autoCreatedAccount() if ( !$autocreated ) { $hookUser = $user; // No way to know the domain, just hope the provider handles that. $this->auth->initUser( $hookUser, $autocreated ); if ( $hookUser !== $user ) { throw new \UnexpectedValueException( get_class( $this->auth ) . '::initUser() tried to replace $user!' ); } } } public function getUniqueId() { return parent::getUniqueId() . ':' . get_class( $this->auth ); } public function getAuthenticationRequests( $action, array $options ) { switch ( $action ) { case AuthManager::ACTION_LOGIN: case AuthManager::ACTION_CREATE: return [ $this->makeAuthReq() ]; case AuthManager::ACTION_CHANGE: case AuthManager::ACTION_REMOVE: // No way to know the domain, just hope the provider handles that. return $this->auth->allowPasswordChange() ? [ $this->makeAuthReq() ] : []; default: return []; } } public function beginPrimaryAuthentication( array $reqs ) { $req = AuthenticationRequest::getRequestByClass( $reqs, $this->requestType ); if ( !$req || $req->username === null || $req->password === null || ( $this->hasDomain && $req->domain === null ) ) { return AuthenticationResponse::newAbstain(); } $username = User::getCanonicalName( $req->username, 'usable' ); if ( $username === false ) { return AuthenticationResponse::newAbstain(); } $this->setDomain( $req ); if ( $this->testUserCanAuthenticateInternal( User::newFromName( $username ) ) && $this->auth->authenticate( $username, $req->password ) ) { return AuthenticationResponse::newPass( $username ); } else { $this->authoritative = $this->auth->strict() || $this->auth->strictUserAuth( $username ); return $this->failResponse( $req ); } } public function testUserCanAuthenticate( $username ) { $username = User::getCanonicalName( $username, 'usable' ); if ( $username === false ) { return false; } // We have to check every domain, because at least LdapAuthentication // interprets AuthPlugin::userExists() as applying only to the current // domain. $curDomain = $this->auth->getDomain(); $domains = $this->auth->domainList() ?: [ '' ]; foreach ( $domains as $domain ) { $this->auth->setDomain( $domain ); if ( $this->testUserCanAuthenticateInternal( User::newFromName( $username ) ) ) { $this->auth->setDomain( $curDomain ); return true; } } $this->auth->setDomain( $curDomain ); return false; } /** * @see self::testUserCanAuthenticate * @note The caller is responsible for calling $this->auth->setDomain() * @param User $user * @return bool */ private function testUserCanAuthenticateInternal( $user ) { if ( $this->auth->userExists( $user->getName() ) ) { return !$this->auth->getUserInstance( $user )->isLocked(); } else { return false; } } public function providerRevokeAccessForUser( $username ) { $username = User::getCanonicalName( $username, 'usable' ); if ( $username === false ) { return; } $user = User::newFromName( $username ); if ( $user ) { // Reset the password on every domain. $curDomain = $this->auth->getDomain(); $domains = $this->auth->domainList() ?: [ '' ]; $failed = []; foreach ( $domains as $domain ) { $this->auth->setDomain( $domain ); if ( $this->testUserCanAuthenticateInternal( $user ) && !$this->auth->setPassword( $user, null ) ) { $failed[] = $domain === '' ? '(default)' : $domain; } } $this->auth->setDomain( $curDomain ); if ( $failed ) { throw new \UnexpectedValueException( "AuthPlugin failed to reset password for $username in the following domains: " . implode( ' ', $failed ) ); } } } public function testUserExists( $username, $flags = User::READ_NORMAL ) { $username = User::getCanonicalName( $username, 'usable' ); if ( $username === false ) { return false; } // We have to check every domain, because at least LdapAuthentication // interprets AuthPlugin::userExists() as applying only to the current // domain. $curDomain = $this->auth->getDomain(); $domains = $this->auth->domainList() ?: [ '' ]; foreach ( $domains as $domain ) { $this->auth->setDomain( $domain ); if ( $this->auth->userExists( $username ) ) { $this->auth->setDomain( $curDomain ); return true; } } $this->auth->setDomain( $curDomain ); return false; } public function providerAllowsPropertyChange( $property ) { // No way to know the domain, just hope the provider handles that. return $this->auth->allowPropChange( $property ); } public function providerAllowsAuthenticationDataChange( AuthenticationRequest $req, $checkData = true ) { if ( get_class( $req ) !== $this->requestType ) { return \StatusValue::newGood( 'ignored' ); } // Hope it works, AuthPlugin gives us no way to do this. $curDomain = $this->auth->getDomain(); $this->setDomain( $req ); try { // If !$checkData the domain might be wrong. Nothing we can do about that. if ( !$this->auth->allowPasswordChange() ) { return \StatusValue::newFatal( 'authmanager-authplugin-setpass-denied' ); } if ( !$checkData ) { return \StatusValue::newGood(); } if ( $this->hasDomain ) { if ( $req->domain === null ) { return \StatusValue::newGood( 'ignored' ); } if ( !$this->auth->validDomain( $req->domain ) ) { return \StatusValue::newFatal( 'authmanager-authplugin-setpass-bad-domain' ); } } $username = User::getCanonicalName( $req->username, 'usable' ); if ( $username !== false ) { $sv = \StatusValue::newGood(); if ( $req->password !== null ) { if ( $req->password !== $req->retype ) { $sv->fatal( 'badretype' ); } else { $sv->merge( $this->checkPasswordValidity( $username, $req->password ) ); } } return $sv; } else { return \StatusValue::newGood( 'ignored' ); } } finally { $this->auth->setDomain( $curDomain ); } } public function providerChangeAuthenticationData( AuthenticationRequest $req ) { if ( get_class( $req ) === $this->requestType ) { $username = $req->username !== null ? User::getCanonicalName( $req->username, 'usable' ) : false; if ( $username === false ) { return; } if ( $this->hasDomain && $req->domain === null ) { return; } $this->setDomain( $req ); $user = User::newFromName( $username ); if ( !$this->auth->setPassword( $user, $req->password ) ) { // This is totally unfriendly and leaves other // AuthenticationProviders in an uncertain state, but what else // can we do? throw new \ErrorPageError( 'authmanager-authplugin-setpass-failed-title', 'authmanager-authplugin-setpass-failed-message' ); } } } public function accountCreationType() { // No way to know the domain, just hope the provider handles that. return $this->auth->canCreateAccounts() ? self::TYPE_CREATE : self::TYPE_NONE; } public function testForAccountCreation( $user, $creator, array $reqs ) { return \StatusValue::newGood(); } public function beginPrimaryAccountCreation( $user, $creator, array $reqs ) { if ( $this->accountCreationType() === self::TYPE_NONE ) { throw new \BadMethodCallException( 'Shouldn\'t call this when accountCreationType() is NONE' ); } $req = AuthenticationRequest::getRequestByClass( $reqs, $this->requestType ); if ( !$req || $req->username === null || $req->password === null || ( $this->hasDomain && $req->domain === null ) ) { return AuthenticationResponse::newAbstain(); } $username = User::getCanonicalName( $req->username, 'usable' ); if ( $username === false ) { return AuthenticationResponse::newAbstain(); } $this->setDomain( $req ); if ( $this->auth->addUser( $user, $req->password, $user->getEmail(), $user->getRealName() ) ) { return AuthenticationResponse::newPass(); } else { return AuthenticationResponse::newFail( new \Message( 'authmanager-authplugin-create-fail' ) ); } } public function autoCreatedAccount( $user, $source ) { $hookUser = $user; // No way to know the domain, just hope the provider handles that. $this->auth->initUser( $hookUser, true ); if ( $hookUser !== $user ) { throw new \UnexpectedValueException( get_class( $this->auth ) . '::initUser() tried to replace $user!' ); } } }