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