2015-11-22 20:17:00 +00:00
|
|
|
<?php
|
|
|
|
|
/**
|
|
|
|
|
* Primary authentication provider wrapper for AuthPlugin
|
|
|
|
|
*
|
|
|
|
|
* 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 Auth
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
namespace MediaWiki\Auth;
|
|
|
|
|
|
|
|
|
|
use AuthPlugin;
|
|
|
|
|
use User;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Primary authentication provider wrapper for AuthPlugin
|
|
|
|
|
* @warning If anything depends on the wrapped AuthPlugin being $wgAuth, it won't work with this!
|
|
|
|
|
* @ingroup Auth
|
|
|
|
|
* @since 1.27
|
|
|
|
|
* @deprecated since 1.27
|
|
|
|
|
*/
|
|
|
|
|
class AuthPluginPrimaryAuthenticationProvider
|
|
|
|
|
extends AbstractPasswordPrimaryAuthenticationProvider
|
|
|
|
|
{
|
|
|
|
|
private $auth;
|
|
|
|
|
private $hasDomain;
|
|
|
|
|
private $requestType = null;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param AuthPlugin $auth AuthPlugin to wrap
|
|
|
|
|
* @param string|null $requestType Class name of the
|
|
|
|
|
* PasswordAuthenticationRequest to use. If $auth->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: "
|
2018-02-17 12:29:13 +00:00
|
|
|
. implode( ' ', $failed )
|
2015-11-22 20:17:00 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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' );
|
|
|
|
|
}
|
2016-05-29 14:00:23 +00:00
|
|
|
if ( !$this->auth->validDomain( $req->domain ) ) {
|
2015-11-22 20:17:00 +00:00
|
|
|
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!'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|