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