Compare commits

...
Author SHA1 Message Date
Prem Palanisamy 1e2a6b5b2f Merge 1.8.x into feat-platform-key-migration 2026-02-13 06:32:10 +00:00
Prem Palanisamy 50cf415359 Fix targetProjectId check: use route path values instead of request params 2026-02-12 22:39:11 +00:00
Prem Palanisamy 4d46879497 Fix settings-key endpoint: use migrations.read scope and hardcode console JWT scopes 2026-02-12 22:26:56 +00:00
Prem Palanisamy ad4fdea0a3 Restore blank line after action closure in report endpoint 2026-02-12 22:08:46 +00:00
Prem Palanisamy 596498896e Remove unnecessary scopes from temp key, add settings-key docs 2026-02-12 22:06:28 +00:00
Prem Palanisamy 2b7d915578 Restore blank lines after action closure openings 2026-02-12 21:43:39 +00:00
Prem Palanisamy d9772e29cc Remove stray blank lines from projects.php endpoints 2026-02-12 21:34:14 +00:00
Prem Palanisamy 455c292313 Remove redundant scope checks, make targetProjectId mandatory 2026-02-12 20:53:29 +00:00
Prem Palanisamy 97fb4cb4b6 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
2026-02-12 20:39:05 +00:00
Prem Palanisamy 0cee469d73 Pass all source project API key scopes to console key 2026-02-12 18:30:59 +00:00
Prem Palanisamy ba723de735 Add settings-key endpoint, move console key generation to source 2026-02-12 18:23:44 +00:00
Prem Palanisamy 6565f2ff91 Derive console API key scopes from project API key
Instead of hardcoding scopes in the console key, read them from the
project API key. Added platforms.read and keys.read to the project
key, and the console key now receives only scopes that the project
key already has.
2026-02-12 17:54:42 +00:00
Prem Palanisamy 06c6fac915 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).
2026-02-12 17:43:28 +00:00
Prem Palanisamy 3a813d8554 Revert targetProjectId scoping from console API key
The same-instance guard is sufficient security — targetProjectId
added unnecessary complexity since projectId already exists in the JWT.
2026-02-12 17:37:55 +00:00
Prem Palanisamy e6d4f4e3a1 Scope console API key to target project via targetProjectId claim 2026-02-12 17:06:06 +00:00
Prem Palanisamy d1170836b8 Only generate consoleApiKey for same-instance migrations 2026-02-12 16:40:20 +00:00
Prem Palanisamy 86ea75026f Guard consoleApiKey to same-instance migrations only
Revert controller to original form without console API key generation
to avoid leaking JWT to untrusted remote endpoints. In the worker,
only set consoleApiKey when the source endpoint matches the local
endpoint, ensuring the locally-signed JWT is never sent cross-instance.
2026-02-12 16:30:57 +00:00
Prem Palanisamy 4350bda137 Use console API key for report endpoint instead of dbForPlatform 2026-02-12 15:49:08 +00:00
Prem Palanisamy 01958a0f4e Move settings migration to source adapter via console API key 2026-02-12 15:26:29 +00:00
Prem Palanisamy b338ed9a2c Pass source document permissions to migrated resources 2026-02-12 12:39:52 +00:00
Prem Palanisamy 5a557e5602 Fix resourceType from 'project' to 'projects' for key queries 2026-02-12 12:35:20 +00:00
Prem Palanisamy ed9d64b25f Use unique IDs for migrated platforms and keys
Source and destination share dbForPlatform, so reusing the source
document IDs causes duplicate key errors.
2026-02-12 12:24:45 +00:00
Prem Palanisamy 28061ccb21 Move platform/key migration from source adapter to worker level
Settings (platforms, keys) live in dbForPlatform. Instead of passing
dbForPlatform to the source adapter, the worker reads from the source
project directly and writes via the destination adapter.
2026-02-12 12:19:00 +00:00
Prem Palanisamy 854a4aca82 Add platform and key counts to migration report endpoint 2026-02-12 11:50:56 +00:00
Prem Palanisamy 29111e5dea Add platform and key migration support
Wire dbForPlatform and projectInternalId to migration source/destination.
Point migration dependency to dev branch with settings support.
2026-02-12 10:24:13 +00:00
8 changed files with 108 additions and 19 deletions
+39
View File
@@ -1,5 +1,6 @@
<?php
use Ahc\Jwt\JWT;
use Appwrite\Event\Event;
use Appwrite\Event\Migration;
use Appwrite\Extend\Exception;
@@ -691,6 +692,44 @@ Http::get('/v1/migrations/:migrationId')
$response->dynamic($migration, Response::MODEL_MIGRATION);
});
Http::get('/v1/migrations/appwrite/settings-key')
->groups(['api', 'migrations'])
->desc('Generate console API key for settings migration')
->label('scope', 'migrations.read')
->label('sdk', new Method(
namespace: 'migrations',
group: null,
name: 'getAppwriteSettingsKey',
description: '/docs/references/migrations/migration-appwrite-settings-key.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_NONE,
)
]
))
->inject('response')
->inject('project')
->action(function (Response $response, Document $project) {
$targetProjectId = $project->getId();
if (empty($targetProjectId)) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 120, 0);
$consoleKey = $jwt->encode([
'projectId' => 'console',
'scopes' => ['platforms.read', 'keys.read'],
'targetProjectId' => $targetProjectId,
]);
$response
->setStatusCode(Response::STATUS_CODE_OK)
->json(['key' => API_KEY_DYNAMIC . '_' . $consoleKey]);
});
Http::get('/v1/migrations/appwrite/report')
->groups(['api', 'migrations'])
->desc('Get Appwrite migration report')
+12
View File
@@ -273,6 +273,18 @@ 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())) {
$pathValues = $route->getPathValues($request);
$routeProjectId = $pathValues['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);
+1 -1
View File
@@ -65,7 +65,7 @@
"utopia-php/locale": "0.8.*",
"utopia-php/logger": "0.6.*",
"utopia-php/messaging": "0.20.*",
"utopia-php/migration": "1.5.*",
"utopia-php/migration": "dev-feat-platform-key-migration as 1.5.0",
"utopia-php/platform": "0.7.*",
"utopia-php/pools": "1.*",
"utopia-php/preloader": "0.2.*",
Generated
+25 -16
View File
@@ -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": "bc64aa37fc3ab6fa2acf7ac8f5456e9f",
"content-hash": "2a3d7f252197be6405480a267c738ea8",
"packages": [
{
"name": "adhocore/jwt",
@@ -3797,16 +3797,16 @@
},
{
"name": "utopia-php/database",
"version": "5.1.1",
"version": "5.1.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "94815bfa605282096272625827d0314f9ed99066"
"reference": "1530a1c7608daf31f412f8f9a487a3b2eadd83af"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/94815bfa605282096272625827d0314f9ed99066",
"reference": "94815bfa605282096272625827d0314f9ed99066",
"url": "https://api.github.com/repos/utopia-php/database/zipball/1530a1c7608daf31f412f8f9a487a3b2eadd83af",
"reference": "1530a1c7608daf31f412f8f9a487a3b2eadd83af",
"shasum": ""
},
"require": {
@@ -3849,9 +3849,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/5.1.1"
"source": "https://github.com/utopia-php/database/tree/5.1.2"
},
"time": "2026-02-12T11:44:58+00:00"
"time": "2026-02-13T01:41:26+00:00"
},
{
"name": "utopia-php/detector",
@@ -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": "d4d2b891bd3a09919c5f93330bb5380250dad43b"
},
"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/d4d2b891bd3a09919c5f93330bb5380250dad43b",
"reference": "d4d2b891bd3a09919c5f93330bb5380250dad43b",
"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-12T22:46:47+00:00"
},
{
"name": "utopia-php/mongo",
@@ -8887,9 +8887,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": {
@@ -8913,5 +8922,5 @@
"platform-overrides": {
"php": "8.3"
},
"plugin-api-version": "2.9.0"
"plugin-api-version": "2.6.0"
}
@@ -0,0 +1 @@
Generate a short-lived console-scoped API key for settings migration. This key allows the migration source to access project platforms and keys on the source instance.
+16 -2
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,7 +167,14 @@ class Key
$projectCheckDisabled = $payload['projectCheckDisabled'] ?? false;
$previewAuthDisabled = $payload['previewAuthDisabled'] ?? false;
$deploymentStatusIgnored = $payload['deploymentStatusIgnored'] ?? false;
$scopes = \array_merge($payload['scopes'] ?? [], $scopes);
$targetProjectId = $payload['targetProjectId'] ?? '';
// 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;
@@ -181,7 +194,8 @@ class Key
$bannerDisabled,
$projectCheckDisabled,
$previewAuthDisabled,
$deploymentStatusIgnored
$deploymentStatusIgnored,
$targetProjectId
);
case API_KEY_STANDARD:
$key = $project->find(
@@ -246,6 +246,8 @@ class Migrations extends Action
$credentials['destinationApiKey'],
$this->dbForProject,
Config::getParam('collections', [])['databases']['collections'],
$this->dbForPlatform,
$this->project->getSequence(),
),
DestinationCSV::getName() => new DestinationCSV(
$this->deviceForFiles,
@@ -53,6 +53,18 @@ class MigrationReport extends Model
'default' => 0,
'example' => 20,
])
->addRule(Resource::TYPE_PLATFORM, [
'type' => self::TYPE_INTEGER,
'description' => 'Number of platforms to be migrated.',
'default' => 0,
'example' => 5,
])
->addRule(Resource::TYPE_KEY, [
'type' => self::TYPE_INTEGER,
'description' => 'Number of API keys to be migrated.',
'default' => 0,
'example' => 10,
])
->addRule('size', [
'type' => self::TYPE_INTEGER,
'description' => 'Size of files to be migrated in mb.',