MediaWiki  1.28.0
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  . join( ' ', $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 }
static newFromName($name, $validate= 'valid')
Static factory method for creation from username.
Definition: User.php:525
the array() calling protocol came about after MediaWiki 1.4rc1.
testUserCanAuthenticate($username)
Test whether the named user can authenticate with this provider.
$property
onLocalUserCreated($user, $autocreated)
Hook function to call AuthPlugin::initUser()
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
static getCanonicalName($name, $validate= 'valid')
Given unvalidated user input, return a canonical username, or false if the username is invalid...
Definition: User.php:1046
testForAccountCreation($user, $creator, array $reqs)
Determine whether an account creation may begin.
$source
strict()
Return true to prevent logins that don't authenticate here from being checked against the local datab...
Definition: AuthPlugin.php:264
it s the revision text itself In either if gzip is the revision text is gzipped $flags
Definition: hooks.txt:2703
testUserExists($username, $flags=User::READ_NORMAL)
Test whether the named user exists.
providerChangeAuthenticationData(AuthenticationRequest $req)
Change or remove authentication data (e.g.
onUserSaveSettings($user)
Hook function to call AuthPlugin::updateExternalDB()
static register($name, $callback)
Attach an event handler to a given hook.
Definition: Hooks.php:49
Backwards-compatibility wrapper for AuthManager via $wgAuth.
Primary authentication provider wrapper for AuthPlugin.
onUserGroupsChanged($user, $added, $removed)
Hook function to call AuthPlugin::updateExternalDBGroups()
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set and then return false from the hook function Ensure you consume the ChangeTagAfterDelete hook to carry out custom deletion actions as context called by AbstractContent::getParserOutput May be used to override the normal model specific rendering of page content as context as context $options
Definition: hooks.txt:1046
const ACTION_CHANGE
Change a user's credentials.
Definition: AuthManager.php:98
Basic framework for a primary authentication provider that uses passwords.
getAuthenticationRequests($action, array $options)
{{Return the applicable list of AuthenticationRequests.Possible values for $action depend on whether ...
static newGood($value=null)
Factory function for good results.
Definition: StatusValue.php:76
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
Definition: distributors.txt:9
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 local account $user
Definition: hooks.txt:242
String $action
Cache what action this request is.
Definition: MediaWiki.php:43
providerAllowsPropertyChange($property)
Determine whether a property can change.
domainList()
Get a list of domains (in HTMLForm options format) used.
Definition: AuthPlugin.php:322
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:35
this hook is for auditing only $req
Definition: hooks.txt:1007
this hook is for auditing only or null if authentication failed before getting that far $username
Definition: hooks.txt:802
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:52
const ACTION_REMOVE
Remove a user's credentials.
static getRequestByClass(array $reqs, $class, $allowSubclasses=false)
Select a request by class name.
const ACTION_CREATE
Create a new user.
Definition: AuthManager.php:88
beginPrimaryAccountCreation($user, $creator, array $reqs)
Start an account creation flow.
failResponse(PasswordAuthenticationRequest $req)
Return the appropriate response for failure.
const ACTION_LOGIN
Log in with an existing (not necessarily local) user.
Definition: AuthManager.php:83
onUserLoggedIn($user)
Hook function to call AuthPlugin::updateUser()
providerAllowsAuthenticationDataChange(AuthenticationRequest $req, $checkData=true)
Validate a change of authentication data (e.g.
This is a value object for authentication requests.