diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cebdc02163..509e1f5b29 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -223,7 +223,7 @@ jobs: -e _APP_DATABASE_SHARED_TABLES \ -e _APP_DATABASE_SHARED_TABLES_V1 \ -e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \ - appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug --exclude-group devKeys,screenshots + appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug --exclude-group abuseEnabled,screenshots - name: Failure Logs if: failure() @@ -312,7 +312,7 @@ jobs: -e _APP_DATABASE_SHARED_TABLES \ -e _APP_DATABASE_SHARED_TABLES_V1 \ -e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \ - appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug --exclude-group devKeys,screenshots + appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug --exclude-group abuseEnabled,screenshots - name: Failure Logs if: failure() @@ -322,8 +322,8 @@ jobs: echo "=== OpenRuntimes Executor Logs ===" docker compose logs openruntimes-executor - e2e_dev_keys: - name: E2E Service Test (Dev Keys) + e2e_abuse_enabled: + name: E2E Service Test (Abuse enabled) runs-on: ubuntu-latest needs: setup steps: @@ -344,7 +344,7 @@ jobs: docker compose up -d sleep 30 - - name: Run Projects tests with dev keys in dedicated table mode + - name: Run Projects tests in dedicated table mode run: | echo "Using project tables" export _APP_DATABASE_SHARED_TABLES= @@ -354,7 +354,7 @@ jobs: -e _APP_DATABASE_SHARED_TABLES \ -e _APP_DATABASE_SHARED_TABLES_V1 \ -e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \ - appwrite test /usr/src/code/tests/e2e/Services/Projects --debug --group=devKeys + appwrite test /usr/src/code/tests/e2e/Services/Projects --debug --group=abuseEnabled - name: Failure Logs if: failure() @@ -364,8 +364,8 @@ jobs: echo "=== OpenRuntimes Executor Logs ===" docker compose logs openruntimes-executor - e2e_dev_keys_shared_mode: - name: E2E Shared Mode Service Test (Dev Keys) + e2e_abuse_enabled_shared_mode: + name: E2E Shared Mode Service Test (Abuse enabled) runs-on: ubuntu-latest needs: [ setup, check_database_changes ] if: needs.check_database_changes.outputs.database_changed == 'true' @@ -394,7 +394,7 @@ jobs: docker compose up -d sleep 30 - - name: Run Projects tests with dev keys in ${{ matrix.tables-mode }} table mode + - name: Run Projects tests in ${{ matrix.tables-mode }} table mode run: | if [ "${{ matrix.tables-mode }}" == "Shared V1" ]; then echo "Using shared tables V1" @@ -410,7 +410,7 @@ jobs: -e _APP_DATABASE_SHARED_TABLES \ -e _APP_DATABASE_SHARED_TABLES_V1 \ -e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \ - appwrite test /usr/src/code/tests/e2e/Services/Projects --debug --group=devKeys + appwrite test /usr/src/code/tests/e2e/Services/Projects --debug --group=abuseEnabled - name: Failure Logs if: failure() @@ -420,7 +420,7 @@ jobs: echo "=== OpenRuntimes Executor Logs ===" docker compose logs openruntimes-executor - e2e_screenshots_keys: + e2e_screenshots: name: E2E Service Test (Site Screenshots) runs-on: ubuntu-latest needs: setup diff --git a/app/config/collections/platform.php b/app/config/collections/platform.php index b2e077ad61..8e214e7999 100644 --- a/app/config/collections/platform.php +++ b/app/config/collections/platform.php @@ -1413,21 +1413,21 @@ $platformCollections = [ '$id' => '_key_type', 'type' => Database::INDEX_KEY, 'attributes' => ['type'], - 'lengths' => [32], + 'lengths' => [], 'orders' => [Database::ORDER_ASC], ], [ '$id' => '_key_trigger', 'type' => Database::INDEX_KEY, 'attributes' => ['trigger'], - 'lengths' => [32], + 'lengths' => [], 'orders' => [Database::ORDER_ASC], ], [ '$id' => '_key_deploymentResourceType', 'type' => Database::INDEX_KEY, 'attributes' => ['deploymentResourceType'], - 'lengths' => [32], + 'lengths' => [], 'orders' => [Database::ORDER_ASC], ], [ @@ -1469,23 +1469,23 @@ $platformCollections = [ '$id' => ID::custom('_key_owner'), 'type' => Database::INDEX_KEY, 'attributes' => ['owner'], - 'lengths' => [16], + 'lengths' => [], 'orders' => [Database::ORDER_ASC], ], [ - '$id' => ID::custom('_key_region'), - 'type' => Database::INDEX_KEY, - 'attributes' => ['region'], - 'lengths' => [16], - 'orders' => [Database::ORDER_ASC], - ], - [ - '$id' => ID::custom('_key_piid_riid_rt'), + '$id' => ID::custom('_key_piid_diid_drt'), 'type' => Database::INDEX_KEY, 'attributes' => ['projectInternalId', 'deploymentInternalId', 'deploymentResourceType'], 'lengths' => [], 'orders' => [], ], + [ + '$id' => '_key_region_status_createdAt', + 'type' => Database::INDEX_KEY, + 'attributes' => ['region', 'status', '$createdAt'], + 'lengths' => [], + 'orders' => [], + ], ], ], diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index d6b99f8855..2c481b500c 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -959,6 +959,7 @@ App::post('/v1/account/sessions/email') )) ->label('abuse-limit', 10) ->label('abuse-key', 'url:{url},email:{param-email}') + ->label('abuse-reset', [201]) ->param('email', '', new EmailValidator(), 'User email.') ->param('password', '', new Password(), 'User password. Must be at least 8 chars.') ->inject('request') @@ -1257,6 +1258,7 @@ App::post('/v1/account/sessions/token') )) ->label('abuse-limit', 10) ->label('abuse-key', 'ip:{ip},userId:{param-userId}') + ->label('abuse-reset', [201]) ->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('secret', '', new Text(256), 'Secret of a token generated by login methods. For example, the `createMagicURLToken` or `createPhoneToken` methods.') ->inject('request') @@ -2645,6 +2647,7 @@ App::put('/v1/account/sessions/magic-url') )) ->label('abuse-limit', 10) ->label('abuse-key', 'ip:{ip},userId:{param-userId}') + ->label('abuse-reset', [201]) ->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('secret', '', new Text(256), 'Valid verification token.') ->inject('request') diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 40bde3baf3..74f734a856 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -1501,6 +1501,7 @@ App::post('/v1/projects/:projectId/keys') Permission::update(Role::any()), Permission::delete(Role::any()), ], + // TODO: @hmacr Remove `projectInternalId` and `projectId` column writes before deleting the column. 'projectInternalId' => $project->getSequence(), 'projectId' => $project->getId(), 'resourceInternalId' => $project->getSequence(), @@ -1553,13 +1554,8 @@ App::get('/v1/projects/:projectId/keys') } $keys = $dbForPlatform->find('keys', [ - Query::or([ - Query::equal('projectInternalId', [$project->getSequence()]), - Query::and([ - Query::equal('resourceType', ['projects']), - Query::equal('resourceInternalId', [$project->getSequence()]), - ]) - ]), + Query::equal('resourceType', ['projects']), + Query::equal('resourceInternalId', [$project->getSequence()]), Query::limit(5000), ]); @@ -1600,13 +1596,8 @@ App::get('/v1/projects/:projectId/keys/:keyId') $key = $dbForPlatform->findOne('keys', [ Query::equal('$id', [$keyId]), - Query::or([ - Query::equal('projectInternalId', [$project->getSequence()]), - Query::and([ - Query::equal('resourceType', ['projects']), - Query::equal('resourceInternalId', [$project->getSequence()]), - ]) - ]) + Query::equal('resourceType', ['projects']), + Query::equal('resourceInternalId', [$project->getSequence()]), ]); if ($key->isEmpty()) { @@ -1650,13 +1641,8 @@ App::put('/v1/projects/:projectId/keys/:keyId') $key = $dbForPlatform->findOne('keys', [ Query::equal('$id', [$keyId]), - Query::or([ - Query::equal('projectInternalId', [$project->getSequence()]), - Query::and([ - Query::equal('resourceType', ['projects']), - Query::equal('resourceInternalId', [$project->getSequence()]), - ]) - ]) + Query::equal('resourceType', ['projects']), + Query::equal('resourceInternalId', [$project->getSequence()]), ]); if ($key->isEmpty()) { @@ -1707,13 +1693,8 @@ App::delete('/v1/projects/:projectId/keys/:keyId') $key = $dbForPlatform->findOne('keys', [ Query::equal('$id', [$keyId]), - Query::or([ - Query::equal('projectInternalId', [$project->getSequence()]), - Query::and([ - Query::equal('resourceType', ['projects']), - Query::equal('resourceInternalId', [$project->getSequence()]), - ]) - ]) + Query::equal('resourceType', ['projects']), + Query::equal('resourceInternalId', [$project->getSequence()]), ]); if ($key->isEmpty()) { diff --git a/app/controllers/mock.php b/app/controllers/mock.php index 6f092a5d19..fd7dae55b4 100644 --- a/app/controllers/mock.php +++ b/app/controllers/mock.php @@ -200,8 +200,12 @@ App::post('/v1/mock/api-key-unprefixed') Permission::update(Role::any()), Permission::delete(Role::any()), ], + // TODO: @hmacr Remove `projectInternalId` and `projectId` column writes before deleting the column. 'projectInternalId' => $project->getSequence(), 'projectId' => $project->getId(), + 'resourceInternalId' => $project->getSequence(), + 'resourceId' => $project->getId(), + 'resourceType' => 'projects', 'name' => 'Outdated key', 'scopes' => $scopes, 'expire' => null, diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 83b56f626a..05c08a2231 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -814,7 +814,8 @@ App::shutdown() ->inject('queueForWebhooks') ->inject('queueForRealtime') ->inject('dbForProject') - ->action(function (App $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject) use ($parseLabel) { + ->inject('timelimit') + ->action(function (App $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, callable $timelimit) use ($parseLabel) { $responsePayload = $response->getPayload(); @@ -848,6 +849,41 @@ App::shutdown() $route = $utopia->getRoute(); $requestParams = $route->getParamsValues(); + /** + * Abuse labels + */ + $abuseEnabled = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled'; + $abuseResetCode = $route->getLabel('abuse-reset', []); + $abuseResetCode = \is_array($abuseResetCode) ? $abuseResetCode : [$abuseResetCode]; + + if ($abuseEnabled && \count($abuseResetCode) > 0 && \in_array($response->getStatusCode(), $abuseResetCode)) { + $abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}'); + $abuseKeyLabel = (!is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel; + + foreach ($abuseKeyLabel as $abuseKey) { + $start = $request->getContentRangeStart(); + $end = $request->getContentRangeEnd(); + $timeLimit = $timelimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600)); + $timeLimit + ->setParam('{projectId}', $project->getId()) + ->setParam('{userId}', $user->getId()) + ->setParam('{userAgent}', $request->getUserAgent('')) + ->setParam('{ip}', $request->getIP()) + ->setParam('{url}', $request->getHostname() . $route->getPath()) + ->setParam('{method}', $request->getMethod()) + ->setParam('{chunkId}', (int)($start / ($end + 1 - $start))); + + foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys + if (!empty($value)) { + $timeLimit->setParam('{param-' . $key . '}', (\is_array($value)) ? \json_encode($value) : $value); + } + } + + $abuse = new Abuse($timeLimit); + $abuse->reset(); + } + } + /** * Audit labels */ diff --git a/app/init/database/filters.php b/app/init/database/filters.php index 49c13c9a0b..c9ad3fce03 100644 --- a/app/init/database/filters.php +++ b/app/init/database/filters.php @@ -136,13 +136,8 @@ Database::addFilter( function (mixed $value, Document $document, Database $database) { return $database ->find('keys', [ - Query::or([ - Query::equal('projectInternalId', [$document->getSequence()]), - Query::and([ - Query::equal('resourceType', ['projects']), - Query::equal('resourceInternalId', [$document->getSequence()]), - ]) - ]), + Query::equal('resourceType', ['projects']), + Query::equal('resourceInternalId', [$document->getSequence()]), Query::limit(APP_LIMIT_SUBQUERY), ]); } diff --git a/composer.json b/composer.json index 04dc96594f..07e9e1b1c7 100644 --- a/composer.json +++ b/composer.json @@ -45,7 +45,7 @@ "ext-sockets": "*", "appwrite/php-runtimes": "0.19.*", "appwrite/php-clamav": "2.0.*", - "utopia-php/abuse": "1.*", + "utopia-php/abuse": "1.*.*", "utopia-php/analytics": "0.10.*", "utopia-php/audit": "2.0.2-rc1", "utopia-php/auth": "0.5.*", diff --git a/composer.lock b/composer.lock index 4060b5014b..9149c1d79a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "672dd44ed8a3889612a3dabd35dced5d", + "content-hash": "078c447eafec076507b5bc8b8c0198e7", "packages": [ { "name": "adhocore/jwt", @@ -69,16 +69,16 @@ }, { "name": "appwrite/appwrite", - "version": "15.1.0", + "version": "19.1.0", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-for-php.git", - "reference": "c438b3885071ac7c0329199dce5e6f6a24dd215b" + "reference": "8738e812062f899c85b2598eef43d6a247f08a56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/c438b3885071ac7c0329199dce5e6f6a24dd215b", - "reference": "c438b3885071ac7c0329199dce5e6f6a24dd215b", + "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/8738e812062f899c85b2598eef43d6a247f08a56", + "reference": "8738e812062f899c85b2598eef43d6a247f08a56", "shasum": "" }, "require": { @@ -87,7 +87,7 @@ "php": ">=7.1.0" }, "require-dev": { - "mockery/mockery": "^1.6.6", + "mockery/mockery": "^1.6.12", "phpunit/phpunit": "^10" }, "type": "library", @@ -104,10 +104,10 @@ "support": { "email": "team@appwrite.io", "issues": "https://github.com/appwrite/sdk-for-php/issues", - "source": "https://github.com/appwrite/sdk-for-php/tree/15.1.0", + "source": "https://github.com/appwrite/sdk-for-php/tree/19.1.0", "url": "https://appwrite.io/support" }, - "time": "2025-08-01T04:50:51+00:00" + "time": "2025-12-18T08:07:43+00:00" }, { "name": "appwrite/php-clamav", @@ -3455,24 +3455,25 @@ }, { "name": "utopia-php/abuse", - "version": "1.0.2", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/utopia-php/abuse.git", - "reference": "611fa66a97e87c0dbbc133a717d970da7a5ca828" + "reference": "3339d057c6bb1fa3e5ac5b2598923f6938425ec2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/abuse/zipball/611fa66a97e87c0dbbc133a717d970da7a5ca828", - "reference": "611fa66a97e87c0dbbc133a717d970da7a5ca828", + "url": "https://api.github.com/repos/utopia-php/abuse/zipball/3339d057c6bb1fa3e5ac5b2598923f6938425ec2", + "reference": "3339d057c6bb1fa3e5ac5b2598923f6938425ec2", "shasum": "" }, "require": { + "appwrite/appwrite": "19.*.*", "ext-curl": "*", "ext-pdo": "*", "ext-redis": "*", "php": ">=8.0", - "utopia-php/database": "*" + "utopia-php/database": "3.*.*" }, "require-dev": { "laravel/pint": "1.*", @@ -3500,9 +3501,9 @@ ], "support": { "issues": "https://github.com/utopia-php/abuse/issues", - "source": "https://github.com/utopia-php/abuse/tree/1.0.2" + "source": "https://github.com/utopia-php/abuse/tree/1.2.0" }, - "time": "2025-10-20T07:18:33+00:00" + "time": "2026-01-05T21:29:10+00:00" }, { "name": "utopia-php/analytics", @@ -4515,20 +4516,20 @@ }, { "name": "utopia-php/migration", - "version": "dev-cleanup-hook", + "version": "1.3.10", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "3236527485034fd14352597ea5b63a9f1c69e9d5" + "reference": "cb357c42a5a5614605b546effbea1204ed64c6b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/3236527485034fd14352597ea5b63a9f1c69e9d5", - "reference": "3236527485034fd14352597ea5b63a9f1c69e9d5", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/cb357c42a5a5614605b546effbea1204ed64c6b0", + "reference": "cb357c42a5a5614605b546effbea1204ed64c6b0", "shasum": "" }, "require": { - "appwrite/appwrite": "15.*", + "appwrite/appwrite": "19.*", "ext-curl": "*", "ext-openssl": "*", "php": ">=8.1", @@ -4564,9 +4565,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/cleanup-hook" + "source": "https://github.com/utopia-php/migration/tree/1.3.10" }, - "time": "2026-01-06T08:31:17+00:00" + "time": "2026-01-06T10:47:11+00:00" }, { "name": "utopia-php/mongo", @@ -8942,18 +8943,10 @@ "time": "2024-03-07T20:33:40+00:00" } ], - "aliases": [ - { - "package": "utopia-php/migration", - "version": "dev-cleanup-hook", - "alias": "1.3.999", - "alias_normalized": "1.3.999.0" - } - ], + "aliases": [], "minimum-stability": "stable", "stability-flags": { - "utopia-php/audit": 5, - "utopia-php/migration": 20 + "utopia-php/audit": 5 }, "prefer-stable": false, "prefer-lowest": false, @@ -8978,5 +8971,5 @@ "platform-overrides": { "php": "8.3" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index dbe1882294..983e13b295 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -568,13 +568,8 @@ class Deletes extends Action // Delete Keys $this->deleteByGroup('keys', [ - Query::or([ - Query::equal('projectInternalId', [$projectInternalId]), - Query::and([ - Query::equal('resourceType', ['projects']), - Query::equal('resourceInternalId', [$projectInternalId]), - ]) - ]), + Query::equal('resourceType', ['projects']), + Query::equal('resourceInternalId', [$projectInternalId]), Query::orderAsc() ], $dbForPlatform); diff --git a/src/Appwrite/Platform/Workers/StatsResources.php b/src/Appwrite/Platform/Workers/StatsResources.php index 967dbc59a4..1c3db8d9c9 100644 --- a/src/Appwrite/Platform/Workers/StatsResources.php +++ b/src/Appwrite/Platform/Workers/StatsResources.php @@ -111,13 +111,8 @@ class StatsResources extends Action Query::equal('projectInternalId', [$project->getSequence()]) ]); $keys = $dbForPlatform->count('keys', [ - Query::or([ - Query::equal('projectInternalId', [$project->getSequence()]), - Query::and([ - Query::equal('resourceType', ['projects']), - Query::equal('resourceInternalId', [$project->getSequence()]), - ]) - ]), + Query::equal('resourceType', ['projects']), + Query::equal('resourceInternalId', [$project->getSequence()]), ]); $domains = $dbForPlatform->count('rules', [ diff --git a/tests/e2e/Services/Account/AccountBase.php b/tests/e2e/Services/Account/AccountBase.php index 0c9d481371..fa0d5f0fab 100644 --- a/tests/e2e/Services/Account/AccountBase.php +++ b/tests/e2e/Services/Account/AccountBase.php @@ -363,4 +363,77 @@ trait AccountBase $this->assertEquals($response['headers']['status-code'], 201); $this->assertEquals('191.0.113.195', $response['body']['clientIp'] ?? $response['body']['ip'] ?? ''); } + + /** + * @group abuseEnabled + */ + public function testAccountAbuseReset(): void + { + $email = \uniqid() . '.abuse.reset.test@example.com'; + $password = 'password'; + $account = $this->client->call(Client::METHOD_POST, '/account', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'userId' => ID::unique(), + 'email' => $email, + 'password' => $password, + 'name' => 'Abuse Reset Test', + ]); + + $this->assertEquals($account['headers']['status-code'], 201); + + // 20 successful requests won't get blocked + for ($i = 0; $i < 20; $i++) { + $session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'email' => $email, + 'password' => $password, + ]); + + $this->assertEquals($session['headers']['status-code'], 201); + } + + // 10 failures are OK + for ($i = 0; $i < 10; $i++) { + $session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'email' => $email, + 'password' => 'wrongPassword', + ]); + + $this->assertEquals($session['headers']['status-code'], 401); + } + + // 11th request gets limited + $session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'email' => $email, + 'password' => 'wrongPassword', + ]); + + $this->assertEquals($session['headers']['status-code'], 429); + + // Even correct password is now blocked, correctness doesn't matter + $session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'email' => $email, + 'password' => $password, + ]); + + $this->assertEquals($session['headers']['status-code'], 429); + } } diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index d055b876f9..e31331574f 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -4783,7 +4783,7 @@ class ProjectsConsoleClientTest extends Scope */ /** - * @group devKeys + * @group abuseEnabled */ public function testCreateProjectDevKey(): void { @@ -4844,7 +4844,7 @@ class ProjectsConsoleClientTest extends Scope /** - * @group devKeys + * @group abuseEnabled */ public function testListProjectDevKey(): void { @@ -4935,7 +4935,7 @@ class ProjectsConsoleClientTest extends Scope /** - * @group devKeys + * @group abuseEnabled */ public function testGetProjectDevKey(): void { @@ -4979,7 +4979,7 @@ class ProjectsConsoleClientTest extends Scope } /** - * @group devKeys + * @group abuseEnabled */ public function testGetDevKeyWithSdks(): void { @@ -5036,7 +5036,7 @@ class ProjectsConsoleClientTest extends Scope } /** - * @group devKeys + * @group abuseEnabled */ public function testNoHostValidationWithDevKey(): void { @@ -5117,7 +5117,7 @@ class ProjectsConsoleClientTest extends Scope } /** - * @group devKeys + * @group abuseEnabled */ public function testCorsWithDevKey(): void { @@ -5174,7 +5174,7 @@ class ProjectsConsoleClientTest extends Scope } /** - * @group devKeys + * @group abuseEnabled */ public function testNoRateLimitWithDevKey(): void { @@ -5279,7 +5279,7 @@ class ProjectsConsoleClientTest extends Scope } /** - * @group devKeys + * @group abuseEnabled */ public function testUpdateProjectDevKey(): void { @@ -5324,7 +5324,7 @@ class ProjectsConsoleClientTest extends Scope } /** - * @group devKeys + * @group abuseEnabled */ public function testDeleteProjectDevKey(): void {