MediaWiki  fundraising/REL1_31
AuthPluginPrimaryAuthenticationProvider.php
Go to the documentation of this file.
1 <?php
24 namespace MediaWiki\Auth;
25 
27 use User;
28 
38 {
39  private $auth;
40  private $hasDomain;
41  private $requestType = null;
42 
49  public function __construct( AuthPlugin $auth, $requestType = null ) {
50  parent::__construct();
51 
52  if ( $auth instanceof AuthManagerAuthPlugin ) {
53  throw new \InvalidArgumentException(
54  'Trying to wrap AuthManagerAuthPlugin in AuthPluginPrimaryAuthenticationProvider ' .
55  'makes no sense.'
56  );
57  }
58 
59  $need = count( $auth->domainList() ) > 1
62  if ( $requestType === null ) {
63  $requestType = $need;
64  } elseif ( $requestType !== $need && !is_subclass_of( $requestType, $need ) ) {
65  throw new \InvalidArgumentException( "$requestType is not a $need" );
66  }
67 
68  $this->auth = $auth;
69  $this->requestType = $requestType;
70  $this->hasDomain = (
73  );
74  $this->authoritative = $auth->strict();
75 
76  // Registering hooks from core is unusual, but is needed here to be
77  // able to call the AuthPlugin methods those hooks replace.
78  \Hooks::register( 'UserSaveSettings', [ $this, 'onUserSaveSettings' ] );
79  \Hooks::register( 'UserGroupsChanged', [ $this, 'onUserGroupsChanged' ] );
80  \Hooks::register( 'UserLoggedIn', [ $this, 'onUserLoggedIn' ] );
81  \Hooks::register( 'LocalUserCreated', [ $this, 'onLocalUserCreated' ] );
82  }
83 
88  protected function makeAuthReq() {
89  $class = $this->requestType;
90  if ( $this->hasDomain ) {
91  return new $class( $this->auth->domainList() );
92  } else {
93  return new $class();
94  }
95  }
96 
101  protected function setDomain( $req ) {
102  if ( $this->hasDomain ) {
103  $domain = $req->domain;
104  } else {
105  // Just grab the first one.
106  $domainList = $this->auth->domainList();
107  $domain = reset( $domainList );
108  }
109 
110  // Special:UserLogin does this. Strange.
111  if ( !$this->auth->validDomain( $domain ) ) {
112  $domain = $this->auth->getDomain();
113  }
114  $this->auth->setDomain( $domain );
115  }
116 
122  public function onUserSaveSettings( $user ) {
123  // No way to know the domain, just hope the provider handles that.
124  $this->auth->updateExternalDB( $user );
125  }
126 
133  public function onUserGroupsChanged( $user, $added, $removed ) {
134  // No way to know the domain, just hope the provider handles that.
135  $this->auth->updateExternalDBGroups( $user, $added, $removed );
136  }
137 
142  public function onUserLoggedIn( $user ) {
143  $hookUser = $user;
144  // No way to know the domain, just hope the provider handles that.
145  $this->auth->updateUser( $hookUser );
146  if ( $hookUser !== $user ) {
147  throw new \UnexpectedValueException(
148  get_class( $this->auth ) . '::updateUser() tried to replace $user!'
149  );
150  }
151  }
152 
158  public function onLocalUserCreated( $user, $autocreated ) {
159  // For $autocreated, see self::autoCreatedAccount()
160  if ( !$autocreated ) {
161  $hookUser = $user;
162  // No way to know the domain, just hope the provider handles that.
163  $this->auth->initUser( $hookUser, $autocreated );
164  if ( $hookUser !== $user ) {
165  throw new \UnexpectedValueException(
166  get_class( $this->auth ) . '::initUser() tried to replace $user!'
167  );
168  }
169  }
170  }
171 
172  public function getUniqueId() {
173  return parent::getUniqueId() . ':' . get_class( $this->auth );
174  }
175 
177  switch ( $action ) {
180  return [ $this->makeAuthReq() ];
181 
184  // No way to know the domain, just hope the provider handles that.
185  return $this->auth->allowPasswordChange() ? [ $this->makeAuthReq() ] : [];
186 
187  default:
188  return [];
189  }
190  }
191 
192  public function beginPrimaryAuthentication( array $reqs ) {
193  $req = AuthenticationRequest::getRequestByClass( $reqs, $this->requestType );
194  if ( !$req || $req->username === null || $req->password === null ||
195  ( $this->hasDomain && $req->domain === null )
196  ) {
198  }
199 
200  $username = User::getCanonicalName( $req->username, 'usable' );
201  if ( $username === false ) {
203  }
204 
205  $this->setDomain( $req );
207  $this->auth->authenticate( $username, $req->password )
208  ) {
210  } else {
211  $this->authoritative = $this->auth->strict() || $this->auth->strictUserAuth( $username );
212  return $this->failResponse( $req );
213  }
214  }
215 
216  public function testUserCanAuthenticate( $username ) {
218  if ( $username === false ) {
219  return false;
220  }
221 
222  // We have to check every domain, because at least LdapAuthentication
223  // interprets AuthPlugin::userExists() as applying only to the current
224  // domain.
225  $curDomain = $this->auth->getDomain();
226  $domains = $this->auth->domainList() ?: [ '' ];
227  foreach ( $domains as $domain ) {
228  $this->auth->setDomain( $domain );
230  $this->auth->setDomain( $curDomain );
231  return true;
232  }
233  }
234  $this->auth->setDomain( $curDomain );
235  return false;
236  }
237 
245  if ( $this->auth->userExists( $user->getName() ) ) {
246  return !$this->auth->getUserInstance( $user )->isLocked();
247  } else {
248  return false;
249  }
250  }
251 
254  if ( $username === false ) {
255  return;
256  }
258  if ( $user ) {
259  // Reset the password on every domain.
260  $curDomain = $this->auth->getDomain();
261  $domains = $this->auth->domainList() ?: [ '' ];
262  $failed = [];
263  foreach ( $domains as $domain ) {
264  $this->auth->setDomain( $domain );
265  if ( $this->testUserCanAuthenticateInternal( $user ) &&
266  !$this->auth->setPassword( $user, null )
267  ) {
268  $failed[] = $domain === '' ? '(default)' : $domain;
269  }
270  }
271  $this->auth->setDomain( $curDomain );
272  if ( $failed ) {
273  throw new \UnexpectedValueException(
274  "AuthPlugin failed to reset password for $username in the following domains: "
275  . implode( ' ', $failed )
276  );
277  }
278  }
279  }
280 
281  public function testUserExists( $username, $flags = User::READ_NORMAL ) {
283  if ( $username === false ) {
284  return false;
285  }
286 
287  // We have to check every domain, because at least LdapAuthentication
288  // interprets AuthPlugin::userExists() as applying only to the current
289  // domain.
290  $curDomain = $this->auth->getDomain();
291  $domains = $this->auth->domainList() ?: [ '' ];
292  foreach ( $domains as $domain ) {
293  $this->auth->setDomain( $domain );
294  if ( $this->auth->userExists( $username ) ) {
295  $this->auth->setDomain( $curDomain );
296  return true;
297  }
298  }
299  $this->auth->setDomain( $curDomain );
300  return false;
301  }
302 
304  // No way to know the domain, just hope the provider handles that.
305  return $this->auth->allowPropChange( $property );
306  }
307 
309  AuthenticationRequest $req, $checkData = true
310  ) {
311  if ( get_class( $req ) !== $this->requestType ) {
312  return \StatusValue::newGood( 'ignored' );
313  }
314 
315  // Hope it works, AuthPlugin gives us no way to do this.
316  $curDomain = $this->auth->getDomain();
317  $this->setDomain( $req );
318  try {
319  // If !$checkData the domain might be wrong. Nothing we can do about that.
320  if ( !$this->auth->allowPasswordChange() ) {
321  return \StatusValue::newFatal( 'authmanager-authplugin-setpass-denied' );
322  }
323 
324  if ( !$checkData ) {
325  return \StatusValue::newGood();
326  }
327 
328  if ( $this->hasDomain ) {
329  if ( $req->domain === null ) {
330  return \StatusValue::newGood( 'ignored' );
331  }
332  if ( !$this->auth->validDomain( $req->domain ) ) {
333  return \StatusValue::newFatal( 'authmanager-authplugin-setpass-bad-domain' );
334  }
335  }
336 
337  $username = User::getCanonicalName( $req->username, 'usable' );
338  if ( $username !== false ) {
339  $sv = \StatusValue::newGood();
340  if ( $req->password !== null ) {
341  if ( $req->password !== $req->retype ) {
342  $sv->fatal( 'badretype' );
343  } else {
344  $sv->merge( $this->checkPasswordValidity( $username, $req->password ) );
345  }
346  }
347  return $sv;
348  } else {
349  return \StatusValue::newGood( 'ignored' );
350  }
351  } finally {
352  $this->auth->setDomain( $curDomain );
353  }
354  }
355 
357  if ( get_class( $req ) === $this->requestType ) {
358  $username = $req->username !== null ? User::getCanonicalName( $req->username, 'usable' ) : false;
359  if ( $username === false ) {
360  return;
361  }
362 
363  if ( $this->hasDomain && $req->domain === null ) {
364  return;
365  }
366 
367  $this->setDomain( $req );
369  if ( !$this->auth->setPassword( $user, $req->password ) ) {
370  // This is totally unfriendly and leaves other
371  // AuthenticationProviders in an uncertain state, but what else
372  // can we do?
373  throw new \ErrorPageError(
374  'authmanager-authplugin-setpass-failed-title',
375  'authmanager-authplugin-setpass-failed-message'
376  );
377  }
378  }
379  }
380 
381  public function accountCreationType() {
382  // No way to know the domain, just hope the provider handles that.
383  return $this->auth->canCreateAccounts() ? self::TYPE_CREATE : self::TYPE_NONE;
384  }
385 
386  public function testForAccountCreation( $user, $creator, array $reqs ) {
387  return \StatusValue::newGood();
388  }
389 
390  public function beginPrimaryAccountCreation( $user, $creator, array $reqs ) {
391  if ( $this->accountCreationType() === self::TYPE_NONE ) {
392  throw new \BadMethodCallException( 'Shouldn\'t call this when accountCreationType() is NONE' );
393  }
394 
395  $req = AuthenticationRequest::getRequestByClass( $reqs, $this->requestType );
396  if ( !$req || $req->username === null || $req->password === null ||
397  ( $this->hasDomain && $req->domain === null )
398  ) {
400  }
401 
402  $username = User::getCanonicalName( $req->username, 'usable' );
403  if ( $username === false ) {
405  }
406 
407  $this->setDomain( $req );
408  if ( $this->auth->addUser(
409  $user, $req->password, $user->getEmail(), $user->getRealName()
410  ) ) {
412  } else {
414  new \Message( 'authmanager-authplugin-create-fail' )
415  );
416  }
417  }
418 
419  public function autoCreatedAccount( $user, $source ) {
420  $hookUser = $user;
421  // No way to know the domain, just hope the provider handles that.
422  $this->auth->initUser( $hookUser, true );
423  if ( $hookUser !== $user ) {
424  throw new \UnexpectedValueException(
425  get_class( $this->auth ) . '::initUser() tried to replace $user!'
426  );
427  }
428  }
429 }
Authentication plugin interface.
Definition: AuthPlugin.php:38
static register( $name, $callback)
Attach an event handler to a given hook.
Definition: Hooks.php:49
Basic framework for a primary authentication provider that uses passwords.
failResponse(PasswordAuthenticationRequest $req)
Return the appropriate response for failure.
Backwards-compatibility wrapper for AuthManager via $wgAuth.
const ACTION_CHANGE
Change a user's credentials.
const ACTION_REMOVE
Remove a user's credentials.
const ACTION_LOGIN
Log in with an existing (not necessarily local) user.
Definition: AuthManager.php:85
const ACTION_CREATE
Create a new user.
Definition: AuthManager.php:90
Primary authentication provider wrapper for AuthPlugin.
providerAllowsPropertyChange( $property)
Determine whether a property can change.
onLocalUserCreated( $user, $autocreated)
Hook function to call AuthPlugin::initUser()
providerAllowsAuthenticationDataChange(AuthenticationRequest $req, $checkData=true)
Validate a change of authentication data (e.g.
testUserCanAuthenticate( $username)
Test whether the named user can authenticate with this provider.
onUserSaveSettings( $user)
Hook function to call AuthPlugin::updateExternalDB()
beginPrimaryAccountCreation( $user, $creator, array $reqs)
Start an account creation flow.
onUserLoggedIn( $user)
Hook function to call AuthPlugin::updateUser()
testForAccountCreation( $user, $creator, array $reqs)
Determine whether an account creation may begin.
providerChangeAuthenticationData(AuthenticationRequest $req)
Change or remove authentication data (e.g.
onUserGroupsChanged( $user, $added, $removed)
Hook function to call AuthPlugin::updateExternalDBGroups()
testUserExists( $username, $flags=User::READ_NORMAL)
Test whether the named user exists.
This is a value object for authentication requests.
static getRequestByClass(array $reqs, $class, $allowSubclasses=false)
Select a request by class name.
String $action
Cache what action this request is.
Definition: MediaWiki.php:48
The Message class provides methods which fulfil two basic services:
Definition: Message.php:159
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:81
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:591
static getCanonicalName( $name, $validate='valid')
Given unvalidated user input, return a canonical username, or false if the username is invalid.
Definition: User.php:1210
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
this hook is for auditing only $req
Definition: hooks.txt:990
the array() calling protocol came about after MediaWiki 1.4rc1.
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition: hooks.txt:2001
this hook is for auditing only or null if authentication failed before getting that far $username
Definition: hooks.txt:785
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a account $user
Definition: hooks.txt:247
The MIT free of to any person obtaining a copy of this software and associated documentation to deal in the Software without including without limitation the rights to use
Definition: LICENSE.txt:7
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:37
const TYPE_NONE
Provider cannot create or link to accounts.
you have access to all of the normal MediaWiki so you can get a DB use the etc For full docs on the Maintenance class
Definition: maintenance.txt:56
$source
$property