1
0
Fork 0
mirror of synced 2024-07-01 04:30:59 +12:00
appwrite/app/controllers/api/proxy.php

340 lines
14 KiB
PHP
Raw Normal View History

2023-03-09 07:30:01 +13:00
<?php
use Appwrite\Event\Certificate;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\CNAME;
use Appwrite\Utopia\Database\Validator\Queries\Rules;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\Document;
2024-02-13 05:02:04 +13:00
use Utopia\Database\Exception\Query as QueryException;
2023-08-06 20:51:53 +12:00
use Utopia\Database\Helpers\ID;
2023-03-09 07:30:01 +13:00
use Utopia\Database\Query;
use Utopia\Database\Validator\UID;
use Utopia\Domains\Domain;
2024-02-12 14:18:19 +13:00
use Utopia\Logger\Log;
2024-04-02 00:08:46 +13:00
use Utopia\System\System;
2023-08-06 20:51:53 +12:00
use Utopia\Validator\Domain as ValidatorDomain;
2023-03-09 07:30:01 +13:00
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.auth', [APP_AUTH_TYPE_ADMIN])
2023-03-09 07:30:01 +13:00
->label('sdk.namespace', 'proxy')
->label('sdk.method', 'createRule')
->label('sdk.description', '/docs/references/proxy/create-rule.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_PROXY_RULE)
2023-08-06 20:51:53 +12:00
->param('domain', null, new ValidatorDomain(), 'Domain name.')
2023-08-20 06:26:47 +12:00
->param('resourceType', null, new WhiteList(['api', 'function']), 'Action definition for the rule. Possible values are "api", "function"')
->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)
2023-03-09 07:30:01 +13:00
->inject('response')
->inject('project')
2023-10-20 04:28:01 +13:00
->inject('queueForCertificates')
->inject('queueForEvents')
2023-03-09 07:30:01 +13:00
->inject('dbForConsole')
->inject('dbForProject')
2023-10-20 04:28:01 +13:00
->action(function (string $domain, string $resourceType, string $resourceId, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForConsole, Database $dbForProject) {
2024-04-02 00:02:47 +13:00
$mainDomain = System::getEnv('_APP_DOMAIN', '');
2023-08-23 06:14:20 +12:00
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.');
}
if ($domain === 'localhost' || $domain === APP_HOSTNAME_INTERNAL) {
2023-08-22 20:16:17 +12:00
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please pick another one.');
2023-08-22 05:43:03 +12:00
}
2023-08-23 01:18:07 +12:00
2023-03-09 07:30:01 +13:00
$document = $dbForConsole->findOne('rules', [
Query::equal('domain', [$domain]),
]);
if ($document && !$document->isEmpty()) {
2023-03-15 08:31:23 +13:00
if ($document->getAttribute('projectId') === $project->getId()) {
2023-03-11 02:36:31 +13:00
$resourceType = $document->getAttribute('resourceType');
$resourceId = $document->getAttribute('resourceId');
$message = "Domain already assigned to '{$resourceType}' service";
2023-03-15 08:31:23 +13:00
if (!empty($resourceId)) {
2023-03-11 02:36:31 +13:00
$message .= " with ID '{$resourceId}'";
}
$message .= '.';
} else {
2023-08-18 18:55:44 +12:00
$message = 'Domain already assigned to different project.';
2023-03-11 02:36:31 +13:00
}
2023-03-15 08:31:23 +13:00
2023-03-11 02:36:31 +13:00
throw new Exception(Exception::RULE_ALREADY_EXISTS, $message);
2023-03-09 07:30:01 +13:00
}
$resourceInternalId = '';
2023-03-15 08:31:23 +13:00
if ($resourceType == 'function') {
if (empty($resourceId)) {
2023-07-28 20:27:16 +12:00
throw new Exception(Exception::FUNCTION_NOT_FOUND);
2023-03-09 07:30:01 +13:00
}
$function = $dbForProject->getDocument('functions', $resourceId);
if ($function->isEmpty()) {
2023-07-28 19:56:07 +12:00
throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND);
2023-03-09 07:30:01 +13:00
}
$resourceInternalId = $function->getInternalId();
}
2023-11-07 03:44:00 +13:00
try {
$domain = new Domain($domain);
} catch (\Throwable) {
2023-11-07 03:49:33 +13:00
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.');
2023-11-07 03:44:00 +13:00
}
2023-11-07 03:48:54 +13:00
2023-03-09 07:30:01 +13:00
$ruleId = ID::unique();
2023-07-12 22:55:33 +12:00
$rule = new Document([
2023-03-09 07:30:01 +13:00
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'domain' => $domain->get(),
'resourceType' => $resourceType,
'resourceId' => $resourceId,
'resourceInternalId' => $resourceInternalId,
'certificateId' => '',
2023-07-12 22:55:33 +12:00
]);
$status = 'created';
2024-04-02 00:02:47 +13:00
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS');
2023-07-25 01:12:36 +12:00
if (!empty($functionsDomain) && \str_ends_with($domain->get(), $functionsDomain)) {
2023-07-12 22:55:33 +12:00
$status = 'verified';
}
if ($status === 'created') {
2024-04-02 00:02:47 +13:00
$target = new Domain(System::getEnv('_APP_DOMAIN_TARGET', ''));
2023-07-12 22:55:33 +12:00
$validator = new CNAME($target->get()); // Verify Domain with DNS records
if ($validator->isValid($domain->get())) {
$status = 'verifying';
2023-10-20 04:28:01 +13:00
$queueForCertificates
2023-07-12 22:55:33 +12:00
->setDomain(new Document([
'domain' => $rule->getAttribute('domain')
]))
->trigger();
}
}
$rule->setAttribute('status', $status);
$rule = $dbForConsole->createDocument('rules', $rule);
2023-03-09 07:30:01 +13:00
2023-10-20 04:28:01 +13:00
$queueForEvents->setParam('ruleId', $rule->getId());
2023-03-09 07:30:01 +13:00
2023-03-14 02:35:34 +13:00
$rule->setAttribute('logs', '');
2023-03-09 07:30:01 +13:00
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
2023-03-10 20:42:52 +13:00
->dynamic($rule, Response::MODEL_PROXY_RULE);
2023-03-09 07:30:01 +13:00
});
App::get('/v1/proxy/rules')
->groups(['api', 'proxy'])
->desc('List Rules')
->label('scope', 'rules.read')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
2023-03-09 07:30:01 +13:00
->label('sdk.namespace', 'proxy')
->label('sdk.method', 'listRules')
->label('sdk.description', '/docs/references/proxy/list-rules.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_PROXY_RULE_LIST)
->param('queries', [], new Rules(), '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 ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Rules::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->inject('response')
->inject('project')
->inject('dbForConsole')
->action(function (array $queries, string $search, Response $response, Document $project, Database $dbForConsole) {
2024-02-13 05:02:04 +13:00
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
2023-03-09 07:30:01 +13:00
if (!empty($search)) {
$queries[] = Query::search('search', $search);
}
$queries[] = Query::equal('projectInternalId', [$project->getInternalId()]);
2024-02-12 22:55:45 +13:00
/**
2024-02-12 23:03:31 +13:00
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
2024-02-12 22:55:45 +13:00
*/
$cursor = \array_filter($queries, function ($query) {
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
});
2023-03-09 07:30:01 +13:00
$cursor = reset($cursor);
if ($cursor) {
/** @var Query $cursor */
$ruleId = $cursor->getValue();
$cursorDocument = $dbForConsole->getDocument('rules', $ruleId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Rule '{$ruleId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
2023-03-14 02:35:34 +13:00
$rules = $dbForConsole->find('rules', $queries);
foreach ($rules as $rule) {
$certificate = $dbForConsole->getDocument('certificates', $rule->getAttribute('certificateId', ''));
$rule->setAttribute('logs', $certificate->getAttribute('logs', ''));
$rule->setAttribute('renewAt', $certificate->getAttribute('renewDate', ''));
2023-03-14 02:35:34 +13:00
}
2023-03-09 07:30:01 +13:00
$response->dynamic(new Document([
2023-03-14 02:35:34 +13:00
'rules' => $rules,
2023-03-09 07:30:01 +13:00
'total' => $dbForConsole->count('rules', $filterQueries, APP_LIMIT_COUNT),
]), Response::MODEL_PROXY_RULE_LIST);
});
App::get('/v1/proxy/rules/:ruleId')
->groups(['api', 'proxy'])
->desc('Get Rule')
->label('scope', 'rules.read')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
2023-03-09 07:30:01 +13:00
->label('sdk.namespace', 'proxy')
->label('sdk.method', 'getRule')
->label('sdk.description', '/docs/references/proxy/get-rule.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_PROXY_RULE)
->param('ruleId', '', new UID(), 'Rule ID.')
->inject('response')
->inject('project')
->inject('dbForConsole')
->action(function (string $ruleId, Response $response, Document $project, Database $dbForConsole) {
2023-03-10 20:42:52 +13:00
$rule = $dbForConsole->getDocument('rules', $ruleId);
2023-03-09 07:30:01 +13:00
if ($rule->isEmpty() || $rule->getAttribute('projectInternalId') !== $project->getInternalId()) {
throw new Exception(Exception::RULE_NOT_FOUND);
}
2023-03-14 02:35:34 +13:00
$certificate = $dbForConsole->getDocument('certificates', $rule->getAttribute('certificateId', ''));
$rule->setAttribute('logs', $certificate->getAttribute('logs', ''));
$rule->setAttribute('renewAt', $certificate->getAttribute('renewDate', ''));
2023-03-14 02:35:34 +13:00
2023-03-09 07:30:01 +13:00
$response->dynamic($rule, Response::MODEL_PROXY_RULE);
});
App::delete('/v1/proxy/rules/:ruleId')
->groups(['api', 'proxy'])
->desc('Delete Rule')
->label('scope', 'rules.write')
->label('event', 'rules.[ruleId].delete')
2023-07-13 23:42:04 +12:00
->label('audits.event', 'rules.delete')
2023-03-09 07:30:01 +13:00
->label('audits.resource', 'rule/{request.ruleId}')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
2023-03-09 07:30:01 +13:00
->label('sdk.namespace', 'proxy')
->label('sdk.method', 'deleteRule')
->label('sdk.description', '/docs/references/proxy/delete-rule.md')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
->param('ruleId', '', new UID(), 'Rule ID.')
->inject('response')
->inject('project')
->inject('dbForConsole')
2023-10-20 04:28:01 +13:00
->inject('queueForDeletes')
->inject('queueForEvents')
->action(function (string $ruleId, Response $response, Document $project, Database $dbForConsole, Delete $queueForDeletes, Event $queueForEvents) {
2023-03-09 07:30:01 +13:00
$rule = $dbForConsole->getDocument('rules', $ruleId);
if ($rule->isEmpty() || $rule->getAttribute('projectInternalId') !== $project->getInternalId()) {
throw new Exception(Exception::RULE_NOT_FOUND);
}
2023-07-28 20:27:16 +12:00
$dbForConsole->deleteDocument('rules', $rule->getId());
2023-03-09 07:30:01 +13:00
2023-10-20 04:28:01 +13:00
$queueForDeletes
2023-03-09 07:30:01 +13:00
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($rule);
2023-10-20 04:28:01 +13:00
$queueForEvents->setParam('ruleId', $rule->getId());
2023-03-09 07:30:01 +13:00
$response->noContent();
});
App::patch('/v1/proxy/rules/:ruleId/verification')
->desc('Update Rule Verification Status')
->groups(['api', 'proxy'])
->label('scope', 'rules.write')
2023-03-11 01:20:24 +13:00
->label('event', 'rules.[ruleId].update')
->label('audits.event', 'rule.update')
->label('audits.resource', 'rule/{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
2023-03-09 07:30:01 +13:00
->label('sdk.namespace', 'proxy')
->label('sdk.method', 'updateRuleVerification')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_PROXY_RULE)
->param('ruleId', '', new UID(), 'Rule ID.')
->inject('response')
2023-10-20 04:28:01 +13:00
->inject('queueForCertificates')
->inject('queueForEvents')
2023-03-09 07:30:01 +13:00
->inject('project')
->inject('dbForConsole')
2024-02-12 14:18:19 +13:00
->inject('log')
->action(function (string $ruleId, Response $response, Certificate $queueForCertificates, Event $queueForEvents, Document $project, Database $dbForConsole, Log $log) {
2023-03-09 07:30:01 +13:00
$rule = $dbForConsole->getDocument('rules', $ruleId);
if ($rule->isEmpty() || $rule->getAttribute('projectInternalId') !== $project->getInternalId()) {
throw new Exception(Exception::RULE_NOT_FOUND);
}
2024-04-02 00:02:47 +13:00
$target = new Domain(System::getEnv('_APP_DOMAIN_TARGET', ''));
2023-03-09 07:30:01 +13:00
if (!$target->isKnown() || $target->isTest()) {
2023-07-28 20:27:16 +12:00
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Domain target must be configured as environment variable.');
2023-03-09 07:30:01 +13:00
}
if ($rule->getAttribute('verification') === true) {
return $response->dynamic($rule, Response::MODEL_PROXY_RULE);
}
$validator = new CNAME($target->get()); // Verify Domain with DNS records
2023-03-10 20:42:52 +13:00
$domain = new Domain($rule->getAttribute('domain', ''));
2023-03-09 07:30:01 +13:00
2024-02-12 14:18:19 +13:00
$validationStart = \microtime(true);
2023-03-10 20:42:52 +13:00
if (!$validator->isValid($domain->get())) {
2024-02-12 14:18:19 +13:00
$log->addExtra('dnsTiming', \strval(\microtime(true) - $validationStart));
$log->addTag('dnsDomain', $domain->get());
$error = $validator->getLogs();
$log->addExtra('dnsResponse', \is_array($error) ? \json_encode($error) : \strval($error));
2023-03-09 07:30:01 +13:00
throw new Exception(Exception::RULE_VERIFICATION_FAILED);
}
2023-03-09 08:50:51 +13:00
$dbForConsole->updateDocument('rules', $rule->getId(), $rule->setAttribute('status', 'verifying'));
2023-03-09 07:30:01 +13:00
// Issue a TLS certificate when domain is verified
2023-10-20 04:28:01 +13:00
$queueForCertificates
2023-03-09 07:30:01 +13:00
->setDomain(new Document([
'domain' => $rule->getAttribute('domain')
]))
->trigger();
2023-10-20 04:28:01 +13:00
$queueForEvents->setParam('ruleId', $rule->getId());
2023-03-11 01:20:24 +13:00
2023-03-14 02:35:34 +13:00
$certificate = $dbForConsole->getDocument('certificates', $rule->getAttribute('certificateId', ''));
$rule->setAttribute('logs', $certificate->getAttribute('logs', ''));
2023-03-09 07:30:01 +13:00
$response->dynamic($rule, Response::MODEL_PROXY_RULE);
2023-03-15 08:31:23 +13:00
});