diff --git a/app/controllers/api/migrations.php b/app/controllers/api/migrations.php index 95501c3b98..dd5dc26ff4 100644 --- a/app/controllers/api/migrations.php +++ b/app/controllers/api/migrations.php @@ -697,7 +697,7 @@ Http::get('/v1/migrations/:migrationId') Http::get('/v1/migrations/appwrite/console-key') ->groups(['api', 'migrations']) ->desc('Generate console API key for migration') - ->label('scope', 'migrations.read') + ->label('scope', 'migrations.write') ->label('sdk', new Method( namespace: 'migrations', group: null, diff --git a/tests/e2e/Services/Migrations/MigrationsBase.php b/tests/e2e/Services/Migrations/MigrationsBase.php index 33d9fe3f13..963d517c3f 100644 --- a/tests/e2e/Services/Migrations/MigrationsBase.php +++ b/tests/e2e/Services/Migrations/MigrationsBase.php @@ -1173,13 +1173,32 @@ trait MigrationsBase /** * Integrations */ - public function testAppwriteMigrationPlatform(): void + public function testGetAppwriteConsoleKey(): void { - // Create platform on source project - $response = $this->client->call(Client::METHOD_POST, '/projects/' . $this->getProject()['$id'] . '/platforms', array_merge([ + $response = $this->client->call(Client::METHOD_GET, '/migrations/appwrite/console-key', [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['key']); + $this->assertStringStartsWith('dynamic_', $response['body']['key']); + $this->assertNotEmpty($response['body']['expire']); + $this->assertGreaterThan(new \DateTime(), new \DateTime($response['body']['expire'])); + } + + public function testAppwriteMigrationPlatform(): void + { + $consoleSessionHeaders = [ + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + 'origin' => 'http://localhost', + 'cookie' => 'a_session_console=' . $this->getRoot()['session'], + ]; + + // Create platform on source project + $response = $this->client->call(Client::METHOD_POST, '/projects/' . $this->getProject()['$id'] . '/platforms', $consoleSessionHeaders, [ 'type' => 'web', 'name' => 'Test Platform', 'hostname' => 'localhost', @@ -1209,11 +1228,22 @@ trait MigrationsBase $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_PLATFORM]['processing']); $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_PLATFORM]['warning']); - // Verify platform on destination project - $response = $this->client->call(Client::METHOD_GET, '/projects/' . $this->getDestinationProject()['$id'] . '/platforms', array_merge([ + // Get a console key for the destination project to access console-scoped endpoints + $consoleKeyResponse = $this->client->call(Client::METHOD_GET, '/migrations/appwrite/console-key', [ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], + ]); + + $this->assertEquals(200, $consoleKeyResponse['headers']['status-code']); + $destConsoleKey = $consoleKeyResponse['body']['key']; + + // Verify platform on destination project using console key + $response = $this->client->call(Client::METHOD_GET, '/projects/' . $this->getDestinationProject()['$id'] . '/platforms', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + 'x-appwrite-key' => $destConsoleKey, + ]); $this->assertEquals(200, $response['headers']['status-code']); $this->assertNotEmpty($response['body']); @@ -1234,17 +1264,15 @@ trait MigrationsBase $this->assertEquals('Test Platform', $foundPlatform['name']); $this->assertEquals('localhost', $foundPlatform['hostname']); - // Cleanup on destination - $this->client->call(Client::METHOD_DELETE, '/projects/' . $this->getDestinationProject()['$id'] . '/platforms/' . $foundPlatform['$id'], array_merge([ + // Cleanup on destination using console key + $this->client->call(Client::METHOD_DELETE, '/projects/' . $this->getDestinationProject()['$id'] . '/platforms/' . $foundPlatform['$id'], [ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); + 'x-appwrite-project' => 'console', + 'x-appwrite-key' => $destConsoleKey, + ]); - // Cleanup on source - $this->client->call(Client::METHOD_DELETE, '/projects/' . $this->getProject()['$id'] . '/platforms/' . $platform['$id'], array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); + // Cleanup on source using console project + session auth + $this->client->call(Client::METHOD_DELETE, '/projects/' . $this->getProject()['$id'] . '/platforms/' . $platform['$id'], $consoleSessionHeaders); } /** diff --git a/tests/unit/Auth/KeyTest.php b/tests/unit/Auth/KeyTest.php index fc1779efad..8e3f560bc9 100644 --- a/tests/unit/Auth/KeyTest.php +++ b/tests/unit/Auth/KeyTest.php @@ -49,6 +49,7 @@ class KeyTest extends TestCase 'projectCheckDisabled' => true, 'previewAuthDisabled' => true, 'deploymentStatusIgnored' => true, + 'source' => KEY_SOURCE_MIGRATION, ]; $key = static::generateKey($projectId, $usage, $scopes, extra: $extra); $decoded = Key::decode( @@ -70,6 +71,28 @@ class KeyTest extends TestCase $this->assertEquals(true, $decoded->isProjectCheckDisabled()); $this->assertEquals(true, $decoded->isPreviewAuthDisabled()); $this->assertEquals(true, $decoded->isDeploymentStatusIgnored()); + $this->assertEquals(KEY_SOURCE_MIGRATION, $decoded->getSource()); + + // Decode dynamic key with scopedProjectId — scopes must NOT be merged with role scopes + $scopedProjectId = 'scoped-project-123'; + $extra = [ + 'scopedProjectId' => $scopedProjectId, + 'source' => KEY_SOURCE_MIGRATION, + ]; + $key = static::generateKey('console', $usage, $scopes, extra: $extra); + $decoded = Key::decode( + project: new Document(['$id' => 'console']), + team: new Document(), + user: new Document(), + key: $key, + ); + $this->assertEquals('console', $decoded->getProjectId()); + $this->assertEquals(API_KEY_DYNAMIC, $decoded->getType()); + $this->assertEquals(User::ROLE_APPS, $decoded->getRole()); + $this->assertEquals($scopes, $decoded->getScopes()); + $this->assertNotEquals(\array_merge($scopes, $roleScopes), $decoded->getScopes()); + $this->assertEquals($scopedProjectId, $decoded->getScopedProjectId()); + $this->assertEquals(KEY_SOURCE_MIGRATION, $decoded->getSource()); // Decode invalid dynamic key $invalidKey = API_KEY_DYNAMIC . '_invalid_jwt_token';