mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Restrict migration console key with targetProjectId enforcement
- Short-lived JWT (120s TTL) for console key - No APPS scope merge when targetProjectId is set in Key.php - Generalized route restriction: migration keys only work on routes with matching :projectId param - Remove per-endpoint migrationKey labels and targetProjectId checks
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?php
|
||||
|
||||
use Ahc\Jwt\JWT;
|
||||
use Appwrite\Auth\Key;
|
||||
use Appwrite\Auth\Validator\MockNumber;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Mail;
|
||||
@@ -1151,6 +1150,7 @@ Http::get('/v1/projects/:projectId/keys')
|
||||
->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()) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Generated
+19
-10
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user