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 3bc58eff56..ffa0ad86c9 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -3389,6 +3389,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..bb66a17904 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -473,6 +473,71 @@ 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/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']) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 0423741e6d..3a615922a0 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';