From d3b0b00917ac2ec78803e9a898b61f0f011aa7b8 Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Mon, 4 Mar 2024 23:12:54 +0100 Subject: [PATCH 1/3] Allow users to disable APIs --- app/config/apis.php | 16 ++++++++++++++++ app/config/collections.php | 11 +++++++++++ app/config/errors.php | 5 +++++ app/controllers/api/graphql.php | 16 ++++++++++++++++ app/controllers/api/projects.php | 31 +++++++++++++++++++++++++++++++ app/controllers/general.php | 7 +++++++ app/controllers/shared/api.php | 9 +++++++++ app/init.php | 1 + app/realtime.php | 9 +++++++++ src/Appwrite/Extend/Exception.php | 1 + 10 files changed, 106 insertions(+) create mode 100644 app/config/apis.php diff --git a/app/config/apis.php b/app/config/apis.php new file mode 100644 index 0000000000..a625999682 --- /dev/null +++ b/app/config/apis.php @@ -0,0 +1,16 @@ + [ + 'key' => 'rest', + 'name' => 'REST', + ], + 'graphql' => [ + 'key' => 'graphql', + 'name' => 'GraphQL', + ], + 'realtime' => [ + 'key' => 'realtime', + 'name' => 'Realtime', + ], +]; diff --git a/app/config/collections.php b/app/config/collections.php index e2f3c11be7..511857197c 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -3367,6 +3367,17 @@ $consoleCollections = array_merge([ 'array' => false, 'filters' => ['json'], ], + [ + '$id' => ID::custom('apis'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 16384, + 'signed' => true, + 'required' => false, + 'default' => [], + 'array' => false, + 'filters' => ['json'], + ], [ '$id' => ID::custom('smtp'), 'type' => Database::VAR_STRING, diff --git a/app/config/errors.php b/app/config/errors.php index 1699157d8c..4241346cef 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -28,6 +28,11 @@ return [ 'description' => 'The request originated from an unknown origin. If you trust this domain, please list it as a trusted platform in the Appwrite console.', 'code' => 403, ], + Exception::GENERAL_API_DISABLED => [ + 'name' => Exception::GENERAL_API_DISABLED, + 'description' => 'The requested API is disabled. You can enable the API from the Appwrite console.', + 'code' => 403, + ], Exception::GENERAL_SERVICE_DISABLED => [ 'name' => Exception::GENERAL_SERVICE_DISABLED, 'description' => 'The requested service is disabled. You can enable the service from the Appwrite console.', diff --git a/app/controllers/api/graphql.php b/app/controllers/api/graphql.php index 830aecbe0c..3e4227a19b 100644 --- a/app/controllers/api/graphql.php +++ b/app/controllers/api/graphql.php @@ -16,6 +16,22 @@ use Utopia\App; use Utopia\Database\Document; use Utopia\Validator\JSON; use Utopia\Validator\Text; +use Appwrite\Auth\Auth; +use Utopia\Database\Validator\Authorization; +use Appwrite\Extend\Exception as AppwriteException; + +App::init() + ->groups(['graphql']) + ->inject('project') + ->action(function (Document $project) { + if ( + array_key_exists('graphql', $project->getAttribute('apis', [])) + && !$project->getAttribute('apis', [])['graphql'] + && !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles())) + ) { + throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED); + } + }); App::get('/v1/graphql') ->desc('GraphQL endpoint') diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 9e08c3774a..b257c4736f 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -473,6 +473,37 @@ App::patch('/v1/projects/:projectId/service/all') $response->dynamic($project, Response::MODEL_PROJECT); }); +App::patch('/v1/projects/:projectId/api') + ->desc('Update API status') + ->groups(['api', 'projects']) + ->label('scope', 'projects.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) + ->label('sdk.namespace', 'projects') + ->label('sdk.method', 'updateApiStatus') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_PROJECT) + ->param('projectId', '', new UID(), 'Project unique ID.') + ->param('api', '', new WhiteList(array_keys(Config::getParam('apis')), true), 'API name.') + ->param('status', null, new Boolean(), 'API status.') + ->inject('response') + ->inject('dbForConsole') + ->action(function (string $projectId, string $api, bool $status, Response $response, Database $dbForConsole) { + + $project = $dbForConsole->getDocument('projects', $projectId); + + if ($project->isEmpty()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + + $apis = $project->getAttribute('apis', []); + $apis[$api] = $status; + + $project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('apis', $apis)); + + $response->dynamic($project, Response::MODEL_PROJECT); + }); + App::patch('/v1/projects/:projectId/oauth2') ->desc('Update project OAuth2') ->groups(['api', 'projects']) diff --git a/app/controllers/general.php b/app/controllers/general.php index e945189b24..0295dd0912 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -92,6 +92,13 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo } } + if (array_key_exists('proxy', $project->getAttribute('apis', []))) { + $status = $project->getAttribute('apis', [])['proxy']; + if (!$status) { + throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED); + } + } + // Skip Appwrite Router for ACME challenge. Nessessary for certificate generation $path = ($swooleRequest->server['request_uri'] ?? '/'); if (\str_starts_with($path, '/.well-known/acme-challenge')) { diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index df6ec002cb..0ddd523b93 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -21,6 +21,7 @@ use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; +use Appwrite\Extend\Exception as AppwriteException; $parseLabel = function (string $label, array $responsePayload, array $requestParams, Document $user) { preg_match_all('/{(.*?)}/', $label, $matches); @@ -164,6 +165,14 @@ App::init() throw new Exception(Exception::PROJECT_UNKNOWN); } + if ( + array_key_exists('rest', $project->getAttribute('apis', [])) + && !$project->getAttribute('apis', [])['rest'] + && !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles())) + ) { + throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED); + } + /* * Abuse Check */ diff --git a/app/init.php b/app/init.php index 9696b08f6c..9bfe0b1c4e 100644 --- a/app/init.php +++ b/app/init.php @@ -236,6 +236,7 @@ App::setMode(App::getEnv('_APP_ENV', App::MODE_TYPE_PRODUCTION)); */ Config::load('events', __DIR__ . '/config/events.php'); Config::load('auth', __DIR__ . '/config/auth.php'); +Config::load('apis', __DIR__ . '/config/apis.php'); // List of APIs Config::load('errors', __DIR__ . '/config/errors.php'); Config::load('providers', __DIR__ . '/config/providers.php'); Config::load('platforms', __DIR__ . '/config/platforms.php'); diff --git a/app/realtime.php b/app/realtime.php index f7fc7070a4..970cb6cdfd 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -28,6 +28,7 @@ use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\WebSocket\Server; use Utopia\WebSocket\Adapter; +use Appwrite\Extend\Exception as AppwriteException; /** * @var \Utopia\Registry\Registry $register @@ -410,6 +411,14 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing or unknown project ID'); } + if ( + array_key_exists('realtime', $project->getAttribute('apis', [])) + && !$project->getAttribute('apis', [])['realtime'] + && !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles())) + ) { + throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED); + } + $dbForProject = getProjectDB($project); $console = $app->getResource('console'); /** @var Document $console */ $user = $app->getResource('user'); /** @var Document $user */ diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index 5d5d1b8f1f..b50cf912f6 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -40,6 +40,7 @@ class Exception extends \Exception public const GENERAL_MOCK = 'general_mock'; public const GENERAL_ACCESS_FORBIDDEN = 'general_access_forbidden'; public const GENERAL_UNKNOWN_ORIGIN = 'general_unknown_origin'; + public const GENERAL_API_DISABLED = 'general_api_disabled'; public const GENERAL_SERVICE_DISABLED = 'general_service_disabled'; public const GENERAL_UNAUTHORIZED_SCOPE = 'general_unauthorized_scope'; public const GENERAL_RATE_LIMIT_EXCEEDED = 'general_rate_limit_exceeded'; From 3d720c749514afacbd0bfe650ca8cd4607305d28 Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Mon, 4 Mar 2024 23:28:42 +0100 Subject: [PATCH 2/3] Disable all APIs --- app/controllers/api/projects.php | 34 ++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index b257c4736f..bb66a17904 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -504,6 +504,40 @@ App::patch('/v1/projects/:projectId/api') $response->dynamic($project, Response::MODEL_PROJECT); }); +App::patch('/v1/projects/:projectId/api/all') + ->desc('Update all API status') + ->groups(['api', 'projects']) + ->label('scope', 'projects.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) + ->label('sdk.namespace', 'projects') + ->label('sdk.method', 'updateAPIStatusAll') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_PROJECT) + ->param('projectId', '', new UID(), 'Project unique ID.') + ->param('status', null, new Boolean(), 'API status.') + ->inject('response') + ->inject('dbForConsole') + ->action(function (string $projectId, bool $status, Response $response, Database $dbForConsole) { + + $project = $dbForConsole->getDocument('projects', $projectId); + + if ($project->isEmpty()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + + $allApis = array_keys(Config::getParam('apis')); + + $apis = []; + foreach ($allApis as $api) { + $apis[$api] = $status; + } + + $project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('apis', $apis)); + + $response->dynamic($project, Response::MODEL_PROJECT); + }); + App::patch('/v1/projects/:projectId/oauth2') ->desc('Update project OAuth2') ->groups(['api', 'projects']) From fae8b6e46502b5e0fe74e17d236b86676ac0006c Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Thu, 7 Mar 2024 18:19:55 +0100 Subject: [PATCH 3/3] Remove extra code --- app/controllers/general.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 0295dd0912..e945189b24 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -92,13 +92,6 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo } } - if (array_key_exists('proxy', $project->getAttribute('apis', []))) { - $status = $project->getAttribute('apis', [])['proxy']; - if (!$status) { - throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED); - } - } - // Skip Appwrite Router for ACME challenge. Nessessary for certificate generation $path = ($swooleRequest->server['request_uri'] ?? '/'); if (\str_starts_with($path, '/.well-known/acme-challenge')) {