diff --git a/app/controllers/api/migrations.php b/app/controllers/api/migrations.php index 6d2cec3f03..d083332cfe 100644 --- a/app/controllers/api/migrations.php +++ b/app/controllers/api/migrations.php @@ -724,7 +724,7 @@ Http::get('/v1/migrations/appwrite/settings-key') throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE); } - $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 86400, 0); + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 120, 0); $consoleKey = $jwt->encode([ 'projectId' => 'console', 'scopes' => $scopes, diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 94244b6962..478a057cfb 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -1,7 +1,6 @@ desc('List keys') ->groups(['api', 'projects']) ->label('scope', 'keys.read') + ->label('sdk', new Method( namespace: 'projects', group: 'keys', @@ -1169,13 +1169,7 @@ Http::get('/v1/projects/:projectId/keys') ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) ->inject('response') ->inject('dbForPlatform') - ->inject('apiKey') - ->action(function (string $projectId, array $queries, bool $includeTotal, Response $response, Database $dbForPlatform, ?Key $apiKey) { - - if ($apiKey !== null && !empty($apiKey->getTargetProjectId()) && $apiKey->getTargetProjectId() !== $projectId) { - throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE); - } - + ->action(function (string $projectId, array $queries, bool $includeTotal, Response $response, Database $dbForPlatform) { $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1229,6 +1223,7 @@ Http::get('/v1/projects/:projectId/keys/:keyId') ->desc('Get key') ->groups(['api', 'projects']) ->label('scope', 'keys.read') + ->label('sdk', new Method( namespace: 'projects', group: 'keys', @@ -1246,13 +1241,7 @@ Http::get('/v1/projects/:projectId/keys/:keyId') ->param('keyId', '', new UID(), 'Key unique ID.') ->inject('response') ->inject('dbForPlatform') - ->inject('apiKey') - ->action(function (string $projectId, string $keyId, Response $response, Database $dbForPlatform, ?Key $apiKey) { - - if ($apiKey !== null && !empty($apiKey->getTargetProjectId()) && $apiKey->getTargetProjectId() !== $projectId) { - throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE); - } - + ->action(function (string $projectId, string $keyId, Response $response, Database $dbForPlatform) { $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1501,6 +1490,7 @@ Http::get('/v1/projects/:projectId/platforms') ->desc('List platforms') ->groups(['api', 'projects']) ->label('scope', 'platforms.read') + ->label('sdk', new Method( namespace: 'projects', group: 'platforms', @@ -1518,13 +1508,7 @@ Http::get('/v1/projects/:projectId/platforms') ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) ->inject('response') ->inject('dbForPlatform') - ->inject('apiKey') - ->action(function (string $projectId, bool $includeTotal, Response $response, Database $dbForPlatform, ?Key $apiKey) { - - if ($apiKey !== null && !empty($apiKey->getTargetProjectId()) && $apiKey->getTargetProjectId() !== $projectId) { - throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE); - } - + ->action(function (string $projectId, bool $includeTotal, Response $response, Database $dbForPlatform) { $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1546,6 +1530,7 @@ Http::get('/v1/projects/:projectId/platforms/:platformId') ->desc('Get platform') ->groups(['api', 'projects']) ->label('scope', 'platforms.read') + ->label('sdk', new Method( namespace: 'projects', group: 'platforms', @@ -1563,13 +1548,7 @@ Http::get('/v1/projects/:projectId/platforms/:platformId') ->param('platformId', '', new UID(), 'Platform unique ID.') ->inject('response') ->inject('dbForPlatform') - ->inject('apiKey') - ->action(function (string $projectId, string $platformId, Response $response, Database $dbForPlatform, ?Key $apiKey) { - - if ($apiKey !== null && !empty($apiKey->getTargetProjectId()) && $apiKey->getTargetProjectId() !== $projectId) { - throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE); - } - + ->action(function (string $projectId, string $platformId, Response $response, Database $dbForPlatform) { $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index f25f9717ed..1f6286f6bb 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -273,6 +273,17 @@ Http::init() $scopes = \array_unique($scopes); + // Migration-scoped keys (with targetProjectId) can only access routes + // that have a :projectId param matching the key's target project. + // This allows any /v1/projects/:projectId/* endpoint to be used during + // migration without needing per-route labels, while blocking all other routes. + if (!empty($apiKey) && !empty($apiKey->getTargetProjectId())) { + $routeProjectId = $request->getParam('projectId', ''); + if (empty($routeProjectId) || $routeProjectId !== $apiKey->getTargetProjectId()) { + throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE); + } + } + $authorization->addRole($role); foreach ($user->getRoles($authorization) as $authRole) { $authorization->addRole($authRole); diff --git a/composer.lock b/composer.lock index 70f1742418..3bc62ae96a 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": "57189be7990142e6a44a13eefdfe3043", + "content-hash": "1988c2437549ecb68bf6dfa294598a5a", "packages": [ { "name": "adhocore/jwt", @@ -4464,16 +4464,16 @@ }, { "name": "utopia-php/migration", - "version": "1.5.2", + "version": "dev-feat-platform-key-migration", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "93904948f6dd07491821615fd9b9acbcaadec12e" + "reference": "aee64c99d03bd04d2961c678bc934bd727c081ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/93904948f6dd07491821615fd9b9acbcaadec12e", - "reference": "93904948f6dd07491821615fd9b9acbcaadec12e", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/aee64c99d03bd04d2961c678bc934bd727c081ec", + "reference": "aee64c99d03bd04d2961c678bc934bd727c081ec", "shasum": "" }, "require": { @@ -4513,9 +4513,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/1.5.2" + "source": "https://github.com/utopia-php/migration/tree/feat-platform-key-migration" }, - "time": "2026-02-11T06:19:35+00:00" + "time": "2026-02-12T15:26:26+00:00" }, { "name": "utopia-php/mongo", @@ -8893,9 +8893,18 @@ "time": "2024-03-07T20:33:40+00:00" } ], - "aliases": [], + "aliases": [ + { + "package": "utopia-php/migration", + "version": "dev-feat-platform-key-migration", + "alias": "1.5.0", + "alias_normalized": "1.5.0.0" + } + ], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": { + "utopia-php/migration": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { @@ -8919,5 +8928,5 @@ "platform-overrides": { "php": "8.3" }, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.6.0" } diff --git a/src/Appwrite/Auth/Key.php b/src/Appwrite/Auth/Key.php index 5d416a6b9c..ebc072bf56 100644 --- a/src/Appwrite/Auth/Key.php +++ b/src/Appwrite/Auth/Key.php @@ -168,7 +168,13 @@ class Key $previewAuthDisabled = $payload['previewAuthDisabled'] ?? false; $deploymentStatusIgnored = $payload['deploymentStatusIgnored'] ?? false; $targetProjectId = $payload['targetProjectId'] ?? ''; - $scopes = \array_merge($payload['scopes'] ?? [], $scopes); + + // Keys with targetProjectId are restricted — only use explicit JWT scopes + if (!empty($targetProjectId)) { + $scopes = $payload['scopes'] ?? []; + } else { + $scopes = \array_merge($payload['scopes'] ?? [], $scopes); + } if (!$projectCheckDisabled && $projectId !== $project->getId()) { return $guestKey;