aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--RELEASE-NOTES-1.443
-rw-r--r--autoload.php1
-rw-r--r--includes/HookContainer/HookRunner.php7
-rw-r--r--includes/auth/Hook/AuthenticationAttemptThrottledHook.php26
-rw-r--r--includes/auth/Throttler.php8
-rw-r--r--tests/phpunit/includes/auth/ThrottlerTest.php34
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() {