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