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