From 21f25b4ce4241923d52ffcefac3fc487dded330b Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Tue, 16 Sep 2025 14:16:02 +0530 Subject: [PATCH 1/8] Auto-allow sites domain for OAuth --- app/init/resources.php | 63 ++++++++++++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/app/init/resources.php b/app/init/resources.php index e4e8fbef5e..99b6e74382 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -151,7 +151,7 @@ App::setResource('queueForMigrations', function (Publisher $publisher) { App::setResource('queueForStatsResources', function (Publisher $publisher) { return new StatsResources($publisher); }, ['publisher']); -App::setResource('platforms', function (Request $request, Document $console, Document $project) { +App::setResource('platforms', function (Request $request, Document $console, Document $project, Database $dbForPlatform) { $console->setAttribute('platforms', [ // Always allow current host '$collection' => ID::custom('platforms'), 'name' => 'Current Host', @@ -190,11 +190,52 @@ App::setResource('platforms', function (Request $request, Document $console, Doc ], Document::SET_TYPE_APPEND); } + $origin = \parse_url($request->getOrigin(), PHP_URL_HOST); + + if (empty($origin)) { + $origin = \parse_url($request->getReferer(), PHP_URL_HOST); + } + + // Safe if rule with same project ID exists + if (!empty($origin)) { + if (System::getEnv('_APP_RULES_FORMAT') === 'md5') { + $rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($origin ?? ''))); + } else { + $rule = Authorization::skip( + fn () => $dbForPlatform->find('rules', [ + Query::equal('domain', [$origin]), + Query::limit(1) + ]) + )[0] ?? new Document(); + } + + var_dump($rule); + + if (!$rule->isEmpty() && $rule->getAttribute('projectInternalId') === $project->getSequence()) { + echo "inside project internal id\n"; + + $project->setAttribute('platforms', [ + '$collection' => ID::custom('platforms'), + 'type' => Platform::TYPE_WEB, + 'name' => $origin, + 'hostname' => $origin, + ], Document::SET_TYPE_APPEND); + } + } + + // Unsafe; Localhost is always safe for ease of local development + $project->setAttribute('platforms', [ + '$collection' => ID::custom('platforms'), + 'type' => Platform::TYPE_WEB, + 'name' => "localhost", + 'hostname' => "localhost", + ], Document::SET_TYPE_APPEND); + return [ ...$console->getAttribute('platforms', []), ...$project->getAttribute('platforms', []), ]; -}, ['request', 'console', 'project']); +}, ['request', 'console', 'project', 'dbForPlatform']); App::setResource('user', function ($mode, $project, $console, $request, $response, $dbForProject, $dbForPlatform) { /** @var Appwrite\Utopia\Request $request */ @@ -1005,24 +1046,6 @@ App::setResource('httpReferrerSafe', function (Request $request, string $httpRef return $referrer; } - // Safe if rule with same project ID exists - if (!empty($origin)) { - if (System::getEnv('_APP_RULES_FORMAT') === 'md5') { - $rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($origin ?? ''))); - } else { - $rule = Authorization::skip( - fn () => $dbForPlatform->find('rules', [ - Query::equal('domain', [$origin]), - Query::limit(1) - ]) - )[0] ?? new Document(); - } - - if (!$rule->isEmpty() && $rule->getAttribute('projectInternalId') === $project->getSequence()) { - return $referrer; - } - } - // Unsafe; Localhost is always safe for ease of local development $origin = 'localhost'; $protocol = \parse_url($request->getOrigin($httpReferrer), PHP_URL_SCHEME); From dedc7a35c96f3e4a7bc89ff363fabf2d35e5be86 Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Thu, 18 Sep 2025 12:38:18 +0530 Subject: [PATCH 2/8] remove debug output --- app/init/resources.php | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/app/init/resources.php b/app/init/resources.php index 99b6e74382..4b5452eb46 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -212,8 +212,6 @@ App::setResource('platforms', function (Request $request, Document $console, Doc var_dump($rule); if (!$rule->isEmpty() && $rule->getAttribute('projectInternalId') === $project->getSequence()) { - echo "inside project internal id\n"; - $project->setAttribute('platforms', [ '$collection' => ID::custom('platforms'), 'type' => Platform::TYPE_WEB, @@ -416,7 +414,7 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform if (\in_array($dsn->getHost(), $sharedTables)) { $database ->setSharedTables(true) - ->setTenant((int)$project->getSequence()) + ->setTenant((int) $project->getSequence()) ->setNamespace($dsn->getParam('namespace')); } else { $database @@ -469,7 +467,7 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform if (\in_array($dsn->getHost(), $sharedTables)) { $database ->setSharedTables(true) - ->setTenant((int)$project->getSequence()) + ->setTenant((int) $project->getSequence()) ->setNamespace($dsn->getParam('namespace')); } else { $database @@ -499,7 +497,7 @@ App::setResource('getLogsDB', function (Group $pools, Cache $cache) { return function (?Document $project = null) use ($pools, $cache, &$database) { if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') { - $database->setTenant((int)$project->getSequence()); + $database->setTenant((int) $project->getSequence()); return $database; } @@ -514,7 +512,7 @@ App::setResource('getLogsDB', function (Group $pools, Cache $cache) { // set tenant if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') { - $database->setTenant((int)$project->getSequence()); + $database->setTenant((int) $project->getSequence()); } return $database; @@ -542,7 +540,7 @@ App::setResource('redis', function () { $pass = System::getEnv('_APP_REDIS_PASS', ''); $redis = new \Redis(); - @$redis->pconnect($host, (int)$port); + @$redis->pconnect($host, (int) $port); if ($pass) { $redis->auth($pass); } @@ -755,7 +753,7 @@ App::setResource('schema', function ($utopia, $dbForProject) { // NOTE: `params` and `urls` are not used internally in the `Schema::build` function below! $params = [ 'list' => function (string $databaseId, string $collectionId, array $args) { - return [ 'queries' => $args['queries']]; + return ['queries' => $args['queries']]; }, 'create' => function (string $databaseId, string $collectionId, array $args) { $id = $args['id'] ?? 'unique()'; @@ -1004,7 +1002,7 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) { } $accessedAt = $token->getAttribute('accessedAt', 0); - if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), - APP_RESOURCE_TOKEN_ACCESS)) > $accessedAt) { + if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_RESOURCE_TOKEN_ACCESS)) > $accessedAt) { $token->setAttribute('accessedAt', DatabaseDateTime::now()); Authorization::skip(fn () => $dbForProject->updateDocument('resourceTokens', $token->getId(), $token)); } From 5fd2dfafb72ab4500cc4e2d3b0279a3bb21583af Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Thu, 18 Sep 2025 14:10:56 +0530 Subject: [PATCH 3/8] remove var_dump --- app/init/resources.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/init/resources.php b/app/init/resources.php index 4b5452eb46..211bc52a8e 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -209,8 +209,6 @@ App::setResource('platforms', function (Request $request, Document $console, Doc )[0] ?? new Document(); } - var_dump($rule); - if (!$rule->isEmpty() && $rule->getAttribute('projectInternalId') === $project->getSequence()) { $project->setAttribute('platforms', [ '$collection' => ID::custom('platforms'), From 2f1046db2b1f63b1fd7bd7989d1b20ec74e86d10 Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Mon, 22 Sep 2025 12:10:55 +0530 Subject: [PATCH 4/8] fix tests --- app/init/resources.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/init/resources.php b/app/init/resources.php index 211bc52a8e..16307edee3 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -219,14 +219,6 @@ App::setResource('platforms', function (Request $request, Document $console, Doc } } - // Unsafe; Localhost is always safe for ease of local development - $project->setAttribute('platforms', [ - '$collection' => ID::custom('platforms'), - 'type' => Platform::TYPE_WEB, - 'name' => "localhost", - 'hostname' => "localhost", - ], Document::SET_TYPE_APPEND); - return [ ...$console->getAttribute('platforms', []), ...$project->getAttribute('platforms', []), From 11bbe3c6ddc9f981186645e54d8f9179393d7fba Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Oct 2025 18:17:35 +0530 Subject: [PATCH 5/8] fix: test file --- .../functions/log-error-truncation/index.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/resources/functions/log-error-truncation/index.js b/tests/resources/functions/log-error-truncation/index.js index 7e94fa5028..35747095f5 100644 --- a/tests/resources/functions/log-error-truncation/index.js +++ b/tests/resources/functions/log-error-truncation/index.js @@ -2,8 +2,21 @@ module.exports = async(context) => { // Create a string that is 1000001 characters long (exceeds the 1000000 limit) const longString = 'z' + 'a'.repeat(1000000); - context.log(longString); - context.error(longString); + // Split the string into chunks of 8000 characters (max limit for each log and error) + const chunkSize = 8000; + const chunks = []; + + for (let i = 0; i < longString.length; i += chunkSize) { + chunks.push(longString.slice(i, i + chunkSize)); + } + + chunks.forEach((chunk, index) => { + context.log(chunk); + }); + + chunks.forEach((chunk, index) => { + context.error(chunk); + }); return context.res.json({ motto: 'Build like a team of hundreds_', From 813bc0d9b331df653c3a9f5d41fc4bb465997909 Mon Sep 17 00:00:00 2001 From: Darshan Date: Wed, 1 Oct 2025 18:25:32 +0530 Subject: [PATCH 6/8] fix: wrong audit resource. --- .../Http/Databases/Collections/Documents/Logs/XList.php | 5 +---- .../Databases/Http/Databases/Collections/Logs/XList.php | 6 +----- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Logs/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Logs/XList.php index 93c8639520..ed9378f644 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Logs/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Logs/XList.php @@ -108,10 +108,7 @@ class XList extends Action $audit = new Audit($dbForProject); $type = $this->getCollectionsEventsContext(); $context = $this->getContext(); - $resource = match ($context) { - ROWS => "database/$databaseId/grid/$type/$collectionId/$context/{$document->getId()}", - default => "database/$databaseId/$type/$collectionId/$context/{$document->getId()}", - }; + $resource = "database/$databaseId/$type/$collectionId/$context/{$document->getId()}"; $logs = $audit->getLogsByResource($resource, $queries); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Logs/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Logs/XList.php index 1309793234..fefc5ba5a0 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Logs/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Logs/XList.php @@ -104,11 +104,7 @@ class XList extends Action $audit = new Audit($dbForProject); $context = $this->getContext(); - $resource = match ($context) { - TABLES => "database/$databaseId/grid/$context/$collectionId", - default => "database/$databaseId/$context/$collectionId", - }; - + $resource = "database/$databaseId/$context/$collectionId"; $logs = $audit->getLogsByResource($resource, $queries); $output = []; From ca1c069e65d38a269111cf8f29504a8fd1a61bf4 Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Thu, 2 Oct 2025 14:58:55 +0530 Subject: [PATCH 7/8] Handle OIDC well-known endpoint errors --- src/Appwrite/Auth/OAuth2/Oidc.php | 3 + .../Account/AccountCustomClientTest.php | 71 +++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/src/Appwrite/Auth/OAuth2/Oidc.php b/src/Appwrite/Auth/OAuth2/Oidc.php index 670169fe89..c9810c48eb 100644 --- a/src/Appwrite/Auth/OAuth2/Oidc.php +++ b/src/Appwrite/Auth/OAuth2/Oidc.php @@ -273,6 +273,9 @@ class Oidc extends OAuth2 { if (empty($this->wellKnownConfiguration)) { $response = $this->request('GET', $this->getWellKnownEndpoint()); + if (empty($response)) { + throw new Exception('Invalid well-known configuration'); + } $this->wellKnownConfiguration = \json_decode($response, true); } diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php index bd3fec8439..8322508cf6 100644 --- a/tests/e2e/Services/Account/AccountCustomClientTest.php +++ b/tests/e2e/Services/Account/AccountCustomClientTest.php @@ -1539,6 +1539,77 @@ class AccountCustomClientTest extends Scope return []; } + public function testCreateOidcOAuth2Token(): array + { + $provider = 'oidc'; + $appId = '1'; + + // Valid well-known configuration + $secret = '{ + "wellKnownEndpoint": "https://accounts.google.com/.well-known/openid-configuration", + "authorizationEndpoint": "https://accounts.google.com/o/oauth2/v2/auth", + "tokenEndpoint": "https://oauth2.googleapis.com/token", + "userinfoEndpoint": "https://openidconnect.googleapis.com/v1/userinfo" + }'; + + $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $this->getProject()['$id'] . '/oauth2', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + 'cookie' => 'a_session_console=' . $this->getRoot()['session'], + ]), [ + 'provider' => $provider, + 'appId' => $appId, + 'secret' => $secret, + 'enabled' => true, + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_GET, '/account/tokens/oauth2/' . $provider, array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'provider' => $provider, + 'success' => 'http://localhost/v1/mock/tests/general/oauth2/success', + 'failure' => 'http://localhost/v1/mock/tests/general/oauth2/failure', + ], true, false); + + $this->assertEquals(301, $response['headers']['status-code']); + + // Invalid well-known configuration + $secret = '{}'; + + $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $this->getProject()['$id'] . '/oauth2', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + 'cookie' => 'a_session_console=' . $this->getRoot()['session'], + ]), [ + 'provider' => $provider, + 'appId' => $appId, + 'secret' => $secret, + 'enabled' => true, + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_GET, '/account/tokens/oauth2/' . $provider, array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'provider' => $provider, + 'success' => 'http://localhost/v1/mock/tests/general/oauth2/success', + 'failure' => 'http://localhost/v1/mock/tests/general/oauth2/failure', + ]); + + $this->assertEquals(500, $response['headers']['status-code']); + + return []; + } + public function testBlockedAccount(): array { $email = uniqid() . 'user@localhost.test'; From 76d3dc3d5a12e8d4223ba82ad27d98de15980e3c Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 2 Oct 2025 18:27:20 +0530 Subject: [PATCH 8/8] * added senstive var copying in `from` method * added tests --- src/Appwrite/Event/Event.php | 1 + .../Realtime/RealtimeCustomClientTest.php | 151 ++++++++++++++++++ 2 files changed, 152 insertions(+) diff --git a/src/Appwrite/Event/Event.php b/src/Appwrite/Event/Event.php index e9f3ccc2a2..16fe76bf8a 100644 --- a/src/Appwrite/Event/Event.php +++ b/src/Appwrite/Event/Event.php @@ -592,6 +592,7 @@ class Event $this->project = $event->getProject(); $this->user = $event->getUser(); $this->payload = $event->getPayload(); + $this->sensitive = $event->sensitive; $this->event = $event->getEvent(); $this->params = $event->getParams(); $this->context = $event->context; diff --git a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php index 3e57c5e9bc..ef39908658 100644 --- a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php +++ b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php @@ -2507,4 +2507,155 @@ class RealtimeCustomClientTest extends Scope $client->close(); } + + public function testRelationshipPayloadHidesRelatedDoc() + { + $user = $this->getUser(); + $session = $user['session'] ?? ''; + $projectId = $this->getProject()['$id']; + + $client = $this->getWebsocket(['documents'], [ + 'origin' => 'http://localhost', + 'cookie' => 'a_session_' . $projectId . '=' . $session + ]); + + $response = json_decode($client->receive(), true); + $this->assertArrayHasKey('type', $response); + $this->assertEquals('connected', $response['type']); + + // Create database + $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'db-rel' + ]); + $databaseId = $database['body']['$id']; + + $level1 = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'level1', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'documentSecurity' => true, + ]); + $level1Id = $level1['body']['$id']; + + $level2 = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'level2', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'documentSecurity' => true, + ]); + $level2Id = $level2['body']['$id']; + + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$level1Id}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => false, + ]); + + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$level2Id}/attributes/string", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => false, + ]); + + sleep(2); + + // two-way one-to-one relationship from level1 to level2 + $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$level1Id}/attributes/relationship", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'relatedCollectionId' => $level2Id, + 'type' => 'oneToOne', + 'twoWay' => true, + 'key' => 'level2Ref', + 'onDelete' => 'cascade', + ]); + + sleep(2); + + $doc2 = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$level2Id}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ], $this->getHeaders()), [ + 'documentId' => ID::unique(), + 'data' => [ 'name' => 'L2' ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + $doc2Id = $doc2['body']['$id']; + + $doc1 = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$level1Id}/documents", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ], $this->getHeaders()), [ + 'documentId' => ID::unique(), + 'data' => [ 'name' => 'L1' ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + $doc1Id = $doc1['body']['$id']; + + json_decode($client->receive(), true); + + $this->client->call(Client::METHOD_PATCH, "/databases/{$databaseId}/collections/{$level1Id}/documents/{$doc1Id}", array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'level2Ref' => $doc2Id, + ], + ]); + + // payload should not contain the relationship attribute 'level2Ref' + $event = json_decode($client->receive(), true); + $this->assertArrayHasKey('type', $event); + $this->assertEquals('event', $event['type']); + $this->assertArrayHasKey('data', $event); + $this->assertNotEmpty($event['data']); + $this->assertArrayHasKey('payload', $event['data']); + $this->assertArrayHasKey('$id', $event['data']['payload']); + $this->assertEquals($doc1Id, $event['data']['payload']['$id']); + $this->assertArrayNotHasKey('level2Ref', $event['data']['payload']); + + $client->close(); + } }