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