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