From 421a9974affd8c125f0f6a3d2cd8648df7291b62 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 14 May 2026 15:12:53 +0530 Subject: [PATCH 1/2] Delete orphaned proxy rules on create --- .../Platform/Modules/Proxy/Action.php | 62 +++++++++++++++++++ .../Modules/Proxy/Http/Rules/API/Create.php | 7 +-- .../Proxy/Http/Rules/Function/Create.php | 7 +-- .../Proxy/Http/Rules/Redirect/Create.php | 7 +-- .../Modules/Proxy/Http/Rules/Site/Create.php | 7 +-- tests/e2e/Services/Proxy/ProxyBase.php | 47 ++++++++++++++ 6 files changed, 113 insertions(+), 24 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Proxy/Action.php b/src/Appwrite/Platform/Modules/Proxy/Action.php index 8baf54c790..1c78735c51 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Action.php +++ b/src/Appwrite/Platform/Modules/Proxy/Action.php @@ -5,7 +5,11 @@ namespace Appwrite\Platform\Modules\Proxy; use Appwrite\Extend\Exception; use Appwrite\Network\Validator\DNS as ValidatorDNS; use Appwrite\Platform\Action as PlatformAction; +use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Database\Exception\Duplicate; +use Utopia\Database\Query; +use Utopia\Database\Validator\Authorization; use Utopia\DNS\Message\Record; use Utopia\Domains\Domain; use Utopia\Logger\Log; @@ -20,6 +24,64 @@ class Action extends PlatformAction { } + protected function createRule(Document $rule, Database $dbForPlatform, Authorization $authorization): Document + { + try { + return $authorization->skip(fn () => $dbForPlatform->createDocument('rules', $rule)); + } catch (Duplicate) { + if (!$this->deleteOrphanedRule($rule, $dbForPlatform, $authorization)) { + throw new Exception(Exception::RULE_ALREADY_EXISTS); + } + } + + try { + return $authorization->skip(fn () => $dbForPlatform->createDocument('rules', $rule)); + } catch (Duplicate) { + throw new Exception(Exception::RULE_ALREADY_EXISTS); + } + } + + private function deleteOrphanedRule(Document $rule, Database $dbForPlatform, Authorization $authorization): bool + { + $existingRule = $authorization->skip(function () use ($rule, $dbForPlatform) { + $existingRule = $dbForPlatform->findOne('rules', [ + Query::equal('domain', [$rule->getAttribute('domain', '')]), + ]); + + if (!$existingRule->isEmpty()) { + return $existingRule; + } + + return $dbForPlatform->getDocument('rules', $rule->getId()); + }); + + if ($existingRule->isEmpty()) { + return false; + } + + if ($existingRule->getAttribute('domain', '') !== $rule->getAttribute('domain', '')) { + return false; + } + + $projectId = $existingRule->getAttribute('projectId', ''); + + if (empty($projectId)) { + return false; + } + + $project = $authorization->skip( + fn () => $dbForPlatform->getDocument('projects', $projectId) + ); + + if (!$project->isEmpty()) { + return false; + } + + $authorization->skip(fn () => $dbForPlatform->deleteDocument('rules', $existingRule->getId())); + + return true; + } + /** * Ensures domain is not in the deny list and is a valid domain * diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php index 6f2e40d13f..9431d24cde 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php @@ -12,7 +12,6 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Document; -use Utopia\Database\Exception\Duplicate; use Utopia\Database\Helpers\ID; use Utopia\Database\Validator\Authorization; use Utopia\Logger\Log; @@ -120,11 +119,7 @@ class Create extends Action } } - try { - $rule = $authorization->skip(fn () => $dbForPlatform->createDocument('rules', $rule)); - } catch (Duplicate $e) { - throw new Exception(Exception::RULE_ALREADY_EXISTS); - } + $rule = $this->createRule($rule, $dbForPlatform, $authorization); if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) { $publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate( diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php index c68574fefe..7cc8b5e59e 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php @@ -12,7 +12,6 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Document; -use Utopia\Database\Exception\Duplicate; use Utopia\Database\Helpers\ID; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; @@ -142,11 +141,7 @@ class Create extends Action } } - try { - $rule = $authorization->skip(fn () => $dbForPlatform->createDocument('rules', $rule)); - } catch (Duplicate $e) { - throw new Exception(Exception::RULE_ALREADY_EXISTS); - } + $rule = $this->createRule($rule, $dbForPlatform, $authorization); if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) { $publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate( diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php index f55405bb48..e8167b44a0 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php @@ -12,7 +12,6 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Document; -use Utopia\Database\Exception\Duplicate; use Utopia\Database\Helpers\ID; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; @@ -149,11 +148,7 @@ class Create extends Action } } - try { - $rule = $authorization->skip(fn () => $dbForPlatform->createDocument('rules', $rule)); - } catch (Duplicate $e) { - throw new Exception(Exception::RULE_ALREADY_EXISTS); - } + $rule = $this->createRule($rule, $dbForPlatform, $authorization); if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) { $publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate( diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php index 7da9a11636..ca45d73e13 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php @@ -12,7 +12,6 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Document; -use Utopia\Database\Exception\Duplicate; use Utopia\Database\Helpers\ID; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; @@ -142,11 +141,7 @@ class Create extends Action } } - try { - $rule = $authorization->skip(fn () => $dbForPlatform->createDocument('rules', $rule)); - } catch (Duplicate $e) { - throw new Exception(Exception::RULE_ALREADY_EXISTS); - } + $rule = $this->createRule($rule, $dbForPlatform, $authorization); if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) { $publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate( diff --git a/tests/e2e/Services/Proxy/ProxyBase.php b/tests/e2e/Services/Proxy/ProxyBase.php index 4811fc3737..c83958afe1 100644 --- a/tests/e2e/Services/Proxy/ProxyBase.php +++ b/tests/e2e/Services/Proxy/ProxyBase.php @@ -70,6 +70,53 @@ trait ProxyBase $this->assertEquals(204, $rule['headers']['status-code']); } + public function testCreateRuleDeletesOrphanedRule(): void + { + $domain = \uniqid() . '-orphan-api.custom.localhost'; + $orphanProject = $this->getProject(true); + + $orphanRule = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $orphanProject['$id'], + 'x-appwrite-key' => $orphanProject['apiKey'], + ], [ + 'domain' => $domain, + ]); + + $this->assertEquals(201, $orphanRule['headers']['status-code']); + $this->assertEquals($domain, $orphanRule['body']['domain']); + + $duplicateRule = $this->createAPIRule($domain); + $this->assertEquals(409, $duplicateRule['headers']['status-code']); + + $deleteProject = $this->client->call(Client::METHOD_DELETE, '/projects/' . $orphanProject['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $orphanProject['$id'], + 'x-appwrite-key' => $orphanProject['apiKey'], + ]); + + $this->assertEquals(204, $deleteProject['headers']['status-code']); + + // Project deletion removes the project document synchronously, while rule cleanup is queued. + // Creating the same domain now should clean up that orphaned rule before retrying. + $rule = $this->createAPIRule($domain); + + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals($domain, $rule['body']['domain']); + + $rules = $this->listRules([ + 'queries' => [ + Query::equal('domain', [$domain])->toString(), + ], + ]); + + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(1, $rules['body']['total']); + $this->assertEquals($rule['body']['$id'], $rules['body']['rules'][0]['$id']); + + $this->cleanupRule($rule['body']['$id']); + } + public function testCreateRuleSetup(): void { $ruleId = $this->setupAPIRule(\uniqid() . '-api2.myapp.com'); From 6b51a56b2baa0ba440a8b9a85a540444cc1e9b1c Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 14 May 2026 15:42:52 +0530 Subject: [PATCH 2/2] Tidy deleteOrphanedRule conditionals --- src/Appwrite/Platform/Modules/Proxy/Action.php | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Proxy/Action.php b/src/Appwrite/Platform/Modules/Proxy/Action.php index 1c78735c51..f2ffc58568 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Action.php +++ b/src/Appwrite/Platform/Modules/Proxy/Action.php @@ -47,7 +47,6 @@ class Action extends PlatformAction $existingRule = $dbForPlatform->findOne('rules', [ Query::equal('domain', [$rule->getAttribute('domain', '')]), ]); - if (!$existingRule->isEmpty()) { return $existingRule; } @@ -55,30 +54,24 @@ class Action extends PlatformAction return $dbForPlatform->getDocument('rules', $rule->getId()); }); - if ($existingRule->isEmpty()) { - return false; - } - - if ($existingRule->getAttribute('domain', '') !== $rule->getAttribute('domain', '')) { + if ( + $existingRule->isEmpty() || + $existingRule->getAttribute('domain', '') !== $rule->getAttribute('domain', '') + ) { return false; } $projectId = $existingRule->getAttribute('projectId', ''); - if (empty($projectId)) { return false; } - $project = $authorization->skip( - fn () => $dbForPlatform->getDocument('projects', $projectId) - ); - + $project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId)); if (!$project->isEmpty()) { return false; } $authorization->skip(fn () => $dbForPlatform->deleteDocument('rules', $existingRule->getId())); - return true; }