diff --git a/.env b/.env index 1ca5d5fc4e..746d6677ce 100644 --- a/.env +++ b/.env @@ -25,6 +25,7 @@ _APP_OPENSSL_KEY_V1=your-secret-key _APP_DNS=172.16.238.100 # CoreDNS _APP_DOMAIN=appwrite.test _APP_CONSOLE_DOMAIN=localhost +_APP_CONSOLE_TRUSTED_PROJECTS=trusted-project,another-trusted-project _APP_DOMAIN_FUNCTIONS=functions.localhost _APP_DOMAIN_SITES=sites.localhost,rebranded.localhost _APP_DOMAIN_TARGET_CNAME=cname.localhost diff --git a/app/init/resources.php b/app/init/resources.php index 163a26c876..ccbb703f50 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -253,7 +253,24 @@ Http::setResource('rule', function (Request $request, Database $dbForPlatform, D ]) ?? new Document(); }); - if ($rule->getAttribute('projectInternalId') !== $project->getSequence()) { + $permitsCurrentProject = $rule->getAttribute('projectInternalId', '') === $project->getSequence(); + + // Temporary implementation until custom wildcard domains are an official feature + // Allow trusted projects; Used for Console (website) previews + if (!$permitsCurrentProject && !$rule->isEmpty() && !empty($rule->getAttribute('projectId', ''))) { + $trustedProjects = []; + foreach (\explode(',', System::getEnv('_APP_CONSOLE_TRUSTED_PROJECTS', '')) as $trustedProject) { + if (empty($trustedProject)) { + continue; + } + $trustedProjects[] = $trustedProject; + } + if (\in_array($rule->getAttribute('projectId', ''), $trustedProjects)) { + $permitsCurrentProject = true; + } + } + + if (!$permitsCurrentProject) { return new Document(); } diff --git a/docker-compose.yml b/docker-compose.yml index 0eee94a999..cc761f20c3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -133,6 +133,7 @@ services: - _APP_OPENSSL_KEY_V1 - _APP_DOMAIN - _APP_CONSOLE_DOMAIN + - _APP_CONSOLE_TRUSTED_PROJECTS - _APP_DOMAIN_TARGET_CNAME - _APP_DOMAIN_TARGET_AAAA - _APP_DOMAIN_TARGET_A @@ -510,6 +511,7 @@ services: - _APP_OPTIONS_ROUTER_FORCE_HTTPS - _APP_DOMAIN - _APP_CONSOLE_DOMAIN + - _APP_CONSOLE_TRUSTED_PROJECTS - _APP_STORAGE_DEVICE - _APP_STORAGE_S3_ACCESS_KEY - _APP_STORAGE_S3_SECRET diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index ff917dea5c..559ffe9f1d 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -5173,6 +5173,68 @@ class ProjectsConsoleClientTest extends Scope $this->assertEquals($origin, $response['headers']['access-control-allow-origin'] ?? null); } + public function testConsoleCorsWithTrustedProject(): void + { + $trustedProjectIds = ['trusted-project', 'another-trusted-project']; // Set in env variable + + $projectIds = \array_merge($trustedProjectIds, ['untrusted-project-id']); + + foreach ($projectIds as $projectId) { + try { + // Create project + $this->setupProject([ + 'projectId' => $projectId, + 'name' => 'Trusted project', + 'region' => System::getEnv('_APP_REGION', 'default') + ]); + + // Add domain to trusted project; API for simplicity, in real work this will be site + $domain = \uniqid() . '.custom.localhost'; + $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-mode' => 'admin', + ], $this->getHeaders()), [ + 'domain' => $domain + ]); + + $this->assertEquals(201, $rule['headers']['status-code']); + + // Talk to Console APIs from trusted project domain + $currencies = $this->client->call( + Client::METHOD_GET, + '/locale/currencies', + array_merge( + $this->getHeaders(), + [ + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + 'origin' => 'http://' . $domain + ] + ) + ); + + if (\in_array($projectId, $trustedProjectIds)) { + // Trusted projects can + $this->assertEquals(200, $currencies['headers']['status-code']); + $this->assertSame('http://' . $domain, $currencies['headers']['access-control-allow-origin']); + } else { + // Untrusted projects cannot + $this->assertEquals(403, $currencies['headers']['status-code']); + $this->assertArrayNotHasKey('access-control-allow-origin', $currencies['headers']); + } + } finally { + // Cleanup + $response = $this->client->call(Client::METHOD_DELETE, '/projects/' . $projectId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(204, $response['headers']['status-code']); + } + } + } + /** * @group abuseEnabled */