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