Merge "Change invalid password reset form language"
[lhc/web/wiklou.git] / tests / phpunit / includes / auth / AuthManagerTest.php
1 <?php
2
3 namespace MediaWiki\Auth;
4
5 use MediaWiki\Session\SessionInfo;
6 use MediaWiki\Session\UserInfo;
7 use Psr\Log\LogLevel;
8 use StatusValue;
9
10 /**
11 * @group AuthManager
12 * @group Database
13 * @covers MediaWiki\Auth\AuthManager
14 */
15 class AuthManagerTest extends \MediaWikiTestCase {
16 /** @var WebRequest */
17 protected $request;
18 /** @var Config */
19 protected $config;
20 /** @var \\Psr\\Log\\LoggerInterface */
21 protected $logger;
22
23 protected $preauthMocks = [];
24 protected $primaryauthMocks = [];
25 protected $secondaryauthMocks = [];
26
27 /** @var AuthManager */
28 protected $manager;
29 /** @var TestingAccessWrapper */
30 protected $managerPriv;
31
32 protected function setUp() {
33 global $wgDisableAuthManager;
34
35 parent::setUp();
36 if ( $wgDisableAuthManager ) {
37 $this->markTestSkipped( '$wgDisableAuthManager is set' );
38 }
39
40 $this->setMwGlobals( [ 'wgAuth' => null ] );
41 $this->stashMwGlobals( [ 'wgHooks' ] );
42 }
43
44 /**
45 * Sets a mock on a hook
46 * @param string $hook
47 * @param object $expect From $this->once(), $this->never(), etc.
48 * @return object $mock->expects( $expect )->method( ... ).
49 */
50 protected function hook( $hook, $expect ) {
51 global $wgHooks;
52 $mock = $this->getMock( __CLASS__, [ "on$hook" ] );
53 $wgHooks[$hook] = [ $mock ];
54 return $mock->expects( $expect )->method( "on$hook" );
55 }
56
57 /**
58 * Unsets a hook
59 * @param string $hook
60 */
61 protected function unhook( $hook ) {
62 global $wgHooks;
63 $wgHooks[$hook] = [];
64 }
65
66 /**
67 * Ensure a value is a clean Message object
68 * @param string|Message $key
69 * @param array $params
70 * @return Message
71 */
72 protected function message( $key, $params = [] ) {
73 if ( $key === null ) {
74 return null;
75 }
76 if ( $key instanceof \MessageSpecifier ) {
77 $params = $key->getParams();
78 $key = $key->getKey();
79 }
80 return new \Message( $key, $params, \Language::factory( 'en' ) );
81 }
82
83 /**
84 * Initialize the AuthManagerConfig variable in $this->config
85 *
86 * Uses data from the various 'mocks' fields.
87 */
88 protected function initializeConfig() {
89 $config = [
90 'preauth' => [
91 ],
92 'primaryauth' => [
93 ],
94 'secondaryauth' => [
95 ],
96 ];
97
98 foreach ( [ 'preauth', 'primaryauth', 'secondaryauth' ] as $type ) {
99 $key = $type . 'Mocks';
100 foreach ( $this->$key as $mock ) {
101 $config[$type][$mock->getUniqueId()] = [ 'factory' => function () use ( $mock ) {
102 return $mock;
103 } ];
104 }
105 }
106
107 $this->config->set( 'AuthManagerConfig', $config );
108 $this->config->set( 'LanguageCode', 'en' );
109 $this->config->set( 'NewUserLog', false );
110 }
111
112 /**
113 * Initialize $this->manager
114 * @param bool $regen Force a call to $this->initializeConfig()
115 */
116 protected function initializeManager( $regen = false ) {
117 if ( $regen || !$this->config ) {
118 $this->config = new \HashConfig();
119 }
120 if ( $regen || !$this->request ) {
121 $this->request = new \FauxRequest();
122 }
123 if ( !$this->logger ) {
124 $this->logger = new \TestLogger();
125 }
126
127 if ( $regen || !$this->config->has( 'AuthManagerConfig' ) ) {
128 $this->initializeConfig();
129 }
130 $this->manager = new AuthManager( $this->request, $this->config );
131 $this->manager->setLogger( $this->logger );
132 $this->managerPriv = \TestingAccessWrapper::newFromObject( $this->manager );
133 }
134
135 /**
136 * Setup SessionManager with a mock session provider
137 * @param bool|null $canChangeUser If non-null, canChangeUser will be mocked to return this
138 * @param array $methods Additional methods to mock
139 * @return array (MediaWiki\Session\SessionProvider, ScopedCallback)
140 */
141 protected function getMockSessionProvider( $canChangeUser = null, array $methods = [] ) {
142 if ( !$this->config ) {
143 $this->config = new \HashConfig();
144 $this->initializeConfig();
145 }
146 $this->config->set( 'ObjectCacheSessionExpiry', 100 );
147
148 $methods[] = '__toString';
149 $methods[] = 'describe';
150 if ( $canChangeUser !== null ) {
151 $methods[] = 'canChangeUser';
152 }
153 $provider = $this->getMockBuilder( 'DummySessionProvider' )
154 ->setMethods( $methods )
155 ->getMock();
156 $provider->expects( $this->any() )->method( '__toString' )
157 ->will( $this->returnValue( 'MockSessionProvider' ) );
158 $provider->expects( $this->any() )->method( 'describe' )
159 ->will( $this->returnValue( 'MockSessionProvider sessions' ) );
160 if ( $canChangeUser !== null ) {
161 $provider->expects( $this->any() )->method( 'canChangeUser' )
162 ->will( $this->returnValue( $canChangeUser ) );
163 }
164 $this->config->set( 'SessionProviders', [
165 [ 'factory' => function () use ( $provider ) {
166 return $provider;
167 } ],
168 ] );
169
170 $manager = new \MediaWiki\Session\SessionManager( [
171 'config' => $this->config,
172 'logger' => new \Psr\Log\NullLogger(),
173 'store' => new \HashBagOStuff(),
174 ] );
175 \TestingAccessWrapper::newFromObject( $manager )->getProvider( (string)$provider );
176
177 $reset = \MediaWiki\Session\TestUtils::setSessionManagerSingleton( $manager );
178
179 if ( $this->request ) {
180 $manager->getSessionForRequest( $this->request );
181 }
182
183 return [ $provider, $reset ];
184 }
185
186 public function testSingleton() {
187 // Temporarily clear out the global singleton, if any, to test creating
188 // one.
189 $rProp = new \ReflectionProperty( AuthManager::class, 'instance' );
190 $rProp->setAccessible( true );
191 $old = $rProp->getValue();
192 $cb = new \ScopedCallback( [ $rProp, 'setValue' ], [ $old ] );
193 $rProp->setValue( null );
194
195 $singleton = AuthManager::singleton();
196 $this->assertInstanceOf( AuthManager::class, AuthManager::singleton() );
197 $this->assertSame( $singleton, AuthManager::singleton() );
198 $this->assertSame( \RequestContext::getMain()->getRequest(), $singleton->getRequest() );
199 $this->assertSame(
200 \RequestContext::getMain()->getConfig(),
201 \TestingAccessWrapper::newFromObject( $singleton )->config
202 );
203
204 $this->setMwGlobals( [ 'wgDisableAuthManager' => true ] );
205 try {
206 AuthManager::singleton();
207 $this->fail( 'Expected exception not thrown' );
208 } catch ( \BadMethodCallException $ex ) {
209 $this->assertSame( '$wgDisableAuthManager is set', $ex->getMessage() );
210 }
211 }
212
213 public function testCanAuthenticateNow() {
214 $this->initializeManager();
215
216 list( $provider, $reset ) = $this->getMockSessionProvider( false );
217 $this->assertFalse( $this->manager->canAuthenticateNow() );
218 \ScopedCallback::consume( $reset );
219
220 list( $provider, $reset ) = $this->getMockSessionProvider( true );
221 $this->assertTrue( $this->manager->canAuthenticateNow() );
222 \ScopedCallback::consume( $reset );
223 }
224
225 public function testNormalizeUsername() {
226 $mocks = [
227 $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
228 $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
229 $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
230 $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
231 ];
232 foreach ( $mocks as $key => $mock ) {
233 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
234 }
235 $mocks[0]->expects( $this->once() )->method( 'providerNormalizeUsername' )
236 ->with( $this->identicalTo( 'XYZ' ) )
237 ->willReturn( 'Foo' );
238 $mocks[1]->expects( $this->once() )->method( 'providerNormalizeUsername' )
239 ->with( $this->identicalTo( 'XYZ' ) )
240 ->willReturn( 'Foo' );
241 $mocks[2]->expects( $this->once() )->method( 'providerNormalizeUsername' )
242 ->with( $this->identicalTo( 'XYZ' ) )
243 ->willReturn( null );
244 $mocks[3]->expects( $this->once() )->method( 'providerNormalizeUsername' )
245 ->with( $this->identicalTo( 'XYZ' ) )
246 ->willReturn( 'Bar!' );
247
248 $this->primaryauthMocks = $mocks;
249
250 $this->initializeManager();
251
252 $this->assertSame( [ 'Foo', 'Bar!' ], $this->manager->normalizeUsername( 'XYZ' ) );
253 }
254
255 /**
256 * @dataProvider provideSecuritySensitiveOperationStatus
257 * @param bool $mutableSession
258 */
259 public function testSecuritySensitiveOperationStatus( $mutableSession ) {
260 $this->logger = new \Psr\Log\NullLogger();
261 $user = \User::newFromName( 'UTSysop' );
262 $provideUser = null;
263 $reauth = $mutableSession ? AuthManager::SEC_REAUTH : AuthManager::SEC_FAIL;
264
265 list( $provider, $reset ) = $this->getMockSessionProvider(
266 $mutableSession, [ 'provideSessionInfo' ]
267 );
268 $provider->expects( $this->any() )->method( 'provideSessionInfo' )
269 ->will( $this->returnCallback( function () use ( $provider, &$provideUser ) {
270 return new SessionInfo( SessionInfo::MIN_PRIORITY, [
271 'provider' => $provider,
272 'id' => \DummySessionProvider::ID,
273 'persisted' => true,
274 'userInfo' => UserInfo::newFromUser( $provideUser, true )
275 ] );
276 } ) );
277 $this->initializeManager();
278
279 $this->config->set( 'ReauthenticateTime', [] );
280 $this->config->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [] );
281 $provideUser = new \User;
282 $session = $provider->getManager()->getSessionForRequest( $this->request );
283 $this->assertSame( 0, $session->getUser()->getId(), 'sanity check' );
284
285 // Anonymous user => reauth
286 $session->set( 'AuthManager:lastAuthId', 0 );
287 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
288 $this->assertSame( $reauth, $this->manager->securitySensitiveOperationStatus( 'foo' ) );
289
290 $provideUser = $user;
291 $session = $provider->getManager()->getSessionForRequest( $this->request );
292 $this->assertSame( $user->getId(), $session->getUser()->getId(), 'sanity check' );
293
294 // Error for no default (only gets thrown for non-anonymous user)
295 $session->set( 'AuthManager:lastAuthId', $user->getId() + 1 );
296 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
297 try {
298 $this->manager->securitySensitiveOperationStatus( 'foo' );
299 $this->fail( 'Expected exception not thrown' );
300 } catch ( \UnexpectedValueException $ex ) {
301 $this->assertSame(
302 $mutableSession
303 ? '$wgReauthenticateTime lacks a default'
304 : '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default',
305 $ex->getMessage()
306 );
307 }
308
309 if ( $mutableSession ) {
310 $this->config->set( 'ReauthenticateTime', [
311 'test' => 100,
312 'test2' => -1,
313 'default' => 10,
314 ] );
315
316 // Mismatched user ID
317 $session->set( 'AuthManager:lastAuthId', $user->getId() + 1 );
318 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
319 $this->assertSame(
320 AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'foo' )
321 );
322 $this->assertSame(
323 AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'test' )
324 );
325 $this->assertSame(
326 AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'test2' )
327 );
328
329 // Missing time
330 $session->set( 'AuthManager:lastAuthId', $user->getId() );
331 $session->set( 'AuthManager:lastAuthTimestamp', null );
332 $this->assertSame(
333 AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'foo' )
334 );
335 $this->assertSame(
336 AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'test' )
337 );
338 $this->assertSame(
339 AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'test2' )
340 );
341
342 // Recent enough to pass
343 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
344 $this->assertSame(
345 AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'foo' )
346 );
347
348 // Not recent enough to pass
349 $session->set( 'AuthManager:lastAuthTimestamp', time() - 20 );
350 $this->assertSame(
351 AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'foo' )
352 );
353 // But recent enough for the 'test' operation
354 $this->assertSame(
355 AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'test' )
356 );
357 } else {
358 $this->config->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [
359 'test' => false,
360 'default' => true,
361 ] );
362
363 $this->assertEquals(
364 AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'foo' )
365 );
366
367 $this->assertEquals(
368 AuthManager::SEC_FAIL, $this->manager->securitySensitiveOperationStatus( 'test' )
369 );
370 }
371
372 // Test hook, all three possible values
373 foreach ( [
374 AuthManager::SEC_OK => AuthManager::SEC_OK,
375 AuthManager::SEC_REAUTH => $reauth,
376 AuthManager::SEC_FAIL => AuthManager::SEC_FAIL,
377 ] as $hook => $expect ) {
378 $this->hook( 'SecuritySensitiveOperationStatus', $this->exactly( 2 ) )
379 ->with(
380 $this->anything(),
381 $this->anything(),
382 $this->callback( function ( $s ) use ( $session ) {
383 return $s->getId() === $session->getId();
384 } ),
385 $mutableSession ? $this->equalTo( 500, 1 ) : $this->equalTo( -1 )
386 )
387 ->will( $this->returnCallback( function ( &$v ) use ( $hook ) {
388 $v = $hook;
389 return true;
390 } ) );
391 $session->set( 'AuthManager:lastAuthTimestamp', time() - 500 );
392 $this->assertEquals(
393 $expect, $this->manager->securitySensitiveOperationStatus( 'test' ), "hook $hook"
394 );
395 $this->assertEquals(
396 $expect, $this->manager->securitySensitiveOperationStatus( 'test2' ), "hook $hook"
397 );
398 $this->unhook( 'SecuritySensitiveOperationStatus' );
399 }
400
401 \ScopedCallback::consume( $reset );
402 }
403
404 public function onSecuritySensitiveOperationStatus( &$status, $operation, $session, $time ) {
405 }
406
407 public static function provideSecuritySensitiveOperationStatus() {
408 return [
409 [ true ],
410 [ false ],
411 ];
412 }
413
414 /**
415 * @dataProvider provideUserCanAuthenticate
416 * @param bool $primary1Can
417 * @param bool $primary2Can
418 * @param bool $expect
419 */
420 public function testUserCanAuthenticate( $primary1Can, $primary2Can, $expect ) {
421 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
422 $mock1->expects( $this->any() )->method( 'getUniqueId' )
423 ->will( $this->returnValue( 'primary1' ) );
424 $mock1->expects( $this->any() )->method( 'testUserCanAuthenticate' )
425 ->with( $this->equalTo( 'UTSysop' ) )
426 ->will( $this->returnValue( $primary1Can ) );
427 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
428 $mock2->expects( $this->any() )->method( 'getUniqueId' )
429 ->will( $this->returnValue( 'primary2' ) );
430 $mock2->expects( $this->any() )->method( 'testUserCanAuthenticate' )
431 ->with( $this->equalTo( 'UTSysop' ) )
432 ->will( $this->returnValue( $primary2Can ) );
433 $this->primaryauthMocks = [ $mock1, $mock2 ];
434
435 $this->initializeManager( true );
436 $this->assertSame( $expect, $this->manager->userCanAuthenticate( 'UTSysop' ) );
437 }
438
439 public static function provideUserCanAuthenticate() {
440 return [
441 [ false, false, false ],
442 [ true, false, true ],
443 [ false, true, true ],
444 [ true, true, true ],
445 ];
446 }
447
448 public function testRevokeAccessForUser() {
449 $this->initializeManager();
450
451 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
452 $mock->expects( $this->any() )->method( 'getUniqueId' )
453 ->will( $this->returnValue( 'primary' ) );
454 $mock->expects( $this->once() )->method( 'providerRevokeAccessForUser' )
455 ->with( $this->equalTo( 'UTSysop' ) );
456 $this->primaryauthMocks = [ $mock ];
457
458 $this->initializeManager( true );
459 $this->logger->setCollect( true );
460
461 $this->manager->revokeAccessForUser( 'UTSysop' );
462
463 $this->assertSame( [
464 [ LogLevel::INFO, 'Revoking access for {user}' ],
465 ], $this->logger->getBuffer() );
466 }
467
468 public function testProviderCreation() {
469 $mocks = [
470 'pre' => $this->getMockForAbstractClass( PreAuthenticationProvider::class ),
471 'primary' => $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
472 'secondary' => $this->getMockForAbstractClass( SecondaryAuthenticationProvider::class ),
473 ];
474 foreach ( $mocks as $key => $mock ) {
475 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
476 $mock->expects( $this->once() )->method( 'setLogger' );
477 $mock->expects( $this->once() )->method( 'setManager' );
478 $mock->expects( $this->once() )->method( 'setConfig' );
479 }
480 $this->preauthMocks = [ $mocks['pre'] ];
481 $this->primaryauthMocks = [ $mocks['primary'] ];
482 $this->secondaryauthMocks = [ $mocks['secondary'] ];
483
484 // Normal operation
485 $this->initializeManager();
486 $this->assertSame(
487 $mocks['primary'],
488 $this->managerPriv->getAuthenticationProvider( 'primary' )
489 );
490 $this->assertSame(
491 $mocks['secondary'],
492 $this->managerPriv->getAuthenticationProvider( 'secondary' )
493 );
494 $this->assertSame(
495 $mocks['pre'],
496 $this->managerPriv->getAuthenticationProvider( 'pre' )
497 );
498 $this->assertSame(
499 [ 'pre' => $mocks['pre'] ],
500 $this->managerPriv->getPreAuthenticationProviders()
501 );
502 $this->assertSame(
503 [ 'primary' => $mocks['primary'] ],
504 $this->managerPriv->getPrimaryAuthenticationProviders()
505 );
506 $this->assertSame(
507 [ 'secondary' => $mocks['secondary'] ],
508 $this->managerPriv->getSecondaryAuthenticationProviders()
509 );
510
511 // Duplicate IDs
512 $mock1 = $this->getMockForAbstractClass( PreAuthenticationProvider::class );
513 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
514 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
515 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
516 $this->preauthMocks = [ $mock1 ];
517 $this->primaryauthMocks = [ $mock2 ];
518 $this->secondaryauthMocks = [];
519 $this->initializeManager( true );
520 try {
521 $this->managerPriv->getAuthenticationProvider( 'Y' );
522 $this->fail( 'Expected exception not thrown' );
523 } catch ( \RuntimeException $ex ) {
524 $class1 = get_class( $mock1 );
525 $class2 = get_class( $mock2 );
526 $this->assertSame(
527 "Duplicate specifications for id X (classes $class1 and $class2)", $ex->getMessage()
528 );
529 }
530
531 // Wrong classes
532 $mock = $this->getMockForAbstractClass( AuthenticationProvider::class );
533 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
534 $class = get_class( $mock );
535 $this->preauthMocks = [ $mock ];
536 $this->primaryauthMocks = [ $mock ];
537 $this->secondaryauthMocks = [ $mock ];
538 $this->initializeManager( true );
539 try {
540 $this->managerPriv->getPreAuthenticationProviders();
541 $this->fail( 'Expected exception not thrown' );
542 } catch ( \RuntimeException $ex ) {
543 $this->assertSame(
544 "Expected instance of MediaWiki\\Auth\\PreAuthenticationProvider, got $class",
545 $ex->getMessage()
546 );
547 }
548 try {
549 $this->managerPriv->getPrimaryAuthenticationProviders();
550 $this->fail( 'Expected exception not thrown' );
551 } catch ( \RuntimeException $ex ) {
552 $this->assertSame(
553 "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
554 $ex->getMessage()
555 );
556 }
557 try {
558 $this->managerPriv->getSecondaryAuthenticationProviders();
559 $this->fail( 'Expected exception not thrown' );
560 } catch ( \RuntimeException $ex ) {
561 $this->assertSame(
562 "Expected instance of MediaWiki\\Auth\\SecondaryAuthenticationProvider, got $class",
563 $ex->getMessage()
564 );
565 }
566
567 // Sorting
568 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
569 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
570 $mock3 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
571 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
572 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
573 $mock3->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'C' ) );
574 $this->preauthMocks = [];
575 $this->primaryauthMocks = [ $mock1, $mock2, $mock3 ];
576 $this->secondaryauthMocks = [];
577 $this->initializeConfig();
578 $config = $this->config->get( 'AuthManagerConfig' );
579
580 $this->initializeManager( false );
581 $this->assertSame(
582 [ 'A' => $mock1, 'B' => $mock2, 'C' => $mock3 ],
583 $this->managerPriv->getPrimaryAuthenticationProviders(),
584 'sanity check'
585 );
586
587 $config['primaryauth']['A']['sort'] = 100;
588 $config['primaryauth']['C']['sort'] = -1;
589 $this->config->set( 'AuthManagerConfig', $config );
590 $this->initializeManager( false );
591 $this->assertSame(
592 [ 'C' => $mock3, 'B' => $mock2, 'A' => $mock1 ],
593 $this->managerPriv->getPrimaryAuthenticationProviders()
594 );
595 }
596
597 public function testSetDefaultUserOptions() {
598 $this->initializeManager();
599
600 $context = \RequestContext::getMain();
601 $reset = new \ScopedCallback( [ $context, 'setLanguage' ], [ $context->getLanguage() ] );
602 $context->setLanguage( 'de' );
603 $this->setMwGlobals( 'wgContLang', \Language::factory( 'zh' ) );
604
605 $user = \User::newFromName( self::usernameForCreation() );
606 $user->addToDatabase();
607 $oldToken = $user->getToken();
608 $this->managerPriv->setDefaultUserOptions( $user, false );
609 $user->saveSettings();
610 $this->assertNotEquals( $oldToken, $user->getToken() );
611 $this->assertSame( 'zh', $user->getOption( 'language' ) );
612 $this->assertSame( 'zh', $user->getOption( 'variant' ) );
613
614 $user = \User::newFromName( self::usernameForCreation() );
615 $user->addToDatabase();
616 $oldToken = $user->getToken();
617 $this->managerPriv->setDefaultUserOptions( $user, true );
618 $user->saveSettings();
619 $this->assertNotEquals( $oldToken, $user->getToken() );
620 $this->assertSame( 'de', $user->getOption( 'language' ) );
621 $this->assertSame( 'zh', $user->getOption( 'variant' ) );
622
623 $this->setMwGlobals( 'wgContLang', \Language::factory( 'en' ) );
624
625 $user = \User::newFromName( self::usernameForCreation() );
626 $user->addToDatabase();
627 $oldToken = $user->getToken();
628 $this->managerPriv->setDefaultUserOptions( $user, true );
629 $user->saveSettings();
630 $this->assertNotEquals( $oldToken, $user->getToken() );
631 $this->assertSame( 'de', $user->getOption( 'language' ) );
632 $this->assertSame( null, $user->getOption( 'variant' ) );
633 }
634
635 public function testForcePrimaryAuthenticationProviders() {
636 $mockA = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
637 $mockB = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
638 $mockB2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
639 $mockA->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
640 $mockB->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
641 $mockB2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
642 $this->primaryauthMocks = [ $mockA ];
643
644 $this->logger = new \TestLogger( true );
645
646 // Test without first initializing the configured providers
647 $this->initializeManager();
648 $this->manager->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
649 $this->assertSame(
650 [ 'B' => $mockB ], $this->managerPriv->getPrimaryAuthenticationProviders()
651 );
652 $this->assertSame( null, $this->managerPriv->getAuthenticationProvider( 'A' ) );
653 $this->assertSame( $mockB, $this->managerPriv->getAuthenticationProvider( 'B' ) );
654 $this->assertSame( [
655 [ LogLevel::WARNING, 'Overriding AuthManager primary authn because testing' ],
656 ], $this->logger->getBuffer() );
657 $this->logger->clearBuffer();
658
659 // Test with first initializing the configured providers
660 $this->initializeManager();
661 $this->assertSame( $mockA, $this->managerPriv->getAuthenticationProvider( 'A' ) );
662 $this->assertSame( null, $this->managerPriv->getAuthenticationProvider( 'B' ) );
663 $this->request->getSession()->setSecret( 'AuthManager::authnState', 'test' );
664 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
665 $this->manager->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
666 $this->assertSame(
667 [ 'B' => $mockB ], $this->managerPriv->getPrimaryAuthenticationProviders()
668 );
669 $this->assertSame( null, $this->managerPriv->getAuthenticationProvider( 'A' ) );
670 $this->assertSame( $mockB, $this->managerPriv->getAuthenticationProvider( 'B' ) );
671 $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::authnState' ) );
672 $this->assertNull(
673 $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
674 );
675 $this->assertSame( [
676 [ LogLevel::WARNING, 'Overriding AuthManager primary authn because testing' ],
677 [
678 LogLevel::WARNING,
679 'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
680 ],
681 ], $this->logger->getBuffer() );
682 $this->logger->clearBuffer();
683
684 // Test duplicate IDs
685 $this->initializeManager();
686 try {
687 $this->manager->forcePrimaryAuthenticationProviders( [ $mockB, $mockB2 ], 'testing' );
688 $this->fail( 'Expected exception not thrown' );
689 } catch ( \RuntimeException $ex ) {
690 $class1 = get_class( $mockB );
691 $class2 = get_class( $mockB2 );
692 $this->assertSame(
693 "Duplicate specifications for id B (classes $class2 and $class1)", $ex->getMessage()
694 );
695 }
696
697 // Wrong classes
698 $mock = $this->getMockForAbstractClass( AuthenticationProvider::class );
699 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
700 $class = get_class( $mock );
701 try {
702 $this->manager->forcePrimaryAuthenticationProviders( [ $mock ], 'testing' );
703 $this->fail( 'Expected exception not thrown' );
704 } catch ( \RuntimeException $ex ) {
705 $this->assertSame(
706 "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
707 $ex->getMessage()
708 );
709 }
710
711 }
712
713 public function testBeginAuthentication() {
714 $this->initializeManager();
715
716 // Immutable session
717 list( $provider, $reset ) = $this->getMockSessionProvider( false );
718 $this->hook( 'UserLoggedIn', $this->never() );
719 $this->request->getSession()->setSecret( 'AuthManager::authnState', 'test' );
720 try {
721 $this->manager->beginAuthentication( [], 'http://localhost/' );
722 $this->fail( 'Expected exception not thrown' );
723 } catch ( \LogicException $ex ) {
724 $this->assertSame( 'Authentication is not possible now', $ex->getMessage() );
725 }
726 $this->unhook( 'UserLoggedIn' );
727 $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::authnState' ) );
728 \ScopedCallback::consume( $reset );
729 $this->initializeManager( true );
730
731 // CreatedAccountAuthenticationRequest
732 $user = \User::newFromName( 'UTSysop' );
733 $reqs = [
734 new CreatedAccountAuthenticationRequest( $user->getId(), $user->getName() )
735 ];
736 $this->hook( 'UserLoggedIn', $this->never() );
737 try {
738 $this->manager->beginAuthentication( $reqs, 'http://localhost/' );
739 $this->fail( 'Expected exception not thrown' );
740 } catch ( \LogicException $ex ) {
741 $this->assertSame(
742 'CreatedAccountAuthenticationRequests are only valid on the same AuthManager ' .
743 'that created the account',
744 $ex->getMessage()
745 );
746 }
747 $this->unhook( 'UserLoggedIn' );
748
749 $this->request->getSession()->clear();
750 $this->request->getSession()->setSecret( 'AuthManager::authnState', 'test' );
751 $this->managerPriv->createdAccountAuthenticationRequests = [ $reqs[0] ];
752 $this->hook( 'UserLoggedIn', $this->once() )
753 ->with( $this->callback( function ( $u ) use ( $user ) {
754 return $user->getId() === $u->getId() && $user->getName() === $u->getName();
755 } ) );
756 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->once() );
757 $this->logger->setCollect( true );
758 $ret = $this->manager->beginAuthentication( $reqs, 'http://localhost/' );
759 $this->logger->setCollect( false );
760 $this->unhook( 'UserLoggedIn' );
761 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
762 $this->assertSame( AuthenticationResponse::PASS, $ret->status );
763 $this->assertSame( $user->getName(), $ret->username );
764 $this->assertSame( $user->getId(), $this->request->getSessionData( 'AuthManager:lastAuthId' ) );
765 $this->assertEquals(
766 time(), $this->request->getSessionData( 'AuthManager:lastAuthTimestamp' ),
767 'timestamp ±1', 1
768 );
769 $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::authnState' ) );
770 $this->assertSame( $user->getId(), $this->request->getSession()->getUser()->getId() );
771 $this->assertSame( [
772 [ LogLevel::INFO, 'Logging in {user} after account creation' ],
773 ], $this->logger->getBuffer() );
774 }
775
776 public function testCreateFromLogin() {
777 $user = \User::newFromName( 'UTSysop' );
778 $req1 = $this->getMock( AuthenticationRequest::class );
779 $req2 = $this->getMock( AuthenticationRequest::class );
780 $req3 = $this->getMock( AuthenticationRequest::class );
781 $userReq = new UsernameAuthenticationRequest;
782 $userReq->username = 'UTDummy';
783
784 $req1->returnToUrl = 'http://localhost/';
785 $req2->returnToUrl = 'http://localhost/';
786 $req3->returnToUrl = 'http://localhost/';
787 $req3->username = 'UTDummy';
788 $userReq->returnToUrl = 'http://localhost/';
789
790 // Passing one into beginAuthentication(), and an immediate FAIL
791 $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider::class );
792 $this->primaryauthMocks = [ $primary ];
793 $this->initializeManager( true );
794 $res = AuthenticationResponse::newFail( wfMessage( 'foo' ) );
795 $res->createRequest = $req1;
796 $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
797 ->will( $this->returnValue( $res ) );
798 $createReq = new CreateFromLoginAuthenticationRequest(
799 null, [ $req2->getUniqueId() => $req2 ]
800 );
801 $this->logger->setCollect( true );
802 $ret = $this->manager->beginAuthentication( [ $createReq ], 'http://localhost/' );
803 $this->logger->setCollect( false );
804 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
805 $this->assertInstanceOf( CreateFromLoginAuthenticationRequest::class, $ret->createRequest );
806 $this->assertSame( $req1, $ret->createRequest->createRequest );
807 $this->assertEquals( [ $req2->getUniqueId() => $req2 ], $ret->createRequest->maybeLink );
808
809 // UI, then FAIL in beginAuthentication()
810 $primary = $this->getMockBuilder( AbstractPrimaryAuthenticationProvider::class )
811 ->setMethods( [ 'continuePrimaryAuthentication' ] )
812 ->getMockForAbstractClass();
813 $this->primaryauthMocks = [ $primary ];
814 $this->initializeManager( true );
815 $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
816 ->will( $this->returnValue(
817 AuthenticationResponse::newUI( [ $req1 ], wfMessage( 'foo' ) )
818 ) );
819 $res = AuthenticationResponse::newFail( wfMessage( 'foo' ) );
820 $res->createRequest = $req2;
821 $primary->expects( $this->any() )->method( 'continuePrimaryAuthentication' )
822 ->will( $this->returnValue( $res ) );
823 $this->logger->setCollect( true );
824 $ret = $this->manager->beginAuthentication( [], 'http://localhost/' );
825 $this->assertSame( AuthenticationResponse::UI, $ret->status, 'sanity check' );
826 $ret = $this->manager->continueAuthentication( [] );
827 $this->logger->setCollect( false );
828 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
829 $this->assertInstanceOf( CreateFromLoginAuthenticationRequest::class, $ret->createRequest );
830 $this->assertSame( $req2, $ret->createRequest->createRequest );
831 $this->assertEquals( [], $ret->createRequest->maybeLink );
832
833 // Pass into beginAccountCreation(), see that maybeLink and createRequest get copied
834 $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider::class );
835 $this->primaryauthMocks = [ $primary ];
836 $this->initializeManager( true );
837 $createReq = new CreateFromLoginAuthenticationRequest( $req3, [ $req2 ] );
838 $createReq->returnToUrl = 'http://localhost/';
839 $createReq->username = 'UTDummy';
840 $res = AuthenticationResponse::newUI( [ $req1 ], wfMessage( 'foo' ) );
841 $primary->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )
842 ->with( $this->anything(), $this->anything(), [ $userReq, $createReq, $req3 ] )
843 ->will( $this->returnValue( $res ) );
844 $primary->expects( $this->any() )->method( 'accountCreationType' )
845 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
846 $this->logger->setCollect( true );
847 $ret = $this->manager->beginAccountCreation(
848 $user, [ $userReq, $createReq ], 'http://localhost/'
849 );
850 $this->logger->setCollect( false );
851 $this->assertSame( AuthenticationResponse::UI, $ret->status );
852 $state = $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' );
853 $this->assertNotNull( $state );
854 $this->assertEquals( [ $userReq, $createReq, $req3 ], $state['reqs'] );
855 $this->assertEquals( [ $req2 ], $state['maybeLink'] );
856 }
857
858 /**
859 * @dataProvider provideAuthentication
860 * @param StatusValue $preResponse
861 * @param array $primaryResponses
862 * @param array $secondaryResponses
863 * @param array $managerResponses
864 * @param bool $link Whether the primary authentication provider is a "link" provider
865 */
866 public function testAuthentication(
867 StatusValue $preResponse, array $primaryResponses, array $secondaryResponses,
868 array $managerResponses, $link = false
869 ) {
870 $this->initializeManager();
871 $user = \User::newFromName( 'UTSysop' );
872 $id = $user->getId();
873 $name = $user->getName();
874
875 // Set up lots of mocks...
876 $req = new RememberMeAuthenticationRequest;
877 $req->rememberMe = (bool)rand( 0, 1 );
878 $req->pre = $preResponse;
879 $req->primary = $primaryResponses;
880 $req->secondary = $secondaryResponses;
881 $mocks = [];
882 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
883 $class = ucfirst( $key ) . 'AuthenticationProvider';
884 $mocks[$key] = $this->getMockForAbstractClass(
885 "MediaWiki\\Auth\\$class", [], "Mock$class"
886 );
887 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
888 ->will( $this->returnValue( $key ) );
889 $mocks[$key . '2'] = $this->getMockForAbstractClass(
890 "MediaWiki\\Auth\\$class", [], "Mock$class"
891 );
892 $mocks[$key . '2']->expects( $this->any() )->method( 'getUniqueId' )
893 ->will( $this->returnValue( $key . '2' ) );
894 $mocks[$key . '3'] = $this->getMockForAbstractClass(
895 "MediaWiki\\Auth\\$class", [], "Mock$class"
896 );
897 $mocks[$key . '3']->expects( $this->any() )->method( 'getUniqueId' )
898 ->will( $this->returnValue( $key . '3' ) );
899 }
900 foreach ( $mocks as $mock ) {
901 $mock->expects( $this->any() )->method( 'getAuthenticationRequests' )
902 ->will( $this->returnValue( [] ) );
903 }
904
905 $mocks['pre']->expects( $this->once() )->method( 'testForAuthentication' )
906 ->will( $this->returnCallback( function ( $reqs ) use ( $req ) {
907 $this->assertContains( $req, $reqs );
908 return $req->pre;
909 } ) );
910
911 $ct = count( $req->primary );
912 $callback = $this->returnCallback( function ( $reqs ) use ( $req ) {
913 $this->assertContains( $req, $reqs );
914 return array_shift( $req->primary );
915 } );
916 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
917 ->method( 'beginPrimaryAuthentication' )
918 ->will( $callback );
919 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
920 ->method( 'continuePrimaryAuthentication' )
921 ->will( $callback );
922 if ( $link ) {
923 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
924 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
925 }
926
927 $ct = count( $req->secondary );
928 $callback = $this->returnCallback( function ( $user, $reqs ) use ( $id, $name, $req ) {
929 $this->assertSame( $id, $user->getId() );
930 $this->assertSame( $name, $user->getName() );
931 $this->assertContains( $req, $reqs );
932 return array_shift( $req->secondary );
933 } );
934 $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
935 ->method( 'beginSecondaryAuthentication' )
936 ->will( $callback );
937 $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
938 ->method( 'continueSecondaryAuthentication' )
939 ->will( $callback );
940
941 $abstain = AuthenticationResponse::newAbstain();
942 $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAuthentication' )
943 ->will( $this->returnValue( StatusValue::newGood() ) );
944 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAuthentication' )
945 ->will( $this->returnValue( $abstain ) );
946 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAuthentication' );
947 $mocks['secondary2']->expects( $this->atMost( 1 ) )->method( 'beginSecondaryAuthentication' )
948 ->will( $this->returnValue( $abstain ) );
949 $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAuthentication' );
950 $mocks['secondary3']->expects( $this->atMost( 1 ) )->method( 'beginSecondaryAuthentication' )
951 ->will( $this->returnValue( $abstain ) );
952 $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAuthentication' );
953
954 $this->preauthMocks = [ $mocks['pre'], $mocks['pre2'] ];
955 $this->primaryauthMocks = [ $mocks['primary'], $mocks['primary2'] ];
956 $this->secondaryauthMocks = [
957 $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2'],
958 // So linking happens
959 new ConfirmLinkSecondaryAuthenticationProvider,
960 ];
961 $this->initializeManager( true );
962 $this->logger->setCollect( true );
963
964 $constraint = \PHPUnit_Framework_Assert::logicalOr(
965 $this->equalTo( AuthenticationResponse::PASS ),
966 $this->equalTo( AuthenticationResponse::FAIL )
967 );
968 $providers = array_filter(
969 array_merge(
970 $this->preauthMocks, $this->primaryauthMocks, $this->secondaryauthMocks
971 ),
972 function ( $p ) {
973 return is_callable( [ $p, 'expects' ] );
974 }
975 );
976 foreach ( $providers as $p ) {
977 $p->postCalled = false;
978 $p->expects( $this->atMost( 1 ) )->method( 'postAuthentication' )
979 ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) {
980 if ( $user !== null ) {
981 $this->assertInstanceOf( 'User', $user );
982 $this->assertSame( 'UTSysop', $user->getName() );
983 }
984 $this->assertInstanceOf( AuthenticationResponse::class, $response );
985 $this->assertThat( $response->status, $constraint );
986 $p->postCalled = $response->status;
987 } );
988 }
989
990 $session = $this->request->getSession();
991 $session->setRememberUser( !$req->rememberMe );
992
993 foreach ( $managerResponses as $i => $response ) {
994 $success = $response instanceof AuthenticationResponse &&
995 $response->status === AuthenticationResponse::PASS;
996 if ( $success ) {
997 $this->hook( 'UserLoggedIn', $this->once() )
998 ->with( $this->callback( function ( $user ) use ( $id, $name ) {
999 return $user->getId() === $id && $user->getName() === $name;
1000 } ) );
1001 } else {
1002 $this->hook( 'UserLoggedIn', $this->never() );
1003 }
1004 if ( $success || (
1005 $response instanceof AuthenticationResponse &&
1006 $response->status === AuthenticationResponse::FAIL &&
1007 $response->message->getKey() !== 'authmanager-authn-not-in-progress' &&
1008 $response->message->getKey() !== 'authmanager-authn-no-primary'
1009 )
1010 ) {
1011 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->once() );
1012 } else {
1013 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->never() );
1014 }
1015
1016 $ex = null;
1017 try {
1018 if ( !$i ) {
1019 $ret = $this->manager->beginAuthentication( [ $req ], 'http://localhost/' );
1020 } else {
1021 $ret = $this->manager->continueAuthentication( [ $req ] );
1022 }
1023 if ( $response instanceof \Exception ) {
1024 $this->fail( 'Expected exception not thrown', "Response $i" );
1025 }
1026 } catch ( \Exception $ex ) {
1027 if ( !$response instanceof \Exception ) {
1028 throw $ex;
1029 }
1030 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
1031 $this->assertNull( $session->getSecret( 'AuthManager::authnState' ),
1032 "Response $i, exception, session state" );
1033 $this->unhook( 'UserLoggedIn' );
1034 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
1035 return;
1036 }
1037
1038 $this->unhook( 'UserLoggedIn' );
1039 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
1040
1041 $this->assertSame( 'http://localhost/', $req->returnToUrl );
1042
1043 $ret->message = $this->message( $ret->message );
1044 $this->assertEquals( $response, $ret, "Response $i, response" );
1045 if ( $success ) {
1046 $this->assertSame( $id, $session->getUser()->getId(),
1047 "Response $i, authn" );
1048 } else {
1049 $this->assertSame( 0, $session->getUser()->getId(),
1050 "Response $i, authn" );
1051 }
1052 if ( $success || $response->status === AuthenticationResponse::FAIL ) {
1053 $this->assertNull( $session->getSecret( 'AuthManager::authnState' ),
1054 "Response $i, session state" );
1055 foreach ( $providers as $p ) {
1056 $this->assertSame( $response->status, $p->postCalled,
1057 "Response $i, post-auth callback called" );
1058 }
1059 } else {
1060 $this->assertNotNull( $session->getSecret( 'AuthManager::authnState' ),
1061 "Response $i, session state" );
1062 foreach ( $ret->neededRequests as $neededReq ) {
1063 $this->assertEquals( AuthManager::ACTION_LOGIN, $neededReq->action,
1064 "Response $i, neededRequest action" );
1065 }
1066 $this->assertEquals(
1067 $ret->neededRequests,
1068 $this->manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN_CONTINUE ),
1069 "Response $i, continuation check"
1070 );
1071 foreach ( $providers as $p ) {
1072 $this->assertFalse( $p->postCalled, "Response $i, post-auth callback not called" );
1073 }
1074 }
1075
1076 $state = $session->getSecret( 'AuthManager::authnState' );
1077 $maybeLink = isset( $state['maybeLink'] ) ? $state['maybeLink'] : [];
1078 if ( $link && $response->status === AuthenticationResponse::RESTART ) {
1079 $this->assertEquals(
1080 $response->createRequest->maybeLink,
1081 $maybeLink,
1082 "Response $i, maybeLink"
1083 );
1084 } else {
1085 $this->assertEquals( [], $maybeLink, "Response $i, maybeLink" );
1086 }
1087 }
1088
1089 if ( $success ) {
1090 $this->assertSame( $req->rememberMe, $session->shouldRememberUser(),
1091 'rememberMe checkbox had effect' );
1092 } else {
1093 $this->assertNotSame( $req->rememberMe, $session->shouldRememberUser(),
1094 'rememberMe checkbox wasn\'t applied' );
1095 }
1096 }
1097
1098 public function provideAuthentication() {
1099 $user = \User::newFromName( 'UTSysop' );
1100 $id = $user->getId();
1101 $name = $user->getName();
1102
1103 $rememberReq = new RememberMeAuthenticationRequest;
1104 $rememberReq->action = AuthManager::ACTION_LOGIN;
1105
1106 $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
1107 $req->foobar = 'baz';
1108 $restartResponse = AuthenticationResponse::newRestart(
1109 $this->message( 'authmanager-authn-no-local-user' )
1110 );
1111 $restartResponse->neededRequests = [ $rememberReq ];
1112
1113 $restartResponse2Pass = AuthenticationResponse::newPass( null );
1114 $restartResponse2Pass->linkRequest = $req;
1115 $restartResponse2 = AuthenticationResponse::newRestart(
1116 $this->message( 'authmanager-authn-no-local-user-link' )
1117 );
1118 $restartResponse2->createRequest = new CreateFromLoginAuthenticationRequest(
1119 null, [ $req->getUniqueId() => $req ]
1120 );
1121 $restartResponse2->createRequest->action = AuthManager::ACTION_LOGIN;
1122 $restartResponse2->neededRequests = [ $rememberReq, $restartResponse2->createRequest ];
1123
1124 return [
1125 'Failure in pre-auth' => [
1126 StatusValue::newFatal( 'fail-from-pre' ),
1127 [],
1128 [],
1129 [
1130 AuthenticationResponse::newFail( $this->message( 'fail-from-pre' ) ),
1131 AuthenticationResponse::newFail(
1132 $this->message( 'authmanager-authn-not-in-progress' )
1133 ),
1134 ]
1135 ],
1136 'Failure in primary' => [
1137 StatusValue::newGood(),
1138 $tmp = [
1139 AuthenticationResponse::newFail( $this->message( 'fail-from-primary' ) ),
1140 ],
1141 [],
1142 $tmp
1143 ],
1144 'All primary abstain' => [
1145 StatusValue::newGood(),
1146 [
1147 AuthenticationResponse::newAbstain(),
1148 ],
1149 [],
1150 [
1151 AuthenticationResponse::newFail( $this->message( 'authmanager-authn-no-primary' ) )
1152 ]
1153 ],
1154 'Primary UI, then redirect, then fail' => [
1155 StatusValue::newGood(),
1156 $tmp = [
1157 AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
1158 AuthenticationResponse::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
1159 AuthenticationResponse::newFail( $this->message( 'fail-in-primary-continue' ) ),
1160 ],
1161 [],
1162 $tmp
1163 ],
1164 'Primary redirect, then abstain' => [
1165 StatusValue::newGood(),
1166 [
1167 $tmp = AuthenticationResponse::newRedirect(
1168 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
1169 ),
1170 AuthenticationResponse::newAbstain(),
1171 ],
1172 [],
1173 [
1174 $tmp,
1175 new \DomainException(
1176 'MockPrimaryAuthenticationProvider::continuePrimaryAuthentication() returned ABSTAIN'
1177 )
1178 ]
1179 ],
1180 'Primary UI, then pass with no local user' => [
1181 StatusValue::newGood(),
1182 [
1183 $tmp = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
1184 AuthenticationResponse::newPass( null ),
1185 ],
1186 [],
1187 [
1188 $tmp,
1189 $restartResponse,
1190 ]
1191 ],
1192 'Primary UI, then pass with no local user (link type)' => [
1193 StatusValue::newGood(),
1194 [
1195 $tmp = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
1196 $restartResponse2Pass,
1197 ],
1198 [],
1199 [
1200 $tmp,
1201 $restartResponse2,
1202 ],
1203 true
1204 ],
1205 'Primary pass with invalid username' => [
1206 StatusValue::newGood(),
1207 [
1208 AuthenticationResponse::newPass( '<>' ),
1209 ],
1210 [],
1211 [
1212 new \DomainException( 'MockPrimaryAuthenticationProvider returned an invalid username: <>' ),
1213 ]
1214 ],
1215 'Secondary fail' => [
1216 StatusValue::newGood(),
1217 [
1218 AuthenticationResponse::newPass( $name ),
1219 ],
1220 $tmp = [
1221 AuthenticationResponse::newFail( $this->message( 'fail-in-secondary' ) ),
1222 ],
1223 $tmp
1224 ],
1225 'Secondary UI, then abstain' => [
1226 StatusValue::newGood(),
1227 [
1228 AuthenticationResponse::newPass( $name ),
1229 ],
1230 [
1231 $tmp = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
1232 AuthenticationResponse::newAbstain()
1233 ],
1234 [
1235 $tmp,
1236 AuthenticationResponse::newPass( $name ),
1237 ]
1238 ],
1239 'Secondary pass' => [
1240 StatusValue::newGood(),
1241 [
1242 AuthenticationResponse::newPass( $name ),
1243 ],
1244 [
1245 AuthenticationResponse::newPass()
1246 ],
1247 [
1248 AuthenticationResponse::newPass( $name ),
1249 ]
1250 ],
1251 ];
1252 }
1253
1254 /**
1255 * @dataProvider provideUserExists
1256 * @param bool $primary1Exists
1257 * @param bool $primary2Exists
1258 * @param bool $expect
1259 */
1260 public function testUserExists( $primary1Exists, $primary2Exists, $expect ) {
1261 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1262 $mock1->expects( $this->any() )->method( 'getUniqueId' )
1263 ->will( $this->returnValue( 'primary1' ) );
1264 $mock1->expects( $this->any() )->method( 'testUserExists' )
1265 ->with( $this->equalTo( 'UTSysop' ) )
1266 ->will( $this->returnValue( $primary1Exists ) );
1267 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1268 $mock2->expects( $this->any() )->method( 'getUniqueId' )
1269 ->will( $this->returnValue( 'primary2' ) );
1270 $mock2->expects( $this->any() )->method( 'testUserExists' )
1271 ->with( $this->equalTo( 'UTSysop' ) )
1272 ->will( $this->returnValue( $primary2Exists ) );
1273 $this->primaryauthMocks = [ $mock1, $mock2 ];
1274
1275 $this->initializeManager( true );
1276 $this->assertSame( $expect, $this->manager->userExists( 'UTSysop' ) );
1277 }
1278
1279 public static function provideUserExists() {
1280 return [
1281 [ false, false, false ],
1282 [ true, false, true ],
1283 [ false, true, true ],
1284 [ true, true, true ],
1285 ];
1286 }
1287
1288 /**
1289 * @dataProvider provideAllowsAuthenticationDataChange
1290 * @param StatusValue $primaryReturn
1291 * @param StatusValue $secondaryReturn
1292 * @param Status $expect
1293 */
1294 public function testAllowsAuthenticationDataChange( $primaryReturn, $secondaryReturn, $expect ) {
1295 $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
1296
1297 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1298 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '1' ) );
1299 $mock1->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
1300 ->with( $this->equalTo( $req ) )
1301 ->will( $this->returnValue( $primaryReturn ) );
1302 $mock2 = $this->getMockForAbstractClass( SecondaryAuthenticationProvider::class );
1303 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '2' ) );
1304 $mock2->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
1305 ->with( $this->equalTo( $req ) )
1306 ->will( $this->returnValue( $secondaryReturn ) );
1307
1308 $this->primaryauthMocks = [ $mock1 ];
1309 $this->secondaryauthMocks = [ $mock2 ];
1310 $this->initializeManager( true );
1311 $this->assertEquals( $expect, $this->manager->allowsAuthenticationDataChange( $req ) );
1312 }
1313
1314 public static function provideAllowsAuthenticationDataChange() {
1315 $ignored = \Status::newGood( 'ignored' );
1316 $ignored->warning( 'authmanager-change-not-supported' );
1317
1318 $okFromPrimary = StatusValue::newGood();
1319 $okFromPrimary->warning( 'warning-from-primary' );
1320 $okFromSecondary = StatusValue::newGood();
1321 $okFromSecondary->warning( 'warning-from-secondary' );
1322
1323 return [
1324 [
1325 StatusValue::newGood(),
1326 StatusValue::newGood(),
1327 \Status::newGood(),
1328 ],
1329 [
1330 StatusValue::newGood(),
1331 StatusValue::newGood( 'ignore' ),
1332 \Status::newGood(),
1333 ],
1334 [
1335 StatusValue::newGood( 'ignored' ),
1336 StatusValue::newGood(),
1337 \Status::newGood(),
1338 ],
1339 [
1340 StatusValue::newGood( 'ignored' ),
1341 StatusValue::newGood( 'ignored' ),
1342 $ignored,
1343 ],
1344 [
1345 StatusValue::newFatal( 'fail from primary' ),
1346 StatusValue::newGood(),
1347 \Status::newFatal( 'fail from primary' ),
1348 ],
1349 [
1350 $okFromPrimary,
1351 StatusValue::newGood(),
1352 \Status::wrap( $okFromPrimary ),
1353 ],
1354 [
1355 StatusValue::newGood(),
1356 StatusValue::newFatal( 'fail from secondary' ),
1357 \Status::newFatal( 'fail from secondary' ),
1358 ],
1359 [
1360 StatusValue::newGood(),
1361 $okFromSecondary,
1362 \Status::wrap( $okFromSecondary ),
1363 ],
1364 ];
1365 }
1366
1367 public function testChangeAuthenticationData() {
1368 $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
1369 $req->username = 'UTSysop';
1370
1371 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1372 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '1' ) );
1373 $mock1->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
1374 ->with( $this->equalTo( $req ) );
1375 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1376 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '2' ) );
1377 $mock2->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
1378 ->with( $this->equalTo( $req ) );
1379
1380 $this->primaryauthMocks = [ $mock1, $mock2 ];
1381 $this->initializeManager( true );
1382 $this->logger->setCollect( true );
1383 $this->manager->changeAuthenticationData( $req );
1384 $this->assertSame( [
1385 [ LogLevel::INFO, 'Changing authentication data for {user} class {what}' ],
1386 ], $this->logger->getBuffer() );
1387 }
1388
1389 public function testCanCreateAccounts() {
1390 $types = [
1391 PrimaryAuthenticationProvider::TYPE_CREATE => true,
1392 PrimaryAuthenticationProvider::TYPE_LINK => true,
1393 PrimaryAuthenticationProvider::TYPE_NONE => false,
1394 ];
1395
1396 foreach ( $types as $type => $can ) {
1397 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1398 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $type ) );
1399 $mock->expects( $this->any() )->method( 'accountCreationType' )
1400 ->will( $this->returnValue( $type ) );
1401 $this->primaryauthMocks = [ $mock ];
1402 $this->initializeManager( true );
1403 $this->assertSame( $can, $this->manager->canCreateAccounts(), $type );
1404 }
1405 }
1406
1407 public function testCheckAccountCreatePermissions() {
1408 global $wgGroupPermissions;
1409
1410 $this->stashMwGlobals( [ 'wgGroupPermissions' ] );
1411
1412 $this->initializeManager( true );
1413
1414 $wgGroupPermissions['*']['createaccount'] = true;
1415 $this->assertEquals(
1416 \Status::newGood(),
1417 $this->manager->checkAccountCreatePermissions( new \User )
1418 );
1419
1420 $this->setMwGlobals( [ 'wgReadOnly' => 'Because' ] );
1421 $this->assertEquals(
1422 \Status::newFatal( 'readonlytext', 'Because' ),
1423 $this->manager->checkAccountCreatePermissions( new \User )
1424 );
1425 $this->setMwGlobals( [ 'wgReadOnly' => false ] );
1426
1427 $wgGroupPermissions['*']['createaccount'] = false;
1428 $status = $this->manager->checkAccountCreatePermissions( new \User );
1429 $this->assertFalse( $status->isOK() );
1430 $this->assertTrue( $status->hasMessage( 'badaccess-groups' ) );
1431 $wgGroupPermissions['*']['createaccount'] = true;
1432
1433 $user = \User::newFromName( 'UTBlockee' );
1434 if ( $user->getID() == 0 ) {
1435 $user->addToDatabase();
1436 \TestUser::setPasswordForUser( $user, 'UTBlockeePassword' );
1437 $user->saveSettings();
1438 }
1439 $oldBlock = \Block::newFromTarget( 'UTBlockee' );
1440 if ( $oldBlock ) {
1441 // An old block will prevent our new one from saving.
1442 $oldBlock->delete();
1443 }
1444 $blockOptions = [
1445 'address' => 'UTBlockee',
1446 'user' => $user->getID(),
1447 'reason' => __METHOD__,
1448 'expiry' => time() + 100500,
1449 'createAccount' => true,
1450 ];
1451 $block = new \Block( $blockOptions );
1452 $block->insert();
1453 $status = $this->manager->checkAccountCreatePermissions( $user );
1454 $this->assertFalse( $status->isOK() );
1455 $this->assertTrue( $status->hasMessage( 'cantcreateaccount-text' ) );
1456
1457 $blockOptions = [
1458 'address' => '127.0.0.0/24',
1459 'reason' => __METHOD__,
1460 'expiry' => time() + 100500,
1461 'createAccount' => true,
1462 ];
1463 $block = new \Block( $blockOptions );
1464 $block->insert();
1465 $scopeVariable = new \ScopedCallback( [ $block, 'delete' ] );
1466 $status = $this->manager->checkAccountCreatePermissions( new \User );
1467 $this->assertFalse( $status->isOK() );
1468 $this->assertTrue( $status->hasMessage( 'cantcreateaccount-range-text' ) );
1469 \ScopedCallback::consume( $scopeVariable );
1470
1471 $this->setMwGlobals( [
1472 'wgEnableDnsBlacklist' => true,
1473 'wgDnsBlacklistUrls' => [
1474 'local.wmftest.net', // This will resolve for every subdomain, which works to test "listed?"
1475 ],
1476 'wgProxyWhitelist' => [],
1477 ] );
1478 $status = $this->manager->checkAccountCreatePermissions( new \User );
1479 $this->assertFalse( $status->isOK() );
1480 $this->assertTrue( $status->hasMessage( 'sorbs_create_account_reason' ) );
1481 $this->setMwGlobals( 'wgProxyWhitelist', [ '127.0.0.1' ] );
1482 $status = $this->manager->checkAccountCreatePermissions( new \User );
1483 $this->assertTrue( $status->isGood() );
1484 }
1485
1486 /**
1487 * @param string $uniq
1488 * @return string
1489 */
1490 private static function usernameForCreation( $uniq = '' ) {
1491 $i = 0;
1492 do {
1493 $username = "UTAuthManagerTestAccountCreation" . $uniq . ++$i;
1494 } while ( \User::newFromName( $username )->getId() !== 0 );
1495 return $username;
1496 }
1497
1498 public function testCanCreateAccount() {
1499 $username = self::usernameForCreation();
1500 $this->initializeManager();
1501
1502 $this->assertEquals(
1503 \Status::newFatal( 'authmanager-create-disabled' ),
1504 $this->manager->canCreateAccount( $username )
1505 );
1506
1507 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1508 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1509 $mock->expects( $this->any() )->method( 'accountCreationType' )
1510 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1511 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
1512 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1513 ->will( $this->returnValue( StatusValue::newGood() ) );
1514 $this->primaryauthMocks = [ $mock ];
1515 $this->initializeManager( true );
1516
1517 $this->assertEquals(
1518 \Status::newFatal( 'userexists' ),
1519 $this->manager->canCreateAccount( $username )
1520 );
1521
1522 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1523 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1524 $mock->expects( $this->any() )->method( 'accountCreationType' )
1525 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1526 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1527 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1528 ->will( $this->returnValue( StatusValue::newGood() ) );
1529 $this->primaryauthMocks = [ $mock ];
1530 $this->initializeManager( true );
1531
1532 $this->assertEquals(
1533 \Status::newFatal( 'noname' ),
1534 $this->manager->canCreateAccount( $username . '<>' )
1535 );
1536
1537 $this->assertEquals(
1538 \Status::newFatal( 'userexists' ),
1539 $this->manager->canCreateAccount( 'UTSysop' )
1540 );
1541
1542 $this->assertEquals(
1543 \Status::newGood(),
1544 $this->manager->canCreateAccount( $username )
1545 );
1546
1547 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1548 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1549 $mock->expects( $this->any() )->method( 'accountCreationType' )
1550 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1551 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1552 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1553 ->will( $this->returnValue( StatusValue::newFatal( 'fail' ) ) );
1554 $this->primaryauthMocks = [ $mock ];
1555 $this->initializeManager( true );
1556
1557 $this->assertEquals(
1558 \Status::newFatal( 'fail' ),
1559 $this->manager->canCreateAccount( $username )
1560 );
1561 }
1562
1563 public function testBeginAccountCreation() {
1564 $creator = \User::newFromName( 'UTSysop' );
1565 $userReq = new UsernameAuthenticationRequest;
1566 $this->logger = new \TestLogger( false, function ( $message, $level ) {
1567 return $level === LogLevel::DEBUG ? null : $message;
1568 } );
1569 $this->initializeManager();
1570
1571 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
1572 $this->hook( 'LocalUserCreated', $this->never() );
1573 try {
1574 $this->manager->beginAccountCreation(
1575 $creator, [], 'http://localhost/'
1576 );
1577 $this->fail( 'Expected exception not thrown' );
1578 } catch ( \LogicException $ex ) {
1579 $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
1580 }
1581 $this->unhook( 'LocalUserCreated' );
1582 $this->assertNull(
1583 $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1584 );
1585
1586 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1587 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1588 $mock->expects( $this->any() )->method( 'accountCreationType' )
1589 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1590 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
1591 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1592 ->will( $this->returnValue( StatusValue::newGood() ) );
1593 $this->primaryauthMocks = [ $mock ];
1594 $this->initializeManager( true );
1595
1596 $this->hook( 'LocalUserCreated', $this->never() );
1597 $ret = $this->manager->beginAccountCreation( $creator, [], 'http://localhost/' );
1598 $this->unhook( 'LocalUserCreated' );
1599 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1600 $this->assertSame( 'noname', $ret->message->getKey() );
1601
1602 $this->hook( 'LocalUserCreated', $this->never() );
1603 $userReq->username = self::usernameForCreation();
1604 $userReq2 = new UsernameAuthenticationRequest;
1605 $userReq2->username = $userReq->username . 'X';
1606 $ret = $this->manager->beginAccountCreation(
1607 $creator, [ $userReq, $userReq2 ], 'http://localhost/'
1608 );
1609 $this->unhook( 'LocalUserCreated' );
1610 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1611 $this->assertSame( 'noname', $ret->message->getKey() );
1612
1613 $this->setMwGlobals( [ 'wgReadOnly' => 'Because' ] );
1614 $this->hook( 'LocalUserCreated', $this->never() );
1615 $userReq->username = self::usernameForCreation();
1616 $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1617 $this->unhook( 'LocalUserCreated' );
1618 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1619 $this->assertSame( 'readonlytext', $ret->message->getKey() );
1620 $this->assertSame( [ 'Because' ], $ret->message->getParams() );
1621 $this->setMwGlobals( [ 'wgReadOnly' => false ] );
1622
1623 $this->hook( 'LocalUserCreated', $this->never() );
1624 $userReq->username = self::usernameForCreation();
1625 $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1626 $this->unhook( 'LocalUserCreated' );
1627 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1628 $this->assertSame( 'userexists', $ret->message->getKey() );
1629
1630 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1631 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1632 $mock->expects( $this->any() )->method( 'accountCreationType' )
1633 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1634 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1635 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1636 ->will( $this->returnValue( StatusValue::newFatal( 'fail' ) ) );
1637 $this->primaryauthMocks = [ $mock ];
1638 $this->initializeManager( true );
1639
1640 $this->hook( 'LocalUserCreated', $this->never() );
1641 $userReq->username = self::usernameForCreation();
1642 $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1643 $this->unhook( 'LocalUserCreated' );
1644 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1645 $this->assertSame( 'fail', $ret->message->getKey() );
1646
1647 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1648 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1649 $mock->expects( $this->any() )->method( 'accountCreationType' )
1650 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1651 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1652 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1653 ->will( $this->returnValue( StatusValue::newGood() ) );
1654 $this->primaryauthMocks = [ $mock ];
1655 $this->initializeManager( true );
1656
1657 $this->hook( 'LocalUserCreated', $this->never() );
1658 $userReq->username = self::usernameForCreation() . '<>';
1659 $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1660 $this->unhook( 'LocalUserCreated' );
1661 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1662 $this->assertSame( 'noname', $ret->message->getKey() );
1663
1664 $this->hook( 'LocalUserCreated', $this->never() );
1665 $userReq->username = $creator->getName();
1666 $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1667 $this->unhook( 'LocalUserCreated' );
1668 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1669 $this->assertSame( 'userexists', $ret->message->getKey() );
1670
1671 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1672 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1673 $mock->expects( $this->any() )->method( 'accountCreationType' )
1674 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1675 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1676 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1677 ->will( $this->returnValue( StatusValue::newGood() ) );
1678 $mock->expects( $this->any() )->method( 'testForAccountCreation' )
1679 ->will( $this->returnValue( StatusValue::newFatal( 'fail' ) ) );
1680 $this->primaryauthMocks = [ $mock ];
1681 $this->initializeManager( true );
1682
1683 $req = $this->getMockBuilder( UserDataAuthenticationRequest::class )
1684 ->setMethods( [ 'populateUser' ] )
1685 ->getMock();
1686 $req->expects( $this->any() )->method( 'populateUser' )
1687 ->willReturn( \StatusValue::newFatal( 'populatefail' ) );
1688 $userReq->username = self::usernameForCreation();
1689 $ret = $this->manager->beginAccountCreation(
1690 $creator, [ $userReq, $req ], 'http://localhost/'
1691 );
1692 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1693 $this->assertSame( 'populatefail', $ret->message->getKey() );
1694
1695 $req = new UserDataAuthenticationRequest;
1696 $userReq->username = self::usernameForCreation();
1697
1698 $ret = $this->manager->beginAccountCreation(
1699 $creator, [ $userReq, $req ], 'http://localhost/'
1700 );
1701 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1702 $this->assertSame( 'fail', $ret->message->getKey() );
1703
1704 $this->manager->beginAccountCreation(
1705 \User::newFromName( $userReq->username ), [ $userReq, $req ], 'http://localhost/'
1706 );
1707 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1708 $this->assertSame( 'fail', $ret->message->getKey() );
1709 }
1710
1711 public function testContinueAccountCreation() {
1712 $creator = \User::newFromName( 'UTSysop' );
1713 $username = self::usernameForCreation();
1714 $this->logger = new \TestLogger( false, function ( $message, $level ) {
1715 return $level === LogLevel::DEBUG ? null : $message;
1716 } );
1717 $this->initializeManager();
1718
1719 $session = [
1720 'userid' => 0,
1721 'username' => $username,
1722 'creatorid' => 0,
1723 'creatorname' => $username,
1724 'reqs' => [],
1725 'primary' => null,
1726 'primaryResponse' => null,
1727 'secondary' => [],
1728 'ranPreTests' => true,
1729 ];
1730
1731 $this->hook( 'LocalUserCreated', $this->never() );
1732 try {
1733 $this->manager->continueAccountCreation( [] );
1734 $this->fail( 'Expected exception not thrown' );
1735 } catch ( \LogicException $ex ) {
1736 $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
1737 }
1738 $this->unhook( 'LocalUserCreated' );
1739
1740 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1741 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1742 $mock->expects( $this->any() )->method( 'accountCreationType' )
1743 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1744 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1745 $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )->will(
1746 $this->returnValue( AuthenticationResponse::newFail( $this->message( 'fail' ) ) )
1747 );
1748 $this->primaryauthMocks = [ $mock ];
1749 $this->initializeManager( true );
1750
1751 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', null );
1752 $this->hook( 'LocalUserCreated', $this->never() );
1753 $ret = $this->manager->continueAccountCreation( [] );
1754 $this->unhook( 'LocalUserCreated' );
1755 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1756 $this->assertSame( 'authmanager-create-not-in-progress', $ret->message->getKey() );
1757
1758 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1759 [ 'username' => "$username<>" ] + $session );
1760 $this->hook( 'LocalUserCreated', $this->never() );
1761 $ret = $this->manager->continueAccountCreation( [] );
1762 $this->unhook( 'LocalUserCreated' );
1763 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1764 $this->assertSame( 'noname', $ret->message->getKey() );
1765 $this->assertNull(
1766 $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1767 );
1768
1769 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', $session );
1770 $this->hook( 'LocalUserCreated', $this->never() );
1771 $cache = \ObjectCache::getLocalClusterInstance();
1772 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1773 $ret = $this->manager->continueAccountCreation( [] );
1774 unset( $lock );
1775 $this->unhook( 'LocalUserCreated' );
1776 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1777 $this->assertSame( 'usernameinprogress', $ret->message->getKey() );
1778 // This error shouldn't remove the existing session, because the
1779 // raced-with process "owns" it.
1780 $this->assertSame(
1781 $session, $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1782 );
1783
1784 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1785 [ 'username' => $creator->getName() ] + $session );
1786 $this->setMwGlobals( [ 'wgReadOnly' => 'Because' ] );
1787 $this->hook( 'LocalUserCreated', $this->never() );
1788 $ret = $this->manager->continueAccountCreation( [] );
1789 $this->unhook( 'LocalUserCreated' );
1790 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1791 $this->assertSame( 'readonlytext', $ret->message->getKey() );
1792 $this->assertSame( [ 'Because' ], $ret->message->getParams() );
1793 $this->setMwGlobals( [ 'wgReadOnly' => false ] );
1794
1795 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1796 [ 'username' => $creator->getName() ] + $session );
1797 $this->hook( 'LocalUserCreated', $this->never() );
1798 $ret = $this->manager->continueAccountCreation( [] );
1799 $this->unhook( 'LocalUserCreated' );
1800 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1801 $this->assertSame( 'userexists', $ret->message->getKey() );
1802 $this->assertNull(
1803 $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1804 );
1805
1806 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1807 [ 'userid' => $creator->getId() ] + $session );
1808 $this->hook( 'LocalUserCreated', $this->never() );
1809 try {
1810 $ret = $this->manager->continueAccountCreation( [] );
1811 $this->fail( 'Expected exception not thrown' );
1812 } catch ( \UnexpectedValueException $ex ) {
1813 $this->assertEquals( "User \"{$username}\" should exist now, but doesn't!", $ex->getMessage() );
1814 }
1815 $this->unhook( 'LocalUserCreated' );
1816 $this->assertNull(
1817 $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1818 );
1819
1820 $id = $creator->getId();
1821 $name = $creator->getName();
1822 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1823 [ 'username' => $name, 'userid' => $id + 1 ] + $session );
1824 $this->hook( 'LocalUserCreated', $this->never() );
1825 try {
1826 $ret = $this->manager->continueAccountCreation( [] );
1827 $this->fail( 'Expected exception not thrown' );
1828 } catch ( \UnexpectedValueException $ex ) {
1829 $this->assertEquals(
1830 "User \"{$name}\" exists, but ID $id != " . ( $id + 1 ) . '!', $ex->getMessage()
1831 );
1832 }
1833 $this->unhook( 'LocalUserCreated' );
1834 $this->assertNull(
1835 $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1836 );
1837
1838 $req = $this->getMockBuilder( UserDataAuthenticationRequest::class )
1839 ->setMethods( [ 'populateUser' ] )
1840 ->getMock();
1841 $req->expects( $this->any() )->method( 'populateUser' )
1842 ->willReturn( \StatusValue::newFatal( 'populatefail' ) );
1843 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1844 [ 'reqs' => [ $req ] ] + $session );
1845 $ret = $this->manager->continueAccountCreation( [] );
1846 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1847 $this->assertSame( 'populatefail', $ret->message->getKey() );
1848 $this->assertNull(
1849 $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1850 );
1851 }
1852
1853 /**
1854 * @dataProvider provideAccountCreation
1855 * @param StatusValue $preTest
1856 * @param StatusValue $primaryTest
1857 * @param StatusValue $secondaryTest
1858 * @param array $primaryResponses
1859 * @param array $secondaryResponses
1860 * @param array $managerResponses
1861 */
1862 public function testAccountCreation(
1863 StatusValue $preTest, $primaryTest, $secondaryTest,
1864 array $primaryResponses, array $secondaryResponses, array $managerResponses
1865 ) {
1866 $creator = \User::newFromName( 'UTSysop' );
1867 $username = self::usernameForCreation();
1868
1869 $this->initializeManager();
1870
1871 // Set up lots of mocks...
1872 $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
1873 $req->preTest = $preTest;
1874 $req->primaryTest = $primaryTest;
1875 $req->secondaryTest = $secondaryTest;
1876 $req->primary = $primaryResponses;
1877 $req->secondary = $secondaryResponses;
1878 $mocks = [];
1879 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
1880 $class = ucfirst( $key ) . 'AuthenticationProvider';
1881 $mocks[$key] = $this->getMockForAbstractClass(
1882 "MediaWiki\\Auth\\$class", [], "Mock$class"
1883 );
1884 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
1885 ->will( $this->returnValue( $key ) );
1886 $mocks[$key]->expects( $this->any() )->method( 'testUserForCreation' )
1887 ->will( $this->returnValue( StatusValue::newGood() ) );
1888 $mocks[$key]->expects( $this->any() )->method( 'testForAccountCreation' )
1889 ->will( $this->returnCallback(
1890 function ( $user, $creatorIn, $reqs )
1891 use ( $username, $creator, $req, $key )
1892 {
1893 $this->assertSame( $username, $user->getName() );
1894 $this->assertSame( $creator->getId(), $creatorIn->getId() );
1895 $this->assertSame( $creator->getName(), $creatorIn->getName() );
1896 $foundReq = false;
1897 foreach ( $reqs as $r ) {
1898 $this->assertSame( $username, $r->username );
1899 $foundReq = $foundReq || get_class( $r ) === get_class( $req );
1900 }
1901 $this->assertTrue( $foundReq, '$reqs contains $req' );
1902 $k = $key . 'Test';
1903 return $req->$k;
1904 }
1905 ) );
1906
1907 for ( $i = 2; $i <= 3; $i++ ) {
1908 $mocks[$key . $i] = $this->getMockForAbstractClass(
1909 "MediaWiki\\Auth\\$class", [], "Mock$class"
1910 );
1911 $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
1912 ->will( $this->returnValue( $key . $i ) );
1913 $mocks[$key . $i]->expects( $this->any() )->method( 'testUserForCreation' )
1914 ->will( $this->returnValue( StatusValue::newGood() ) );
1915 $mocks[$key . $i]->expects( $this->atMost( 1 ) )->method( 'testForAccountCreation' )
1916 ->will( $this->returnValue( StatusValue::newGood() ) );
1917 }
1918 }
1919
1920 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
1921 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1922 $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
1923 ->will( $this->returnValue( false ) );
1924 $ct = count( $req->primary );
1925 $callback = $this->returnCallback( function ( $user, $creator, $reqs ) use ( $username, $req ) {
1926 $this->assertSame( $username, $user->getName() );
1927 $this->assertSame( 'UTSysop', $creator->getName() );
1928 $foundReq = false;
1929 foreach ( $reqs as $r ) {
1930 $this->assertSame( $username, $r->username );
1931 $foundReq = $foundReq || get_class( $r ) === get_class( $req );
1932 }
1933 $this->assertTrue( $foundReq, '$reqs contains $req' );
1934 return array_shift( $req->primary );
1935 } );
1936 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
1937 ->method( 'beginPrimaryAccountCreation' )
1938 ->will( $callback );
1939 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
1940 ->method( 'continuePrimaryAccountCreation' )
1941 ->will( $callback );
1942
1943 $ct = count( $req->secondary );
1944 $callback = $this->returnCallback( function ( $user, $creator, $reqs ) use ( $username, $req ) {
1945 $this->assertSame( $username, $user->getName() );
1946 $this->assertSame( 'UTSysop', $creator->getName() );
1947 $foundReq = false;
1948 foreach ( $reqs as $r ) {
1949 $this->assertSame( $username, $r->username );
1950 $foundReq = $foundReq || get_class( $r ) === get_class( $req );
1951 }
1952 $this->assertTrue( $foundReq, '$reqs contains $req' );
1953 return array_shift( $req->secondary );
1954 } );
1955 $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
1956 ->method( 'beginSecondaryAccountCreation' )
1957 ->will( $callback );
1958 $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
1959 ->method( 'continueSecondaryAccountCreation' )
1960 ->will( $callback );
1961
1962 $abstain = AuthenticationResponse::newAbstain();
1963 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
1964 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
1965 $mocks['primary2']->expects( $this->any() )->method( 'testUserExists' )
1966 ->will( $this->returnValue( false ) );
1967 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountCreation' )
1968 ->will( $this->returnValue( $abstain ) );
1969 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
1970 $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
1971 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_NONE ) );
1972 $mocks['primary3']->expects( $this->any() )->method( 'testUserExists' )
1973 ->will( $this->returnValue( false ) );
1974 $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountCreation' );
1975 $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
1976 $mocks['secondary2']->expects( $this->atMost( 1 ) )
1977 ->method( 'beginSecondaryAccountCreation' )
1978 ->will( $this->returnValue( $abstain ) );
1979 $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
1980 $mocks['secondary3']->expects( $this->atMost( 1 ) )
1981 ->method( 'beginSecondaryAccountCreation' )
1982 ->will( $this->returnValue( $abstain ) );
1983 $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
1984
1985 $this->preauthMocks = [ $mocks['pre'], $mocks['pre2'] ];
1986 $this->primaryauthMocks = [ $mocks['primary3'], $mocks['primary'], $mocks['primary2'] ];
1987 $this->secondaryauthMocks = [
1988 $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2']
1989 ];
1990
1991 $this->logger = new \TestLogger( true, function ( $message, $level ) {
1992 return $level === LogLevel::DEBUG ? null : $message;
1993 } );
1994 $expectLog = [];
1995 $this->initializeManager( true );
1996
1997 $constraint = \PHPUnit_Framework_Assert::logicalOr(
1998 $this->equalTo( AuthenticationResponse::PASS ),
1999 $this->equalTo( AuthenticationResponse::FAIL )
2000 );
2001 $providers = array_merge(
2002 $this->preauthMocks, $this->primaryauthMocks, $this->secondaryauthMocks
2003 );
2004 foreach ( $providers as $p ) {
2005 $p->postCalled = false;
2006 $p->expects( $this->atMost( 1 ) )->method( 'postAccountCreation' )
2007 ->willReturnCallback( function ( $user, $creator, $response )
2008 use ( $constraint, $p, $username )
2009 {
2010 $this->assertInstanceOf( 'User', $user );
2011 $this->assertSame( $username, $user->getName() );
2012 $this->assertSame( 'UTSysop', $creator->getName() );
2013 $this->assertInstanceOf( AuthenticationResponse::class, $response );
2014 $this->assertThat( $response->status, $constraint );
2015 $p->postCalled = $response->status;
2016 } );
2017 }
2018
2019 // We're testing with $wgNewUserLog = false, so assert that it worked
2020 $dbw = wfGetDB( DB_MASTER );
2021 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2022
2023 $first = true;
2024 $created = false;
2025 foreach ( $managerResponses as $i => $response ) {
2026 $success = $response instanceof AuthenticationResponse &&
2027 $response->status === AuthenticationResponse::PASS;
2028 if ( $i === 'created' ) {
2029 $created = true;
2030 $this->hook( 'LocalUserCreated', $this->once() )
2031 ->with(
2032 $this->callback( function ( $user ) use ( $username ) {
2033 return $user->getName() === $username;
2034 } ),
2035 $this->equalTo( false )
2036 );
2037 $expectLog[] = [ LogLevel::INFO, "Creating user {user} during account creation" ];
2038 } else {
2039 $this->hook( 'LocalUserCreated', $this->never() );
2040 }
2041
2042 $ex = null;
2043 try {
2044 if ( $first ) {
2045 $userReq = new UsernameAuthenticationRequest;
2046 $userReq->username = $username;
2047 $ret = $this->manager->beginAccountCreation(
2048 $creator, [ $userReq, $req ], 'http://localhost/'
2049 );
2050 } else {
2051 $ret = $this->manager->continueAccountCreation( [ $req ] );
2052 }
2053 if ( $response instanceof \Exception ) {
2054 $this->fail( 'Expected exception not thrown', "Response $i" );
2055 }
2056 } catch ( \Exception $ex ) {
2057 if ( !$response instanceof \Exception ) {
2058 throw $ex;
2059 }
2060 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
2061 $this->assertNull(
2062 $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2063 "Response $i, exception, session state"
2064 );
2065 $this->unhook( 'LocalUserCreated' );
2066 return;
2067 }
2068
2069 $this->unhook( 'LocalUserCreated' );
2070
2071 $this->assertSame( 'http://localhost/', $req->returnToUrl );
2072
2073 if ( $success ) {
2074 $this->assertNotNull( $ret->loginRequest, "Response $i, login marker" );
2075 $this->assertContains(
2076 $ret->loginRequest, $this->managerPriv->createdAccountAuthenticationRequests,
2077 "Response $i, login marker"
2078 );
2079
2080 $expectLog[] = [
2081 LogLevel::INFO,
2082 "MediaWiki\Auth\AuthManager::continueAccountCreation: Account creation succeeded for {user}"
2083 ];
2084
2085 // Set some fields in the expected $response that we couldn't
2086 // know in provideAccountCreation().
2087 $response->username = $username;
2088 $response->loginRequest = $ret->loginRequest;
2089 } else {
2090 $this->assertNull( $ret->loginRequest, "Response $i, login marker" );
2091 $this->assertSame( [], $this->managerPriv->createdAccountAuthenticationRequests,
2092 "Response $i, login marker" );
2093 }
2094 $ret->message = $this->message( $ret->message );
2095 $this->assertEquals( $response, $ret, "Response $i, response" );
2096 if ( $success || $response->status === AuthenticationResponse::FAIL ) {
2097 $this->assertNull(
2098 $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2099 "Response $i, session state"
2100 );
2101 foreach ( $providers as $p ) {
2102 $this->assertSame( $response->status, $p->postCalled,
2103 "Response $i, post-auth callback called" );
2104 }
2105 } else {
2106 $this->assertNotNull(
2107 $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2108 "Response $i, session state"
2109 );
2110 foreach ( $ret->neededRequests as $neededReq ) {
2111 $this->assertEquals( AuthManager::ACTION_CREATE, $neededReq->action,
2112 "Response $i, neededRequest action" );
2113 }
2114 $this->assertEquals(
2115 $ret->neededRequests,
2116 $this->manager->getAuthenticationRequests( AuthManager::ACTION_CREATE_CONTINUE ),
2117 "Response $i, continuation check"
2118 );
2119 foreach ( $providers as $p ) {
2120 $this->assertFalse( $p->postCalled, "Response $i, post-auth callback not called" );
2121 }
2122 }
2123
2124 if ( $created ) {
2125 $this->assertNotEquals( 0, \User::idFromName( $username ) );
2126 } else {
2127 $this->assertEquals( 0, \User::idFromName( $username ) );
2128 }
2129
2130 $first = false;
2131 }
2132
2133 $this->assertSame( $expectLog, $this->logger->getBuffer() );
2134
2135 $this->assertSame(
2136 $maxLogId,
2137 $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
2138 );
2139 }
2140
2141 public function provideAccountCreation() {
2142 $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
2143 $good = StatusValue::newGood();
2144
2145 return [
2146 'Pre-creation test fail in pre' => [
2147 StatusValue::newFatal( 'fail-from-pre' ), $good, $good,
2148 [],
2149 [],
2150 [
2151 AuthenticationResponse::newFail( $this->message( 'fail-from-pre' ) ),
2152 ]
2153 ],
2154 'Pre-creation test fail in primary' => [
2155 $good, StatusValue::newFatal( 'fail-from-primary' ), $good,
2156 [],
2157 [],
2158 [
2159 AuthenticationResponse::newFail( $this->message( 'fail-from-primary' ) ),
2160 ]
2161 ],
2162 'Pre-creation test fail in secondary' => [
2163 $good, $good, StatusValue::newFatal( 'fail-from-secondary' ),
2164 [],
2165 [],
2166 [
2167 AuthenticationResponse::newFail( $this->message( 'fail-from-secondary' ) ),
2168 ]
2169 ],
2170 'Failure in primary' => [
2171 $good, $good, $good,
2172 $tmp = [
2173 AuthenticationResponse::newFail( $this->message( 'fail-from-primary' ) ),
2174 ],
2175 [],
2176 $tmp
2177 ],
2178 'All primary abstain' => [
2179 $good, $good, $good,
2180 [
2181 AuthenticationResponse::newAbstain(),
2182 ],
2183 [],
2184 [
2185 AuthenticationResponse::newFail( $this->message( 'authmanager-create-no-primary' ) )
2186 ]
2187 ],
2188 'Primary UI, then redirect, then fail' => [
2189 $good, $good, $good,
2190 $tmp = [
2191 AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
2192 AuthenticationResponse::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
2193 AuthenticationResponse::newFail( $this->message( 'fail-in-primary-continue' ) ),
2194 ],
2195 [],
2196 $tmp
2197 ],
2198 'Primary redirect, then abstain' => [
2199 $good, $good, $good,
2200 [
2201 $tmp = AuthenticationResponse::newRedirect(
2202 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
2203 ),
2204 AuthenticationResponse::newAbstain(),
2205 ],
2206 [],
2207 [
2208 $tmp,
2209 new \DomainException(
2210 'MockPrimaryAuthenticationProvider::continuePrimaryAccountCreation() returned ABSTAIN'
2211 )
2212 ]
2213 ],
2214 'Primary UI, then pass; secondary abstain' => [
2215 $good, $good, $good,
2216 [
2217 $tmp1 = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
2218 AuthenticationResponse::newPass(),
2219 ],
2220 [
2221 AuthenticationResponse::newAbstain(),
2222 ],
2223 [
2224 $tmp1,
2225 'created' => AuthenticationResponse::newPass( '' ),
2226 ]
2227 ],
2228 'Primary pass; secondary UI then pass' => [
2229 $good, $good, $good,
2230 [
2231 AuthenticationResponse::newPass( '' ),
2232 ],
2233 [
2234 $tmp1 = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
2235 AuthenticationResponse::newPass( '' ),
2236 ],
2237 [
2238 'created' => $tmp1,
2239 AuthenticationResponse::newPass( '' ),
2240 ]
2241 ],
2242 'Primary pass; secondary fail' => [
2243 $good, $good, $good,
2244 [
2245 AuthenticationResponse::newPass(),
2246 ],
2247 [
2248 AuthenticationResponse::newFail( $this->message( '...' ) ),
2249 ],
2250 [
2251 'created' => new \DomainException(
2252 'MockSecondaryAuthenticationProvider::beginSecondaryAccountCreation() returned FAIL. ' .
2253 'Secondary providers are not allowed to fail account creation, ' .
2254 'that should have been done via testForAccountCreation().'
2255 )
2256 ]
2257 ],
2258 ];
2259 }
2260
2261 /**
2262 * @dataProvider provideAccountCreationLogging
2263 * @param bool $isAnon
2264 * @param string|null $logSubtype
2265 */
2266 public function testAccountCreationLogging( $isAnon, $logSubtype ) {
2267 $creator = $isAnon ? new \User : \User::newFromName( 'UTSysop' );
2268 $username = self::usernameForCreation();
2269
2270 $this->initializeManager();
2271
2272 // Set up lots of mocks...
2273 $mock = $this->getMockForAbstractClass(
2274 "MediaWiki\\Auth\\PrimaryAuthenticationProvider", []
2275 );
2276 $mock->expects( $this->any() )->method( 'getUniqueId' )
2277 ->will( $this->returnValue( 'primary' ) );
2278 $mock->expects( $this->any() )->method( 'testUserForCreation' )
2279 ->will( $this->returnValue( StatusValue::newGood() ) );
2280 $mock->expects( $this->any() )->method( 'testForAccountCreation' )
2281 ->will( $this->returnValue( StatusValue::newGood() ) );
2282 $mock->expects( $this->any() )->method( 'accountCreationType' )
2283 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
2284 $mock->expects( $this->any() )->method( 'testUserExists' )
2285 ->will( $this->returnValue( false ) );
2286 $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )
2287 ->will( $this->returnValue( AuthenticationResponse::newPass( $username ) ) );
2288 $mock->expects( $this->any() )->method( 'finishAccountCreation' )
2289 ->will( $this->returnValue( $logSubtype ) );
2290
2291 $this->primaryauthMocks = [ $mock ];
2292 $this->initializeManager( true );
2293 $this->logger->setCollect( true );
2294
2295 $this->config->set( 'NewUserLog', true );
2296
2297 $dbw = wfGetDB( DB_MASTER );
2298 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2299
2300 $userReq = new UsernameAuthenticationRequest;
2301 $userReq->username = $username;
2302 $reasonReq = new CreationReasonAuthenticationRequest;
2303 $reasonReq->reason = $this->toString();
2304 $ret = $this->manager->beginAccountCreation(
2305 $creator, [ $userReq, $reasonReq ], 'http://localhost/'
2306 );
2307
2308 $this->assertSame( AuthenticationResponse::PASS, $ret->status );
2309
2310 $user = \User::newFromName( $username );
2311 $this->assertNotEquals( 0, $user->getId(), 'sanity check' );
2312 $this->assertNotEquals( $creator->getId(), $user->getId(), 'sanity check' );
2313
2314 $data = \DatabaseLogEntry::getSelectQueryData();
2315 $rows = iterator_to_array( $dbw->select(
2316 $data['tables'],
2317 $data['fields'],
2318 [
2319 'log_id > ' . (int)$maxLogId,
2320 'log_type' => 'newusers'
2321 ] + $data['conds'],
2322 __METHOD__,
2323 $data['options'],
2324 $data['join_conds']
2325 ) );
2326 $this->assertCount( 1, $rows );
2327 $entry = \DatabaseLogEntry::newFromRow( reset( $rows ) );
2328
2329 $this->assertSame( $logSubtype ?: ( $isAnon ? 'create' : 'create2' ), $entry->getSubtype() );
2330 $this->assertSame(
2331 $isAnon ? $user->getId() : $creator->getId(),
2332 $entry->getPerformer()->getId()
2333 );
2334 $this->assertSame(
2335 $isAnon ? $user->getName() : $creator->getName(),
2336 $entry->getPerformer()->getName()
2337 );
2338 $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
2339 $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
2340 $this->assertSame( $this->toString(), $entry->getComment() );
2341 }
2342
2343 public static function provideAccountCreationLogging() {
2344 return [
2345 [ true, null ],
2346 [ true, 'foobar' ],
2347 [ false, null ],
2348 [ false, 'byemail' ],
2349 ];
2350 }
2351
2352 public function testAutoAccountCreation() {
2353 global $wgGroupPermissions, $wgHooks;
2354
2355 // PHPUnit seems to have a bug where it will call the ->with()
2356 // callbacks for our hooks again after the test is run (WTF?), which
2357 // breaks here because $username no longer matches $user by the end of
2358 // the testing.
2359 $workaroundPHPUnitBug = false;
2360
2361 $username = self::usernameForCreation();
2362 $this->initializeManager();
2363
2364 $this->stashMwGlobals( [ 'wgGroupPermissions' ] );
2365 $wgGroupPermissions['*']['createaccount'] = true;
2366 $wgGroupPermissions['*']['autocreateaccount'] = false;
2367
2368 \ObjectCache::$instances[__METHOD__] = new \HashBagOStuff();
2369 $this->setMwGlobals( [ 'wgMainCacheType' => __METHOD__ ] );
2370
2371 // Set up lots of mocks...
2372 $mocks = [];
2373 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
2374 $class = ucfirst( $key ) . 'AuthenticationProvider';
2375 $mocks[$key] = $this->getMockForAbstractClass(
2376 "MediaWiki\\Auth\\$class", [], "Mock$class"
2377 );
2378 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
2379 ->will( $this->returnValue( $key ) );
2380 }
2381
2382 $good = StatusValue::newGood();
2383 $callback = $this->callback( function ( $user ) use ( &$username, &$workaroundPHPUnitBug ) {
2384 return $workaroundPHPUnitBug || $user->getName() === $username;
2385 } );
2386
2387 $mocks['pre']->expects( $this->exactly( 12 ) )->method( 'testUserForCreation' )
2388 ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) )
2389 ->will( $this->onConsecutiveCalls(
2390 StatusValue::newFatal( 'ok' ), StatusValue::newFatal( 'ok' ), // For testing permissions
2391 StatusValue::newFatal( 'fail-in-pre' ), $good, $good,
2392 $good, // backoff test
2393 $good, // addToDatabase fails test
2394 $good, // addToDatabase throws test
2395 $good, // addToDatabase exists test
2396 $good, $good, $good // success
2397 ) );
2398
2399 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
2400 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
2401 $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
2402 ->will( $this->returnValue( true ) );
2403 $mocks['primary']->expects( $this->exactly( 9 ) )->method( 'testUserForCreation' )
2404 ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) )
2405 ->will( $this->onConsecutiveCalls(
2406 StatusValue::newFatal( 'fail-in-primary' ), $good,
2407 $good, // backoff test
2408 $good, // addToDatabase fails test
2409 $good, // addToDatabase throws test
2410 $good, // addToDatabase exists test
2411 $good, $good, $good
2412 ) );
2413 $mocks['primary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
2414 ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) );
2415
2416 $mocks['secondary']->expects( $this->exactly( 8 ) )->method( 'testUserForCreation' )
2417 ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) )
2418 ->will( $this->onConsecutiveCalls(
2419 StatusValue::newFatal( 'fail-in-secondary' ),
2420 $good, // backoff test
2421 $good, // addToDatabase fails test
2422 $good, // addToDatabase throws test
2423 $good, // addToDatabase exists test
2424 $good, $good, $good
2425 ) );
2426 $mocks['secondary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
2427 ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) );
2428
2429 $this->preauthMocks = [ $mocks['pre'] ];
2430 $this->primaryauthMocks = [ $mocks['primary'] ];
2431 $this->secondaryauthMocks = [ $mocks['secondary'] ];
2432 $this->initializeManager( true );
2433 $session = $this->request->getSession();
2434
2435 $logger = new \TestLogger( true, function ( $m ) {
2436 $m = str_replace( 'MediaWiki\\Auth\\AuthManager::autoCreateUser: ', '', $m );
2437 return $m;
2438 } );
2439 $this->manager->setLogger( $logger );
2440
2441 try {
2442 $user = \User::newFromName( 'UTSysop' );
2443 $this->manager->autoCreateUser( $user, 'InvalidSource', true );
2444 $this->fail( 'Expected exception not thrown' );
2445 } catch ( \InvalidArgumentException $ex ) {
2446 $this->assertSame( 'Unknown auto-creation source: InvalidSource', $ex->getMessage() );
2447 }
2448
2449 // First, check an existing user
2450 $session->clear();
2451 $user = \User::newFromName( 'UTSysop' );
2452 $this->hook( 'LocalUserCreated', $this->never() );
2453 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2454 $this->unhook( 'LocalUserCreated' );
2455 $expect = \Status::newGood();
2456 $expect->warning( 'userexists' );
2457 $this->assertEquals( $expect, $ret );
2458 $this->assertNotEquals( 0, $user->getId() );
2459 $this->assertSame( 'UTSysop', $user->getName() );
2460 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2461 $this->assertSame( [
2462 [ LogLevel::DEBUG, '{username} already exists locally' ],
2463 ], $logger->getBuffer() );
2464 $logger->clearBuffer();
2465
2466 $session->clear();
2467 $user = \User::newFromName( 'UTSysop' );
2468 $this->hook( 'LocalUserCreated', $this->never() );
2469 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, false );
2470 $this->unhook( 'LocalUserCreated' );
2471 $expect = \Status::newGood();
2472 $expect->warning( 'userexists' );
2473 $this->assertEquals( $expect, $ret );
2474 $this->assertNotEquals( 0, $user->getId() );
2475 $this->assertSame( 'UTSysop', $user->getName() );
2476 $this->assertEquals( 0, $session->getUser()->getId() );
2477 $this->assertSame( [
2478 [ LogLevel::DEBUG, '{username} already exists locally' ],
2479 ], $logger->getBuffer() );
2480 $logger->clearBuffer();
2481
2482 // Wiki is read-only
2483 $session->clear();
2484 $this->setMwGlobals( [ 'wgReadOnly' => 'Because' ] );
2485 $user = \User::newFromName( $username );
2486 $this->hook( 'LocalUserCreated', $this->never() );
2487 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2488 $this->unhook( 'LocalUserCreated' );
2489 $this->assertEquals( \Status::newFatal( 'readonlytext', 'Because' ), $ret );
2490 $this->assertEquals( 0, $user->getId() );
2491 $this->assertNotEquals( $username, $user->getName() );
2492 $this->assertEquals( 0, $session->getUser()->getId() );
2493 $this->assertSame( [
2494 [ LogLevel::DEBUG, 'denied by wfReadOnly(): {reason}' ],
2495 ], $logger->getBuffer() );
2496 $logger->clearBuffer();
2497 $this->setMwGlobals( [ 'wgReadOnly' => false ] );
2498
2499 // Session blacklisted
2500 $session->clear();
2501 $session->set( 'AuthManager::AutoCreateBlacklist', 'test' );
2502 $user = \User::newFromName( $username );
2503 $this->hook( 'LocalUserCreated', $this->never() );
2504 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2505 $this->unhook( 'LocalUserCreated' );
2506 $this->assertEquals( \Status::newFatal( 'test' ), $ret );
2507 $this->assertEquals( 0, $user->getId() );
2508 $this->assertNotEquals( $username, $user->getName() );
2509 $this->assertEquals( 0, $session->getUser()->getId() );
2510 $this->assertSame( [
2511 [ LogLevel::DEBUG, 'blacklisted in session {sessionid}' ],
2512 ], $logger->getBuffer() );
2513 $logger->clearBuffer();
2514
2515 $session->clear();
2516 $session->set( 'AuthManager::AutoCreateBlacklist', StatusValue::newFatal( 'test2' ) );
2517 $user = \User::newFromName( $username );
2518 $this->hook( 'LocalUserCreated', $this->never() );
2519 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2520 $this->unhook( 'LocalUserCreated' );
2521 $this->assertEquals( \Status::newFatal( 'test2' ), $ret );
2522 $this->assertEquals( 0, $user->getId() );
2523 $this->assertNotEquals( $username, $user->getName() );
2524 $this->assertEquals( 0, $session->getUser()->getId() );
2525 $this->assertSame( [
2526 [ LogLevel::DEBUG, 'blacklisted in session {sessionid}' ],
2527 ], $logger->getBuffer() );
2528 $logger->clearBuffer();
2529
2530 // Uncreatable name
2531 $session->clear();
2532 $user = \User::newFromName( $username . '@' );
2533 $this->hook( 'LocalUserCreated', $this->never() );
2534 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2535 $this->unhook( 'LocalUserCreated' );
2536 $this->assertEquals( \Status::newFatal( 'noname' ), $ret );
2537 $this->assertEquals( 0, $user->getId() );
2538 $this->assertNotEquals( $username . '@', $user->getId() );
2539 $this->assertEquals( 0, $session->getUser()->getId() );
2540 $this->assertSame( [
2541 [ LogLevel::DEBUG, 'name "{username}" is not creatable' ],
2542 ], $logger->getBuffer() );
2543 $logger->clearBuffer();
2544 $this->assertSame( 'noname', $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2545
2546 // IP unable to create accounts
2547 $wgGroupPermissions['*']['createaccount'] = false;
2548 $wgGroupPermissions['*']['autocreateaccount'] = false;
2549 $session->clear();
2550 $user = \User::newFromName( $username );
2551 $this->hook( 'LocalUserCreated', $this->never() );
2552 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2553 $this->unhook( 'LocalUserCreated' );
2554 $this->assertEquals( \Status::newFatal( 'authmanager-autocreate-noperm' ), $ret );
2555 $this->assertEquals( 0, $user->getId() );
2556 $this->assertNotEquals( $username, $user->getName() );
2557 $this->assertEquals( 0, $session->getUser()->getId() );
2558 $this->assertSame( [
2559 [ LogLevel::DEBUG, 'IP lacks the ability to create or autocreate accounts' ],
2560 ], $logger->getBuffer() );
2561 $logger->clearBuffer();
2562 $this->assertSame(
2563 'authmanager-autocreate-noperm', $session->get( 'AuthManager::AutoCreateBlacklist' )
2564 );
2565
2566 // Test that both permutations of permissions are allowed
2567 // (this hits the two "ok" entries in $mocks['pre'])
2568 $wgGroupPermissions['*']['createaccount'] = false;
2569 $wgGroupPermissions['*']['autocreateaccount'] = true;
2570 $session->clear();
2571 $user = \User::newFromName( $username );
2572 $this->hook( 'LocalUserCreated', $this->never() );
2573 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2574 $this->unhook( 'LocalUserCreated' );
2575 $this->assertEquals( \Status::newFatal( 'ok' ), $ret );
2576
2577 $wgGroupPermissions['*']['createaccount'] = true;
2578 $wgGroupPermissions['*']['autocreateaccount'] = false;
2579 $session->clear();
2580 $user = \User::newFromName( $username );
2581 $this->hook( 'LocalUserCreated', $this->never() );
2582 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2583 $this->unhook( 'LocalUserCreated' );
2584 $this->assertEquals( \Status::newFatal( 'ok' ), $ret );
2585 $logger->clearBuffer();
2586
2587 // Test lock fail
2588 $session->clear();
2589 $user = \User::newFromName( $username );
2590 $this->hook( 'LocalUserCreated', $this->never() );
2591 $cache = \ObjectCache::getLocalClusterInstance();
2592 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
2593 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2594 unset( $lock );
2595 $this->unhook( 'LocalUserCreated' );
2596 $this->assertEquals( \Status::newFatal( 'usernameinprogress' ), $ret );
2597 $this->assertEquals( 0, $user->getId() );
2598 $this->assertNotEquals( $username, $user->getName() );
2599 $this->assertEquals( 0, $session->getUser()->getId() );
2600 $this->assertSame( [
2601 [ LogLevel::DEBUG, 'Could not acquire account creation lock' ],
2602 ], $logger->getBuffer() );
2603 $logger->clearBuffer();
2604
2605 // Test pre-authentication provider fail
2606 $session->clear();
2607 $user = \User::newFromName( $username );
2608 $this->hook( 'LocalUserCreated', $this->never() );
2609 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2610 $this->unhook( 'LocalUserCreated' );
2611 $this->assertEquals( \Status::newFatal( 'fail-in-pre' ), $ret );
2612 $this->assertEquals( 0, $user->getId() );
2613 $this->assertNotEquals( $username, $user->getName() );
2614 $this->assertEquals( 0, $session->getUser()->getId() );
2615 $this->assertSame( [
2616 [ LogLevel::DEBUG, 'Provider denied creation of {username}: {reason}' ],
2617 ], $logger->getBuffer() );
2618 $logger->clearBuffer();
2619 $this->assertEquals(
2620 StatusValue::newFatal( 'fail-in-pre' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2621 );
2622
2623 $session->clear();
2624 $user = \User::newFromName( $username );
2625 $this->hook( 'LocalUserCreated', $this->never() );
2626 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2627 $this->unhook( 'LocalUserCreated' );
2628 $this->assertEquals( \Status::newFatal( 'fail-in-primary' ), $ret );
2629 $this->assertEquals( 0, $user->getId() );
2630 $this->assertNotEquals( $username, $user->getName() );
2631 $this->assertEquals( 0, $session->getUser()->getId() );
2632 $this->assertSame( [
2633 [ LogLevel::DEBUG, 'Provider denied creation of {username}: {reason}' ],
2634 ], $logger->getBuffer() );
2635 $logger->clearBuffer();
2636 $this->assertEquals(
2637 StatusValue::newFatal( 'fail-in-primary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2638 );
2639
2640 $session->clear();
2641 $user = \User::newFromName( $username );
2642 $this->hook( 'LocalUserCreated', $this->never() );
2643 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2644 $this->unhook( 'LocalUserCreated' );
2645 $this->assertEquals( \Status::newFatal( 'fail-in-secondary' ), $ret );
2646 $this->assertEquals( 0, $user->getId() );
2647 $this->assertNotEquals( $username, $user->getName() );
2648 $this->assertEquals( 0, $session->getUser()->getId() );
2649 $this->assertSame( [
2650 [ LogLevel::DEBUG, 'Provider denied creation of {username}: {reason}' ],
2651 ], $logger->getBuffer() );
2652 $logger->clearBuffer();
2653 $this->assertEquals(
2654 StatusValue::newFatal( 'fail-in-secondary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2655 );
2656
2657 // Test backoff
2658 $cache = \ObjectCache::getLocalClusterInstance();
2659 $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2660 $cache->set( $backoffKey, true );
2661 $session->clear();
2662 $user = \User::newFromName( $username );
2663 $this->hook( 'LocalUserCreated', $this->never() );
2664 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2665 $this->unhook( 'LocalUserCreated' );
2666 $this->assertEquals( \Status::newFatal( 'authmanager-autocreate-exception' ), $ret );
2667 $this->assertEquals( 0, $user->getId() );
2668 $this->assertNotEquals( $username, $user->getName() );
2669 $this->assertEquals( 0, $session->getUser()->getId() );
2670 $this->assertSame( [
2671 [ LogLevel::DEBUG, '{username} denied by prior creation attempt failures' ],
2672 ], $logger->getBuffer() );
2673 $logger->clearBuffer();
2674 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2675 $cache->delete( $backoffKey );
2676
2677 // Test addToDatabase fails
2678 $session->clear();
2679 $user = $this->getMock( 'User', [ 'addToDatabase' ] );
2680 $user->expects( $this->once() )->method( 'addToDatabase' )
2681 ->will( $this->returnValue( \Status::newFatal( 'because' ) ) );
2682 $user->setName( $username );
2683 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2684 $this->assertEquals( \Status::newFatal( 'because' ), $ret );
2685 $this->assertEquals( 0, $user->getId() );
2686 $this->assertNotEquals( $username, $user->getName() );
2687 $this->assertEquals( 0, $session->getUser()->getId() );
2688 $this->assertSame( [
2689 [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
2690 [ LogLevel::ERROR, '{username} failed with message {message}' ],
2691 ], $logger->getBuffer() );
2692 $logger->clearBuffer();
2693 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2694
2695 // Test addToDatabase throws an exception
2696 $cache = \ObjectCache::getLocalClusterInstance();
2697 $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2698 $this->assertFalse( $cache->get( $backoffKey ), 'sanity check' );
2699 $session->clear();
2700 $user = $this->getMock( 'User', [ 'addToDatabase' ] );
2701 $user->expects( $this->once() )->method( 'addToDatabase' )
2702 ->will( $this->throwException( new \Exception( 'Excepted' ) ) );
2703 $user->setName( $username );
2704 try {
2705 $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2706 $this->fail( 'Expected exception not thrown' );
2707 } catch ( \Exception $ex ) {
2708 $this->assertSame( 'Excepted', $ex->getMessage() );
2709 }
2710 $this->assertEquals( 0, $user->getId() );
2711 $this->assertEquals( 0, $session->getUser()->getId() );
2712 $this->assertSame( [
2713 [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
2714 [ LogLevel::ERROR, '{username} failed with exception {exception}' ],
2715 ], $logger->getBuffer() );
2716 $logger->clearBuffer();
2717 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2718 $this->assertNotEquals( false, $cache->get( $backoffKey ) );
2719 $cache->delete( $backoffKey );
2720
2721 // Test addToDatabase fails because the user already exists.
2722 $session->clear();
2723 $user = $this->getMock( 'User', [ 'addToDatabase' ] );
2724 $user->expects( $this->once() )->method( 'addToDatabase' )
2725 ->will( $this->returnCallback( function () use ( $username ) {
2726 $status = \User::newFromName( $username )->addToDatabase();
2727 $this->assertTrue( $status->isOK(), 'sanity check' );
2728 return \Status::newFatal( 'userexists' );
2729 } ) );
2730 $user->setName( $username );
2731 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2732 $expect = \Status::newGood();
2733 $expect->warning( 'userexists' );
2734 $this->assertEquals( $expect, $ret );
2735 $this->assertNotEquals( 0, $user->getId() );
2736 $this->assertEquals( $username, $user->getName() );
2737 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2738 $this->assertSame( [
2739 [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
2740 [ LogLevel::INFO, '{username} already exists locally (race)' ],
2741 ], $logger->getBuffer() );
2742 $logger->clearBuffer();
2743 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2744
2745 // Success!
2746 $session->clear();
2747 $username = self::usernameForCreation();
2748 $user = \User::newFromName( $username );
2749 $this->hook( 'AuthPluginAutoCreate', $this->once() )
2750 ->with( $callback );
2751 $this->hideDeprecated( 'AuthPluginAutoCreate hook (used in ' .
2752 get_class( $wgHooks['AuthPluginAutoCreate'][0] ) . '::onAuthPluginAutoCreate)' );
2753 $this->hook( 'LocalUserCreated', $this->once() )
2754 ->with( $callback, $this->equalTo( true ) );
2755 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2756 $this->unhook( 'LocalUserCreated' );
2757 $this->unhook( 'AuthPluginAutoCreate' );
2758 $this->assertEquals( \Status::newGood(), $ret );
2759 $this->assertNotEquals( 0, $user->getId() );
2760 $this->assertEquals( $username, $user->getName() );
2761 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2762 $this->assertSame( [
2763 [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
2764 ], $logger->getBuffer() );
2765 $logger->clearBuffer();
2766
2767 $dbw = wfGetDB( DB_MASTER );
2768 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2769 $session->clear();
2770 $username = self::usernameForCreation();
2771 $user = \User::newFromName( $username );
2772 $this->hook( 'LocalUserCreated', $this->once() )
2773 ->with( $callback, $this->equalTo( true ) );
2774 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, false );
2775 $this->unhook( 'LocalUserCreated' );
2776 $this->assertEquals( \Status::newGood(), $ret );
2777 $this->assertNotEquals( 0, $user->getId() );
2778 $this->assertEquals( $username, $user->getName() );
2779 $this->assertEquals( 0, $session->getUser()->getId() );
2780 $this->assertSame( [
2781 [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
2782 ], $logger->getBuffer() );
2783 $logger->clearBuffer();
2784 $this->assertSame(
2785 $maxLogId,
2786 $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
2787 );
2788
2789 $this->config->set( 'NewUserLog', true );
2790 $session->clear();
2791 $username = self::usernameForCreation();
2792 $user = \User::newFromName( $username );
2793 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, false );
2794 $this->assertEquals( \Status::newGood(), $ret );
2795 $logger->clearBuffer();
2796
2797 $data = \DatabaseLogEntry::getSelectQueryData();
2798 $rows = iterator_to_array( $dbw->select(
2799 $data['tables'],
2800 $data['fields'],
2801 [
2802 'log_id > ' . (int)$maxLogId,
2803 'log_type' => 'newusers'
2804 ] + $data['conds'],
2805 __METHOD__,
2806 $data['options'],
2807 $data['join_conds']
2808 ) );
2809 $this->assertCount( 1, $rows );
2810 $entry = \DatabaseLogEntry::newFromRow( reset( $rows ) );
2811
2812 $this->assertSame( 'autocreate', $entry->getSubtype() );
2813 $this->assertSame( $user->getId(), $entry->getPerformer()->getId() );
2814 $this->assertSame( $user->getName(), $entry->getPerformer()->getName() );
2815 $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
2816 $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
2817
2818 $workaroundPHPUnitBug = true;
2819 }
2820
2821 /**
2822 * @dataProvider provideGetAuthenticationRequests
2823 * @param string $action
2824 * @param array $expect
2825 * @param array $state
2826 */
2827 public function testGetAuthenticationRequests( $action, $expect, $state = [] ) {
2828 $makeReq = function ( $key ) use ( $action ) {
2829 $req = $this->getMock( AuthenticationRequest::class );
2830 $req->expects( $this->any() )->method( 'getUniqueId' )
2831 ->will( $this->returnValue( $key ) );
2832 $req->action = $action === AuthManager::ACTION_UNLINK ? AuthManager::ACTION_REMOVE : $action;
2833 $req->key = $key;
2834 return $req;
2835 };
2836 $cmpReqs = function ( $a, $b ) {
2837 $ret = strcmp( get_class( $a ), get_class( $b ) );
2838 if ( !$ret ) {
2839 $ret = strcmp( $a->key, $b->key );
2840 }
2841 return $ret;
2842 };
2843
2844 $good = StatusValue::newGood();
2845
2846 $mocks = [];
2847 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
2848 $class = ucfirst( $key ) . 'AuthenticationProvider';
2849 $mocks[$key] = $this->getMockForAbstractClass(
2850 "MediaWiki\\Auth\\$class", [], "Mock$class"
2851 );
2852 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
2853 ->will( $this->returnValue( $key ) );
2854 $mocks[$key]->expects( $this->any() )->method( 'getAuthenticationRequests' )
2855 ->will( $this->returnCallback( function ( $action ) use ( $key, $makeReq ) {
2856 return [ $makeReq( "$key-$action" ), $makeReq( 'generic' ) ];
2857 } ) );
2858 $mocks[$key]->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
2859 ->will( $this->returnValue( $good ) );
2860 }
2861
2862 $primaries = [];
2863 foreach ( [
2864 PrimaryAuthenticationProvider::TYPE_NONE,
2865 PrimaryAuthenticationProvider::TYPE_CREATE,
2866 PrimaryAuthenticationProvider::TYPE_LINK
2867 ] as $type ) {
2868 $class = 'PrimaryAuthenticationProvider';
2869 $mocks["primary-$type"] = $this->getMockForAbstractClass(
2870 "MediaWiki\\Auth\\$class", [], "Mock$class"
2871 );
2872 $mocks["primary-$type"]->expects( $this->any() )->method( 'getUniqueId' )
2873 ->will( $this->returnValue( "primary-$type" ) );
2874 $mocks["primary-$type"]->expects( $this->any() )->method( 'accountCreationType' )
2875 ->will( $this->returnValue( $type ) );
2876 $mocks["primary-$type"]->expects( $this->any() )->method( 'getAuthenticationRequests' )
2877 ->will( $this->returnCallback( function ( $action ) use ( $type, $makeReq ) {
2878 return [ $makeReq( "primary-$type-$action" ), $makeReq( 'generic' ) ];
2879 } ) );
2880 $mocks["primary-$type"]->expects( $this->any() )
2881 ->method( 'providerAllowsAuthenticationDataChange' )
2882 ->will( $this->returnValue( $good ) );
2883 $this->primaryauthMocks[] = $mocks["primary-$type"];
2884 }
2885
2886 $mocks['primary2'] = $this->getMockForAbstractClass(
2887 PrimaryAuthenticationProvider::class, [], "MockPrimaryAuthenticationProvider"
2888 );
2889 $mocks['primary2']->expects( $this->any() )->method( 'getUniqueId' )
2890 ->will( $this->returnValue( 'primary2' ) );
2891 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
2892 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
2893 $mocks['primary2']->expects( $this->any() )->method( 'getAuthenticationRequests' )
2894 ->will( $this->returnValue( [] ) );
2895 $mocks['primary2']->expects( $this->any() )
2896 ->method( 'providerAllowsAuthenticationDataChange' )
2897 ->will( $this->returnCallback( function ( $req ) use ( $good ) {
2898 return $req->key === 'generic' ? StatusValue::newFatal( 'no' ) : $good;
2899 } ) );
2900 $this->primaryauthMocks[] = $mocks['primary2'];
2901
2902 $this->preauthMocks = [ $mocks['pre'] ];
2903 $this->secondaryauthMocks = [ $mocks['secondary'] ];
2904 $this->initializeManager( true );
2905
2906 if ( $state ) {
2907 if ( isset( $state['continueRequests'] ) ) {
2908 $state['continueRequests'] = array_map( $makeReq, $state['continueRequests'] );
2909 }
2910 if ( $action === AuthManager::ACTION_LOGIN_CONTINUE ) {
2911 $this->request->getSession()->setSecret( 'AuthManager::authnState', $state );
2912 } elseif ( $action === AuthManager::ACTION_CREATE_CONTINUE ) {
2913 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', $state );
2914 } elseif ( $action === AuthManager::ACTION_LINK_CONTINUE ) {
2915 $this->request->getSession()->setSecret( 'AuthManager::accountLinkState', $state );
2916 }
2917 }
2918
2919 $expectReqs = array_map( $makeReq, $expect );
2920 if ( $action === AuthManager::ACTION_LOGIN ) {
2921 $req = new RememberMeAuthenticationRequest;
2922 $req->action = $action;
2923 $req->required = AuthenticationRequest::REQUIRED;
2924 $expectReqs[] = $req;
2925 } elseif ( $action === AuthManager::ACTION_CREATE ) {
2926 $req = new UsernameAuthenticationRequest;
2927 $req->action = $action;
2928 $expectReqs[] = $req;
2929 $req = new UserDataAuthenticationRequest;
2930 $req->action = $action;
2931 $req->required = AuthenticationRequest::REQUIRED;
2932 $expectReqs[] = $req;
2933 }
2934 usort( $expectReqs, $cmpReqs );
2935
2936 $actual = $this->manager->getAuthenticationRequests( $action );
2937 foreach ( $actual as $req ) {
2938 // Don't test this here.
2939 $req->required = AuthenticationRequest::REQUIRED;
2940 }
2941 usort( $actual, $cmpReqs );
2942
2943 $this->assertEquals( $expectReqs, $actual );
2944
2945 // Test CreationReasonAuthenticationRequest gets returned
2946 if ( $action === AuthManager::ACTION_CREATE ) {
2947 $req = new CreationReasonAuthenticationRequest;
2948 $req->action = $action;
2949 $req->required = AuthenticationRequest::REQUIRED;
2950 $expectReqs[] = $req;
2951 usort( $expectReqs, $cmpReqs );
2952
2953 $actual = $this->manager->getAuthenticationRequests( $action, \User::newFromName( 'UTSysop' ) );
2954 foreach ( $actual as $req ) {
2955 // Don't test this here.
2956 $req->required = AuthenticationRequest::REQUIRED;
2957 }
2958 usort( $actual, $cmpReqs );
2959
2960 $this->assertEquals( $expectReqs, $actual );
2961 }
2962 }
2963
2964 public static function provideGetAuthenticationRequests() {
2965 return [
2966 [
2967 AuthManager::ACTION_LOGIN,
2968 [ 'pre-login', 'primary-none-login', 'primary-create-login',
2969 'primary-link-login', 'secondary-login', 'generic' ],
2970 ],
2971 [
2972 AuthManager::ACTION_CREATE,
2973 [ 'pre-create', 'primary-none-create', 'primary-create-create',
2974 'primary-link-create', 'secondary-create', 'generic' ],
2975 ],
2976 [
2977 AuthManager::ACTION_LINK,
2978 [ 'primary-link-link', 'generic' ],
2979 ],
2980 [
2981 AuthManager::ACTION_CHANGE,
2982 [ 'primary-none-change', 'primary-create-change', 'primary-link-change',
2983 'secondary-change' ],
2984 ],
2985 [
2986 AuthManager::ACTION_REMOVE,
2987 [ 'primary-none-remove', 'primary-create-remove', 'primary-link-remove',
2988 'secondary-remove' ],
2989 ],
2990 [
2991 AuthManager::ACTION_UNLINK,
2992 [ 'primary-link-remove' ],
2993 ],
2994 [
2995 AuthManager::ACTION_LOGIN_CONTINUE,
2996 [],
2997 ],
2998 [
2999 AuthManager::ACTION_LOGIN_CONTINUE,
3000 $reqs = [ 'continue-login', 'foo', 'bar' ],
3001 [
3002 'continueRequests' => $reqs,
3003 ],
3004 ],
3005 [
3006 AuthManager::ACTION_CREATE_CONTINUE,
3007 [],
3008 ],
3009 [
3010 AuthManager::ACTION_CREATE_CONTINUE,
3011 $reqs = [ 'continue-create', 'foo', 'bar' ],
3012 [
3013 'continueRequests' => $reqs,
3014 ],
3015 ],
3016 [
3017 AuthManager::ACTION_LINK_CONTINUE,
3018 [],
3019 ],
3020 [
3021 AuthManager::ACTION_LINK_CONTINUE,
3022 $reqs = [ 'continue-link', 'foo', 'bar' ],
3023 [
3024 'continueRequests' => $reqs,
3025 ],
3026 ],
3027 ];
3028 }
3029
3030 public function testGetAuthenticationRequestsRequired() {
3031 $makeReq = function ( $key, $required ) {
3032 $req = $this->getMock( AuthenticationRequest::class );
3033 $req->expects( $this->any() )->method( 'getUniqueId' )
3034 ->will( $this->returnValue( $key ) );
3035 $req->action = AuthManager::ACTION_LOGIN;
3036 $req->key = $key;
3037 $req->required = $required;
3038 return $req;
3039 };
3040 $cmpReqs = function ( $a, $b ) {
3041 $ret = strcmp( get_class( $a ), get_class( $b ) );
3042 if ( !$ret ) {
3043 $ret = strcmp( $a->key, $b->key );
3044 }
3045 return $ret;
3046 };
3047
3048 $good = StatusValue::newGood();
3049
3050 $primary1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3051 $primary1->expects( $this->any() )->method( 'getUniqueId' )
3052 ->will( $this->returnValue( 'primary1' ) );
3053 $primary1->expects( $this->any() )->method( 'accountCreationType' )
3054 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
3055 $primary1->expects( $this->any() )->method( 'getAuthenticationRequests' )
3056 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3057 return [
3058 $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
3059 $makeReq( "required", AuthenticationRequest::REQUIRED ),
3060 $makeReq( "optional", AuthenticationRequest::OPTIONAL ),
3061 $makeReq( "foo", AuthenticationRequest::REQUIRED ),
3062 $makeReq( "bar", AuthenticationRequest::REQUIRED ),
3063 $makeReq( "baz", AuthenticationRequest::OPTIONAL ),
3064 ];
3065 } ) );
3066
3067 $primary2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3068 $primary2->expects( $this->any() )->method( 'getUniqueId' )
3069 ->will( $this->returnValue( 'primary2' ) );
3070 $primary2->expects( $this->any() )->method( 'accountCreationType' )
3071 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
3072 $primary2->expects( $this->any() )->method( 'getAuthenticationRequests' )
3073 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3074 return [
3075 $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
3076 $makeReq( "required2", AuthenticationRequest::REQUIRED ),
3077 $makeReq( "optional2", AuthenticationRequest::OPTIONAL ),
3078 ];
3079 } ) );
3080
3081 $secondary = $this->getMockForAbstractClass( SecondaryAuthenticationProvider::class );
3082 $secondary->expects( $this->any() )->method( 'getUniqueId' )
3083 ->will( $this->returnValue( 'secondary' ) );
3084 $secondary->expects( $this->any() )->method( 'getAuthenticationRequests' )
3085 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3086 return [
3087 $makeReq( "foo", AuthenticationRequest::OPTIONAL ),
3088 $makeReq( "bar", AuthenticationRequest::REQUIRED ),
3089 $makeReq( "baz", AuthenticationRequest::REQUIRED ),
3090 ];
3091 } ) );
3092
3093 $rememberReq = new RememberMeAuthenticationRequest;
3094 $rememberReq->action = AuthManager::ACTION_LOGIN;
3095
3096 $this->primaryauthMocks = [ $primary1, $primary2 ];
3097 $this->secondaryauthMocks = [ $secondary ];
3098 $this->initializeManager( true );
3099
3100 $actual = $this->manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN );
3101 $expected = [
3102 $rememberReq,
3103 $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
3104 $makeReq( "required", AuthenticationRequest::PRIMARY_REQUIRED ),
3105 $makeReq( "required2", AuthenticationRequest::PRIMARY_REQUIRED ),
3106 $makeReq( "optional", AuthenticationRequest::OPTIONAL ),
3107 $makeReq( "optional2", AuthenticationRequest::OPTIONAL ),
3108 $makeReq( "foo", AuthenticationRequest::PRIMARY_REQUIRED ),
3109 $makeReq( "bar", AuthenticationRequest::REQUIRED ),
3110 $makeReq( "baz", AuthenticationRequest::REQUIRED ),
3111 ];
3112 usort( $actual, $cmpReqs );
3113 usort( $expected, $cmpReqs );
3114 $this->assertEquals( $expected, $actual );
3115
3116 $this->primaryauthMocks = [ $primary1 ];
3117 $this->secondaryauthMocks = [ $secondary ];
3118 $this->initializeManager( true );
3119
3120 $actual = $this->manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN );
3121 $expected = [
3122 $rememberReq,
3123 $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
3124 $makeReq( "required", AuthenticationRequest::REQUIRED ),
3125 $makeReq( "optional", AuthenticationRequest::OPTIONAL ),
3126 $makeReq( "foo", AuthenticationRequest::REQUIRED ),
3127 $makeReq( "bar", AuthenticationRequest::REQUIRED ),
3128 $makeReq( "baz", AuthenticationRequest::REQUIRED ),
3129 ];
3130 usort( $actual, $cmpReqs );
3131 usort( $expected, $cmpReqs );
3132 $this->assertEquals( $expected, $actual );
3133 }
3134
3135 public function testAllowsPropertyChange() {
3136 $mocks = [];
3137 foreach ( [ 'primary', 'secondary' ] as $key ) {
3138 $class = ucfirst( $key ) . 'AuthenticationProvider';
3139 $mocks[$key] = $this->getMockForAbstractClass(
3140 "MediaWiki\\Auth\\$class", [], "Mock$class"
3141 );
3142 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
3143 ->will( $this->returnValue( $key ) );
3144 $mocks[$key]->expects( $this->any() )->method( 'providerAllowsPropertyChange' )
3145 ->will( $this->returnCallback( function ( $prop ) use ( $key ) {
3146 return $prop !== $key;
3147 } ) );
3148 }
3149
3150 $this->primaryauthMocks = [ $mocks['primary'] ];
3151 $this->secondaryauthMocks = [ $mocks['secondary'] ];
3152 $this->initializeManager( true );
3153
3154 $this->assertTrue( $this->manager->allowsPropertyChange( 'foo' ) );
3155 $this->assertFalse( $this->manager->allowsPropertyChange( 'primary' ) );
3156 $this->assertFalse( $this->manager->allowsPropertyChange( 'secondary' ) );
3157 }
3158
3159 public function testAutoCreateOnLogin() {
3160 $username = self::usernameForCreation();
3161
3162 $req = $this->getMock( AuthenticationRequest::class );
3163
3164 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3165 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
3166 $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
3167 ->will( $this->returnValue( AuthenticationResponse::newPass( $username ) ) );
3168 $mock->expects( $this->any() )->method( 'accountCreationType' )
3169 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
3170 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
3171 $mock->expects( $this->any() )->method( 'testUserForCreation' )
3172 ->will( $this->returnValue( StatusValue::newGood() ) );
3173
3174 $mock2 = $this->getMockForAbstractClass( SecondaryAuthenticationProvider::class );
3175 $mock2->expects( $this->any() )->method( 'getUniqueId' )
3176 ->will( $this->returnValue( 'secondary' ) );
3177 $mock2->expects( $this->any() )->method( 'beginSecondaryAuthentication' )->will(
3178 $this->returnValue(
3179 AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) )
3180 )
3181 );
3182 $mock2->expects( $this->any() )->method( 'continueSecondaryAuthentication' )
3183 ->will( $this->returnValue( AuthenticationResponse::newAbstain() ) );
3184 $mock2->expects( $this->any() )->method( 'testUserForCreation' )
3185 ->will( $this->returnValue( StatusValue::newGood() ) );
3186
3187 $this->primaryauthMocks = [ $mock ];
3188 $this->secondaryauthMocks = [ $mock2 ];
3189 $this->initializeManager( true );
3190 $this->manager->setLogger( new \Psr\Log\NullLogger() );
3191 $session = $this->request->getSession();
3192 $session->clear();
3193
3194 $this->assertSame( 0, \User::newFromName( $username )->getId(),
3195 'sanity check' );
3196
3197 $callback = $this->callback( function ( $user ) use ( $username ) {
3198 return $user->getName() === $username;
3199 } );
3200
3201 $this->hook( 'UserLoggedIn', $this->never() );
3202 $this->hook( 'LocalUserCreated', $this->once() )->with( $callback, $this->equalTo( true ) );
3203 $ret = $this->manager->beginAuthentication( [], 'http://localhost/' );
3204 $this->unhook( 'LocalUserCreated' );
3205 $this->unhook( 'UserLoggedIn' );
3206 $this->assertSame( AuthenticationResponse::UI, $ret->status );
3207
3208 $id = (int)\User::newFromName( $username )->getId();
3209 $this->assertNotSame( 0, \User::newFromName( $username )->getId() );
3210 $this->assertSame( 0, $session->getUser()->getId() );
3211
3212 $this->hook( 'UserLoggedIn', $this->once() )->with( $callback );
3213 $this->hook( 'LocalUserCreated', $this->never() );
3214 $ret = $this->manager->continueAuthentication( [] );
3215 $this->unhook( 'LocalUserCreated' );
3216 $this->unhook( 'UserLoggedIn' );
3217 $this->assertSame( AuthenticationResponse::PASS, $ret->status );
3218 $this->assertSame( $username, $ret->username );
3219 $this->assertSame( $id, $session->getUser()->getId() );
3220 }
3221
3222 public function testAutoCreateFailOnLogin() {
3223 $username = self::usernameForCreation();
3224
3225 $mock = $this->getMockForAbstractClass(
3226 PrimaryAuthenticationProvider::class, [], "MockPrimaryAuthenticationProvider" );
3227 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
3228 $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
3229 ->will( $this->returnValue( AuthenticationResponse::newPass( $username ) ) );
3230 $mock->expects( $this->any() )->method( 'accountCreationType' )
3231 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
3232 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
3233 $mock->expects( $this->any() )->method( 'testUserForCreation' )
3234 ->will( $this->returnValue( StatusValue::newFatal( 'fail-from-primary' ) ) );
3235
3236 $this->primaryauthMocks = [ $mock ];
3237 $this->initializeManager( true );
3238 $this->manager->setLogger( new \Psr\Log\NullLogger() );
3239 $session = $this->request->getSession();
3240 $session->clear();
3241
3242 $this->assertSame( 0, $session->getUser()->getId(),
3243 'sanity check' );
3244 $this->assertSame( 0, \User::newFromName( $username )->getId(),
3245 'sanity check' );
3246
3247 $this->hook( 'UserLoggedIn', $this->never() );
3248 $this->hook( 'LocalUserCreated', $this->never() );
3249 $ret = $this->manager->beginAuthentication( [], 'http://localhost/' );
3250 $this->unhook( 'LocalUserCreated' );
3251 $this->unhook( 'UserLoggedIn' );
3252 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
3253 $this->assertSame( 'authmanager-authn-autocreate-failed', $ret->message->getKey() );
3254
3255 $this->assertSame( 0, \User::newFromName( $username )->getId() );
3256 $this->assertSame( 0, $session->getUser()->getId() );
3257 }
3258
3259 public function testAuthenticationSessionData() {
3260 $this->initializeManager( true );
3261
3262 $this->assertNull( $this->manager->getAuthenticationSessionData( 'foo' ) );
3263 $this->manager->setAuthenticationSessionData( 'foo', 'foo!' );
3264 $this->manager->setAuthenticationSessionData( 'bar', 'bar!' );
3265 $this->assertSame( 'foo!', $this->manager->getAuthenticationSessionData( 'foo' ) );
3266 $this->assertSame( 'bar!', $this->manager->getAuthenticationSessionData( 'bar' ) );
3267 $this->manager->removeAuthenticationSessionData( 'foo' );
3268 $this->assertNull( $this->manager->getAuthenticationSessionData( 'foo' ) );
3269 $this->assertSame( 'bar!', $this->manager->getAuthenticationSessionData( 'bar' ) );
3270 $this->manager->removeAuthenticationSessionData( 'bar' );
3271 $this->assertNull( $this->manager->getAuthenticationSessionData( 'bar' ) );
3272
3273 $this->manager->setAuthenticationSessionData( 'foo', 'foo!' );
3274 $this->manager->setAuthenticationSessionData( 'bar', 'bar!' );
3275 $this->manager->removeAuthenticationSessionData( null );
3276 $this->assertNull( $this->manager->getAuthenticationSessionData( 'foo' ) );
3277 $this->assertNull( $this->manager->getAuthenticationSessionData( 'bar' ) );
3278
3279 }
3280
3281 public function testCanLinkAccounts() {
3282 $types = [
3283 PrimaryAuthenticationProvider::TYPE_CREATE => true,
3284 PrimaryAuthenticationProvider::TYPE_LINK => true,
3285 PrimaryAuthenticationProvider::TYPE_NONE => false,
3286 ];
3287
3288 foreach ( $types as $type => $can ) {
3289 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3290 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $type ) );
3291 $mock->expects( $this->any() )->method( 'accountCreationType' )
3292 ->will( $this->returnValue( $type ) );
3293 $this->primaryauthMocks = [ $mock ];
3294 $this->initializeManager( true );
3295 $this->assertSame( $can, $this->manager->canCreateAccounts(), $type );
3296 }
3297 }
3298
3299 public function testBeginAccountLink() {
3300 $user = \User::newFromName( 'UTSysop' );
3301 $this->initializeManager();
3302
3303 $this->request->getSession()->setSecret( 'AuthManager::accountLinkState', 'test' );
3304 try {
3305 $this->manager->beginAccountLink( $user, [], 'http://localhost/' );
3306 $this->fail( 'Expected exception not thrown' );
3307 } catch ( \LogicException $ex ) {
3308 $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
3309 }
3310 $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3311
3312 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3313 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
3314 $mock->expects( $this->any() )->method( 'accountCreationType' )
3315 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
3316 $this->primaryauthMocks = [ $mock ];
3317 $this->initializeManager( true );
3318
3319 $ret = $this->manager->beginAccountLink( new \User, [], 'http://localhost/' );
3320 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
3321 $this->assertSame( 'noname', $ret->message->getKey() );
3322
3323 $ret = $this->manager->beginAccountLink(
3324 \User::newFromName( 'UTDoesNotExist' ), [], 'http://localhost/'
3325 );
3326 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
3327 $this->assertSame( 'authmanager-userdoesnotexist', $ret->message->getKey() );
3328 }
3329
3330 public function testContinueAccountLink() {
3331 $user = \User::newFromName( 'UTSysop' );
3332 $this->initializeManager();
3333
3334 $session = [
3335 'userid' => $user->getId(),
3336 'username' => $user->getName(),
3337 'primary' => 'X',
3338 ];
3339
3340 try {
3341 $this->manager->continueAccountLink( [] );
3342 $this->fail( 'Expected exception not thrown' );
3343 } catch ( \LogicException $ex ) {
3344 $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
3345 }
3346
3347 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3348 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
3349 $mock->expects( $this->any() )->method( 'accountCreationType' )
3350 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
3351 $mock->expects( $this->any() )->method( 'beginPrimaryAccountLink' )->will(
3352 $this->returnValue( AuthenticationResponse::newFail( $this->message( 'fail' ) ) )
3353 );
3354 $this->primaryauthMocks = [ $mock ];
3355 $this->initializeManager( true );
3356
3357 $this->request->getSession()->setSecret( 'AuthManager::accountLinkState', null );
3358 $ret = $this->manager->continueAccountLink( [] );
3359 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
3360 $this->assertSame( 'authmanager-link-not-in-progress', $ret->message->getKey() );
3361
3362 $this->request->getSession()->setSecret( 'AuthManager::accountLinkState',
3363 [ 'username' => $user->getName() . '<>' ] + $session );
3364 $ret = $this->manager->continueAccountLink( [] );
3365 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
3366 $this->assertSame( 'noname', $ret->message->getKey() );
3367 $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3368
3369 $id = $user->getId();
3370 $this->request->getSession()->setSecret( 'AuthManager::accountLinkState',
3371 [ 'userid' => $id + 1 ] + $session );
3372 try {
3373 $ret = $this->manager->continueAccountLink( [] );
3374 $this->fail( 'Expected exception not thrown' );
3375 } catch ( \UnexpectedValueException $ex ) {
3376 $this->assertEquals(
3377 "User \"{$user->getName()}\" is valid, but ID $id != " . ( $id + 1 ) . '!',
3378 $ex->getMessage()
3379 );
3380 }
3381 $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3382 }
3383
3384 /**
3385 * @dataProvider provideAccountLink
3386 * @param StatusValue $preTest
3387 * @param array $primaryResponses
3388 * @param array $managerResponses
3389 */
3390 public function testAccountLink(
3391 StatusValue $preTest, array $primaryResponses, array $managerResponses
3392 ) {
3393 $user = \User::newFromName( 'UTSysop' );
3394
3395 $this->initializeManager();
3396
3397 // Set up lots of mocks...
3398 $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
3399 $req->primary = $primaryResponses;
3400 $mocks = [];
3401
3402 foreach ( [ 'pre', 'primary' ] as $key ) {
3403 $class = ucfirst( $key ) . 'AuthenticationProvider';
3404 $mocks[$key] = $this->getMockForAbstractClass(
3405 "MediaWiki\\Auth\\$class", [], "Mock$class"
3406 );
3407 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
3408 ->will( $this->returnValue( $key ) );
3409
3410 for ( $i = 2; $i <= 3; $i++ ) {
3411 $mocks[$key . $i] = $this->getMockForAbstractClass(
3412 "MediaWiki\\Auth\\$class", [], "Mock$class"
3413 );
3414 $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
3415 ->will( $this->returnValue( $key . $i ) );
3416 }
3417 }
3418
3419 $mocks['pre']->expects( $this->any() )->method( 'testForAccountLink' )
3420 ->will( $this->returnCallback(
3421 function ( $u )
3422 use ( $user, $preTest )
3423 {
3424 $this->assertSame( $user->getId(), $u->getId() );
3425 $this->assertSame( $user->getName(), $u->getName() );
3426 return $preTest;
3427 }
3428 ) );
3429
3430 $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAccountLink' )
3431 ->will( $this->returnValue( StatusValue::newGood() ) );
3432
3433 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
3434 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
3435 $ct = count( $req->primary );
3436 $callback = $this->returnCallback( function ( $u, $reqs ) use ( $user, $req ) {
3437 $this->assertSame( $user->getId(), $u->getId() );
3438 $this->assertSame( $user->getName(), $u->getName() );
3439 $foundReq = false;
3440 foreach ( $reqs as $r ) {
3441 $this->assertSame( $user->getName(), $r->username );
3442 $foundReq = $foundReq || get_class( $r ) === get_class( $req );
3443 }
3444 $this->assertTrue( $foundReq, '$reqs contains $req' );
3445 return array_shift( $req->primary );
3446 } );
3447 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
3448 ->method( 'beginPrimaryAccountLink' )
3449 ->will( $callback );
3450 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
3451 ->method( 'continuePrimaryAccountLink' )
3452 ->will( $callback );
3453
3454 $abstain = AuthenticationResponse::newAbstain();
3455 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
3456 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
3457 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountLink' )
3458 ->will( $this->returnValue( $abstain ) );
3459 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
3460 $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
3461 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
3462 $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountLink' );
3463 $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
3464
3465 $this->preauthMocks = [ $mocks['pre'], $mocks['pre2'] ];
3466 $this->primaryauthMocks = [ $mocks['primary3'], $mocks['primary2'], $mocks['primary'] ];
3467 $this->logger = new \TestLogger( true, function ( $message, $level ) {
3468 return $level === LogLevel::DEBUG ? null : $message;
3469 } );
3470 $this->initializeManager( true );
3471
3472 $constraint = \PHPUnit_Framework_Assert::logicalOr(
3473 $this->equalTo( AuthenticationResponse::PASS ),
3474 $this->equalTo( AuthenticationResponse::FAIL )
3475 );
3476 $providers = array_merge( $this->preauthMocks, $this->primaryauthMocks );
3477 foreach ( $providers as $p ) {
3478 $p->postCalled = false;
3479 $p->expects( $this->atMost( 1 ) )->method( 'postAccountLink' )
3480 ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) {
3481 $this->assertInstanceOf( 'User', $user );
3482 $this->assertSame( 'UTSysop', $user->getName() );
3483 $this->assertInstanceOf( AuthenticationResponse::class, $response );
3484 $this->assertThat( $response->status, $constraint );
3485 $p->postCalled = $response->status;
3486 } );
3487 }
3488
3489 $first = true;
3490 $created = false;
3491 $expectLog = [];
3492 foreach ( $managerResponses as $i => $response ) {
3493 if ( $response instanceof AuthenticationResponse &&
3494 $response->status === AuthenticationResponse::PASS
3495 ) {
3496 $expectLog[] = [ LogLevel::INFO, 'Account linked to {user} by primary' ];
3497 }
3498
3499 $ex = null;
3500 try {
3501 if ( $first ) {
3502 $ret = $this->manager->beginAccountLink( $user, [ $req ], 'http://localhost/' );
3503 } else {
3504 $ret = $this->manager->continueAccountLink( [ $req ] );
3505 }
3506 if ( $response instanceof \Exception ) {
3507 $this->fail( 'Expected exception not thrown', "Response $i" );
3508 }
3509 } catch ( \Exception $ex ) {
3510 if ( !$response instanceof \Exception ) {
3511 throw $ex;
3512 }
3513 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
3514 $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3515 "Response $i, exception, session state" );
3516 return;
3517 }
3518
3519 $this->assertSame( 'http://localhost/', $req->returnToUrl );
3520
3521 $ret->message = $this->message( $ret->message );
3522 $this->assertEquals( $response, $ret, "Response $i, response" );
3523 if ( $response->status === AuthenticationResponse::PASS ||
3524 $response->status === AuthenticationResponse::FAIL
3525 ) {
3526 $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3527 "Response $i, session state" );
3528 foreach ( $providers as $p ) {
3529 $this->assertSame( $response->status, $p->postCalled,
3530 "Response $i, post-auth callback called" );
3531 }
3532 } else {
3533 $this->assertNotNull(
3534 $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3535 "Response $i, session state"
3536 );
3537 foreach ( $ret->neededRequests as $neededReq ) {
3538 $this->assertEquals( AuthManager::ACTION_LINK, $neededReq->action,
3539 "Response $i, neededRequest action" );
3540 }
3541 $this->assertEquals(
3542 $ret->neededRequests,
3543 $this->manager->getAuthenticationRequests( AuthManager::ACTION_LINK_CONTINUE ),
3544 "Response $i, continuation check"
3545 );
3546 foreach ( $providers as $p ) {
3547 $this->assertFalse( $p->postCalled, "Response $i, post-auth callback not called" );
3548 }
3549 }
3550
3551 $first = false;
3552 }
3553
3554 $this->assertSame( $expectLog, $this->logger->getBuffer() );
3555 }
3556
3557 public function provideAccountLink() {
3558 $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
3559 $good = StatusValue::newGood();
3560
3561 return [
3562 'Pre-link test fail in pre' => [
3563 StatusValue::newFatal( 'fail-from-pre' ),
3564 [],
3565 [
3566 AuthenticationResponse::newFail( $this->message( 'fail-from-pre' ) ),
3567 ]
3568 ],
3569 'Failure in primary' => [
3570 $good,
3571 $tmp = [
3572 AuthenticationResponse::newFail( $this->message( 'fail-from-primary' ) ),
3573 ],
3574 $tmp
3575 ],
3576 'All primary abstain' => [
3577 $good,
3578 [
3579 AuthenticationResponse::newAbstain(),
3580 ],
3581 [
3582 AuthenticationResponse::newFail( $this->message( 'authmanager-link-no-primary' ) )
3583 ]
3584 ],
3585 'Primary UI, then redirect, then fail' => [
3586 $good,
3587 $tmp = [
3588 AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
3589 AuthenticationResponse::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
3590 AuthenticationResponse::newFail( $this->message( 'fail-in-primary-continue' ) ),
3591 ],
3592 $tmp
3593 ],
3594 'Primary redirect, then abstain' => [
3595 $good,
3596 [
3597 $tmp = AuthenticationResponse::newRedirect(
3598 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
3599 ),
3600 AuthenticationResponse::newAbstain(),
3601 ],
3602 [
3603 $tmp,
3604 new \DomainException(
3605 'MockPrimaryAuthenticationProvider::continuePrimaryAccountLink() returned ABSTAIN'
3606 )
3607 ]
3608 ],
3609 'Primary UI, then pass' => [
3610 $good,
3611 [
3612 $tmp1 = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
3613 AuthenticationResponse::newPass(),
3614 ],
3615 [
3616 $tmp1,
3617 AuthenticationResponse::newPass( '' ),
3618 ]
3619 ],
3620 'Primary pass' => [
3621 $good,
3622 [
3623 AuthenticationResponse::newPass( '' ),
3624 ],
3625 [
3626 AuthenticationResponse::newPass( '' ),
3627 ]
3628 ],
3629 ];
3630 }
3631 }