diff --git a/src/Appwrite/Platform/Modules/Proxy/Action.php b/src/Appwrite/Platform/Modules/Proxy/Action.php index 8baf54c790..f2ffc58568 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,57 @@ 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() || + $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');