diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 51d141dc27..94244b6962 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -1,6 +1,7 @@ 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') - ->action(function (string $projectId, array $queries, bool $includeTotal, Response $response, Database $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); + } $project = $dbForPlatform->getDocument('projects', $projectId); @@ -1240,7 +1246,12 @@ Http::get('/v1/projects/:projectId/keys/:keyId') ->param('keyId', '', new UID(), 'Key unique ID.') ->inject('response') ->inject('dbForPlatform') - ->action(function (string $projectId, string $keyId, Response $response, Database $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); + } $project = $dbForPlatform->getDocument('projects', $projectId); @@ -1507,7 +1518,12 @@ 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') - ->action(function (string $projectId, bool $includeTotal, Response $response, Database $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); + } $project = $dbForPlatform->getDocument('projects', $projectId); @@ -1547,7 +1563,12 @@ Http::get('/v1/projects/:projectId/platforms/:platformId') ->param('platformId', '', new UID(), 'Platform unique ID.') ->inject('response') ->inject('dbForPlatform') - ->action(function (string $projectId, string $platformId, Response $response, Database $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); + } $project = $dbForPlatform->getDocument('projects', $projectId); diff --git a/src/Appwrite/Auth/Key.php b/src/Appwrite/Auth/Key.php index 8f645f6f08..5d416a6b9c 100644 --- a/src/Appwrite/Auth/Key.php +++ b/src/Appwrite/Auth/Key.php @@ -28,6 +28,7 @@ class Key protected bool $projectCheckDisabled = false, protected bool $previewAuthDisabled = false, protected bool $deploymentStatusIgnored = false, + protected string $targetProjectId = '', ) { } @@ -103,6 +104,11 @@ class Key return $this->projectCheckDisabled; } + public function getTargetProjectId(): string + { + return $this->targetProjectId; + } + /** * Decode the given secret key into a Key object, containing the project ID, type, role, scopes, and name. * Can be a stored API key or a dynamic key (JWT). @@ -161,6 +167,7 @@ class Key $projectCheckDisabled = $payload['projectCheckDisabled'] ?? false; $previewAuthDisabled = $payload['previewAuthDisabled'] ?? false; $deploymentStatusIgnored = $payload['deploymentStatusIgnored'] ?? false; + $targetProjectId = $payload['targetProjectId'] ?? ''; $scopes = \array_merge($payload['scopes'] ?? [], $scopes); if (!$projectCheckDisabled && $projectId !== $project->getId()) { @@ -181,7 +188,8 @@ class Key $bannerDisabled, $projectCheckDisabled, $previewAuthDisabled, - $deploymentStatusIgnored + $deploymentStatusIgnored, + $targetProjectId ); case API_KEY_STANDARD: $key = $project->find( diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 2ebbf80edd..fa0cb6ddb5 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -333,7 +333,7 @@ class Migrations extends Action * * @throws Exception */ - protected function generateConsoleAPIKey(): string + protected function generateConsoleAPIKey(string $targetProjectId): string { $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 86400, 0); @@ -343,6 +343,7 @@ class Migrations extends Action 'platforms.read', 'keys.read', ], + 'targetProjectId' => $targetProjectId, ]); return API_KEY_DYNAMIC . '_' . $apiKey; @@ -388,7 +389,7 @@ class Migrations extends Action // Only generate and set consoleApiKey for same-instance migrations // to avoid leaking a locally-signed JWT to untrusted remote servers if ($credentials['endpoint'] === $endpoint) { - $credentials['consoleApiKey'] = $this->generateConsoleAPIKey(); + $credentials['consoleApiKey'] = $this->generateConsoleAPIKey($credentials['projectId']); $credentials['sourceProjectId'] = $credentials['projectId']; } }