Merge pull request #9395 from appwrite/fix-apex-rules

Feat: Enhance rules
This commit is contained in:
Matej Bačo 2025-02-25 15:34:24 +01:00 committed by GitHub
commit ac9ac3e279
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 2110 additions and 350 deletions

2
.env
View file

@ -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

View file

@ -146,6 +146,7 @@ jobs:
Projects,
Realtime,
Sites,
Proxy,
Storage,
Teams,
Users,
@ -212,6 +213,7 @@ jobs:
Projects,
Realtime,
Sites,
Proxy,
Storage,
Teams,
Users,

View file

@ -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],
],
],

View file

@ -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": "<RESOURCE_ID>"
"description": "ID of function to be executed.",
"x-example": "<FUNCTION_ID>"
}
},
"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": "<SITE_ID>"
}
},
"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": {

View file

@ -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": "<RESOURCE_ID>"
"x-example": "<FUNCTION_ID>"
}
},
"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": "<SITE_ID>"
}
},
"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": {

View file

@ -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);

View file

@ -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) {

View file

@ -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);
});

View file

@ -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));

View file

@ -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.*",

27
composer.lock generated
View file

@ -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": {

View file

@ -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]),
]))
);

View file

@ -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));
}
}

View file

@ -0,0 +1,152 @@
<?php
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\API;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\CNAME;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Domains\Domain;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\Domain as ValidatorDomain;
class Create extends Action
{
use HTTP;
public static function getName()
{
return 'createAPIRule';
}
public function __construct()
{
$this
->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: <<<EOT
Create a new proxy rule for serving Appwrite's API on custom domain.
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_PROXY_RULE,
)
]
))
->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);
}
}

View file

@ -1,6 +1,6 @@
<?php
namespace Appwrite\Platform\Modules\Proxy\Http\Rules;
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Function;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
@ -10,8 +10,10 @@ use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\UID;
use Utopia\Domains\Domain;
@ -19,7 +21,6 @@ use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\Domain as ValidatorDomain;
use Utopia\Validator\WhiteList;
class Create extends Action
{
@ -27,25 +28,25 @@ class Create extends Action
public static function getName()
{
return 'createRule';
return 'createFunctionRule';
}
public function __construct()
{
$this
->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: <<<EOT
Create a new proxy rule.
Create a new proxy rule for executing Appwrite Function on custom domain.
EOT,
auth: [AuthType::ADMIN],
responses: [
@ -59,8 +60,7 @@ class Create extends Action
->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());

View file

@ -0,0 +1,155 @@
<?php
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Redirect;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\CNAME;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Domains\Domain;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\Domain as ValidatorDomain;
class Create extends Action
{
use HTTP;
public static function getName()
{
return 'createRedirectRule';
}
public function __construct()
{
$this
->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: <<<EOT
Create a new proxy rule for to redirect from custom domain to another domain.
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_PROXY_RULE,
)
]
))
->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);
}
}

View file

@ -0,0 +1,159 @@
<?php
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Site;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\CNAME;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\UID;
use Utopia\Domains\Domain;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\Domain as ValidatorDomain;
use Utopia\Validator\Text;
class Create extends Action
{
use HTTP;
public static function getName()
{
return 'createSiteRule';
}
public function __construct()
{
$this
->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: <<<EOT
Create a new proxy rule for serving Appwrite Site on custom domain.
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_PROXY_RULE,
)
]
))
->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);
}
}

View file

@ -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());
}
}

View file

@ -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]),
]))
);

View file

@ -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 {

View file

@ -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' => '',
])));
}

View file

@ -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]),
]))
);

View file

@ -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);

View file

@ -6,8 +6,9 @@ class Rules extends Base
{
public const ALLOWED_ATTRIBUTES = [
'domain',
'resourceType',
'resourceId',
'type',
'value',
'automation',
'url'
];

View file

@ -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',
]
);
}

View file

@ -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,

View file

@ -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,
],
);

View file

@ -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
}
}

View file

@ -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(),
],
]);

View file

@ -130,7 +130,7 @@ class FunctionsServerTest extends Scope
$deployment = $deployment['body']['data']['functionsGetDeployment'];
$this->assertEquals('ready', $deployment['status']);
});
}, 30000);
return $deployment;
}

View file

@ -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,
]);

View file

@ -0,0 +1,295 @@
<?php
namespace Tests\E2E\Services\Proxy;
use Appwrite\ID;
use Appwrite\Tests\Async;
use CURLFile;
use Tests\E2E\Client;
use Utopia\CLI\Console;
trait ProxyBase
{
use Async;
protected function listRules(array $params = []): mixed
{
$rule = $this->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));
}
}

View file

@ -0,0 +1,436 @@
<?php
namespace Tests\E2E\Services\Proxy;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
use Utopia\App;
use Utopia\Database\Query;
class ProxyCustomServerTest extends Scope
{
use ProxyBase;
use ProjectCustom;
use SideServer;
public function testCreateRule(): void
{
$domain = \uniqid() . '-api.myapp.com';
$rule = $this->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']);
}
}

View file

@ -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(),
],
]);

View file

@ -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);

View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Contact page</title>
</head>
<body>
<h1>Contact page</h1>
</body>
</html>