Add targetProjectId to console API key for project-scoped access

The console key with projectId='console' grants access to all projects'
platforms and keys. targetProjectId restricts it to only the migration
source project, enforced at the 4 read endpoints (listKeys, getKey,
listPlatforms, getPlatform).
This commit is contained in:
Prem Palanisamy
2026-02-12 17:43:28 +00:00
parent 3a813d8554
commit 06c6fac915
3 changed files with 37 additions and 7 deletions
+25 -4
View File
@@ -1,6 +1,7 @@
<?php
use Ahc\Jwt\JWT;
use Appwrite\Auth\Key;
use Appwrite\Auth\Validator\MockNumber;
use Appwrite\Event\Delete;
use Appwrite\Event\Mail;
@@ -1168,7 +1169,12 @@ 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')
->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);
+9 -1
View File
@@ -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(
+3 -2
View File
@@ -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'];
}
}