diff options
-rw-r--r-- | RELEASE-NOTES-1.44 | 3 | ||||
-rw-r--r-- | autoload.php | 1 | ||||
-rw-r--r-- | includes/HookContainer/HookRunner.php | 7 | ||||
-rw-r--r-- | includes/auth/Hook/AuthenticationAttemptThrottledHook.php | 26 | ||||
-rw-r--r-- | includes/auth/Throttler.php | 8 | ||||
-rw-r--r-- | tests/phpunit/includes/auth/ThrottlerTest.php | 34 |
6 files changed, 77 insertions, 2 deletions
diff --git a/RELEASE-NOTES-1.44 b/RELEASE-NOTES-1.44 index 265c91c49eff..dad267ea74fe 100644 --- a/RELEASE-NOTES-1.44 +++ b/RELEASE-NOTES-1.44 @@ -45,6 +45,7 @@ For notes on 1.42.x and older releases, see HISTORY. === New developer features in 1.44 === +* The AuthenticationAttemptThrottled hook was added. * … === External library changes in 1.44 === @@ -143,4 +144,4 @@ It's highly recommended that you sign up for one of these lists if you're going to run a public MediaWiki, so you can be notified of security fixes. == IRC help == -There's usually someone online in #mediawiki on irc.libera.chat.
\ No newline at end of file +There's usually someone online in #mediawiki on irc.libera.chat. diff --git a/autoload.php b/autoload.php index a9e470f62690..d00a4ab60290 100644 --- a/autoload.php +++ b/autoload.php @@ -1010,6 +1010,7 @@ $wgAutoloadLocalClasses = [ 'MediaWiki\\Auth\\Hook\\AuthManagerLoginAuthenticateAuditHook' => __DIR__ . '/includes/auth/Hook/AuthManagerLoginAuthenticateAuditHook.php', 'MediaWiki\\Auth\\Hook\\AuthManagerVerifyAuthenticationHook' => __DIR__ . '/includes/auth/Hook/AuthManagerVerifyAuthenticationHook.php', 'MediaWiki\\Auth\\Hook\\AuthPreserveQueryParamsHook' => __DIR__ . '/includes/auth/Hook/AuthPreserveQueryParamsHook.php', + 'MediaWiki\\Auth\\Hook\\AuthenticationAttemptThrottledHook' => __DIR__ . '/includes/auth/Hook/AuthenticationAttemptThrottledHook.php', 'MediaWiki\\Auth\\Hook\\ExemptFromAccountCreationThrottleHook' => __DIR__ . '/includes/auth/Hook/ExemptFromAccountCreationThrottleHook.php', 'MediaWiki\\Auth\\Hook\\LocalUserCreatedHook' => __DIR__ . '/includes/auth/Hook/LocalUserCreatedHook.php', 'MediaWiki\\Auth\\Hook\\ResetPasswordExpirationHook' => __DIR__ . '/includes/auth/Hook/ResetPasswordExpirationHook.php', diff --git a/includes/HookContainer/HookRunner.php b/includes/HookContainer/HookRunner.php index f002e936fc22..601b8a43848c 100644 --- a/includes/HookContainer/HookRunner.php +++ b/includes/HookContainer/HookRunner.php @@ -49,6 +49,7 @@ use WikiPage; */ class HookRunner implements \MediaWiki\Actions\Hook\GetActionNameHook, + \MediaWiki\Auth\Hook\AuthenticationAttemptThrottledHook, \MediaWiki\Auth\Hook\AuthManagerFilterProvidersHook, \MediaWiki\Auth\Hook\AuthManagerLoginAuthenticateAuditHook, \MediaWiki\Auth\Hook\AuthManagerVerifyAuthenticationHook, @@ -934,6 +935,12 @@ class HookRunner implements ); } + public function onAuthenticationAttemptThrottled( string $type, ?string $username, ?string $ip ) { + return $this->container->run( + 'AuthenticationAttemptThrottled', [ $type, $username, $ip ] + ); + } + public function onAutopromoteCondition( $type, $args, $user, &$result ) { return $this->container->run( 'AutopromoteCondition', diff --git a/includes/auth/Hook/AuthenticationAttemptThrottledHook.php b/includes/auth/Hook/AuthenticationAttemptThrottledHook.php new file mode 100644 index 000000000000..0dc5c882ef01 --- /dev/null +++ b/includes/auth/Hook/AuthenticationAttemptThrottledHook.php @@ -0,0 +1,26 @@ +<?php + +namespace MediaWiki\Auth\Hook; + +/** + * This is a hook handler interface, see docs/Hooks.md. + * Use the hook name "AuthenticationAttemptThrottled" to register handlers implementing this interface. + * + * @stable to implement + * @ingroup Hooks + */ +interface AuthenticationAttemptThrottledHook { + /** + * This hook is called when a {@link Throttler} has throttled an authentication attempt. + * An authentication attempt includes account creation, logins, and temporary account auto-creation. + * + * @since 1.43 + * + * @param string $type The name of the authentication throttle that caused the throttling + * @param string|null $username The username associated with the action that was throttled, or null if not + * relevant. + * @param string|null $ip The IP used to make the action that was throttled, or null if not provided. + * @return bool|void True or no return value to continue or false to abort + */ + public function onAuthenticationAttemptThrottled( string $type, ?string $username, ?string $ip ); +} diff --git a/includes/auth/Throttler.php b/includes/auth/Throttler.php index c7e97b835bae..e875a8beb39c 100644 --- a/includes/auth/Throttler.php +++ b/includes/auth/Throttler.php @@ -22,6 +22,7 @@ namespace MediaWiki\Auth; use InvalidArgumentException; +use MediaWiki\HookContainer\HookRunner; use MediaWiki\Logger\LoggerFactory; use MediaWiki\MainConfigNames; use MediaWiki\MediaWikiServices; @@ -53,6 +54,8 @@ class Throttler implements LoggerAwareInterface { /** @var int|float */ protected $warningLimit; + private HookRunner $hookRunner; + /** * @param array|null $conditions An array of arrays describing throttling conditions. * Defaults to $wgPasswordAttemptThrottle. See documentation of that variable for format. @@ -71,6 +74,8 @@ class Throttler implements LoggerAwareInterface { } $services = MediaWikiServices::getInstance(); + $this->hookRunner = new HookRunner( $services->getHookContainer() ); + $objectCacheFactory = $services->getObjectCacheFactory(); if ( $conditions === null ) { @@ -148,6 +153,9 @@ class Throttler implements LoggerAwareInterface { // @codeCoverageIgnoreEnd ] ); + // Allow extensions to perform actions when a throttle causes throttling. + $this->hookRunner->onAuthenticationAttemptThrottled( $this->type, $username, $ip ); + return [ 'throttleIndex' => $index, 'count' => $count, 'wait' => $expiry ]; } else { $this->cache->incrWithInit( $throttleKey, $expiry, 1 ); diff --git a/tests/phpunit/includes/auth/ThrottlerTest.php b/tests/phpunit/includes/auth/ThrottlerTest.php index b687319fb60f..ee451442186b 100644 --- a/tests/phpunit/includes/auth/ThrottlerTest.php +++ b/tests/phpunit/includes/auth/ThrottlerTest.php @@ -18,6 +18,14 @@ use Wikimedia\TestingAccessWrapper; * @covers \MediaWiki\Auth\Throttler */ class ThrottlerTest extends MediaWikiIntegrationTestCase { + + public function setUp(): void { + parent::setUp(); + // Avoid issues where extensions attempt to interact with the DB when handling this hook then causing these + // tests to fail. + $this->clearHook( 'AuthenticationAttemptThrottled' ); + } + public function testConstructor() { $cache = new HashBagOStuff(); $logger = $this->getMockBuilder( AbstractLogger::class ) @@ -189,18 +197,40 @@ class ThrottlerTest extends MediaWikiIntegrationTestCase { $throttler->increase(); } - public function testLog() { + public function testLogAndHook() { + // Add a implementation of the AuthenticationAttemptThrottled hook that expects no calls. + $this->setTemporaryHook( + 'AuthenticationAttemptThrottled', + function () { + $this->fail( 'Did not expect the AuthenticationAttemptThrottled hook to be run.' ); + } + ); $cache = new HashBagOStuff(); $throttler = new Throttler( [ [ 'count' => 1, 'seconds' => 10 ] ], [ 'cache' => $cache ] ); + // Make the logger expect no calls $logger = $this->getMockBuilder( AbstractLogger::class ) ->onlyMethods( [ 'log' ] ) ->getMockForAbstractClass(); $logger->expects( $this->never() )->method( 'log' ); $throttler->setLogger( $logger ); + // Call the increase method and expect that the throttling did not occur. $result = $throttler->increase( 'SomeUser', '1.2.3.4' ); $this->assertFalse( $result, 'should not throttle' ); + // Replace the implementation of the AuthenticationAttemptThrottled hook which one that tests that it is called + // with the correct data. + $hookCalled = false; + $this->setTemporaryHook( + 'AuthenticationAttemptThrottled', + function ( $type, $username, $ip ) use ( &$hookCalled ) { + $hookCalled = true; + $this->assertSame( 'custom', $type ); + $this->assertSame( 'SomeUser', $username ); + $this->assertSame( '1.2.3.4', $ip ); + } + ); + // Create a mock logger that expects a call. $logger = $this->getMockBuilder( AbstractLogger::class ) ->onlyMethods( [ 'log' ] ) ->getMockForAbstractClass(); @@ -214,8 +244,10 @@ class ThrottlerTest extends MediaWikiIntegrationTestCase { 'method' => 'foo', ] ); $throttler->setLogger( $logger ); + // Call the increase method and expect that the throttling occurred. $result = $throttler->increase( 'SomeUser', '1.2.3.4', 'foo' ); $this->assertSame( [ 'throttleIndex' => 0, 'count' => 1, 'wait' => 10 ], $result ); + $this->assertTrue( $hookCalled ); } public function testClear() { |