Merge pull request #9352 from appwrite/feat-all-rules-in-proxy

Move rule creation to create rule
This commit is contained in:
Matej Bačo
2025-02-18 09:28:39 +01:00
committed by GitHub
15 changed files with 402 additions and 434 deletions
-194
View File
@@ -15,176 +15,13 @@ use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Domains\Domain;
use Utopia\Logger\Log;
use Utopia\System\System;
use Utopia\Validator\Domain as ValidatorDomain;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
App::post('/v1/proxy/rules')
->groups(['api', 'proxy'])
->desc('Create 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',
description: '/docs/references/proxy/create-rule.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_PROXY_RULE,
)
]
))
->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)
->inject('response')
->inject('project')
->inject('queueForCertificates')
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('dbForProject')
->action(function (string $domain, string $resourceType, string $resourceId, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject) {
$mainDomain = System::getEnv('_APP_DOMAIN', '');
if ($domain === $mainDomain) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'You cannot assign your main domain to specific resource. Please use subdomain or a different domain.');
}
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
if (
($functionsDomain !== '' && str_ends_with($domain, $functionsDomain)) ||
($sitesDomain !== '' && str_ends_with($domain, $sitesDomain))
) {
// TODO: Refactor later
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'You cannot assign your functions or sites domain or their subdomains to a specific resource. Please use a different domain.');
}
if ($domain === 'localhost' || $domain === APP_HOSTNAME_INTERNAL) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please pick another one.');
}
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$document = $dbForPlatform->getDocument('rules', md5($domain));
} else {
$document = $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain]),
]);
}
if (!$document->isEmpty()) {
if ($document->getAttribute('projectId') === $project->getId()) {
$resourceType = $document->getAttribute('resourceType');
$resourceId = $document->getAttribute('resourceId');
$message = "Domain already assigned to '{$resourceType}' service";
if (!empty($resourceId)) {
$message .= " with ID '{$resourceId}'";
}
$message .= '.';
} else {
$message = 'Domain already assigned to different project.';
}
throw new Exception(Exception::RULE_ALREADY_EXISTS, $message);
}
$resourceInternalId = '';
switch ($resourceType) {
case 'function':
if (empty($resourceId)) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$function = $dbForProject->getDocument('functions', $resourceId);
if ($function->isEmpty()) {
throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND);
}
$resourceInternalId = $function->getInternalId();
break;
case 'site':
if (empty($resourceId)) {
throw new Exception(Exception::SITE_NOT_FOUND);
}
$site = $dbForProject->getDocument('sites', $resourceId);
if ($site->isEmpty()) {
throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND);
}
$resourceInternalId = $site->getInternalId();
break;
}
try {
$domain = new Domain($domain);
} catch (\Throwable) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.');
}
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique();
$rule = new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'domain' => $domain->get(),
'resourceType' => $resourceType,
'resourceId' => $resourceId,
'resourceInternalId' => $resourceInternalId,
'certificateId' => '',
]);
$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
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);
$queueForEvents->setParam('ruleId', $rule->getId());
$rule->setAttribute('logs', '');
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($rule, Response::MODEL_PROXY_RULE);
});
App::get('/v1/proxy/rules')
->groups(['api', 'proxy'])
@@ -411,34 +248,3 @@ App::patch('/v1/proxy/rules/:ruleId/verification')
$response->dynamic($rule, Response::MODEL_PROXY_RULE);
});
App::get('/v1/proxy/subdomains')
->desc('Check if subdomain is available')
->groups(['api', 'proxy'])
->label('scope', 'rules.read')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'proxy')
->label('sdk.method', 'checkSubdomain')
->label('sdk.description', '/docs/references/proxy/check-subdomain.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_NONE)
->param('resourceType', null, new WhiteList(['function', 'site']), 'Action definition for the rule. Possible values are "function" and "site"')
->param('subdomain', '', new Text(256), 'Subdomain name.')
->inject('response')
->inject('dbForPlatform')
->action(function (string $resourceType, string $subdomain, Response $response, Database $dbForPlatform) {
//TODO: Add tests for this endpoint
$resourceDomain = $resourceType === 'site' ? System::getEnv('_APP_DOMAIN_SITES', '') : System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
$domain = $subdomain . '.' . $resourceDomain;
$document = $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain]),
]);
if ($document && !$document->isEmpty()) {
throw new Exception(Exception::RULE_ALREADY_EXISTS, 'Subdomain already assigned to different project.');
}
$response->noContent();
});
+12 -2
View File
@@ -737,9 +737,19 @@ App::init()
$validator = new Hostname($clients);
if ($validator->isValid($origin)) {
$refDomainOrigin = $origin;
} else {
} elseif (!empty($origin)) {
// Auto-allow domains with linked rule
$rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($origin)));
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($origin)));
} else {
$rule = Authorization::skip(
fn () => $dbForPlatform->find('rules', [
Query::equal('domain', [$origin]),
Query::limit(1)
])
)[0] ?? new Document();
}
if (!$rule->isEmpty() && $rule->getAttribute('projectInternalId') === $project->getInternalId()) {
$refDomainOrigin = $origin;
}
-1
View File
@@ -1 +0,0 @@
Create a new proxy rule.
+2
View File
@@ -5,6 +5,7 @@ namespace Appwrite\Platform;
use Appwrite\Platform\Modules\Console;
use Appwrite\Platform\Modules\Core;
use Appwrite\Platform\Modules\Functions;
use Appwrite\Platform\Modules\Proxy;
use Appwrite\Platform\Modules\Sites;
use Utopia\Platform\Platform;
@@ -16,5 +17,6 @@ class Appwrite extends Platform
$this->addModule(new Functions\Module());
$this->addModule(new Sites\Module());
$this->addModule(new Console\Module());
$this->addModule(new Proxy\Module());
}
}
@@ -6,7 +6,6 @@ use Appwrite\Event\Event;
use Appwrite\Event\Validator\FunctionEvent;
use Appwrite\Extend\Exception;
use Appwrite\Functions\Validator\RuntimeSpecification;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
@@ -14,7 +13,6 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Task\Validator\Cron;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Rule;
use Utopia\Abuse\Abuse;
use Utopia\App;
use Utopia\Config\Config;
@@ -229,72 +227,6 @@ class Create extends Base
$function = $dbForProject->updateDocument('functions', $function->getId(), $function);
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
if (!empty($functionsDomain)) {
$routeSubdomain = ID::unique();
$domain = "{$routeSubdomain}.{$functionsDomain}";
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain) : ID::unique();
$rule = Authorization::skip(
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'domain' => $domain,
'resourceType' => 'function',
'resourceId' => $function->getId(),
'resourceInternalId' => $function->getInternalId(),
'status' => 'verified',
'certificateId' => '',
]))
);
/** Trigger Webhook */
$ruleModel = new Rule();
$ruleCreate =
$queueForEvents
->setClass(Event::WEBHOOK_CLASS_NAME)
->setQueue(Event::WEBHOOK_QUEUE_NAME);
$ruleCreate
->setProject($project)
->setEvent('rules.[ruleId].create')
->setParam('ruleId', $rule->getId())
->setPayload($rule->getArrayCopy(array_keys($ruleModel->getRules())))
->trigger();
/** Trigger Functions */
$ruleCreate
->setClass(Event::FUNCTIONS_CLASS_NAME)
->setQueue(Event::FUNCTIONS_QUEUE_NAME)
->trigger();
/** Trigger realtime event */
$allEvents = Event::generateEvents('rules.[ruleId].create', [
'ruleId' => $rule->getId(),
]);
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
event: $allEvents[0],
payload: $rule,
project: $project
);
Realtime::send(
projectId: 'console',
payload: $rule->getArrayCopy(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles']
);
Realtime::send(
projectId: $project->getId(),
payload: $rule->getArrayCopy(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles']
);
}
$queueForEvents->setParam('functionId', $function->getId());
$response
@@ -0,0 +1,179 @@
<?php
namespace Appwrite\Platform\Modules\Proxy\Http\Rules;
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\Database\Database;
use Utopia\Database\Document;
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\WhiteList;
class Create extends Action
{
use HTTP;
public static function getName()
{
return 'createRule';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/proxy/rules')
->groups(['api', 'proxy'])
->desc('Create 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',
description: <<<EOT
Create a new proxy rule.
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('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)
->inject('response')
->inject('project')
->inject('queueForCertificates')
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(string $domain, string $resourceType, string $resourceId, 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, true)) {
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://.');
}
// 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
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);
$queueForEvents->setParam('ruleId', $rule->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($rule, Response::MODEL_PROXY_RULE);
}
}
@@ -0,0 +1,14 @@
<?php
namespace Appwrite\Platform\Modules\Proxy;
use Appwrite\Platform\Modules\Proxy\Services\Http;
use Utopia\Platform;
class Module extends Platform\Module
{
public function __construct()
{
$this->addService('http', new Http());
}
}
@@ -0,0 +1,16 @@
<?php
namespace Appwrite\Platform\Modules\Proxy\Services;
use Appwrite\Platform\Modules\Proxy\Http\Rules\Create as CreateRule;
use Utopia\Platform\Service;
class Http extends Service
{
public function __construct()
{
$this->type = Service::TYPE_HTTP;
// Rules
$this->addAction(CreateRule::getName(), new CreateRule());
}
}
@@ -4,7 +4,6 @@ namespace Appwrite\Platform\Modules\Sites\Http\Sites;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
@@ -12,7 +11,6 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Sites\Validator\FrameworkSpecification;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Rule;
use Utopia\App;
use Utopia\Config\Config;
use Utopia\Database\Database;
@@ -20,7 +18,6 @@ use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
@@ -71,7 +68,6 @@ class Create extends Base
->param('installCommand', '', new Text(8192, 0), 'Install Command.', true)
->param('buildCommand', '', new Text(8192, 0), 'Build Command.', true)
->param('outputDirectory', '', new Text(8192, 0), 'Output Directory for site.', true)
->param('subdomain', '', new CustomId(), 'Unique custom sub-domain. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', true)
->param('buildRuntime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Runtime to use during build step.')
->param('adapter', '', new Text(8192, 0), 'Framework adapter. Allows: static, ssr', true)
->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Control System) deployment.', true)
@@ -94,7 +90,7 @@ class Create extends Base
->callback([$this, 'action']);
}
public function action(string $siteId, string $name, string $framework, bool $enabled, int $timeout, string $installCommand, string $buildCommand, string $outputDirectory, string $subdomain, string $buildRuntime, string $adapter, string $installationId, ?string $fallbackFile, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $specification, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Database $dbForPlatform)
public function action(string $siteId, string $name, string $framework, bool $enabled, int $timeout, string $installCommand, string $buildCommand, string $outputDirectory, string $buildRuntime, string $adapter, string $installationId, ?string $fallbackFile, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $specification, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Database $dbForPlatform)
{
if (!empty($adapter)) {
$configFramework = Config::getParam('frameworks')[$framework] ?? [];
@@ -105,21 +101,6 @@ class Create extends Base
}
}
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$routeSubdomain = '';
$domain = '';
if (!empty($sitesDomain)) {
$routeSubdomain = $subdomain ?: ID::unique();
$domain = "{$routeSubdomain}.{$sitesDomain}";
$subdomain = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', \md5($domain)));
if ($subdomain && !$subdomain->isEmpty()) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Subdomain already exists. Please choose a different subdomain.');
}
}
$siteId = ($siteId == 'unique()') ? ID::unique() : $siteId;
$allowList = \array_filter(\explode(',', System::getEnv('_APP_SITES_FRAMEWORKS', '')));
@@ -195,67 +176,6 @@ class Create extends Base
$site = $dbForProject->updateDocument('sites', $site->getId(), $site);
if (!empty($sitesDomain)) {
$rule = Authorization::skip(
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => \md5($domain),
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'domain' => $domain,
'resourceType' => 'site',
'resourceId' => $site->getId(),
'resourceInternalId' => $site->getInternalId(),
'status' => 'verified',
'certificateId' => '',
]))
);
/** Trigger Webhook */
$ruleModel = new Rule();
$ruleCreate =
$queueForEvents
->setClass(Event::WEBHOOK_CLASS_NAME)
->setQueue(Event::WEBHOOK_QUEUE_NAME);
$ruleCreate
->setProject($project)
->setEvent('rules.[ruleId].create')
->setParam('ruleId', $rule->getId())
->setPayload($rule->getArrayCopy(array_keys($ruleModel->getRules())))
->trigger();
/** Trigger Sites */
$ruleCreate
->setClass(Event::SITES_CLASS_NAME)
->setQueue(Event::SITES_QUEUE_NAME)
->trigger();
/** Trigger realtime event */
$allEvents = Event::generateEvents('rules.[ruleId].create', [
'ruleId' => $rule->getId(),
]);
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
event: $allEvents[0],
payload: $rule,
project: $project
);
Realtime::send(
projectId: 'console',
payload: $rule->getArrayCopy(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles']
);
Realtime::send(
projectId: $project->getId(),
payload: $rule->getArrayCopy(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles']
);
}
$queueForEvents->setParam('siteId', $site->getId());
$response
+18 -13
View File
@@ -12,10 +12,12 @@ use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
use Tests\E2E\Services\Functions\FunctionsBase;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\System\System;
class UsageTest extends Scope
{
@@ -1090,22 +1092,25 @@ class UsageTest extends Scope
$this->assertEquals(200, $response['headers']['status-code']);
$rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::equal('resourceId', [$functionId])->toString(),
Query::equal('resourceType', ['function'])->toString(),
$rule = $this->client->call(
Client::METHOD_POST,
'/proxy/rules',
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,
],
]);
);
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertEquals(1, $rules['body']['total']);
$this->assertCount(1, $rules['body']['rules']);
$this->assertNotEmpty($rules['body']['rules'][0]['domain']);
$this->assertEquals(201, $rule['headers']['status-code']);
$this->assertNotEmpty($rule['body']['$id']);
$this->assertNotEmpty($rule['body']['domain']);
$domain = $rules['body']['rules'][0]['domain'];
$domain = $rule['body']['domain'];
$response = $this->client->call(
Client::METHOD_GET,
@@ -6,6 +6,9 @@ use Appwrite\Tests\Async;
use CURLFile;
use Tests\E2E\Client;
use Utopia\CLI\Console;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\System\System;
trait FunctionsBase
{
@@ -255,4 +258,47 @@ trait FunctionsBase
return $function;
}
protected function setupFunctionDomain(string $functionId, string $subdomain = ''): string
{
$subdomain = $subdomain ? $subdomain : ID::unique();
$rule = $this->client->call(Client::METHOD_POST, '/proxy/rules', 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,
]);
$this->assertEquals(201, $rule['headers']['status-code']);
$this->assertNotEmpty($rule['body']['$id']);
$this->assertNotEmpty($rule['body']['domain']);
$domain = $rule['body']['domain'];
return $domain;
}
protected function getFunctionDomain(string $functionId): string
{
$rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::equal('resourceId', [$functionId])->toString(),
Query::equal('resourceType', ['function'])->toString(),
],
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertGreaterThanOrEqual(1, $rules['body']['total']);
$this->assertGreaterThanOrEqual(1, \count($rules['body']['rules']));
$this->assertNotEmpty($rules['body']['rules'][0]['domain']);
$domain = $rules['body']['rules'][0]['domain'];
return $domain;
}
}
@@ -1654,22 +1654,7 @@ class FunctionsCustomServerTest extends Scope
'execute' => ['any']
]);
$rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::equal('resourceId', [$functionId])->toString(),
Query::equal('resourceType', ['function'])->toString(),
],
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertEquals(1, $rules['body']['total']);
$this->assertCount(1, $rules['body']['rules']);
$this->assertNotEmpty($rules['body']['rules'][0]['domain']);
$domain = $rules['body']['rules'][0]['domain'];
$domain = $this->setupFunctionDomain($functionId);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
@@ -1730,22 +1715,7 @@ class FunctionsCustomServerTest extends Scope
'execute' => ['any']
]);
$rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::equal('resourceId', [$functionId])->toString(),
Query::equal('resourceType', ['function'])->toString(),
],
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertEquals(1, $rules['body']['total']);
$this->assertCount(1, $rules['body']['rules']);
$this->assertNotEmpty($rules['body']['rules'][0]['domain']);
$domain = $rules['body']['rules'][0]['domain'];
$domain = $this->setupFunctionDomain($functionId);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
@@ -1780,22 +1750,7 @@ class FunctionsCustomServerTest extends Scope
'execute' => ['any']
]);
$rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::equal('resourceId', [$functionId])->toString(),
Query::equal('resourceType', ['function'])->toString(),
],
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertEquals(1, $rules['body']['total']);
$this->assertCount(1, $rules['body']['rules']);
$this->assertNotEmpty($rules['body']['rules'][0]['domain']);
$domain = $rules['body']['rules'][0]['domain'];
$domain = $this->setupFunctionDomain($functionId);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
@@ -1915,6 +1870,8 @@ class FunctionsCustomServerTest extends Scope
$functionId = $function['body']['$id'] ?? '';
$domain = $this->setupFunctionDomain($functionId);
$this->setupDeployment($functionId, [
'code' => $this->packageFunction('node'),
'activate' => true
@@ -1949,20 +1906,7 @@ class FunctionsCustomServerTest extends Scope
}, 10000, 500);
// Domain Executions test
$rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::equal('resourceId', [$functionId])->toString(),
Query::equal('resourceType', ['function'])->toString(),
],
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertNotEmpty($rules['body']['rules'][0]['domain']);
$domain = $rules['body']['rules'][0]['domain'];
$domain = $this->getFunctionDomain($functionId);
$proxyClient = new Client();
$proxyClient->setEndpoint('http://' . $domain);
@@ -31,11 +31,25 @@ class ProjectsCustomServerTest extends Scope
$this->assertEquals(201, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [
'resourceType' => 'api',
'domain' => 'abc.test.io',
]);
$this->assertEquals(201, $response['headers']['status-code']);
// duplicate rule
$response2 = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [
'resourceType' => 'api',
'domain' => 'abc.test.io',
]);
$this->assertEquals(409, $response2['headers']['status-code']);
$response = $this->client->call(Client::METHOD_DELETE, '/proxy/rules/' . $response['body']['$id'], $headers);
$this->assertEquals(204, $response['headers']['status-code']);
// prevent functions domain
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
$response = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [
@@ -45,7 +59,7 @@ class ProjectsCustomServerTest extends Scope
$this->assertEquals(400, $response['headers']['status-code']);
// prevent sites domain
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$response = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [
@@ -54,5 +68,42 @@ class ProjectsCustomServerTest extends Scope
]);
$this->assertEquals(400, $response['headers']['status-code']);
// prevent functions domain
$response = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [
'resourceType' => 'function',
'domain' => $functionsDomain,
]);
$this->assertEquals(400, $response['headers']['status-code']);
// prevent sites domain
$response = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [
'resourceType' => 'site',
'domain' => $sitesDomain,
]);
$this->assertEquals(400, $response['headers']['status-code']);
$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,
];
foreach ($deniedDomains as $deniedDomain) {
$response = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [
'resourceType' => 'api',
'domain' => $deniedDomain,
]);
$this->assertEquals(400, $response['headers']['status-code']);
}
}
}
+23
View File
@@ -6,7 +6,9 @@ use Appwrite\Tests\Async;
use CURLFile;
use Tests\E2E\Client;
use Utopia\CLI\Console;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\System\System;
trait SitesBase
{
@@ -267,6 +269,27 @@ trait SitesBase
return $site;
}
protected function setupSiteDomain(string $siteId, string $subdomain = ''): string
{
$subdomain = $subdomain ? $subdomain : ID::unique();
$rule = $this->client->call(Client::METHOD_POST, '/proxy/rules', 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,
]);
$this->assertEquals(201, $rule['headers']['status-code']);
$this->assertNotEmpty($rule['body']['$id']);
$this->assertNotEmpty($rule['body']['domain']);
$domain = $rule['body']['domain'];
return $domain;
}
protected function getSiteDomain(string $siteId): string
{
$rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([
@@ -11,6 +11,7 @@ use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\System\System;
class SitesCustomServerTest extends Scope
{
@@ -73,13 +74,12 @@ class SitesCustomServerTest extends Scope
'framework' => 'other',
'buildRuntime' => 'ssr-22',
'outputDirectory' => './',
'subdomain' => 'test-site',
'fallbackFile' => null,
]);
$this->assertNotEmpty($siteId);
$rule = $this->getSiteDomain($siteId);
$rule = $this->setupSiteDomain($siteId);
$response = $this->client->call(Client::METHOD_GET, '/console/resources', [
'origin' => 'http://localhost',
@@ -289,6 +289,8 @@ class SitesCustomServerTest extends Scope
$this->assertNotEmpty($siteId);
$domain = $this->setupSiteDomain($siteId);
$secretVariable = $this->createVariable($siteId, [
'key' => 'name',
'value' => 'Appwrite',
@@ -1243,7 +1245,7 @@ class SitesCustomServerTest extends Scope
$this->assertNotEmpty($site['body']['deploymentId']);
}, 50000, 500);
$domain = $this->getSiteDomain($siteId);
$domain = $this->setupSiteDomain($siteId);
$proxyClient = new Client();
$proxyClient->setEndpoint('http://' . $domain);
@@ -1270,8 +1272,6 @@ class SitesCustomServerTest extends Scope
public function testSiteDomainReclaiming(): void
{
$subdomain = 'startup' . \uniqid();
$siteId = $this->setupSite([
'siteId' => ID::unique(),
'name' => 'Startup site',
@@ -1282,11 +1282,13 @@ class SitesCustomServerTest extends Scope
'buildCommand' => '',
'installCommand' => '',
'fallbackFile' => '',
'subdomain' => $subdomain
]);
$this->assertNotEmpty($siteId);
$subdomain = 'startup' . \uniqid();
$domain = $this->setupSiteDomain($siteId, $subdomain);
$deploymentId = $this->setupDeployment($siteId, [
'code' => $this->packageSite('static'),
'activate' => 'true'
@@ -1306,7 +1308,7 @@ class SitesCustomServerTest extends Scope
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertStringNotContainsString("This domain is not connected to any Appwrite resource yet", $response['body']);
$site = $this->createSite([
$site2 = $this->createSite([
'siteId' => ID::unique(),
'name' => 'Startup 2 site',
'framework' => 'other',
@@ -1316,11 +1318,21 @@ class SitesCustomServerTest extends Scope
'buildCommand' => '',
'installCommand' => '',
'fallbackFile' => '',
'subdomain' => $subdomain
]);
$this->assertEquals(400, $site['headers']['status-code']);
$this->assertStringContainsString("Subdomain already exists.", $site['body']['message']);
$siteId2 = $site2['body']['$id'];
$rule = $this->client->call(Client::METHOD_POST, '/proxy/rules', 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,
]);
$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);
@@ -1356,10 +1368,16 @@ class SitesCustomServerTest extends Scope
'buildCommand' => '',
'installCommand' => '',
'fallbackFile' => '',
'subdomain' => $subdomain
]);
$this->assertEquals(201, $site['headers']['status-code']);
$this->assertNotEmpty($site['body']['$id']);
$siteId = $site['body']['$id'];
$domain = $this->setupSiteDomain($siteId, $subdomain);
$this->assertNotEmpty($domain);
$this->cleanupSite($site['body']['$id']);
}
@@ -1380,6 +1398,8 @@ class SitesCustomServerTest extends Scope
$this->assertNotEmpty($siteId);
$domain = $this->setupSiteDomain($siteId);
$deploymentId = $this->setupDeployment($siteId, [
'code' => $this->packageSite('static'),
'activate' => 'true'
@@ -1443,6 +1463,7 @@ class SitesCustomServerTest extends Scope
$this->assertNotEmpty($siteId);
$this->setupSiteDomain($siteId, $subdomain);
$domain = $this->getSiteDomain($siteId);
$this->assertNotEmpty($domain);