All callers in Wikimedia gerrit repos have since been migrated to incrWithInit(), which is usually what is desired and is easier to implement in various backends. Newer memcached versions of the memcached protocol even support auto-initializing increments (including an initial TTL) via the 'ma' command. The incr()/decr() methods currently bloat the interface and subclasses (often with slow and buggy implementations). These methods are also hard to implement in a multi-DC store (either extremely slow or racey) even though callers might assume they handle high concurrency. Also, force each MediumSpecificBagOStuff subclass to implement incrWithInit() instead of having some rely on the base method In mcc.php, add incrWithInit() and simplify parameters, making it possible to test non-memcached backends. Change-Id: I53c9c2c839a1e71d5c104913fea0680c30d11108
240 lines
8.8 KiB
PHP
240 lines
8.8 KiB
PHP
<?php
|
|
|
|
namespace MediaWiki\Auth;
|
|
|
|
use BagOStuff;
|
|
use HashBagOStuff;
|
|
use Psr\Log\AbstractLogger;
|
|
use Psr\Log\LoggerInterface;
|
|
use Psr\Log\NullLogger;
|
|
use Wikimedia\TestingAccessWrapper;
|
|
|
|
/**
|
|
* @group AuthManager
|
|
* @covers \MediaWiki\Auth\Throttler
|
|
*/
|
|
class ThrottlerTest extends \MediaWikiIntegrationTestCase {
|
|
public function testConstructor() {
|
|
$cache = new \HashBagOStuff();
|
|
$logger = $this->getMockBuilder( AbstractLogger::class )
|
|
->onlyMethods( [ 'log' ] )
|
|
->getMockForAbstractClass();
|
|
|
|
$throttler = new Throttler(
|
|
[ [ 'count' => 123, 'seconds' => 456 ] ],
|
|
[ 'type' => 'foo', 'cache' => $cache ]
|
|
);
|
|
$throttler->setLogger( $logger );
|
|
$throttlerPriv = TestingAccessWrapper::newFromObject( $throttler );
|
|
$this->assertSame( [ [ 'count' => 123, 'seconds' => 456 ] ], $throttlerPriv->conditions );
|
|
$this->assertSame( 'foo', $throttlerPriv->type );
|
|
$this->assertSame( $cache, $throttlerPriv->cache );
|
|
$this->assertSame( $logger, $throttlerPriv->logger );
|
|
|
|
$throttler = new Throttler( [ [ 'count' => 123, 'seconds' => 456 ] ] );
|
|
$throttler->setLogger( new NullLogger() );
|
|
$throttlerPriv = TestingAccessWrapper::newFromObject( $throttler );
|
|
$this->assertSame( [ [ 'count' => 123, 'seconds' => 456 ] ], $throttlerPriv->conditions );
|
|
$this->assertSame( 'custom', $throttlerPriv->type );
|
|
$this->assertInstanceOf( BagOStuff::class, $throttlerPriv->cache );
|
|
$this->assertInstanceOf( LoggerInterface::class, $throttlerPriv->logger );
|
|
|
|
$this->setMwGlobals( [ 'wgPasswordAttemptThrottle' => [ [ 'count' => 321,
|
|
'seconds' => 654 ] ] ] );
|
|
$throttler = new Throttler();
|
|
$throttler->setLogger( new NullLogger() );
|
|
$throttlerPriv = TestingAccessWrapper::newFromObject( $throttler );
|
|
$this->assertSame( [ [ 'count' => 321, 'seconds' => 654 ] ], $throttlerPriv->conditions );
|
|
$this->assertSame( 'password', $throttlerPriv->type );
|
|
$this->assertInstanceOf( BagOStuff::class, $throttlerPriv->cache );
|
|
$this->assertInstanceOf( LoggerInterface::class, $throttlerPriv->logger );
|
|
|
|
try {
|
|
new Throttler( [], [ 'foo' => 1, 'bar' => 2, 'baz' => 3 ] );
|
|
$this->fail( 'Expected exception not thrown' );
|
|
} catch ( \InvalidArgumentException $ex ) {
|
|
$this->assertSame( 'unrecognized parameters: foo, bar, baz', $ex->getMessage() );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideNormalizeThrottleConditions
|
|
*/
|
|
public function testNormalizeThrottleConditions( $condition, $normalized ) {
|
|
$throttler = new Throttler( $condition );
|
|
$throttler->setLogger( new NullLogger() );
|
|
$throttlerPriv = TestingAccessWrapper::newFromObject( $throttler );
|
|
$this->assertSame( $normalized, $throttlerPriv->conditions );
|
|
}
|
|
|
|
public function provideNormalizeThrottleConditions() {
|
|
return [
|
|
[
|
|
[],
|
|
[],
|
|
],
|
|
[
|
|
[ 'count' => 1, 'seconds' => 2 ],
|
|
[ [ 'count' => 1, 'seconds' => 2 ] ],
|
|
],
|
|
[
|
|
[ [ 'count' => 1, 'seconds' => 2 ], [ 'count' => 2, 'seconds' => 3 ] ],
|
|
[ [ 'count' => 1, 'seconds' => 2 ], [ 'count' => 2, 'seconds' => 3 ] ],
|
|
],
|
|
];
|
|
}
|
|
|
|
public function testNormalizeThrottleConditions2() {
|
|
$priv = TestingAccessWrapper::newFromClass( Throttler::class );
|
|
$this->assertSame( [], $priv->normalizeThrottleConditions( null ) );
|
|
$this->assertSame( [], $priv->normalizeThrottleConditions( 'bad' ) );
|
|
}
|
|
|
|
public function testIncrease() {
|
|
$cache = new \HashBagOStuff();
|
|
$throttler = new Throttler( [
|
|
[ 'count' => 2, 'seconds' => 10, ],
|
|
[ 'count' => 4, 'seconds' => 15, 'allIPs' => true ],
|
|
], [ 'cache' => $cache ] );
|
|
$throttler->setLogger( new NullLogger() );
|
|
|
|
$result = $throttler->increase( 'SomeUser', '1.2.3.4' );
|
|
$this->assertFalse( $result, 'should not throttle' );
|
|
|
|
$result = $throttler->increase( 'SomeUser', '1.2.3.4' );
|
|
$this->assertFalse( $result, 'should not throttle' );
|
|
|
|
$result = $throttler->increase( 'SomeUser', '1.2.3.4' );
|
|
$this->assertSame( [ 'throttleIndex' => 0, 'count' => 2, 'wait' => 10 ], $result );
|
|
|
|
$result = $throttler->increase( 'OtherUser', '1.2.3.4' );
|
|
$this->assertFalse( $result, 'should not throttle' );
|
|
|
|
$result = $throttler->increase( 'SomeUser', '2.3.4.5' );
|
|
$this->assertFalse( $result, 'should not throttle' );
|
|
|
|
$result = $throttler->increase( 'SomeUser', '3.4.5.6' );
|
|
$this->assertFalse( $result, 'should not throttle' );
|
|
|
|
$result = $throttler->increase( 'SomeUser', '3.4.5.6' );
|
|
$this->assertSame( [ 'throttleIndex' => 1, 'count' => 4, 'wait' => 15 ], $result );
|
|
}
|
|
|
|
public function testZeroCount() {
|
|
$cache = new \HashBagOStuff();
|
|
$throttler = new Throttler( [ [ 'count' => 0, 'seconds' => 10 ] ], [ 'cache' => $cache ] );
|
|
$throttler->setLogger( new NullLogger() );
|
|
|
|
$result = $throttler->increase( 'SomeUser', '1.2.3.4' );
|
|
$this->assertFalse( $result, 'should not throttle, count=0 is ignored' );
|
|
|
|
$result = $throttler->increase( 'SomeUser', '1.2.3.4' );
|
|
$this->assertFalse( $result, 'should not throttle, count=0 is ignored' );
|
|
|
|
$result = $throttler->increase( 'SomeUser', '1.2.3.4' );
|
|
$this->assertFalse( $result, 'should not throttle, count=0 is ignored' );
|
|
}
|
|
|
|
public function testNamespacing() {
|
|
$cache = new \HashBagOStuff();
|
|
$throttler1 = new Throttler( [ [ 'count' => 1, 'seconds' => 10 ] ],
|
|
[ 'cache' => $cache, 'type' => 'foo' ] );
|
|
$throttler2 = new Throttler( [ [ 'count' => 1, 'seconds' => 10 ] ],
|
|
[ 'cache' => $cache, 'type' => 'foo' ] );
|
|
$throttler3 = new Throttler( [ [ 'count' => 1, 'seconds' => 10 ] ],
|
|
[ 'cache' => $cache, 'type' => 'bar' ] );
|
|
$throttler1->setLogger( new NullLogger() );
|
|
$throttler2->setLogger( new NullLogger() );
|
|
$throttler3->setLogger( new NullLogger() );
|
|
|
|
$throttled = [ 'throttleIndex' => 0, 'count' => 1, 'wait' => 10 ];
|
|
|
|
$result = $throttler1->increase( 'SomeUser', '1.2.3.4' );
|
|
$this->assertFalse( $result, 'should not throttle' );
|
|
|
|
$result = $throttler1->increase( 'SomeUser', '1.2.3.4' );
|
|
$this->assertEquals( $throttled, $result, 'should throttle' );
|
|
|
|
$result = $throttler2->increase( 'SomeUser', '1.2.3.4' );
|
|
$this->assertEquals( $throttled, $result, 'should throttle, same namespace' );
|
|
|
|
$result = $throttler3->increase( 'SomeUser', '1.2.3.4' );
|
|
$this->assertFalse( $result, 'should not throttle, different namespace' );
|
|
}
|
|
|
|
public function testExpiration() {
|
|
$cache = $this->getMockBuilder( HashBagOStuff::class )
|
|
->onlyMethods( [ 'add', 'incrWithInit' ] )->getMock();
|
|
$throttler = new Throttler( [ [ 'count' => 3, 'seconds' => 10 ] ], [ 'cache' => $cache ] );
|
|
$throttler->setLogger( new NullLogger() );
|
|
|
|
$cache->expects( $this->once() )
|
|
->method( 'incrWithInit' )
|
|
->with( $this->anything(), 10, 1 );
|
|
$throttler->increase( 'SomeUser' );
|
|
}
|
|
|
|
/**
|
|
*/
|
|
public function testException() {
|
|
$throttler = new Throttler( [ [ 'count' => 3, 'seconds' => 10 ] ] );
|
|
$throttler->setLogger( new NullLogger() );
|
|
$this->expectException( \InvalidArgumentException::class );
|
|
$throttler->increase();
|
|
}
|
|
|
|
public function testLog() {
|
|
$cache = new \HashBagOStuff();
|
|
$throttler = new Throttler( [ [ 'count' => 1, 'seconds' => 10 ] ], [ 'cache' => $cache ] );
|
|
|
|
$logger = $this->getMockBuilder( AbstractLogger::class )
|
|
->onlyMethods( [ 'log' ] )
|
|
->getMockForAbstractClass();
|
|
$logger->expects( $this->never() )->method( 'log' );
|
|
$throttler->setLogger( $logger );
|
|
$result = $throttler->increase( 'SomeUser', '1.2.3.4' );
|
|
$this->assertFalse( $result, 'should not throttle' );
|
|
|
|
$logger = $this->getMockBuilder( AbstractLogger::class )
|
|
->onlyMethods( [ 'log' ] )
|
|
->getMockForAbstractClass();
|
|
$logger->expects( $this->once() )->method( 'log' )->with( $this->anything(), $this->anything(), [
|
|
'throttle' => 'custom',
|
|
'index' => 0,
|
|
'ipKey' => '1.2.3.4',
|
|
'username' => 'SomeUser',
|
|
'count' => 1,
|
|
'expiry' => 10,
|
|
'method' => 'foo',
|
|
] );
|
|
$throttler->setLogger( $logger );
|
|
$result = $throttler->increase( 'SomeUser', '1.2.3.4', 'foo' );
|
|
$this->assertSame( [ 'throttleIndex' => 0, 'count' => 1, 'wait' => 10 ], $result );
|
|
}
|
|
|
|
public function testClear() {
|
|
$cache = new \HashBagOStuff();
|
|
$throttler = new Throttler( [ [ 'count' => 1, 'seconds' => 10 ] ], [ 'cache' => $cache ] );
|
|
$throttler->setLogger( new NullLogger() );
|
|
|
|
$result = $throttler->increase( 'SomeUser', '1.2.3.4' );
|
|
$this->assertFalse( $result, 'should not throttle' );
|
|
|
|
$result = $throttler->increase( 'SomeUser', '1.2.3.4' );
|
|
$this->assertSame( [ 'throttleIndex' => 0, 'count' => 1, 'wait' => 10 ], $result );
|
|
|
|
$result = $throttler->increase( 'OtherUser', '1.2.3.4' );
|
|
$this->assertFalse( $result, 'should not throttle' );
|
|
|
|
$result = $throttler->increase( 'OtherUser', '1.2.3.4' );
|
|
$this->assertSame( [ 'throttleIndex' => 0, 'count' => 1, 'wait' => 10 ], $result );
|
|
|
|
$throttler->clear( 'SomeUser', '1.2.3.4' );
|
|
|
|
$result = $throttler->increase( 'SomeUser', '1.2.3.4' );
|
|
$this->assertFalse( $result, 'should not throttle' );
|
|
|
|
$result = $throttler->increase( 'OtherUser', '1.2.3.4' );
|
|
$this->assertSame( [ 'throttleIndex' => 0, 'count' => 1, 'wait' => 10 ], $result );
|
|
}
|
|
}
|