diff --git a/repo/config/Wikibase.ci.php b/repo/config/Wikibase.ci.php index 3ae3ffb1dc..81885a13da 100644 --- a/repo/config/Wikibase.ci.php +++ b/repo/config/Wikibase.ci.php @@ -83,3 +83,7 @@ if ( $request->getHeader( 'X-Wikibase-CI-Anon-Rate-Limit-Zero', WebRequest::GETHEADER_LIST ) ) { $wgRateLimits = [ 'edit' => [ 'anon' => [ 0, 60 ] ] ]; } + +if ( $request->getHeader( 'X-Wikibase-CI-Temp-Account-Limit-One', WebRequest::GETHEADER_LIST ) ) { + $wgTempAccountCreationThrottle = [ [ 'count' => 1, 'seconds' => 86400 ] ]; +} diff --git a/repo/rest-api/README.md b/repo/rest-api/README.md index 96332385d2..6ad6724258 100644 --- a/repo/rest-api/README.md +++ b/repo/rest-api/README.md @@ -106,7 +106,7 @@ Descriptions of the different kinds of tests can be found in the @ref restApiTes #### e2e and schema tests -These tests can be run with the command `npm run api-testing`. +These tests can be run with the command `npm run api-testing`. The following needs to be correctly set up in order for all the tests to pass: * the targeted wiki to act as both [client and repo], so that Items can have sitelinks to pages on the same wiki @@ -117,6 +117,7 @@ The following needs to be correctly set up in order for all the tests to pass: - `X-Wikibase-CI-Redirect-Badges` - `X-Wikibase-Ci-Tempuser-Config` - `X-Wikibase-CI-Anon-Rate-Limit-Zero` + - `X-Wikibase-CI-Temp-Account-Limit-One` [1]: https://www.mediawiki.org/wiki/MediaWiki_API_integration_tests diff --git a/repo/rest-api/src/Application/UseCases/UpdateExceptionHandler.php b/repo/rest-api/src/Application/UseCases/UpdateExceptionHandler.php index fb7c27dac5..9dad21a551 100644 --- a/repo/rest-api/src/Application/UseCases/UpdateExceptionHandler.php +++ b/repo/rest-api/src/Application/UseCases/UpdateExceptionHandler.php @@ -5,6 +5,7 @@ use Wikibase\Repo\RestApi\Domain\Services\Exceptions\AbuseFilterException; use Wikibase\Repo\RestApi\Domain\Services\Exceptions\RateLimitReached; use Wikibase\Repo\RestApi\Domain\Services\Exceptions\ResourceTooLargeException; +use Wikibase\Repo\RestApi\Domain\Services\Exceptions\TempAccountCreationLimitReached; /** * @license GPL-2.0-or-later @@ -33,6 +34,8 @@ public function executeWithExceptionHandling( callable $callback ) { ] ); } catch ( RateLimitReached $e ) { throw UseCaseError::newRateLimitReached( UseCaseError::REQUEST_LIMIT_REASON_RATE_LIMIT ); + } catch ( TempAccountCreationLimitReached $e ) { + throw UseCaseError::newRateLimitReached( UseCaseError::REQUEST_LIMIT_REASON_TEMP_ACCOUNT_CREATION_LIMIT ); } } diff --git a/repo/rest-api/src/Application/UseCases/UseCaseError.php b/repo/rest-api/src/Application/UseCases/UseCaseError.php index 87584784e1..f29327815e 100644 --- a/repo/rest-api/src/Application/UseCases/UseCaseError.php +++ b/repo/rest-api/src/Application/UseCases/UseCaseError.php @@ -43,6 +43,7 @@ class UseCaseError extends UseCaseException { public const REFERENCED_RESOURCE_NOT_FOUND = 'referenced-resource-not-found'; public const REQUEST_LIMIT_REACHED = 'request-limit-reached'; public const REQUEST_LIMIT_REASON_RATE_LIMIT = 'rate-limit-reached'; + public const REQUEST_LIMIT_REASON_TEMP_ACCOUNT_CREATION_LIMIT = 'temp-account-creation-limit-reached'; public const RESOURCE_NOT_FOUND = 'resource-not-found'; public const RESOURCE_TOO_LARGE = 'resource-too-large'; public const STATEMENT_GROUP_PROPERTY_ID_MISMATCH = 'statement-group-property-id-mismatch'; diff --git a/repo/rest-api/src/Domain/Services/Exceptions/TempAccountCreationLimitReached.php b/repo/rest-api/src/Domain/Services/Exceptions/TempAccountCreationLimitReached.php new file mode 100644 index 0000000000..43e02be3b5 --- /dev/null +++ b/repo/rest-api/src/Domain/Services/Exceptions/TempAccountCreationLimitReached.php @@ -0,0 +1,11 @@ +createOrUpdate( $entity, $editMetadata, EDIT_NEW ); @@ -75,6 +77,7 @@ public function create( EntityDocument $entity, EditMetadata $editMetadata ): En * @throws ResourceTooLargeException * @throws AbuseFilterException * @throws RateLimitReached + * @throws TempAccountCreationLimitReached */ public function update( EntityDocument $entity, EditMetadata $editMetadata ): EntityRevision { return $this->createOrUpdate( $entity, $editMetadata, EDIT_UPDATE ); @@ -85,6 +88,7 @@ public function update( EntityDocument $entity, EditMetadata $editMetadata ): En * @throws ResourceTooLargeException * @throws AbuseFilterException * @throws RateLimitReached + * @throws TempAccountCreationLimitReached */ private function createOrUpdate( EntityDocument $entity, @@ -131,6 +135,10 @@ private function createOrUpdate( throw new RateLimitReached(); } + if ( $this->findErrorInStatus( $status, 'acct_creation_throttle_hit' ) ) { + throw new TempAccountCreationLimitReached(); + } + if ( $this->isPreventedEdit( $status ) ) { throw new EntityUpdatePrevented( (string)$status ); } diff --git a/repo/rest-api/tests/mocha/api-testing/TempUserTest.js b/repo/rest-api/tests/mocha/api-testing/TempUserTest.js index 72cf5ebd11..c6ed7b1223 100644 --- a/repo/rest-api/tests/mocha/api-testing/TempUserTest.js +++ b/repo/rest-api/tests/mocha/api-testing/TempUserTest.js @@ -9,6 +9,7 @@ const { getItemCreateRequest } = require( '../helpers/happyPathRequestBuilders' ); const entityHelper = require( '../helpers/entityHelper' ); +const { assertValidError } = require( '../helpers/responseValidator' ); describeWithTestData( 'IP masking', ( itemRequestInputs, propertyRequestInputs, describeEachRouteWithReset ) => { function withTempUserConfig( newRequestBuilder, config ) { @@ -41,6 +42,27 @@ describeWithTestData( 'IP masking', ( itemRequestInputs, propertyRequestInputs, const { user } = await entityHelper.getLatestEditMetadata( requestInputs.mainTestSubject ); assert.include( user, tempUserPrefix ); } ); + + // Note: If this test fails, it might be due to the throttler relying on caching. + // Ensure caching is enabled for the wiki under test, as the throttler won't work without it. + it( 'responds 429 when the temp user creation limit is reached', async () => { + await newRequestBuilder() + .withHeader( 'X-Wikibase-Ci-Tempuser-Config', JSON.stringify( { enabled: true } ) ) + .withHeader( 'X-Wikibase-CI-Temp-Account-Limit-One', true ) + .makeRequest(); + + const response = await newRequestBuilder() + .withHeader( 'X-Wikibase-Ci-Tempuser-Config', JSON.stringify( { enabled: true } ) ) + .withHeader( 'X-Wikibase-CI-Temp-Account-Limit-One', true ) + .makeRequest(); + + assertValidError( + response, + 429, + 'request-limit-reached', + { reason: 'temp-account-creation-limit-reached' } + ); + } ); } ); // checking the latest metadata for the newly created item diff --git a/repo/rest-api/tests/phpunit/Infrastructure/DataAccess/EntityUpdaterTest.php b/repo/rest-api/tests/phpunit/Infrastructure/DataAccess/EntityUpdaterTest.php index ab00cc630a..55b30321bc 100644 --- a/repo/rest-api/tests/phpunit/Infrastructure/DataAccess/EntityUpdaterTest.php +++ b/repo/rest-api/tests/phpunit/Infrastructure/DataAccess/EntityUpdaterTest.php @@ -33,6 +33,7 @@ use Wikibase\Repo\RestApi\Domain\Services\Exceptions\AbuseFilterException; use Wikibase\Repo\RestApi\Domain\Services\Exceptions\RateLimitReached; use Wikibase\Repo\RestApi\Domain\Services\Exceptions\ResourceTooLargeException; +use Wikibase\Repo\RestApi\Domain\Services\Exceptions\TempAccountCreationLimitReached; use Wikibase\Repo\RestApi\Infrastructure\DataAccess\EntityUpdater; use Wikibase\Repo\RestApi\Infrastructure\DataAccess\Exceptions\EntityUpdateFailed; use Wikibase\Repo\RestApi\Infrastructure\DataAccess\Exceptions\EntityUpdatePrevented; @@ -260,6 +261,23 @@ public function testGivenAbuseFilterMatch_throwsCorrespondingException( EntityDo $this->newEntityUpdater()->update( $entity, $this->createStub( EditMetadata::class ) ); } + /** + * @dataProvider provideEntity + */ + public function testGivenAcctCreationThrottleHit_throwsTempAccountCreationLimitReached( EntityDocument $entityToUpdate ): void { + $errorStatus = EditEntityStatus::newFatal( 'acct_creation_throttle_hit' ); + + $editEntity = $this->createStub( EditEntity::class ); + $editEntity->method( 'attemptSave' )->willReturn( $errorStatus ); + + $this->editEntityFactory = $this->createStub( MediaWikiEditEntityFactory::class ); + $this->editEntityFactory->method( 'newEditEntity' )->willReturn( $editEntity ); + + $this->expectException( TempAccountCreationLimitReached::class ); + + $this->newEntityUpdater()->update( $entityToUpdate, $this->createStub( EditMetadata::class ) ); + } + /** * @dataProvider provideEntity */