diff --git a/.env b/.env index 73283cb4ff..0c60f99880 100644 --- a/.env +++ b/.env @@ -23,7 +23,7 @@ _APP_OPENSSL_KEY_V1=your-secret-key _APP_DOMAIN=traefik _APP_DOMAIN_FUNCTIONS=functions.localhost _APP_DOMAIN_SITES=sites.localhost -_APP_DOMAIN_TARGET=localhost +_APP_DOMAIN_TARGET=test.appwrite.io _APP_RULES_FORMAT=md5 _APP_REDIS_HOST=redis _APP_REDIS_PORT=6379 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fef758a613..f7c5fe7c5c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -146,6 +146,7 @@ jobs: Projects, Realtime, Sites, + Proxy, Storage, Teams, Users, @@ -212,6 +213,7 @@ jobs: Projects, Realtime, Sites, + Proxy, Storage, Teams, Users, diff --git a/app/config/collections/platform.php b/app/config/collections/platform.php index 8a46bfd3ec..58867bf2ba 100644 --- a/app/config/collections/platform.php +++ b/app/config/collections/platform.php @@ -1013,10 +1013,10 @@ return [ 'filters' => [], ], [ - '$id' => ID::custom('resourceType'), + '$id' => ID::custom('type'), // 'api', 'redirect', 'deployment' (site or function) 'type' => Database::VAR_STRING, 'format' => '', - 'size' => 100, + 'size' => 32, 'signed' => true, 'required' => true, 'default' => null, @@ -1024,24 +1024,28 @@ return [ 'filters' => [], ], [ - '$id' => ID::custom('resourceInternalId'), + // If 'api', then (empty) + // If 'redirect', then URL + // If 'deployment', then deployment ID + '$id' => ID::custom('value'), 'type' => Database::VAR_STRING, 'format' => '', - 'size' => Database::LENGTH_KEY, + 'size' => 512, 'signed' => true, 'required' => false, - 'default' => null, + 'default' => '', 'array' => false, 'filters' => [], ], [ - '$id' => ID::custom('resourceId'), + // Examples: branch=main + '$id' => ID::custom('automation'), 'type' => Database::VAR_STRING, 'format' => '', 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => false, - 'default' => null, + 'default' => '', 'array' => false, 'filters' => [], ], @@ -1066,9 +1070,27 @@ return [ 'default' => null, 'array' => false, 'filters' => [], - ] + ], + [ + '$id' => ID::custom('search'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 16384, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], ], 'indexes' => [ + [ + '$id' => ID::custom('_key_search'), + 'type' => Database::INDEX_FULLTEXT, + 'attributes' => ['search'], + 'lengths' => [], + 'orders' => [], + ], [ '$id' => ID::custom('_key_domain'), 'type' => Database::INDEX_UNIQUE, @@ -1091,24 +1113,24 @@ return [ 'orders' => [Database::ORDER_ASC], ], [ - '$id' => '_key_resourceInternalId', + '$id' => '_key_type', 'type' => Database::INDEX_KEY, - 'attributes' => ['resourceInternalId'], - 'lengths' => [Database::LENGTH_KEY], + 'attributes' => ['type'], + 'lengths' => [32], 'orders' => [Database::ORDER_ASC], ], [ - '$id' => '_key_resourceId', + '$id' => '_key_value', 'type' => Database::INDEX_KEY, - 'attributes' => ['resourceId'], - 'lengths' => [Database::LENGTH_KEY], + 'attributes' => ['value'], + 'lengths' => [512], 'orders' => [Database::ORDER_ASC], ], [ - '$id' => '_key_resourceType', + '$id' => '_key_automation', 'type' => Database::INDEX_KEY, - 'attributes' => ['resourceType'], - 'lengths' => [], + 'attributes' => ['automation'], + 'lengths' => [Database::LENGTH_KEY], 'orders' => [Database::ORDER_ASC], ], ], diff --git a/app/config/specs/open-api3-latest-console.json b/app/config/specs/open-api3-latest-console.json index dc4df59cf2..1558f3a0c4 100644 --- a/app/config/specs/open-api3-latest-console.json +++ b/app/config/specs/open-api3-latest-console.json @@ -23683,7 +23683,7 @@ "parameters": [ { "name": "queries", - "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/databases#querying-documents). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: domain, resourceType, resourceId, url", + "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/databases#querying-documents). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: domain, type, value, automation, url", "required": false, "schema": { "type": "array", @@ -23706,10 +23706,12 @@ "in": "query" } ] - }, + } + }, + "\/proxy\/rules\/api": { "post": { - "summary": "Create rule", - "operationId": "proxyCreateRule", + "summary": "Create API rule", + "operationId": "proxyCreateAPIRule", "tags": [ "proxy" ], @@ -23727,13 +23729,79 @@ } }, "x-appwrite": { - "method": "createRule", + "method": "createAPIRule", "weight": 423, "cookies": false, "type": "", "deprecated": false, - "demo": "proxy\/create-rule.md", - "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new proxy rule.", + "demo": "proxy\/create-a-p-i-rule.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new proxy rule for serving Appwrite's API on custom domain.", + "rate-limit": 10, + "rate-time": 60, + "rate-key": "userId:{userId}, url:{url}", + "scope": "rules.write", + "platforms": [ + "console" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [] + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain name.", + "x-example": null + } + }, + "required": [ + "domain" + ] + } + } + } + } + } + }, + "\/proxy\/rules\/function": { + "post": { + "summary": "Create function rule", + "operationId": "proxyCreateFunctionRule", + "tags": [ + "proxy" + ], + "description": "", + "responses": { + "201": { + "description": "Rule", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/proxyRule" + } + } + } + } + }, + "x-appwrite": { + "method": "createFunctionRule", + "weight": 425, + "cookies": false, + "type": "", + "deprecated": false, + "demo": "proxy\/create-function-rule.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new proxy rule for executing Appwrite Function on custom domain.", "rate-limit": 10, "rate-time": 60, "rate-key": "userId:{userId}, url:{url}", @@ -23762,27 +23830,159 @@ "description": "Domain name.", "x-example": null }, - "resourceType": { + "functionId": { "type": "string", - "description": "Action definition for the rule. Possible values are \"api\", \"function\" and \"site\"", - "x-example": "api", - "enum": [ - "api", - "function", - "site" - ], - "x-enum-name": null, - "x-enum-keys": [] - }, - "resourceId": { - "type": "string", - "description": "ID of resource for the action type. If resourceType is \"api\", leave empty. If resourceType is \"function\", provide ID of the function.", - "x-example": "" + "description": "ID of function to be executed.", + "x-example": "" } }, "required": [ "domain", - "resourceType" + "functionId" + ] + } + } + } + } + } + }, + "\/proxy\/rules\/redirect": { + "post": { + "summary": "Create Redirect rule", + "operationId": "proxyCreateRedirectRule", + "tags": [ + "proxy" + ], + "description": "", + "responses": { + "201": { + "description": "Rule", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/proxyRule" + } + } + } + } + }, + "x-appwrite": { + "method": "createRedirectRule", + "weight": 426, + "cookies": false, + "type": "", + "deprecated": false, + "demo": "proxy\/create-redirect-rule.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new proxy rule for to redirect from custom domain to another domain.", + "rate-limit": 10, + "rate-time": 60, + "rate-key": "userId:{userId}, url:{url}", + "scope": "rules.write", + "platforms": [ + "console" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [] + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain name.", + "x-example": null + }, + "target": { + "type": "string", + "description": "Target URL of redirection", + "x-example": "https:\/\/example.com" + } + }, + "required": [ + "domain", + "target" + ] + } + } + } + } + } + }, + "\/proxy\/rules\/site": { + "post": { + "summary": "Create site rule", + "operationId": "proxyCreateSiteRule", + "tags": [ + "proxy" + ], + "description": "", + "responses": { + "201": { + "description": "Rule", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/proxyRule" + } + } + } + } + }, + "x-appwrite": { + "method": "createSiteRule", + "weight": 424, + "cookies": false, + "type": "", + "deprecated": false, + "demo": "proxy\/create-site-rule.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new proxy rule for serving Appwrite Site on custom domain.", + "rate-limit": 10, + "rate-time": 60, + "rate-key": "userId:{userId}, url:{url}", + "scope": "rules.write", + "platforms": [ + "console" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [] + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain name.", + "x-example": null + }, + "siteId": { + "type": "string", + "description": "ID of site to be executed.", + "x-example": "" + } + }, + "required": [ + "domain", + "siteId" ] } } @@ -39780,15 +39980,20 @@ "description": "Domain name.", "x-example": "appwrite.company.com" }, - "resourceType": { + "type": { "type": "string", - "description": "Action definition for the rule. Possible values are \"api\", \"function\", or \"redirect\"", - "x-example": "function" + "description": "Action definition for the rule. Possible values are \"api\", \"deployment\", or \"redirect\"", + "x-example": "deployment" }, - "resourceId": { + "value": { "type": "string", - "description": "ID of resource for the action type. If resourceType is \"api\" or \"url\", it is empty. If resourceType is \"function\", it is ID of the function.", - "x-example": "myAwesomeFunction" + "description": "Detail specification for the type. If type is \"api\", this is empty. If type is \"redirect\", this is URL. If type is \"deployment\", this is deployment ID.", + "x-example": "67a9cf1a00150ee93abd" + }, + "automation": { + "type": "string", + "description": "Action that result in update of rule. If VCS branch, value can be of syntax \"branch=[name]\"", + "x-example": "branch=dev" }, "status": { "type": "string", @@ -39811,8 +40016,9 @@ "$createdAt", "$updatedAt", "domain", - "resourceType", - "resourceId", + "type", + "value", + "automation", "status", "logs", "renewAt" @@ -39944,6 +40150,11 @@ "type": "string", "description": "Defines if HTTPS is enforced for all requests.", "x-example": "enabled" + }, + "_APP_DOMAINS_NAMESERVERS": { + "type": "string", + "description": "Comma-separated list of nameservers.", + "x-example": "ns1.example.com,ns2.example.com" } }, "required": [ @@ -39955,7 +40166,8 @@ "_APP_DOMAIN_ENABLED", "_APP_ASSISTANT_ENABLED", "_APP_DOMAIN_SITES", - "_APP_OPTIONS_FORCE_HTTPS" + "_APP_OPTIONS_FORCE_HTTPS", + "_APP_DOMAINS_NAMESERVERS" ] }, "mfaChallenge": { diff --git a/app/config/specs/swagger2-latest-console.json b/app/config/specs/swagger2-latest-console.json index 849a1fde91..5322f41ed7 100644 --- a/app/config/specs/swagger2-latest-console.json +++ b/app/config/specs/swagger2-latest-console.json @@ -24168,7 +24168,7 @@ "parameters": [ { "name": "queries", - "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/databases#querying-documents). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: domain, resourceType, resourceId, url", + "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/databases#querying-documents). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: domain, type, value, automation, url", "required": false, "type": "array", "collectionFormat": "multi", @@ -24188,10 +24188,12 @@ "in": "query" } ] - }, + } + }, + "\/proxy\/rules\/api": { "post": { - "summary": "Create rule", - "operationId": "proxyCreateRule", + "summary": "Create API rule", + "operationId": "proxyCreateAPIRule", "consumes": [ "application\/json" ], @@ -24211,13 +24213,82 @@ } }, "x-appwrite": { - "method": "createRule", + "method": "createAPIRule", "weight": 423, "cookies": false, "type": "", "deprecated": false, - "demo": "proxy\/create-rule.md", - "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new proxy rule.", + "demo": "proxy\/create-a-p-i-rule.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new proxy rule for serving Appwrite's API on custom domain.", + "rate-limit": 10, + "rate-time": 60, + "rate-key": "userId:{userId}, url:{url}", + "scope": "rules.write", + "platforms": [ + "console" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [] + } + ], + "parameters": [ + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain name.", + "default": null, + "x-example": null + } + }, + "required": [ + "domain" + ] + } + } + ] + } + }, + "\/proxy\/rules\/function": { + "post": { + "summary": "Create function rule", + "operationId": "proxyCreateFunctionRule", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "proxy" + ], + "description": "", + "responses": { + "201": { + "description": "Rule", + "schema": { + "$ref": "#\/definitions\/proxyRule" + } + } + }, + "x-appwrite": { + "method": "createFunctionRule", + "weight": 425, + "cookies": false, + "type": "", + "deprecated": false, + "demo": "proxy\/create-function-rule.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new proxy rule for executing Appwrite Function on custom domain.", "rate-limit": 10, "rate-time": 60, "rate-key": "userId:{userId}, url:{url}", @@ -24248,29 +24319,168 @@ "default": null, "x-example": null }, - "resourceType": { + "functionId": { "type": "string", - "description": "Action definition for the rule. Possible values are \"api\", \"function\" and \"site\"", + "description": "ID of function to be executed.", "default": null, - "x-example": "api", - "enum": [ - "api", - "function", - "site" - ], - "x-enum-name": null, - "x-enum-keys": [] - }, - "resourceId": { - "type": "string", - "description": "ID of resource for the action type. If resourceType is \"api\", leave empty. If resourceType is \"function\", provide ID of the function.", - "default": "", - "x-example": "" + "x-example": "" } }, "required": [ "domain", - "resourceType" + "functionId" + ] + } + } + ] + } + }, + "\/proxy\/rules\/redirect": { + "post": { + "summary": "Create Redirect rule", + "operationId": "proxyCreateRedirectRule", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "proxy" + ], + "description": "", + "responses": { + "201": { + "description": "Rule", + "schema": { + "$ref": "#\/definitions\/proxyRule" + } + } + }, + "x-appwrite": { + "method": "createRedirectRule", + "weight": 426, + "cookies": false, + "type": "", + "deprecated": false, + "demo": "proxy\/create-redirect-rule.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new proxy rule for to redirect from custom domain to another domain.", + "rate-limit": 10, + "rate-time": 60, + "rate-key": "userId:{userId}, url:{url}", + "scope": "rules.write", + "platforms": [ + "console" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [] + } + ], + "parameters": [ + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain name.", + "default": null, + "x-example": null + }, + "target": { + "type": "string", + "description": "Target URL of redirection", + "default": null, + "x-example": "https:\/\/example.com" + } + }, + "required": [ + "domain", + "target" + ] + } + } + ] + } + }, + "\/proxy\/rules\/site": { + "post": { + "summary": "Create site rule", + "operationId": "proxyCreateSiteRule", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "proxy" + ], + "description": "", + "responses": { + "201": { + "description": "Rule", + "schema": { + "$ref": "#\/definitions\/proxyRule" + } + } + }, + "x-appwrite": { + "method": "createSiteRule", + "weight": 424, + "cookies": false, + "type": "", + "deprecated": false, + "demo": "proxy\/create-site-rule.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new proxy rule for serving Appwrite Site on custom domain.", + "rate-limit": 10, + "rate-time": 60, + "rate-key": "userId:{userId}, url:{url}", + "scope": "rules.write", + "platforms": [ + "console" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [] + } + ], + "parameters": [ + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain name.", + "default": null, + "x-example": null + }, + "siteId": { + "type": "string", + "description": "ID of site to be executed.", + "default": null, + "x-example": "" + } + }, + "required": [ + "domain", + "siteId" ] } } @@ -40434,15 +40644,20 @@ "description": "Domain name.", "x-example": "appwrite.company.com" }, - "resourceType": { + "type": { "type": "string", - "description": "Action definition for the rule. Possible values are \"api\", \"function\", or \"redirect\"", - "x-example": "function" + "description": "Action definition for the rule. Possible values are \"api\", \"deployment\", or \"redirect\"", + "x-example": "deployment" }, - "resourceId": { + "value": { "type": "string", - "description": "ID of resource for the action type. If resourceType is \"api\" or \"url\", it is empty. If resourceType is \"function\", it is ID of the function.", - "x-example": "myAwesomeFunction" + "description": "Detail specification for the type. If type is \"api\", this is empty. If type is \"redirect\", this is URL. If type is \"deployment\", this is deployment ID.", + "x-example": "67a9cf1a00150ee93abd" + }, + "automation": { + "type": "string", + "description": "Action that result in update of rule. If VCS branch, value can be of syntax \"branch=[name]\"", + "x-example": "branch=dev" }, "status": { "type": "string", @@ -40465,8 +40680,9 @@ "$createdAt", "$updatedAt", "domain", - "resourceType", - "resourceId", + "type", + "value", + "automation", "status", "logs", "renewAt" @@ -40598,6 +40814,11 @@ "type": "string", "description": "Defines if HTTPS is enforced for all requests.", "x-example": "enabled" + }, + "_APP_DOMAINS_NAMESERVERS": { + "type": "string", + "description": "Comma-separated list of nameservers.", + "x-example": "ns1.example.com,ns2.example.com" } }, "required": [ @@ -40609,7 +40830,8 @@ "_APP_DOMAIN_ENABLED", "_APP_ASSISTANT_ENABLED", "_APP_DOMAIN_SITES", - "_APP_OPTIONS_FORCE_HTTPS" + "_APP_OPTIONS_FORCE_HTTPS", + "_APP_DOMAINS_NAMESERVERS" ] }, "mfaChallenge": { diff --git a/app/controllers/api/console.php b/app/controllers/api/console.php index d83cdc79f8..d4b191cd62 100644 --- a/app/controllers/api/console.php +++ b/app/controllers/api/console.php @@ -62,7 +62,8 @@ App::get('/v1/console/variables') '_APP_DOMAIN_ENABLED' => $isDomainEnabled, '_APP_ASSISTANT_ENABLED' => $isAssistantEnabled, '_APP_DOMAIN_SITES' => System::getEnv('_APP_DOMAIN_SITES'), - '_APP_OPTIONS_FORCE_HTTPS' => System::getEnv('_APP_OPTIONS_FORCE_HTTPS') + '_APP_OPTIONS_FORCE_HTTPS' => System::getEnv('_APP_OPTIONS_FORCE_HTTPS'), + '_APP_DOMAINS_NAMESERVERS' => System::getEnv('_APP_DOMAINS_NAMESERVERS'), ]); $response->dynamic($variables, Response::MODEL_CONSOLE_VARIABLES); diff --git a/app/controllers/api/vcs.php b/app/controllers/api/vcs.php index d8394a3b4f..0f277a4661 100644 --- a/app/controllers/api/vcs.php +++ b/app/controllers/api/vcs.php @@ -19,6 +19,7 @@ use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; +use Utopia\Database\Exception\Duplicate; use Utopia\Database\Exception\Query as QueryException; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; @@ -231,10 +232,10 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId 'activate' => $activate, ])); - // Preview deployments for sites if ($resource->getCollection() === 'sites') { $projectId = $project->getId(); + // Deployment preview $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); $domain = ID::unique() . "." . $sitesDomain; $ruleId = md5($domain); @@ -244,13 +245,61 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId 'projectId' => $project->getId(), 'projectInternalId' => $project->getInternalId(), 'domain' => $domain, - 'resourceType' => 'deployment', - 'resourceId' => $deploymentId, - 'resourceInternalId' => $deployment->getInternalId(), + 'type' => 'deployment', + 'value' => $deployment->getId(), 'status' => 'verified', 'certificateId' => '', + 'search' => implode(' ', [$ruleId, $domain]), ])) ); + + // VCS branch preview + if (!empty($providerBranch)) { + $domain = "branch-{$providerBranch}-{$resource->getId()}-{$project->getId()}.{$sitesDomain}"; + $ruleId = md5($domain); + try { + Authorization::skip( + fn () => $dbForPlatform->createDocument('rules', new Document([ + '$id' => $ruleId, + 'projectId' => $project->getId(), + 'projectInternalId' => $project->getInternalId(), + 'domain' => $domain, + 'type' => 'deployment', + 'value' => $deployment->getId(), + 'automation' => 'branch=' . $providerBranch, + 'status' => 'verified', + 'certificateId' => '', + 'search' => implode(' ', [$ruleId, $domain]), + ])) + ); + } catch (Duplicate $err) { + // Ignore, rule already exists; will be updated by builds worker + } + } + + // VCS commit preview + if (!empty($providerCommitHash)) { + $domain = "commit-{$providerCommitHash}-{$resource->getId()}-{$project->getId()}.{$sitesDomain}"; + $ruleId = md5($domain); + try { + Authorization::skip( + fn () => $dbForPlatform->createDocument('rules', new Document([ + '$id' => $ruleId, + 'projectId' => $project->getId(), + 'projectInternalId' => $project->getInternalId(), + 'domain' => $domain, + 'type' => 'deployment', + 'value' => $deployment->getId(), + 'automation' => 'commit=' . $providerCommitHash, + 'status' => 'verified', + 'certificateId' => '', + 'search' => implode(' ', [$ruleId, $domain]), + ])) + ); + } catch (Duplicate $err) { + // Ignore, rule already exists; will be updated by builds worker + } + } } if (!empty($providerCommitHash) && $resource->getAttribute('providerSilentMode', false) === false) { diff --git a/app/controllers/general.php b/app/controllers/general.php index 6907a869e4..bedb3c676b 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -123,17 +123,9 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw return false; } - $type = $rule->getAttribute('resourceType'); + $type = $rule->getAttribute('type', ''); - if ($type === 'function' || $type === 'site' || $type === 'deployment') { - $resourceCollection = match ($type) { - 'function' => 'functions', - 'site' => 'sites', - 'deployment' => 'deployments', - }; - } - - if ($type === 'function' || $type === 'site' || $type === 'deployment') { + if ($type === 'deployment') { $method = $utopia->getRoute()?->getLabel('sdk', null); if (empty($method)) { @@ -167,8 +159,22 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw } } - $resourceId = $rule->getAttribute('resourceId'); - $projectId = $rule->getAttribute('projectId'); + /** @var Database $dbForProject */ + $dbForProject = $getProjectDB($project); + + $deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $rule->getAttribute('value'))); + + if ($deployment->getAttribute('resourceType', '') === 'functions') { + $type = 'function'; + } elseif ($deployment->getAttribute('resourceType', '') === 'sites') { + $type = 'site'; + } + + $resource = $type === 'function' ? + Authorization::skip(fn () => $dbForProject->getDocument('functions', $deployment->getAttribute('resourceId', ''))) : + Authorization::skip(fn () => $dbForProject->getDocument('sites', $deployment->getAttribute('resourceId', ''))); + + $isPreview = $type === 'function' ? false : (!\str_starts_with($rule->getAttribute('automation', ''), 'site=')); $path = ($swooleRequest->server['request_uri'] ?? '/'); $query = ($swooleRequest->server['query_string'] ?? ''); @@ -181,30 +187,17 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $requestHeaders = $request->getHeaders(); - $project = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $projectId)); - - /** @var Database $dbForProject */ - $dbForProject = $getProjectDB($project); - - if ($resourceCollection === 'deployments') { - $subResource = Authorization::skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId)); - $resource = Authorization::skip(fn () => $dbForProject->getDocument($subResource->getAttribute('resourceType'), $subResource->getAttribute('resourceId'))); - } else { - $resource = Authorization::skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId)); - } - if ($resource->isEmpty() || !$resource->getAttribute('enabled')) { throw new AppwriteException(AppwriteException::FUNCTION_NOT_FOUND); } - if ($isResourceBlocked($project, RESOURCE_TYPE_FUNCTIONS, $resourceId)) { + if ($isResourceBlocked($project, $type === 'function' ? RESOURCE_TYPE_FUNCTIONS : RESOURCE_TYPE_SITES, $resource->getId())) { throw new AppwriteException(AppwriteException::GENERAL_RESOURCE_BLOCKED); } $version = match ($type) { 'function' => $resource->getAttribute('version', 'v2'), 'site' => 'v4', - 'deployment' => 'v4' }; $runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []); @@ -213,34 +206,13 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $runtime = match ($type) { 'function' => $runtimes[$resource->getAttribute('runtime')] ?? null, 'site' => $runtimes[$resource->getAttribute('buildRuntime')] ?? null, - 'deployment' => $runtimes[$resource->getAttribute('buildRuntime')] ?? null, default => null }; - if ($resource->getAttribute('adapter', '') === 'static') { - $runtime = $runtimes['static-1'] ?? null; - } - if (\is_null($runtime)) { throw new AppwriteException(AppwriteException::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $resource->getAttribute('runtime', '') . '" is not supported'); } - $deploymentId = match ($type) { - 'function' => $resource->getAttribute('deployment', ''), - 'site' => $resource->getAttribute('deploymentId', ''), - 'deployment' => $subResource->getId() - }; - - $deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $deploymentId)); - - if ($deployment->getAttribute('resourceId') !== $resource->getId()) { - throw new AppwriteException(AppwriteException::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function'); - } - - if ($deployment->isEmpty()) { - throw new AppwriteException(AppwriteException::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function'); - } - /** Check if build has completed */ $build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); if ($build->isEmpty()) { @@ -251,10 +223,8 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw throw new AppwriteException(AppwriteException::BUILD_NOT_READY); } - //todo: figure out for sites/functions if ($type === 'function') { $permissions = $resource->getAttribute('execute'); - if (!(\in_array('any', $permissions)) && !(\in_array('guests', $permissions))) { throw new AppwriteException(AppwriteException::USER_UNAUTHORIZED, 'To execute function using domain, execute permissions must include "any" or "guests"'); } @@ -266,18 +236,15 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $headers['x-appwrite-continent-code'] = ''; $headers['x-appwrite-continent-eu'] = 'false'; - //todo: check if this would work for sites - if ($type === 'function') { - $jwtExpiry = $resource->getAttribute('timeout', 900); - $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0); - $jwtKey = $jwtObj->encode([ - 'projectId' => $project->getId(), - 'scopes' => $resource->getAttribute('scopes', []) - ]); - $headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $jwtKey; - $headers['x-appwrite-trigger'] = 'http'; - $headers['x-appwrite-user-jwt'] = ''; - } + $jwtExpiry = $resource->getAttribute('timeout', 900); + $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0); + $jwtKey = $jwtObj->encode([ + 'projectId' => $project->getId(), + 'scopes' => $resource->getAttribute('scopes', []) + ]); + $headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $jwtKey; + $headers['x-appwrite-trigger'] = 'http'; + $headers['x-appwrite-user-jwt'] = ''; $ip = $headers['x-real-ip'] ?? ''; if (!empty($ip)) { @@ -316,21 +283,26 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw 'errors' => '', 'logs' => '', 'duration' => 0.0, - 'search' => implode(' ', [$resourceId, $executionId]), + 'search' => implode(' ', [$resource->getId(), $executionId]), ]); if ($type === 'function') { $execution->setAttribute('resourceType', 'functions'); $execution->setAttribute('trigger', 'http'); // http / schedule / event $execution->setAttribute('status', 'processing'); // waiting / processing / completed / failed + + $queueForEvents + ->setParam('functionId', $resource->getId()) + ->setParam('executionId', $execution->getId()) + ->setContext('function', $resource); } elseif ($type === 'site') { $execution->setAttribute('resourceType', 'sites'); - } - $queueForEvents - ->setParam('functionId', $resource->getId()) - ->setParam('executionId', $execution->getId()) - ->setContext('function', $resource); + $queueForEvents + ->setParam('siteId', $resource->getId()) + ->setParam('executionId', $execution->getId()) + ->setContext('site', $resource); + } $durationStart = \microtime(true); @@ -363,7 +335,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw // Appwrite vars $vars = \array_merge($vars, [ 'APPWRITE_FUNCTION_API_ENDPOINT' => $endpoint, - 'APPWRITE_FUNCTION_ID' => $resourceId, + 'APPWRITE_FUNCTION_ID' => $resource->getId(), 'APPWRITE_FUNCTION_NAME' => $resource->getAttribute('name'), 'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(), 'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(), @@ -394,12 +366,10 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $version = match ($type) { 'function' => $resource->getAttribute('version', 'v2'), 'site' => 'v4', - 'deployment' => 'v4' }; $entrypoint = match ($type) { 'function' => $deployment->getAttribute('entrypoint', ''), 'site' => '', - 'deployment' => '' }; if ($type === 'function') { @@ -407,7 +377,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw 'v2' => '', default => 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "' . $runtime['startCommand'] . '"' }; - } elseif ($type === 'site' || $type === 'deployment') { + } elseif ($type === 'site') { $frameworks = Config::getParam('frameworks', []); $framework = $frameworks[$resource->getAttribute('framework', '')] ?? null; @@ -426,7 +396,6 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $entrypoint = match ($type) { 'function' => $deployment->getAttribute('entrypoint', ''), 'site' => '', - 'deployment' => '' }; $executionResponse = $executor->createExecution( @@ -455,7 +424,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $transformation->addAdapter(new Preview()); $transformation->setInput($executionResponse['body']); $transformation->setTraits($executionResponse['headers']); - if ($type === 'deployment' && $transformation->transform()) { + if ($isPreview && $transformation->transform()) { $executionResponse['body'] = $transformation->getOutput(); foreach ($executionResponse['headers'] as $key => $value) { @@ -475,9 +444,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw /** Update execution status */ $status = $executionResponse['statusCode'] >= 500 ? 'failed' : 'completed'; - if ($type === 'function') { - $execution->setAttribute('status', $status); - } + $execution->setAttribute('status', $status); $execution->setAttribute('logs', $executionResponse['logs']); $execution->setAttribute('errors', $executionResponse['errors']); $execution->setAttribute('responseStatusCode', $executionResponse['statusCode']); @@ -566,6 +533,17 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw } elseif ($type === 'api') { $utopia->getRoute()?->label('error', ''); return false; + } elseif ($type === 'redirect') { + $path = ($swooleRequest->server['request_uri'] ?? '/'); + $query = ($swooleRequest->server['query_string'] ?? ''); + if (!empty($query)) { + $path .= '?' . $query; + } + + $url = 'https://' . $rule->getAttribute('value', '') . $path; + + $response->redirect($url); + return true; } else { throw new AppwriteException(AppwriteException::GENERAL_SERVER_ERROR, 'Unknown resource type ' . $type); } @@ -698,14 +676,16 @@ App::init() } if ($domainDocument->isEmpty()) { + $ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique(); $domainDocument = new Document([ // TODO: @christyjacob remove once we migrate the rules in 1.7.x - '$id' => System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique(), + '$id' => $ruleId, 'domain' => $domain->get(), 'resourceType' => 'api', 'status' => 'verifying', 'projectId' => 'console', - 'projectInternalId' => 'console' + 'projectInternalId' => 'console', + 'search' => implode(' ', [$ruleId, $domain->get()]), ]); $domainDocument = $dbForPlatform->createDocument('rules', $domainDocument); @@ -742,7 +722,7 @@ App::init() } elseif (!empty($origin)) { // Auto-allow domains with linked rule if (System::getEnv('_APP_RULES_FORMAT') === 'md5') { - $rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($origin))); + $rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($origin ?? ''))); } else { $rule = Authorization::skip( fn () => $dbForPlatform->find('rules', [ @@ -1311,13 +1291,7 @@ App::get('/v1/ping') App::wildcard() ->groups(['api']) ->label('scope', 'global') - ->inject('utopia') - ->action(function (App $utopia) { - $handeledByRouter = $utopia->getRoute()?->getLabel('router', false); - if ($handeledByRouter === true) { - return; - } - + ->action(function () { throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND); }); diff --git a/app/http.php b/app/http.php index 2b1f038777..f4e888f87b 100644 --- a/app/http.php +++ b/app/http.php @@ -519,7 +519,6 @@ $http->on('Task', function () use ($register, $domains) { if ($lastSyncUpdate != null) { $queries[] = Query::greaterThanEqual('$updatedAt', $lastSyncUpdate); } - $queries[] = Query::equal('resourceType', ['function']); $results = []; try { $results = Authorization::skip(fn () => $dbForPlatform->find('rules', $queries)); diff --git a/composer.json b/composer.json index 00a75a5465..5f40f4d593 100644 --- a/composer.json +++ b/composer.json @@ -55,7 +55,7 @@ "utopia-php/database": "0.59.0", "utopia-php/domains": "0.5.*", "utopia-php/dsn": "0.2.1", - "utopia-php/framework": "dev-fix-prevent-duplicate-compression as 0.33.99", + "utopia-php/framework": "0.33.*", "utopia-php/fetch": "0.3.*", "utopia-php/image": "0.7.*", "utopia-php/locale": "0.4.*", diff --git a/composer.lock b/composer.lock index 223a6a7a4e..d08f47ca09 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "236b24287cd5bdcf705df586f12c560c", + "content-hash": "6883b3e81cfb0c5355997def668d5df2", "packages": [ { "name": "adhocore/jwt", @@ -3919,16 +3919,16 @@ }, { "name": "utopia-php/framework", - "version": "dev-fix-prevent-duplicate-compression", + "version": "0.33.16", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "a1efe3e10038afe4109af833ce7a25a8ec4b5ed2" + "reference": "e91d4c560d1b809e25faa63d564fef034363b50f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/a1efe3e10038afe4109af833ce7a25a8ec4b5ed2", - "reference": "a1efe3e10038afe4109af833ce7a25a8ec4b5ed2", + "url": "https://api.github.com/repos/utopia-php/http/zipball/e91d4c560d1b809e25faa63d564fef034363b50f", + "reference": "e91d4c560d1b809e25faa63d564fef034363b50f", "shasum": "" }, "require": { @@ -3960,9 +3960,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/fix-prevent-duplicate-compression" + "source": "https://github.com/utopia-php/http/tree/0.33.16" }, - "time": "2025-02-03T12:02:35+00:00" + "time": "2025-01-16T15:58:50+00:00" }, { "name": "utopia-php/image", @@ -8804,18 +8804,9 @@ "time": "2024-03-07T20:33:40+00:00" } ], - "aliases": [ - { - "package": "utopia-php/framework", - "version": "dev-fix-prevent-duplicate-compression", - "alias": "0.33.99", - "alias_normalized": "0.33.99.0" - } - ], + "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "utopia-php/framework": 20 - }, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/src/Appwrite/Platform/Modules/Compute/Base.php b/src/Appwrite/Platform/Modules/Compute/Base.php index 81e20b240f..157654c486 100644 --- a/src/Appwrite/Platform/Modules/Compute/Base.php +++ b/src/Appwrite/Platform/Modules/Compute/Base.php @@ -181,11 +181,11 @@ class Base extends Action 'projectId' => $project->getId(), 'projectInternalId' => $project->getInternalId(), 'domain' => $domain, - 'resourceType' => 'deployment', - 'resourceId' => $deploymentId, - 'resourceInternalId' => $deployment->getInternalId(), + 'type' => 'deployment', + 'value' => $deployment->getId(), 'status' => 'verified', 'certificateId' => '', + 'search' => implode(' ', [$ruleId, $domain]), ])) ); diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index 3a9e6a9ee3..8ecc0353f7 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -724,8 +724,8 @@ class Builds extends Action try { $rule = Authorization::skip(fn () => $dbForPlatform->findOne('rules', [ Query::equal("projectInternalId", [$project->getInternalId()]), - Query::equal("resourceType", ["deployment"]), - Query::equal("resourceInternalId", [$deployment->getInternalId()]) + Query::equal("type", ["deployment"]), + Query::equal("value", [$deployment->getId()]) ])); if ($rule->isEmpty()) { @@ -831,14 +831,51 @@ class Builds extends Action case 'functions': $resource->setAttribute('deployment', $deployment->getId()); $resource = $dbForProject->updateDocument('functions', $resource->getId(), $resource); + + $this->listRules($project, [ + Query::equal("automation", ["function=" . $resource->getId()]), + ], $dbForPlatform, function (Document $rule) use ($dbForPlatform, $deployment) { + $rule = $rule->setAttribute('value', $deployment->getId()); + $dbForPlatform->updateDocument('rules', $rule->getId(), $rule); + }); break; case 'sites': $resource->setAttribute('deploymentId', $deployment->getId()); $resource = $dbForProject->updateDocument('sites', $resource->getId(), $resource); + + $this->listRules($project, [ + Query::equal("automation", ["site=" . $resource->getId()]), + ], $dbForPlatform, function (Document $rule) use ($dbForPlatform, $deployment) { + $rule = $rule->setAttribute('value', $deployment->getId()); + $dbForPlatform->updateDocument('rules', $rule->getId(), $rule); + }); + + // VCS branch + $branchName = $deployment->getAttribute('providerBranch'); + if (!empty($branchName)) { + $this->listRules($project, [ + Query::equal("automation", ["branch=" . $branchName]), + ], $dbForPlatform, function (Document $rule) use ($dbForPlatform, $deployment) { + $rule = $rule->setAttribute('value', $deployment->getId()); + $dbForPlatform->updateDocument('rules', $rule->getId(), $rule); + }); + } + + // VCS commit + $commitHash = $deployment->getAttribute('providerCommitHash', ''); + if (!empty($commitHash)) { + $this->listRules($project, [ + Query::equal("automation", ["commit=" . $commitHash]), + ], $dbForPlatform, function (Document $rule) use ($dbForPlatform, $deployment) { + $rule = $rule->setAttribute('value', $deployment->getId()); + $dbForPlatform->updateDocument('rules', $rule->getId(), $rule); + }); + } break; } } + if ($dbForProject->getDocument('builds', $buildId)->getAttribute('status') === 'canceled') { Console::info('Build has been canceled'); return; @@ -1105,8 +1142,8 @@ class Builds extends Action $rule = Authorization::skip(fn () => $dbForPlatform->findOne('rules', [ Query::equal("projectInternalId", [$project->getInternalId()]), - Query::equal("resourceType", ["deployment"]), - Query::equal("resourceInternalId", [$deployment->getInternalId()]) + Query::equal("type", ["deployment"]), + Query::equal("value", [$deployment->getId()]) ])); $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https'; @@ -1131,4 +1168,38 @@ class Builds extends Action } } } + + protected function listRules(Document $project, array $queries, Database $database, callable $callback = null): void + { + $limit = 100; + $cursor = null; + + do { + $queries = \array_merge([ + Query::limit($limit), + Query::equal("projectInternalId", [$project->getInternalId()]) + ], $queries); + + if ($cursor !== null) { + $queries[] = Query::cursorAfter($cursor); + } + + $results = $database->find('rules', $queries); + + $total = \count($results); + if ($total > 0) { + $cursor = $results[$total - 1]; + } + + if ($total < $limit) { + $cursor = null; + } + + foreach ($results as $document) { + if (is_callable($callback)) { + $callback($document); + } + } + } while (!\is_null($cursor)); + } } diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php new file mode 100644 index 0000000000..36e2bcdf12 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php @@ -0,0 +1,152 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/proxy/rules/api') + ->groups(['api', 'proxy']) + ->desc('Create API rule') + ->label('scope', 'rules.write') + ->label('event', 'rules.[ruleId].create') + ->label('audits.event', 'rule.create') + ->label('audits.resource', 'rule/{response.$id}') + ->label('sdk', new Method( + namespace: 'proxy', + name: 'createAPIRule', + description: <<label('abuse-limit', 10) + ->label('abuse-key', 'userId:{userId}, url:{url}') + ->label('abuse-time', 60) + ->param('domain', null, new ValidatorDomain(), 'Domain name.') + ->inject('response') + ->inject('project') + ->inject('queueForCertificates') + ->inject('queueForEvents') + ->inject('dbForPlatform') + ->callback([$this, 'action']); + } + + public function action(string $domain, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform) + { + $mainDomain = System::getEnv('_APP_DOMAIN', ''); + $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); + $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); + + $deniedDomains = [ + $mainDomain, + $sitesDomain, + $functionsDomain, + 'localhost', + APP_HOSTNAME_INTERNAL, + ]; + + if (\in_array($domain, $deniedDomains)) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please pick another one.'); + } + + if (\str_starts_with($domain, 'commit-') || \str_starts_with($domain, 'branch-')) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please pick another one.'); + } + + try { + $domain = new Domain($domain); + } catch (\Throwable) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.'); + } + + // Apex domain prevention due to CNAME limitations + if (empty(App::getEnv('_APP_DOMAINS_NAMESERVERS', ''))) { + if ($domain->get() === $domain->getRegisterable()) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'The instance does not allow root-level (apex) domains.'); + } + } + + // TODO: @christyjacob remove once we migrate the rules in 1.7.x + $ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique(); + + $status = 'created'; + if (\str_ends_with($domain->get(), $functionsDomain) || \str_ends_with($domain->get(), $sitesDomain)) { + $status = 'verified'; + } + if ($status === 'created') { + $target = new Domain(System::getEnv('_APP_DOMAIN_TARGET', '')); + $validator = new CNAME($target->get()); + if ($validator->isValid($domain->get())) { + $status = 'verifying'; + } + } + + $rule = new Document([ + '$id' => $ruleId, + 'projectId' => $project->getId(), + 'projectInternalId' => $project->getInternalId(), + 'domain' => $domain->get(), + 'status' => $status, + 'type' => 'api', + 'value' => '', + 'certificateId' => '', + 'search' => implode(' ', [$ruleId, $domain->get()]), + ]); + + try { + $rule = $dbForPlatform->createDocument('rules', $rule); + } catch (Duplicate $e) { + throw new Exception(Exception::RULE_ALREADY_EXISTS); + } + + if ($rule->getAttribute('status', '') === 'verifying') { + $queueForCertificates + ->setDomain(new Document([ + 'domain' => $rule->getAttribute('domain') + ])) + ->trigger(); + } + + $queueForEvents->setParam('ruleId', $rule->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($rule, Response::MODEL_PROXY_RULE); + } +} diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php similarity index 50% rename from src/Appwrite/Platform/Modules/Proxy/Http/Rules/Create.php rename to src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php index b989310a3d..f09957aa04 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php @@ -1,6 +1,6 @@ setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) - ->setHttpPath('/v1/proxy/rules') + ->setHttpPath('/v1/proxy/rules/function') ->groups(['api', 'proxy']) - ->desc('Create rule') + ->desc('Create function rule') ->label('scope', 'rules.write') ->label('event', 'rules.[ruleId].create') ->label('audits.event', 'rule.create') ->label('audits.resource', 'rule/{response.$id}') ->label('sdk', new Method( namespace: 'proxy', - name: 'createRule', + name: 'createFunctionRule', description: <<label('abuse-key', 'userId:{userId}, url:{url}') ->label('abuse-time', 60) ->param('domain', null, new ValidatorDomain(), 'Domain name.') - ->param('resourceType', null, new WhiteList(['api', 'function', 'site']), 'Action definition for the rule. Possible values are "api", "function" and "site"') - ->param('resourceId', '', new UID(), 'ID of resource for the action type. If resourceType is "api", leave empty. If resourceType is "function", provide ID of the function.', true) + ->param('functionId', '', new UID(), 'ID of function to be executed.') ->inject('response') ->inject('project') ->inject('queueForCertificates') @@ -70,7 +70,7 @@ class Create extends Action ->callback([$this, 'action']); } - public function action(string $domain, string $resourceType, string $resourceId, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject) + public function action(string $domain, string $functionId, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject) { $mainDomain = System::getEnv('_APP_DOMAIN', ''); $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); @@ -84,91 +84,69 @@ class Create extends Action APP_HOSTNAME_INTERNAL, ]; - if (in_array($domain, $deniedDomains, true)) { + if (\in_array($domain, $deniedDomains)) { throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please pick another one.'); } - $resourceInternalId = ''; - - switch ($resourceType) { - case 'function': - case 'site': - if (empty($resourceId)) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'resourceId cannot be empty for resourceType "' . $resourceType . '".'); - } - - $expectedDomain = ($resourceType === 'function') ? $functionsDomain : $sitesDomain; - if (!\str_ends_with($domain, $expectedDomain)) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain must end with ' . $expectedDomain . ' for resourceType "' . $resourceType . '".'); - } - - $collection = ($resourceType === 'function') ? 'functions' : 'sites'; - $document = $dbForProject->getDocument($collection, $resourceId); - - if ($document->isEmpty()) { - throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND); - } - - $resourceInternalId = $document->getInternalId(); - break; - case 'api': - if (\str_ends_with($domain, $functionsDomain) || \str_ends_with($domain, $sitesDomain)) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain must not end with ' . $functionsDomain . ' or ' . $sitesDomain . ' for resourceType "api".'); - } - break; - } - try { $domain = new Domain($domain); } catch (\Throwable) { throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.'); } + // Apex domain prevention due to CNAME limitations + if (empty(App::getEnv('_APP_DOMAINS_NAMESERVERS', ''))) { + if ($domain->get() === $domain->getRegisterable()) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'The instance does not allow root-level (apex) domains.'); + } + } + + $function = $dbForProject->getDocument('functions', $functionId); + if ($function->isEmpty()) { + throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND); + } + // TODO: @christyjacob remove once we migrate the rules in 1.7.x $ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique(); - try { - $rule = new Document([ - '$id' => $ruleId, - 'projectId' => $project->getId(), - 'projectInternalId' => $project->getInternalId(), - 'domain' => $domain->get(), - 'resourceType' => $resourceType, - 'resourceId' => $resourceId, - 'resourceInternalId' => $resourceInternalId, - 'certificateId' => '', - ]); - } catch (\Throwable $e) { - if ($e->getCode() === Exception::DOCUMENT_ALREADY_EXISTS) { - throw new Exception(Exception::RULE_ALREADY_EXISTS); - } - - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'An unexpected error occurred: ' . $e->getMessage()); - } - $status = 'created'; - if (\str_ends_with($domain->get(), $functionsDomain) || \str_ends_with($domain->get(), $sitesDomain)) { $status = 'verified'; } - if ($status === 'created') { $target = new Domain(System::getEnv('_APP_DOMAIN_TARGET', '')); - $validator = new CNAME($target->get()); // Verify Domain with DNS records - + $validator = new CNAME($target->get()); if ($validator->isValid($domain->get())) { $status = 'verifying'; - - $queueForCertificates - ->setDomain(new Document([ - 'domain' => $rule->getAttribute('domain') - ])) - ->trigger(); } } - $rule->setAttribute('status', $status); - $rule = $dbForPlatform->createDocument('rules', $rule); + $rule = new Document([ + '$id' => $ruleId, + 'projectId' => $project->getId(), + 'projectInternalId' => $project->getInternalId(), + 'domain' => $domain->get(), + 'status' => $status, + 'type' => 'deployment', + 'value' => $function->getAttribute('deployment', ''), + 'certificateId' => '', + 'automation' => 'function=' . $function->getId(), + 'search' => implode(' ', [$ruleId, $domain->get()]), + ]); + + try { + $rule = $dbForPlatform->createDocument('rules', $rule); + } catch (Duplicate $e) { + throw new Exception(Exception::RULE_ALREADY_EXISTS); + } + + if ($rule->getAttribute('status', '') === 'verifying') { + $queueForCertificates + ->setDomain(new Document([ + 'domain' => $rule->getAttribute('domain') + ])) + ->trigger(); + } $queueForEvents->setParam('ruleId', $rule->getId()); diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php new file mode 100644 index 0000000000..ac23cca168 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php @@ -0,0 +1,155 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/proxy/rules/redirect') + ->groups(['api', 'proxy']) + ->desc('Create Redirect rule') + ->label('scope', 'rules.write') + ->label('event', 'rules.[ruleId].create') + ->label('audits.event', 'rule.create') + ->label('audits.resource', 'rule/{response.$id}') + ->label('sdk', new Method( + namespace: 'proxy', + name: 'createRedirectRule', + description: <<label('abuse-limit', 10) + ->label('abuse-key', 'userId:{userId}, url:{url}') + ->label('abuse-time', 60) + ->param('domain', null, new ValidatorDomain(), 'Domain name.') + ->param('target', null, new ValidatorDomain(), 'Target domain (hostname) of redirection') + ->inject('response') + ->inject('project') + ->inject('queueForCertificates') + ->inject('queueForEvents') + ->inject('dbForPlatform') + ->callback([$this, 'action']); + } + + public function action(string $domain, string $target, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform) + { + $mainDomain = System::getEnv('_APP_DOMAIN', ''); + $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); + $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); + + $deniedDomains = [ + $mainDomain, + $sitesDomain, + $functionsDomain, + 'localhost', + APP_HOSTNAME_INTERNAL, + ]; + + if (\in_array($domain, $deniedDomains)) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please pick another one.'); + } + + try { + $domain = new Domain($domain); + } catch (\Throwable) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.'); + } + + try { + $target = new Domain($target); + } catch (\Throwable) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Target may not start with http:// or https://.'); + } + + // Apex domain prevention due to CNAME limitations + if (empty(App::getEnv('_APP_DOMAINS_NAMESERVERS', ''))) { + if ($domain->get() === $domain->getRegisterable()) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'The instance does not allow root-level (apex) domains.'); + } + } + + // TODO: @christyjacob remove once we migrate the rules in 1.7.x + $ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique(); + + $status = 'created'; + if (\str_ends_with($domain->get(), $functionsDomain) || \str_ends_with($domain->get(), $sitesDomain)) { + $status = 'verified'; + } + if ($status === 'created') { + $dnsTarget = new Domain(System::getEnv('_APP_DOMAIN_TARGET', '')); + $validator = new CNAME($dnsTarget->get()); + if ($validator->isValid($domain->get())) { + $status = 'verifying'; + } + } + + $rule = new Document([ + '$id' => $ruleId, + 'projectId' => $project->getId(), + 'projectInternalId' => $project->getInternalId(), + 'domain' => $domain->get(), + 'status' => $status, + 'type' => 'redirect', + 'value' => $target->get(), + 'certificateId' => '', + 'search' => implode(' ', [$ruleId, $domain->get()]), + ]); + + try { + $rule = $dbForPlatform->createDocument('rules', $rule); + } catch (Duplicate $e) { + throw new Exception(Exception::RULE_ALREADY_EXISTS); + } + + if ($rule->getAttribute('status', '') === 'verifying') { + $queueForCertificates + ->setDomain(new Document([ + 'domain' => $rule->getAttribute('domain') + ])) + ->trigger(); + } + + $queueForEvents->setParam('ruleId', $rule->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($rule, Response::MODEL_PROXY_RULE); + } +} diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php new file mode 100644 index 0000000000..0a3a528f30 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php @@ -0,0 +1,159 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/proxy/rules/site') + ->groups(['api', 'proxy']) + ->desc('Create site rule') + ->label('scope', 'rules.write') + ->label('event', 'rules.[ruleId].create') + ->label('audits.event', 'rule.create') + ->label('audits.resource', 'rule/{response.$id}') + ->label('sdk', new Method( + namespace: 'proxy', + name: 'createSiteRule', + description: <<label('abuse-limit', 10) + ->label('abuse-key', 'userId:{userId}, url:{url}') + ->label('abuse-time', 60) + ->param('domain', null, new ValidatorDomain(), 'Domain name.') + ->param('siteId', '', new UID(), 'ID of site to be executed.') + ->param('branch', '', new Text(255, 0), 'Name of VCS branch to deploy changes automatically', true) + ->inject('response') + ->inject('project') + ->inject('queueForCertificates') + ->inject('queueForEvents') + ->inject('dbForPlatform') + ->inject('dbForProject') + ->callback([$this, 'action']); + } + + public function action(string $domain, string $siteId, string $branch, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject) + { + $mainDomain = System::getEnv('_APP_DOMAIN', ''); + $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); + $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); + + $deniedDomains = [ + $mainDomain, + $sitesDomain, + $functionsDomain, + 'localhost', + APP_HOSTNAME_INTERNAL, + ]; + + if (\in_array($domain, $deniedDomains)) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please pick another one.'); + } + + try { + $domain = new Domain($domain); + } catch (\Throwable) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.'); + } + + // Apex domain prevention due to CNAME limitations + if (empty(App::getEnv('_APP_DOMAINS_NAMESERVERS', ''))) { + if ($domain->get() === $domain->getRegisterable()) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'The instance does not allow root-level (apex) domains.'); + } + } + + $site = $dbForProject->getDocument('sites', $siteId); + if ($site->isEmpty()) { + throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND); + } + + // TODO: @christyjacob remove once we migrate the rules in 1.7.x + $ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique(); + + $status = 'created'; + if (\str_ends_with($domain->get(), $functionsDomain) || \str_ends_with($domain->get(), $sitesDomain)) { + $status = 'verified'; + } + if ($status === 'created') { + $target = new Domain(System::getEnv('_APP_DOMAIN_TARGET', '')); + $validator = new CNAME($target->get()); + if ($validator->isValid($domain->get())) { + $status = 'verifying'; + } + } + + $rule = new Document([ + '$id' => $ruleId, + 'projectId' => $project->getId(), + 'projectInternalId' => $project->getInternalId(), + 'domain' => $domain->get(), + 'status' => $status, + 'type' => 'deployment', + 'value' => $site->getAttribute('deploymentId', ''), + 'certificateId' => '', + 'automation' => !empty($branch) ? ('branch=' . $branch) : ('site=' . $site->getId()), + 'search' => implode(' ', [$ruleId, $domain->get()]), + ]); + + try { + $rule = $dbForPlatform->createDocument('rules', $rule); + } catch (Duplicate $e) { + throw new Exception(Exception::RULE_ALREADY_EXISTS); + } + + if ($rule->getAttribute('status', '') === 'verifying') { + $queueForCertificates + ->setDomain(new Document([ + 'domain' => $rule->getAttribute('domain') + ])) + ->trigger(); + } + + $queueForEvents->setParam('ruleId', $rule->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($rule, Response::MODEL_PROXY_RULE); + } +} diff --git a/src/Appwrite/Platform/Modules/Proxy/Services/Http.php b/src/Appwrite/Platform/Modules/Proxy/Services/Http.php index bc564f3714..c5f11ad5be 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Proxy/Services/Http.php @@ -2,7 +2,10 @@ namespace Appwrite\Platform\Modules\Proxy\Services; -use Appwrite\Platform\Modules\Proxy\Http\Rules\Create as CreateRule; +use Appwrite\Platform\Modules\Proxy\Http\Rules\API\Create as CreateAPIRule; +use Appwrite\Platform\Modules\Proxy\Http\Rules\Function\Create as CreateFunctionRule; +use Appwrite\Platform\Modules\Proxy\Http\Rules\Redirect\Create as CreateRedirectRule; +use Appwrite\Platform\Modules\Proxy\Http\Rules\Site\Create as CreateSiteRule; use Utopia\Platform\Service; class Http extends Service @@ -10,7 +13,11 @@ class Http extends Service public function __construct() { $this->type = Service::TYPE_HTTP; + // Rules - $this->addAction(CreateRule::getName(), new CreateRule()); + $this->addAction(CreateAPIRule::getName(), new CreateAPIRule()); + $this->addAction(CreateSiteRule::getName(), new CreateSiteRule()); + $this->addAction(CreateFunctionRule::getName(), new CreateFunctionRule()); + $this->addAction(CreateRedirectRule::getName(), new CreateRedirectRule()); } } diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Create.php index c76a4c3ffe..59944f8bc8 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Create.php @@ -112,11 +112,11 @@ class Create extends Action 'projectId' => $project->getId(), 'projectInternalId' => $project->getInternalId(), 'domain' => $domain, - 'resourceType' => 'deployment', - 'resourceId' => $deploymentId, - 'resourceInternalId' => $deployment->getInternalId(), + 'type' => 'deployment', + 'value' => $deployment->getId(), 'status' => 'verified', 'certificateId' => '', + 'search' => implode(' ', [$ruleId, $domain]), ])) ); diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php index fb536a3123..9bb83a53b9 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php @@ -237,11 +237,11 @@ class Create extends Action 'projectId' => $project->getId(), 'projectInternalId' => $project->getInternalId(), 'domain' => $domain, - 'resourceType' => 'deployment', - 'resourceId' => $deploymentId, - 'resourceInternalId' => $deployment->getInternalId(), + 'type' => 'deployment', + 'value' => $deployment->getId(), 'status' => 'verified', 'certificateId' => '', + 'search' => implode(' ', [$ruleId, $domain]), ])) ); } else { @@ -288,11 +288,11 @@ class Create extends Action 'projectId' => $project->getId(), 'projectInternalId' => $project->getInternalId(), 'domain' => $domain, - 'resourceType' => 'deployment', - 'resourceId' => $deploymentId, - 'resourceInternalId' => $deployment->getInternalId(), + 'type' => 'deployment', + 'value' => $deployment->getId(), 'status' => 'verified', 'certificateId' => '', + 'search' => implode(' ', [$ruleId, $domain]), ])) ); } else { diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Delete.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Delete.php index 133e9a1906..0809443aa1 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Delete.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Delete.php @@ -91,7 +91,7 @@ class Delete extends Action if ($site->getAttribute('deployment') === $deployment->getId()) { // Reset site deployment $site = $dbForProject->updateDocument('sites', $site->getId(), new Document(array_merge($site->getArrayCopy(), [ - 'deployment' => '', + 'deploymentId' => '', 'deploymentInternalId' => '', ]))); } diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php index 08be94776d..992a20150f 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php @@ -148,11 +148,11 @@ class Create extends Base 'projectId' => $project->getId(), 'projectInternalId' => $project->getInternalId(), 'domain' => $domain, - 'resourceType' => 'deployment', - 'resourceId' => $deploymentId, - 'resourceInternalId' => $deployment->getInternalId(), + 'type' => 'deployment', + 'value' => $deployment->getId(), 'status' => 'verified', 'certificateId' => '', + 'search' => implode(' ', [$ruleId, $domain]), ])) ); diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index cfc9a3ff79..57a2fed653 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -761,8 +761,8 @@ class Deletes extends Action */ Console::info("Deleting rules for site " . $siteId); $this->deleteByGroup('rules', [ - Query::equal('resourceType', ['site']), - Query::equal('resourceInternalId', [$siteInternalId]), + Query::equal('type', ['deployment']), + Query::equal('automation', ['site=' . $siteId]), Query::equal('projectInternalId', [$project->getInternalId()]) ], $dbForPlatform, function (Document $document) use ($dbForPlatform, $certificates) { $this->deleteRule($dbForPlatform, $document, $certificates); @@ -782,23 +782,25 @@ class Deletes extends Action */ Console::info("Deleting deployments for site " . $siteId); $deploymentInternalIds = []; + $deploymentIds = []; $this->deleteByGroup('deployments', [ Query::equal('resourceInternalId', [$siteInternalId]) - ], $dbForProject, function (Document $document) use ($deviceForFunctions, $deviceForFiles, $dbForPlatform, &$deploymentInternalIds) { + ], $dbForProject, function (Document $document) use ($project, $certificates, $deviceForFunctions, $deviceForFiles, $dbForPlatform, &$deploymentInternalIds) { $deploymentInternalIds[] = $document->getInternalId(); + $deploymentIds[] = $document->getId(); $this->deleteDeploymentFiles($deviceForFunctions, $document); $this->deleteDeploymentScreenshots($deviceForFiles, $dbForPlatform, $document); + $this->deleteDeploymentRules($dbForPlatform, $document, $project, $certificates); }); /** * Delete rules for all deployments of the site */ - //TODO: If functions also have previews in the future, change the logic here to use unique identifier for sites and functions - foreach ($deploymentInternalIds as $deploymentInternalId) { - Console::info("Deleting rules for site " . $siteId . "'s deployment " . $deploymentInternalId); + foreach ($deploymentIds as $deploymentId) { + Console::info("Deleting rules for site " . $siteId . "'s deployment " . $deploymentId); $this->deleteByGroup('rules', [ - Query::equal('resourceType', ['deployment']), - Query::equal('resourceInternalId', [$deploymentInternalId]), + Query::equal('type', ['deployment']), + Query::equal('value', [$deploymentId]), Query::equal('projectInternalId', [$project->getInternalId()]) ], $dbForPlatform, function (Document $document) use ($dbForPlatform, $certificates) { $this->deleteRule($dbForPlatform, $document, $certificates); @@ -856,8 +858,8 @@ class Deletes extends Action */ Console::info("Deleting rules for function " . $functionId); $this->deleteByGroup('rules', [ - Query::equal('resourceType', ['function']), - Query::equal('resourceInternalId', [$functionInternalId]), + Query::equal('type', ['deployment']), + Query::equal('automation', ['function=' . $functionId]), Query::equal('projectInternalId', [$project->getInternalId()]) ], $dbForPlatform, function (Document $document) use ($project, $dbForPlatform, $certificates) { $this->deleteRule($dbForPlatform, $document, $certificates); @@ -880,9 +882,10 @@ class Deletes extends Action $deploymentInternalIds = []; $this->deleteByGroup('deployments', [ Query::equal('resourceInternalId', [$functionInternalId]) - ], $dbForProject, function (Document $document) use ($deviceForFunctions, &$deploymentInternalIds) { + ], $dbForProject, function (Document $document) use ($dbForPlatform, $project, $certificates, $deviceForFunctions, &$deploymentInternalIds) { $deploymentInternalIds[] = $document->getInternalId(); $this->deleteDeploymentFiles($deviceForFunctions, $document); + $this->deleteDeploymentRules($dbForPlatform, $document, $project, $certificates); }); /** @@ -930,6 +933,18 @@ class Deletes extends Action $this->deleteRuntimes($getProjectDB, $document, $project); } + private function deleteDeploymentRules(Database $dbForPlatform, Document $deployment, Document $project, CertificatesAdapter $certificates): void + { + Console::info("Deleting rules for site " . $deployment->getId()); + $this->deleteByGroup('rules', [ + Query::equal('type', ['deployment']), + Query::equal('value', [$deployment->getId()]), + Query::equal('projectInternalId', [$project->getInternalId()]) + ], $dbForPlatform, function (Document $document) use ($dbForPlatform, $certificates) { + $this->deleteRule($dbForPlatform, $document, $certificates); + }); + } + private function deleteDeploymentScreenshots(Device $deviceForFiles, Database $dbForPlatform, Document $deployment): void { $screenshotIds = []; @@ -1081,8 +1096,8 @@ class Deletes extends Action */ Console::info("Deleting rules for deployment " . $deploymentId); $this->deleteByGroup('rules', [ - Query::equal('resourceType', ['deployment']), - Query::equal('resourceInternalId', [$deploymentInternalId]), + Query::equal('type', ['deployment']), + Query::equal('value', [$deploymentId]), Query::equal('projectInternalId', [$project->getInternalId()]) ], $dbForPlatform, function (Document $document) use ($dbForPlatform, $certificates) { $this->deleteRule($dbForPlatform, $document, $certificates); diff --git a/src/Appwrite/Utopia/Database/Validator/Queries/Rules.php b/src/Appwrite/Utopia/Database/Validator/Queries/Rules.php index 24cb4475f2..61701f0b2c 100644 --- a/src/Appwrite/Utopia/Database/Validator/Queries/Rules.php +++ b/src/Appwrite/Utopia/Database/Validator/Queries/Rules.php @@ -6,8 +6,9 @@ class Rules extends Base { public const ALLOWED_ATTRIBUTES = [ 'domain', - 'resourceType', - 'resourceId', + 'type', + 'value', + 'automation', 'url' ]; diff --git a/src/Appwrite/Utopia/Response/Model/ConsoleVariables.php b/src/Appwrite/Utopia/Response/Model/ConsoleVariables.php index 59d22296d1..2c1688969d 100644 --- a/src/Appwrite/Utopia/Response/Model/ConsoleVariables.php +++ b/src/Appwrite/Utopia/Response/Model/ConsoleVariables.php @@ -66,6 +66,15 @@ class ConsoleVariables extends Model 'default' => '', 'example' => 'enabled', ] + ) + ->addRule( + '_APP_DOMAINS_NAMESERVERS', + [ + 'type' => self::TYPE_STRING, + 'description' => 'Comma-separated list of nameservers.', + 'default' => '', + 'example' => 'ns1.example.com,ns2.example.com', + ] ); } diff --git a/src/Appwrite/Utopia/Response/Model/Rule.php b/src/Appwrite/Utopia/Response/Model/Rule.php index 932591b90f..c365f241f8 100644 --- a/src/Appwrite/Utopia/Response/Model/Rule.php +++ b/src/Appwrite/Utopia/Response/Model/Rule.php @@ -34,17 +34,24 @@ class Rule extends Model 'default' => '', 'example' => 'appwrite.company.com', ]) - ->addRule('resourceType', [ + ->addRule('type', [ 'type' => self::TYPE_STRING, - 'description' => 'Action definition for the rule. Possible values are "api", "function", or "redirect"', + 'description' => 'Action definition for the rule. Possible values are "api", "deployment", or "redirect"', 'default' => '', - 'example' => 'function', + 'example' => 'deployment', ]) - ->addRule('resourceId', [ + ->addRule('value', [ 'type' => self::TYPE_STRING, - 'description' => 'ID of resource for the action type. If resourceType is "api" or "url", it is empty. If resourceType is "function", it is ID of the function.', + 'description' => 'Detail specification for the type. If type is "api", this is empty. If type is "redirect", this is URL. If type is "deployment", this is deployment ID.', 'default' => '', - 'example' => 'myAwesomeFunction', + 'example' => '67a9cf1a00150ee93abd', + ]) + ->addRule('automation', [ + 'type' => self::TYPE_STRING, + 'description' => 'Action that results in a rule update. If VCS branch, value can be of syntax "branch=[name]"', + 'array' => false, + 'default' => '', + 'example' => 'branch=dev', ]) ->addRule('status', [ 'type' => self::TYPE_STRING, diff --git a/tests/e2e/General/UsageTest.php b/tests/e2e/General/UsageTest.php index 504f0a696d..00dc790869 100644 --- a/tests/e2e/General/UsageTest.php +++ b/tests/e2e/General/UsageTest.php @@ -1101,15 +1101,14 @@ class UsageTest extends Scope $rule = $this->client->call( Client::METHOD_POST, - '/proxy/rules', + '/proxy/rules/function', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'domain' => 'test-' . ID::unique() . System::getEnv('_APP_DOMAIN_FUNCTIONS'), - 'resourceType' => 'function', - 'resourceId' => $functionId, + 'functionId' => $functionId, ], ); diff --git a/tests/e2e/Services/Console/ConsoleConsoleClientTest.php b/tests/e2e/Services/Console/ConsoleConsoleClientTest.php index fb65adc299..f70eafc8e0 100644 --- a/tests/e2e/Services/Console/ConsoleConsoleClientTest.php +++ b/tests/e2e/Services/Console/ConsoleConsoleClientTest.php @@ -24,7 +24,7 @@ class ConsoleConsoleClientTest extends Scope ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertCount(9, $response['body']); + $this->assertCount(10, $response['body']); $this->assertIsString($response['body']['_APP_DOMAIN_TARGET']); $this->assertIsInt($response['body']['_APP_STORAGE_LIMIT']); $this->assertIsInt($response['body']['_APP_COMPUTE_SIZE_LIMIT']); @@ -34,5 +34,7 @@ class ConsoleConsoleClientTest extends Scope $this->assertIsBool($response['body']['_APP_ASSISTANT_ENABLED']); $this->assertIsString($response['body']['_APP_DOMAIN_SITES']); $this->assertIsString($response['body']['_APP_OPTIONS_FORCE_HTTPS']); + $this->assertIsString($response['body']['_APP_DOMAINS_NAMESERVERS']); + // When adding new keys, dont forget to update count a few lines above } } diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php index b7624c24a1..167094aec7 100644 --- a/tests/e2e/Services/Functions/FunctionsBase.php +++ b/tests/e2e/Services/Functions/FunctionsBase.php @@ -274,13 +274,12 @@ trait FunctionsBase protected function setupFunctionDomain(string $functionId, string $subdomain = ''): string { $subdomain = $subdomain ? $subdomain : ID::unique(); - $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules', array_merge([ + $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/function', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'domain' => $subdomain . '.' . System::getEnv('_APP_DOMAIN_FUNCTIONS', ''), - 'resourceType' => 'function', - 'resourceId' => $functionId, + 'functionId' => $functionId, ]); $this->assertEquals(201, $rule['headers']['status-code']); @@ -299,8 +298,8 @@ trait FunctionsBase 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'queries' => [ - Query::equal('resourceId', [$functionId])->toString(), - Query::equal('resourceType', ['function'])->toString(), + Query::equal('automation', ['function=' . $functionId])->toString(), + Query::equal('type', ['deployment'])->toString(), ], ]); diff --git a/tests/e2e/Services/GraphQL/FunctionsServerTest.php b/tests/e2e/Services/GraphQL/FunctionsServerTest.php index e49ac43619..abd86b30ec 100644 --- a/tests/e2e/Services/GraphQL/FunctionsServerTest.php +++ b/tests/e2e/Services/GraphQL/FunctionsServerTest.php @@ -130,7 +130,7 @@ class FunctionsServerTest extends Scope $deployment = $deployment['body']['data']['functionsGetDeployment']; $this->assertEquals('ready', $deployment['status']); - }); + }, 30000); return $deployment; } diff --git a/tests/e2e/Services/Projects/ProjectsCustomServerTest.php b/tests/e2e/Services/Projects/ProjectsCustomServerTest.php index 60ae7e0bbb..6d9431290f 100644 --- a/tests/e2e/Services/Projects/ProjectsCustomServerTest.php +++ b/tests/e2e/Services/Projects/ProjectsCustomServerTest.php @@ -24,14 +24,13 @@ class ProjectsCustomServerTest extends Scope 'cookie' => 'a_session_console=' . $this->getRoot()['session'], ]); - $response = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [ - 'resourceType' => 'api', + $response = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', $headers, [ 'domain' => 'api.appwrite.test', ]); $this->assertEquals(201, $response['headers']['status-code']); - $response = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [ + $response = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', $headers, [ 'resourceType' => 'api', 'domain' => 'abc.test.io', ]); @@ -39,8 +38,7 @@ class ProjectsCustomServerTest extends Scope $this->assertEquals(201, $response['headers']['status-code']); // duplicate rule - $response2 = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [ - 'resourceType' => 'api', + $response2 = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', $headers, [ 'domain' => 'abc.test.io', ]); @@ -52,8 +50,7 @@ class ProjectsCustomServerTest extends Scope $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); - $response = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [ - 'resourceType' => 'api', + $response = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', $headers, [ 'domain' => $functionsDomain, ]); @@ -62,24 +59,21 @@ class ProjectsCustomServerTest extends Scope $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $response = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [ - 'resourceType' => 'api', + $response = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', $headers, [ 'domain' => $sitesDomain, ]); $this->assertEquals(400, $response['headers']['status-code']); // prevent functions domain - $response = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [ - 'resourceType' => 'function', + $response = $this->client->call(Client::METHOD_POST, '/proxy/rules/function', $headers, [ 'domain' => $functionsDomain, ]); $this->assertEquals(400, $response['headers']['status-code']); // prevent sites domain - $response = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [ - 'resourceType' => 'site', + $response = $this->client->call(Client::METHOD_POST, '/proxy/rules/site', $headers, [ 'domain' => $sitesDomain, ]); @@ -98,8 +92,7 @@ class ProjectsCustomServerTest extends Scope ]; foreach ($deniedDomains as $deniedDomain) { - $response = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [ - 'resourceType' => 'api', + $response = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', $headers, [ 'domain' => $deniedDomain, ]); diff --git a/tests/e2e/Services/Proxy/ProxyBase.php b/tests/e2e/Services/Proxy/ProxyBase.php new file mode 100644 index 0000000000..3a701f8795 --- /dev/null +++ b/tests/e2e/Services/Proxy/ProxyBase.php @@ -0,0 +1,295 @@ +client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), $params); + + return $rule; + } + + protected function createAPIRule(string $domain): mixed + { + $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'domain' => $domain, + ]); + + return $rule; + } + + protected function updateRuleVerification(string $ruleId): mixed + { + $rule = $this->client->call(Client::METHOD_PATCH, '/proxy/rules/' . $ruleId . '/verification', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + return $rule; + } + + protected function createSiteRule(string $domain, string $siteId, string $branch = ''): mixed + { + $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/site', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'domain' => $domain, + 'siteId' => $siteId, + 'branch' => $branch, + ]); + + return $rule; + } + + protected function getRule(string $ruleId): mixed + { + $rule = $this->client->call(Client::METHOD_GET, '/proxy/rules/' . $ruleId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + return $rule; + } + + protected function createRedirectRule(string $domain, string $target): mixed + { + $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/redirect', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'domain' => $domain, + 'target' => $target, + ]); + + return $rule; + } + + protected function createFunctionRule(string $domain, string $functionId): mixed + { + $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/function', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'domain' => $domain, + 'functionId' => $functionId, + ]); + + return $rule; + } + + protected function deleteRule(string $ruleId): mixed + { + $rule = $this->client->call(Client::METHOD_DELETE, '/proxy/rules/' . $ruleId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + return $rule; + } + + protected function setupAPIRule(string $domain): string + { + $rule = $this->createAPIRule($domain); + + $this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule)); + + return $rule['body']['$id']; + } + + protected function setupRedirectRule(string $domain, string $target): string + { + $rule = $this->createRedirectRule($domain, $target); + + $this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule)); + + return $rule['body']['$id']; + } + + protected function setupFunctionRule(string $domain, string $functionId): string + { + $rule = $this->createFunctionRule($domain, $functionId); + + $this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule)); + + return $rule['body']['$id']; + } + + protected function setupSiteRule(string $domain, string $siteId, string $branch = ''): string + { + $rule = $this->createSiteRule($domain, $siteId, $branch); + + $this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule)); + + return $rule['body']['$id']; + } + + protected function cleanupRule(string $ruleId): void + { + $rule = $this->deleteRule($ruleId); + $this->assertEquals(204, $rule['headers']['status-code'], 'Failed to cleanup rule: ' . \json_encode($rule)); + } + + protected function cleanupSite(string $siteId): void + { + $site = $this->client->call(Client::METHOD_DELETE, '/sites/' . $siteId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(204, $site['headers']['status-code'], 'Failed to cleanup site: ' . \json_encode($site)); + } + + protected function cleanupFunction(string $functionId): void + { + $function = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(204, $function['headers']['status-code'], 'Failed to cleanup function: ' . \json_encode($function)); + } + + protected function setupSite(): mixed + { + // Site + $site = $this->client->call(Client::METHOD_POST, '/sites', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), [ + 'siteId' => ID::unique(), + 'name' => 'Proxy site', + 'framework' => 'other', + 'adapter' => 'static', + 'buildRuntime' => 'static-1', + 'outputDirectory' => './', + 'buildCommand' => '', + 'installCommand' => '', + 'fallbackFile' => '', + ]); + + $this->assertEquals($site['headers']['status-code'], 201, 'Setup site failed with status code: ' . $site['headers']['status-code'] . ' and response: ' . json_encode($site['body'], JSON_PRETTY_PRINT)); + + $siteId = $site['body']['$id']; + + // Deployment + $deployment = $this->client->call(Client::METHOD_POST, '/sites/' . $siteId . '/deployments', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), [ + 'code' => $this->packageSite('static'), + 'activate' => 'true' + ]); + + $this->assertEquals($deployment['headers']['status-code'], 202, 'Setup deployment failed with status code: ' . $deployment['headers']['status-code'] . ' and response: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); + $deploymentId = $deployment['body']['$id'] ?? ''; + + $this->assertEventually(function () use ($siteId, $deploymentId) { + $site = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ])); + $this->assertEquals($deploymentId, $site['body']['deploymentId'], 'Deployment is not activated, deployment: ' . json_encode($site['body'], JSON_PRETTY_PRINT)); + }, 100000, 500); + + return ['siteId' => $siteId, 'deploymentId' => $deploymentId]; + } + + protected function setupFunction(): mixed + { + // Function + $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), [ + 'functionId' => ID::unique(), + 'runtime' => 'node-18.0', + 'name' => 'Proxy Function', + 'entrypoint' => 'index.js', + 'commands' => '', + 'execute' => ['any'] + ]); + + $this->assertEquals($function['headers']['status-code'], 201, 'Setup function failed with status code: ' . $function['headers']['status-code'] . ' and response: ' . json_encode($function['body'], JSON_PRETTY_PRINT)); + + $functionId = $function['body']['$id']; + + // Deployment + $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), [ + 'code' => $this->packageFunction('node'), + 'activate' => 'true' + ]); + + $this->assertEquals($deployment['headers']['status-code'], 202, 'Setup deployment failed with status code: ' . $deployment['headers']['status-code'] . ' and response: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); + $deploymentId = $deployment['body']['$id'] ?? ''; + + $this->assertEventually(function () use ($functionId, $deploymentId) { + $function = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ])); + $this->assertEquals($deploymentId, $function['body']['deployment'], 'Deployment is not activated, deployment: ' . json_encode($function['body'], JSON_PRETTY_PRINT)); + }, 100000, 500); + + return ['functionId' => $functionId, 'deploymentId' => $deploymentId]; + } + + private function packageSite(string $site): CURLFile + { + $stdout = ''; + $stderr = ''; + + $folderPath = realpath(__DIR__ . '/../../../resources/sites') . "/$site"; + $tarPath = "$folderPath/code.tar.gz"; + + Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $stdout, $stderr); + + if (filesize($tarPath) > 1024 * 1024 * 5) { + throw new \Exception('Code package is too large. Use the chunked upload method instead.'); + } + + return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath)); + } + + private function packageFunction(string $function): CURLFile + { + $stdout = ''; + $stderr = ''; + + $folderPath = realpath(__DIR__ . '/../../../resources/functions') . "/$function"; + $tarPath = "$folderPath/code.tar.gz"; + + Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $stdout, $stderr); + + if (filesize($tarPath) > 1024 * 1024 * 5) { + throw new \Exception('Code package is too large. Use the chunked upload method instead.'); + } + + return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath)); + } +} diff --git a/tests/e2e/Services/Proxy/ProxyCustomServerTest.php b/tests/e2e/Services/Proxy/ProxyCustomServerTest.php new file mode 100644 index 0000000000..79690dbd5a --- /dev/null +++ b/tests/e2e/Services/Proxy/ProxyCustomServerTest.php @@ -0,0 +1,436 @@ +createAPIRule($domain); + + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals($domain, $rule['body']['domain']); + $this->assertArrayHasKey('$id', $rule['body']); + $this->assertArrayHasKey('type', $rule['body']); + $this->assertArrayHasKey('value', $rule['body']); + $this->assertArrayHasKey('automation', $rule['body']); + $this->assertArrayHasKey('status', $rule['body']); + $this->assertArrayHasKey('logs', $rule['body']); + $this->assertArrayHasKey('renewAt', $rule['body']); + + $ruleId = $rule['body']['$id']; + + $rule = $this->createAPIRule($domain); + $this->assertEquals(409, $rule['headers']['status-code']); + + $rule = $this->deleteRule($ruleId); + + $this->assertEquals(204, $rule['headers']['status-code']); + } + + public function testCreateRuleSetup(): void + { + $ruleId = $this->setupAPIRule(\uniqid() . '-api2.myapp.com'); + $this->cleanupRule($ruleId); + } + + public function testCreateRuleApex(): void + { + $rule = $this->createAPIRule('myapp.com'); + $this->assertEquals(400, $rule['headers']['status-code']); + } + + public function testCreateRuleVcs(): void + { + $domain = \uniqid() . '-vcs.myapp.com'; + + $rule = $this->createAPIRule('commit-' . $domain); + $this->assertEquals(400, $rule['headers']['status-code']); + + $rule = $this->createAPIRule('branch-' . $domain); + $this->assertEquals(400, $rule['headers']['status-code']); + + $rule = $this->createAPIRule('anything-' . $domain); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->cleanupRule($rule['body']['$id']); + } + + public function testCreateAPIRule(): void + { + $domain = \uniqid() . '-api.custom.localhost'; + + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://' . $domain); + + // We should ideally assert 400, but server allows unknown domains, and serves API by default + $response = $proxyClient->call(Client::METHOD_GET, '/versions'); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(APP_VERSION_STABLE, $response['body']['server']); + + $ruleId = $this->setupAPIRule($domain); + + $this->assertNotEmpty($ruleId); + + $response = $proxyClient->call(Client::METHOD_GET, '/versions'); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(APP_VERSION_STABLE, $response['body']['server']); + + $this->cleanupRule($ruleId); + + $rule = $this->createAPIRule('http://' . $domain); + $this->assertEquals(400, $rule['headers']['status-code']); + + $rule = $this->createAPIRule('https://' . $domain); + $this->assertEquals(400, $rule['headers']['status-code']); + + // Unexpected I would say, but it is the current behaviour + $rule = $this->createAPIRule('wss://' . $domain); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->cleanupRule($rule['body']['$id']); + + // Unexpected I would say, but it is the current behaviour + $rule = $this->createAPIRule($domain . '/some-path'); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->cleanupRule($rule['body']['$id']); + } + + public function testCreateRedirectRule(): void + { + $domain = \uniqid() . '-redirect.custom.localhost'; + + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://appwrite'); + $proxyClient->addHeader('x-appwrite-hostname', $domain); + + $response = $proxyClient->call(Client::METHOD_GET, '/todos/1'); + $this->assertEquals(404, $response['headers']['status-code']); + + $ruleId = $this->setupRedirectRule($domain, 'jsonplaceholder.typicode.com'); + $this->assertNotEmpty($ruleId); + + $response = $proxyClient->call(Client::METHOD_GET, '/todos/1'); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(1, $response['body']['id']); + + $this->cleanupRule($ruleId); + } + + public function testCreateFunctionRule(): void + { + $domain = \uniqid() . '-function.custom.localhost'; + + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://appwrite'); + $proxyClient->addHeader('x-appwrite-hostname', $domain); + + $response = $proxyClient->call(Client::METHOD_GET, '/ping'); + $this->assertEquals(404, $response['headers']['status-code']); + + $setup = $this->setupFunction(); + $functionId = $setup['functionId']; + $deploymentId = $setup['deploymentId']; + + $this->assertNotEmpty($functionId); + $this->assertNotEmpty($deploymentId); + + $ruleId = $this->setupFunctionRule($domain, $functionId); + $this->assertNotEmpty($ruleId); + + $response = $proxyClient->call(Client::METHOD_GET, '/ping'); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($functionId, $response['body']['APPWRITE_FUNCTION_ID']); + + $this->cleanupRule($ruleId); + + $this->cleanupFunction($functionId); + + $this->assertEventually(function () use ($functionId, $deploymentId) { + $rules = $this->listRules([ + 'queries' => [ + Query::limit(1)->toString(), + Query::equal('type', ['deployment'])->toString(), + Query::equal('automation', ['function=' . $functionId])->toString() + ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(0, $rules['body']['total']); + $this->assertCount(0, $rules['body']['rules']); + + $rules = $this->listRules([ + 'queries' => [ + Query::limit(1)->toString(), + Query::equal('type', ['deployment'])->toString(), + Query::equal('value', [$deploymentId])->toString() + ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(0, $rules['body']['total']); + $this->assertCount(0, $rules['body']['rules']); + }); + } + + public function testCreateSiteRule(): void + { + $domain = \uniqid() . '-site.custom.localhost'; + + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://appwrite'); + $proxyClient->addHeader('x-appwrite-hostname', $domain); + + $response = $proxyClient->call(Client::METHOD_GET, '/contact'); + $this->assertEquals(404, $response['headers']['status-code']); + + $setup = $this->setupSite(); + $siteId = $setup['siteId']; + $deploymentId = $setup['deploymentId']; + + $this->assertNotEmpty($siteId); + $this->assertNotEmpty($deploymentId); + + $ruleId = $this->setupSiteRule($domain, $siteId); + $this->assertNotEmpty($ruleId); + + $response = $proxyClient->call(Client::METHOD_GET, '/contact'); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString('Contact page', $response['body']); + + $this->cleanupRule($ruleId); + + $this->cleanupSite($siteId); + + $this->assertEventually(function () use ($siteId, $deploymentId) { + $rules = $this->listRules([ + 'queries' => [ + Query::limit(1)->toString(), + Query::equal('type', ['deployment'])->toString(), + Query::equal('automation', ['site=' . $siteId])->toString() + ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(0, $rules['body']['total']); + $this->assertCount(0, $rules['body']['rules']); + + $rules = $this->listRules([ + 'queries' => [ + Query::limit(1)->toString(), + Query::equal('type', ['deployment'])->toString(), + Query::equal('value', [$deploymentId])->toString() + ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(0, $rules['body']['total']); + $this->assertCount(0, $rules['body']['rules']); + }); + } + + public function testCreatSiteBranchRule(): void + { + $domain = \uniqid() . '-site-branch.custom.localhost'; + + $setup = $this->setupSite(); + $siteId = $setup['siteId']; + $deploymentId = $setup['deploymentId']; + + $this->assertNotEmpty($siteId); + $this->assertNotEmpty($deploymentId); + + $ruleId = $this->setupSiteRule($domain, $siteId, 'dev'); + $this->assertNotEmpty($ruleId); + + $rule = $this->getRule($ruleId); + $this->assertEquals(200, $rule['headers']['status-code']); + $this->assertEquals('branch=dev', $rule['body']['automation']); + + $this->cleanupRule($ruleId); + } + + public function testUpdateRule(): void + { + // Create function appwrite-network domain + $domain = \uniqid() . '-cname-api.' . App::getEnv('_APP_DOMAIN_FUNCTIONS'); + + $rule = $this->createAPIRule($domain); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('verified', $rule['body']['status']); + + $this->cleanupRule($rule['body']['$id']); + + // Create site appwrite-network domain + $domain = \uniqid() . '-cname-api.' . App::getEnv('_APP_DOMAIN_SITES'); + + $rule = $this->createAPIRule($domain); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('verified', $rule['body']['status']); + + $this->cleanupRule($rule['body']['$id']); + + // Create + update + $domain = \uniqid() . '-cname-api.custom.localhost'; + + $rule = $this->createAPIRule($domain); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('created', $rule['body']['status']); + + $ruleId = $rule['body']['$id']; + + $rule = $this->updateRuleVerification($ruleId); + $this->assertEquals(401, $rule['headers']['status-code']); + + $this->cleanupRule($ruleId); + } + + public function testGetRule() + { + $domain = \uniqid() . '-get.custom.localhost'; + $ruleId = $this->setupAPIRule($domain); + + $this->assertNotEmpty($ruleId); + + $rule = $this->getRule($ruleId); + $this->assertEquals(200, $rule['headers']['status-code']); + $this->assertEquals($domain, $rule['body']['domain']); + $this->assertArrayHasKey('$id', $rule['body']); + $this->assertArrayHasKey('type', $rule['body']); + $this->assertArrayHasKey('value', $rule['body']); + $this->assertArrayHasKey('automation', $rule['body']); + $this->assertArrayHasKey('status', $rule['body']); + $this->assertArrayHasKey('logs', $rule['body']); + $this->assertArrayHasKey('renewAt', $rule['body']); + + $this->cleanupRule($ruleId); + } + + public function testListRules() + { + $rules = $this->listRules(); + $this->assertEquals(200, $rules['headers']['status-code']); + foreach ($rules['body']['rules'] as $rule) { + $rule = $this->deleteRule($rule['$id']); + $this->assertEquals(204, $rule['headers']['status-code']); + } + + $rules = $this->listRules(); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(0, $rules['body']['total']); + $this->assertCount(0, $rules['body']['rules']); + + $rule1Domain = \uniqid() . '-list1.custom.localhost'; + $rule1Id = $this->setupAPIRule($rule1Domain); + $this->assertNotEmpty($rule1Id); + + $rules = $this->listRules(); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(1, $rules['body']['total']); + $this->assertCount(1, $rules['body']['rules']); + $this->assertEquals($rule1Domain, $rules['body']['rules'][0]['domain']); + $this->assertArrayHasKey('$id', $rules['body']['rules'][0]); + $this->assertArrayHasKey('type', $rules['body']['rules'][0]); + $this->assertArrayHasKey('value', $rules['body']['rules'][0]); + $this->assertArrayHasKey('automation', $rules['body']['rules'][0]); + $this->assertArrayHasKey('status', $rules['body']['rules'][0]); + $this->assertArrayHasKey('logs', $rules['body']['rules'][0]); + $this->assertArrayHasKey('renewAt', $rules['body']['rules'][0]); + + $rule2Domain = \uniqid() . '-list1.custom.localhost'; + $rule2Id = $this->setupAPIRule($rule2Domain); + $this->assertNotEmpty($rule2Id); + + $rules = $this->listRules(); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(2, $rules['body']['total']); + $this->assertCount(2, $rules['body']['rules']); + + $rules = $this->listRules([ + 'queries' => [ + Query::limit(1)->toString() + ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(2, $rules['body']['total']); + $this->assertCount(1, $rules['body']['rules']); + + $rules = $this->listRules([ + 'queries' => [ + Query::equal('$id', [$rule1Id])->toString() + ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertCount(1, $rules['body']['rules']); + $this->assertEquals($rule1Domain, $rules['body']['rules'][0]['domain']); + + $rules = $this->listRules([ + 'queries' => [ + Query::orderDesc('$id')->toString() + ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertCount(2, $rules['body']['rules']); + $this->assertEquals($rule2Id, $rules['body']['rules'][0]['$id']); + + $rules = $this->listRules([ + 'queries' => [ + Query::equal('domain', [$rule2Domain])->toString() + ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertCount(1, $rules['body']['rules']); + $this->assertEquals($rule2Id, $rules['body']['rules'][0]['$id']); + + $rules = $this->listRules([ + 'search' => $rule1Domain, + 'queries' => [ Query::orderDesc('$createdAt') ] + ]); + + $this->assertEquals(200, $rules['headers']['status-code']); + $ruleIds = \array_column($rules['body']['rules'], '$id'); + $this->assertContains($rule1Id, $ruleIds); + + $rules = $this->listRules([ + 'search' => $rule2Domain, + 'queries' => [ Query::orderDesc('$createdAt') ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $ruleIds = \array_column($rules['body']['rules'], '$id'); + $this->assertContains($rule2Id, $ruleIds); + + $rules = $this->listRules([ + 'search' => $rule1Id, + 'queries' => [ Query::orderDesc('$createdAt') ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $ruleDomains = \array_column($rules['body']['rules'], 'domain'); + $this->assertContains($rule1Domain, $ruleDomains); + + $rules = $this->listRules([ + 'search' => $rule2Id, + 'queries' => [ Query::orderDesc('$createdAt') ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $ruleDomains = \array_column($rules['body']['rules'], 'domain'); + $this->assertContains($rule2Domain, $ruleDomains); + + $rules = $this->listRules(); + $this->assertEquals(200, $rules['headers']['status-code']); + foreach ($rules['body']['rules'] as $rule) { + $rule = $this->deleteRule($rule['$id']); + $this->assertEquals(204, $rule['headers']['status-code']); + } + + $rules = $this->listRules(); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(0, $rules['body']['total']); + $this->assertCount(0, $rules['body']['rules']); + } +} diff --git a/tests/e2e/Services/Sites/SitesBase.php b/tests/e2e/Services/Sites/SitesBase.php index 4c11e78d76..7d65e40db6 100644 --- a/tests/e2e/Services/Sites/SitesBase.php +++ b/tests/e2e/Services/Sites/SitesBase.php @@ -284,13 +284,12 @@ trait SitesBase protected function setupSiteDomain(string $siteId, string $subdomain = ''): string { $subdomain = $subdomain ? $subdomain : ID::unique(); - $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules', array_merge([ + $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/site', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'domain' => $subdomain . '.' . System::getEnv('_APP_DOMAIN_SITES', ''), - 'resourceType' => 'site', - 'resourceId' => $siteId, + 'siteId' => $siteId, ]); $this->assertEquals(201, $rule['headers']['status-code']); @@ -309,8 +308,8 @@ trait SitesBase 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'queries' => [ - Query::equal('resourceId', [$siteId])->toString(), - Query::equal('resourceType', ['site'])->toString(), + Query::equal('automation', ['site=' . $siteId])->toString(), + Query::equal('type', ['deployment'])->toString(), ], ]); @@ -324,7 +323,6 @@ trait SitesBase return $domain; } - protected function getDeploymentDomain(string $deploymentId): string { $rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ @@ -332,8 +330,9 @@ trait SitesBase 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'queries' => [ - Query::equal('resourceId', [$deploymentId])->toString(), - Query::equal('resourceType', ['deployment'])->toString(), + Query::equal('value', [$deploymentId])->toString(), + Query::equal('type', ['deployment'])->toString(), + Query::equal('automation', [''])->toString(), ], ]); diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index 1961e40ce5..f66e7de0f5 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -79,7 +79,7 @@ class SitesCustomServerTest extends Scope $this->assertNotEmpty($siteId); - $rule = $this->setupSiteDomain($siteId); + $domain = $this->setupSiteDomain($siteId); $response = $this->client->call(Client::METHOD_GET, '/console/resources', [ 'origin' => 'http://localhost', @@ -88,7 +88,7 @@ class SitesCustomServerTest extends Scope 'x-appwrite-project' => 'console', ], [ 'type' => 'rules', - 'value' => $rule, + 'value' => $domain, ]); $this->assertEquals(409, $response['headers']['status-code']); // domain unavailable @@ -115,7 +115,7 @@ class SitesCustomServerTest extends Scope 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'queries' => [ - Query::equal('resourceId', [$siteId]) + Query::equal('automation', ['site=' . $siteId]) ] ]); @@ -130,7 +130,7 @@ class SitesCustomServerTest extends Scope 'x-appwrite-project' => 'console', ], [ 'type' => 'rules', - 'value' => $rule, + 'value' => $domain, ]); $this->assertEquals(204, $response['headers']['status-code']); // domain available as site is deleted @@ -273,6 +273,8 @@ class SitesCustomServerTest extends Scope $this->cleanupSite($siteId); } + // This is first Sites test with Proxy + // If this fails, it may not be related to variables; but Router flow failing public function testVariablesE2E(): void { $siteId = $this->setupSite([ @@ -1310,17 +1312,15 @@ class SitesCustomServerTest extends Scope $siteId2 = $site2['body']['$id']; - $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules', array_merge([ + $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/site', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'domain' => $subdomain . '.' . System::getEnv('_APP_DOMAIN_SITES', ''), - 'resourceType' => 'site', - 'resourceId' => $siteId2, + 'siteId' => $siteId2, ]); $this->assertEquals(409, $rule['headers']['status-code']); - $this->assertStringContainsString("Document with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.", $rule['body']['message']); $this->cleanupSite($siteId); diff --git a/tests/resources/sites/static/contact.html b/tests/resources/sites/static/contact.html new file mode 100644 index 0000000000..b2c16fc471 --- /dev/null +++ b/tests/resources/sites/static/contact.html @@ -0,0 +1,11 @@ + + + + + + Contact page + + +

Contact page

+ + \ No newline at end of file