Add "bot passwords"
[lhc/web/wiklou.git] / tests / phpunit / includes / user / BotPasswordTest.php
1 <?php
2
3 use MediaWiki\Session\SessionManager;
4
5 /**
6 * @covers BotPassword
7 * @group Database
8 */
9 class BotPasswordTest extends MediaWikiTestCase {
10 protected function setUp() {
11 parent::setUp();
12
13 $this->setMwGlobals( array(
14 'wgEnableBotPasswords' => true,
15 'wgBotPasswordsDatabase' => false,
16 'wgCentralIdLookupProvider' => 'BotPasswordTest OkMock',
17 'wgGrantPermissions' => array(
18 'test' => array( 'read' => true ),
19 ),
20 'wgUserrightsInterwikiDelimiter' => '@',
21 ) );
22
23 $mock1 = $this->getMockForAbstractClass( 'CentralIdLookup' );
24 $mock1->expects( $this->any() )->method( 'isAttached' )
25 ->will( $this->returnValue( true ) );
26 $mock1->expects( $this->any() )->method( 'lookupUserNames' )
27 ->will( $this->returnValue( array( 'UTSysop' => 42, 'UTDummy' => 43, 'UTInvalid' => 0 ) ) );
28 $mock1->expects( $this->never() )->method( 'lookupCentralIds' );
29
30 $mock2 = $this->getMockForAbstractClass( 'CentralIdLookup' );
31 $mock2->expects( $this->any() )->method( 'isAttached' )
32 ->will( $this->returnValue( false ) );
33 $mock2->expects( $this->any() )->method( 'lookupUserNames' )
34 ->will( $this->returnArgument( 0 ) );
35 $mock2->expects( $this->never() )->method( 'lookupCentralIds' );
36
37 $this->mergeMwGlobalArrayValue( 'wgCentralIdLookupProviders', array(
38 'BotPasswordTest OkMock' => array( 'factory' => function () use ( $mock1 ) {
39 return $mock1;
40 } ),
41 'BotPasswordTest FailMock' => array( 'factory' => function () use ( $mock2 ) {
42 return $mock2;
43 } ),
44 ) );
45
46 CentralIdLookup::resetCache();
47 }
48
49 public function addDBData() {
50 $passwordFactory = new \PasswordFactory();
51 $passwordFactory->init( \RequestContext::getMain()->getConfig() );
52 // A is unsalted MD5 (thus fast) ... we don't care about security here, this is test only
53 $passwordFactory->setDefaultType( 'A' );
54 $pwhash = $passwordFactory->newFromPlaintext( 'foobaz' );
55
56 $dbw = wfGetDB( DB_MASTER );
57 $dbw->delete(
58 'bot_passwords',
59 array( 'bp_user' => array( 42, 43 ), 'bp_app_id' => 'BotPassword' ),
60 __METHOD__
61 );
62 $dbw->insert(
63 'bot_passwords',
64 array(
65 array(
66 'bp_user' => 42,
67 'bp_app_id' => 'BotPassword',
68 'bp_password' => $pwhash->toString(),
69 'bp_token' => 'token!',
70 'bp_restrictions' => '{"IPAddresses":["127.0.0.0/8"]}',
71 'bp_grants' => '["test"]',
72 ),
73 array(
74 'bp_user' => 43,
75 'bp_app_id' => 'BotPassword',
76 'bp_password' => $pwhash->toString(),
77 'bp_token' => 'token!',
78 'bp_restrictions' => '{"IPAddresses":["127.0.0.0/8"]}',
79 'bp_grants' => '["test"]',
80 ),
81 ),
82 __METHOD__
83 );
84 }
85
86 public function testBasics() {
87 $user = User::newFromName( 'UTSysop' );
88 $bp = BotPassword::newFromUser( $user, 'BotPassword' );
89 $this->assertInstanceOf( 'BotPassword', $bp );
90 $this->assertTrue( $bp->isSaved() );
91 $this->assertSame( 42, $bp->getUserCentralId() );
92 $this->assertSame( 'BotPassword', $bp->getAppId() );
93 $this->assertSame( 'token!', trim( $bp->getToken(), " \0" ) );
94 $this->assertEquals( '{"IPAddresses":["127.0.0.0/8"]}', $bp->getRestrictions()->toJson() );
95 $this->assertSame( array( 'test' ), $bp->getGrants() );
96
97 $this->assertNull( BotPassword::newFromUser( $user, 'DoesNotExist' ) );
98
99 $this->setMwGlobals( array(
100 'wgCentralIdLookupProvider' => 'BotPasswordTest FailMock'
101 ) );
102 $this->assertNull( BotPassword::newFromUser( $user, 'BotPassword' ) );
103
104 $this->assertSame( '@', BotPassword::getSeparator() );
105 $this->setMwGlobals( array(
106 'wgUserrightsInterwikiDelimiter' => '#',
107 ) );
108 $this->assertSame( '#', BotPassword::getSeparator() );
109 }
110
111 public function testUnsaved() {
112 $user = User::newFromName( 'UTSysop' );
113 $bp = BotPassword::newUnsaved( array(
114 'user' => $user,
115 'appId' => 'DoesNotExist'
116 ) );
117 $this->assertInstanceOf( 'BotPassword', $bp );
118 $this->assertFalse( $bp->isSaved() );
119 $this->assertSame( 42, $bp->getUserCentralId() );
120 $this->assertSame( 'DoesNotExist', $bp->getAppId() );
121 $this->assertEquals( MWRestrictions::newDefault(), $bp->getRestrictions() );
122 $this->assertSame( array(), $bp->getGrants() );
123
124 $bp = BotPassword::newUnsaved( array(
125 'username' => 'UTDummy',
126 'appId' => 'DoesNotExist2',
127 'restrictions' => MWRestrictions::newFromJson( '{"IPAddresses":["127.0.0.0/8"]}' ),
128 'grants' => array( 'test' ),
129 ) );
130 $this->assertInstanceOf( 'BotPassword', $bp );
131 $this->assertFalse( $bp->isSaved() );
132 $this->assertSame( 43, $bp->getUserCentralId() );
133 $this->assertSame( 'DoesNotExist2', $bp->getAppId() );
134 $this->assertEquals( '{"IPAddresses":["127.0.0.0/8"]}', $bp->getRestrictions()->toJson() );
135 $this->assertSame( array( 'test' ), $bp->getGrants() );
136
137 $user = User::newFromName( 'UTSysop' );
138 $bp = BotPassword::newUnsaved( array(
139 'centralId' => 45,
140 'appId' => 'DoesNotExist'
141 ) );
142 $this->assertInstanceOf( 'BotPassword', $bp );
143 $this->assertFalse( $bp->isSaved() );
144 $this->assertSame( 45, $bp->getUserCentralId() );
145 $this->assertSame( 'DoesNotExist', $bp->getAppId() );
146
147 $user = User::newFromName( 'UTSysop' );
148 $bp = BotPassword::newUnsaved( array(
149 'user' => $user,
150 'appId' => 'BotPassword'
151 ) );
152 $this->assertInstanceOf( 'BotPassword', $bp );
153 $this->assertFalse( $bp->isSaved() );
154
155 $this->assertNull( BotPassword::newUnsaved( array(
156 'user' => $user,
157 'appId' => '',
158 ) ) );
159 $this->assertNull( BotPassword::newUnsaved( array(
160 'user' => $user,
161 'appId' => str_repeat( 'X', BotPassword::APPID_MAXLENGTH + 1 ),
162 ) ) );
163 $this->assertNull( BotPassword::newUnsaved( array(
164 'user' => 'UTSysop',
165 'appId' => 'Ok',
166 ) ) );
167 $this->assertNull( BotPassword::newUnsaved( array(
168 'username' => 'UTInvalid',
169 'appId' => 'Ok',
170 ) ) );
171 $this->assertNull( BotPassword::newUnsaved( array(
172 'appId' => 'Ok',
173 ) ) );
174 }
175
176 public function testGetPassword() {
177 $bp = TestingAccessWrapper::newFromObject( BotPassword::newFromCentralId( 42, 'BotPassword' ) );
178
179 $password = $bp->getPassword();
180 $this->assertInstanceOf( 'Password', $password );
181 $this->assertTrue( $password->equals( 'foobaz' ) );
182
183 $bp->centralId = 44;
184 $password = $bp->getPassword();
185 $this->assertInstanceOf( 'InvalidPassword', $password );
186
187 $bp = TestingAccessWrapper::newFromObject( BotPassword::newFromCentralId( 42, 'BotPassword' ) );
188 $dbw = wfGetDB( DB_MASTER );
189 $dbw->update(
190 'bot_passwords',
191 array( 'bp_password' => 'garbage' ),
192 array( 'bp_user' => 42, 'bp_app_id' => 'BotPassword' ),
193 __METHOD__
194 );
195 $password = $bp->getPassword();
196 $this->assertInstanceOf( 'InvalidPassword', $password );
197 }
198
199 public function testInvalidateAllPasswordsForUser() {
200 $bp1 = TestingAccessWrapper::newFromObject( BotPassword::newFromCentralId( 42, 'BotPassword' ) );
201 $bp2 = TestingAccessWrapper::newFromObject( BotPassword::newFromCentralId( 43, 'BotPassword' ) );
202
203 $this->assertNotInstanceOf( 'InvalidPassword', $bp1->getPassword(), 'sanity check' );
204 $this->assertNotInstanceOf( 'InvalidPassword', $bp2->getPassword(), 'sanity check' );
205 BotPassword::invalidateAllPasswordsForUser( 'UTSysop' );
206 $this->assertInstanceOf( 'InvalidPassword', $bp1->getPassword() );
207 $this->assertNotInstanceOf( 'InvalidPassword', $bp2->getPassword() );
208
209 $bp = TestingAccessWrapper::newFromObject( BotPassword::newFromCentralId( 42, 'BotPassword' ) );
210 $this->assertInstanceOf( 'InvalidPassword', $bp->getPassword() );
211 }
212
213 public function testRemoveAllPasswordsForUser() {
214 $this->assertNotNull( BotPassword::newFromCentralId( 42, 'BotPassword' ), 'sanity check' );
215 $this->assertNotNull( BotPassword::newFromCentralId( 43, 'BotPassword' ), 'sanity check' );
216
217 BotPassword::removeAllPasswordsForUser( 'UTSysop' );
218
219 $this->assertNull( BotPassword::newFromCentralId( 42, 'BotPassword' ) );
220 $this->assertNotNull( BotPassword::newFromCentralId( 43, 'BotPassword' ) );
221 }
222
223 public function testLogin() {
224 // Test failure when bot passwords aren't enabled
225 $this->setMwGlobals( 'wgEnableBotPasswords', false );
226 $status = BotPassword::login( 'UTSysop@BotPassword', 'foobaz', new FauxRequest );
227 $this->assertEquals( Status::newFatal( 'botpasswords-disabled' ), $status );
228 $this->setMwGlobals( 'wgEnableBotPasswords', true );
229
230 // Test failure when BotPasswordSessionProvider isn't configured
231 $manager = new SessionManager( array(
232 'logger' => new Psr\Log\NullLogger,
233 'store' => new EmptyBagOStuff,
234 ) );
235 $reset = MediaWiki\Session\TestUtils::setSessionManagerSingleton( $manager );
236 $this->assertNull(
237 $manager->getProvider( 'MediaWiki\\Session\\BotPasswordSessionProvider' ),
238 'sanity check'
239 );
240 $status = BotPassword::login( 'UTSysop@BotPassword', 'foobaz', new FauxRequest );
241 $this->assertEquals( Status::newFatal( 'botpasswords-no-provider' ), $status );
242 ScopedCallback::consume( $reset );
243
244 // Now configure BotPasswordSessionProvider for further tests...
245 $mainConfig = RequestContext::getMain()->getConfig();
246 $config = new HashConfig( array(
247 'SessionProviders' => $mainConfig->get( 'SessionProviders' ) + array(
248 'MediaWiki\\Session\\BotPasswordSessionProvider' => array(
249 'class' => 'MediaWiki\\Session\\BotPasswordSessionProvider',
250 'args' => array( array( 'priority' => 40 ) ),
251 )
252 ),
253 ) );
254 $manager = new SessionManager( array(
255 'config' => new MultiConfig( array( $config, RequestContext::getMain()->getConfig() ) ),
256 'logger' => new Psr\Log\NullLogger,
257 'store' => new EmptyBagOStuff,
258 ) );
259 $reset = MediaWiki\Session\TestUtils::setSessionManagerSingleton( $manager );
260
261 // No "@"-thing in the username
262 $status = BotPassword::login( 'UTSysop', 'foobaz', new FauxRequest );
263 $this->assertEquals( Status::newFatal( 'botpasswords-invalid-name', '@' ), $status );
264
265 // No base user
266 $status = BotPassword::login( 'UTDummy@BotPassword', 'foobaz', new FauxRequest );
267 $this->assertEquals( Status::newFatal( 'nosuchuser', 'UTDummy' ), $status );
268
269 // No bot password
270 $status = BotPassword::login( 'UTSysop@DoesNotExist', 'foobaz', new FauxRequest );
271 $this->assertEquals(
272 Status::newFatal( 'botpasswords-not-exist', 'UTSysop', 'DoesNotExist' ),
273 $status
274 );
275
276 // Failed restriction
277 $request = $this->getMock( 'FauxRequest', array( 'getIP' ) );
278 $request->expects( $this->any() )->method( 'getIP' )
279 ->will( $this->returnValue( '10.0.0.1' ) );
280 $status = BotPassword::login( 'UTSysop@BotPassword', 'foobaz', $request );
281 $this->assertEquals( Status::newFatal( 'botpasswords-restriction-failed' ), $status );
282
283 // Wrong password
284 $status = BotPassword::login( 'UTSysop@BotPassword', 'UTSysopPassword', new FauxRequest );
285 $this->assertEquals( Status::newFatal( 'wrongpassword' ), $status );
286
287 // Success!
288 $request = new FauxRequest;
289 $this->assertNotInstanceOf(
290 'MediaWiki\\Session\\BotPasswordSessionProvider',
291 $request->getSession()->getProvider(),
292 'sanity check'
293 );
294 $status = BotPassword::login( 'UTSysop@BotPassword', 'foobaz', $request );
295 $this->assertInstanceOf( 'Status', $status );
296 $this->assertTrue( $status->isGood() );
297 $session = $status->getValue();
298 $this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session );
299 $this->assertInstanceOf(
300 'MediaWiki\\Session\\BotPasswordSessionProvider', $session->getProvider()
301 );
302 $this->assertSame( $session->getId(), $request->getSession()->getId() );
303
304 ScopedCallback::consume( $reset );
305 }
306
307 /**
308 * @dataProvider provideSave
309 * @param string|null $password
310 */
311 public function testSave( $password ) {
312 $passwordFactory = new \PasswordFactory();
313 $passwordFactory->init( \RequestContext::getMain()->getConfig() );
314 // A is unsalted MD5 (thus fast) ... we don't care about security here, this is test only
315 $passwordFactory->setDefaultType( 'A' );
316
317 $bp = BotPassword::newUnsaved( array(
318 'centralId' => 42,
319 'appId' => 'TestSave',
320 'restrictions' => MWRestrictions::newFromJson( '{"IPAddresses":["127.0.0.0/8"]}' ),
321 'grants' => array( 'test' ),
322 ) );
323 $this->assertFalse( $bp->isSaved(), 'sanity check' );
324 $this->assertNull(
325 BotPassword::newFromCentralId( 42, 'TestSave', BotPassword::READ_LATEST ), 'sanity check'
326 );
327
328 $pwhash = $password ? $passwordFactory->newFromPlaintext( $password ) : null;
329 $this->assertFalse( $bp->save( 'update', $pwhash ) );
330 $this->assertTrue( $bp->save( 'insert', $pwhash ) );
331 $bp2 = BotPassword::newFromCentralId( 42, 'TestSave', BotPassword::READ_LATEST );
332 $this->assertInstanceOf( 'BotPassword', $bp2 );
333 $this->assertEquals( $bp->getUserCentralId(), $bp2->getUserCentralId() );
334 $this->assertEquals( $bp->getAppId(), $bp2->getAppId() );
335 $this->assertEquals( $bp->getToken(), $bp2->getToken() );
336 $this->assertEquals( $bp->getRestrictions(), $bp2->getRestrictions() );
337 $this->assertEquals( $bp->getGrants(), $bp2->getGrants() );
338 $pw = TestingAccessWrapper::newFromObject( $bp )->getPassword();
339 if ( $password === null ) {
340 $this->assertInstanceOf( 'InvalidPassword', $pw );
341 } else {
342 $this->assertTrue( $pw->equals( $password ) );
343 }
344
345 $token = $bp->getToken();
346 $this->assertFalse( $bp->save( 'insert' ) );
347 $this->assertTrue( $bp->save( 'update' ) );
348 $this->assertNotEquals( $token, $bp->getToken() );
349 $bp2 = BotPassword::newFromCentralId( 42, 'TestSave', BotPassword::READ_LATEST );
350 $this->assertInstanceOf( 'BotPassword', $bp2 );
351 $this->assertEquals( $bp->getToken(), $bp2->getToken() );
352 $pw = TestingAccessWrapper::newFromObject( $bp )->getPassword();
353 if ( $password === null ) {
354 $this->assertInstanceOf( 'InvalidPassword', $pw );
355 } else {
356 $this->assertTrue( $pw->equals( $password ) );
357 }
358
359 $pwhash = $passwordFactory->newFromPlaintext( 'XXX' );
360 $token = $bp->getToken();
361 $this->assertTrue( $bp->save( 'update', $pwhash ) );
362 $this->assertNotEquals( $token, $bp->getToken() );
363 $pw = TestingAccessWrapper::newFromObject( $bp )->getPassword();
364 $this->assertTrue( $pw->equals( 'XXX' ) );
365
366 $this->assertTrue( $bp->delete() );
367 $this->assertFalse( $bp->isSaved() );
368 $this->assertNull( BotPassword::newFromCentralId( 42, 'TestSave', BotPassword::READ_LATEST ) );
369
370 $this->assertFalse( $bp->save( 'foobar' ) );
371 }
372
373 public static function provideSave() {
374 return array(
375 array( null ),
376 array( 'foobar' ),
377 );
378 }
379 }