diff --git a/.env b/.env index 2470c786b1..8f7d7996e7 100644 --- a/.env +++ b/.env @@ -109,3 +109,4 @@ _APP_MESSAGE_EMAIL_TEST_DSN= _APP_MESSAGE_PUSH_TEST_DSN= _APP_WEBHOOK_MAX_FAILED_ATTEMPTS=10 _APP_PROJECT_REGIONS=default +_APP_FUNCTIONS_CREATION_ABUSE_LIMIT=5000 diff --git a/app/controllers/api/console.php b/app/controllers/api/console.php index eeb823a3d3..1b307b1d14 100644 --- a/app/controllers/api/console.php +++ b/app/controllers/api/console.php @@ -71,7 +71,7 @@ App::post('/v1/console/assistant') ->param('prompt', '', new Text(2000), 'Prompt. A string containing questions asked to the AI assistant.') ->inject('response') ->action(function (string $prompt, Response $response) { - $ch = curl_init('http://appwrite-assistant:3003/'); + $ch = curl_init('http://appwrite-assistant:3003/v1/models/assistant/prompt'); $responseHeaders = []; $query = json_encode(['prompt' => $prompt]); $headers = ['accept: text/event-stream']; diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 003efbad03..5934258037 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -23,6 +23,8 @@ use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Model\Rule; use Executor\Executor; use MaxMind\Db\Reader; +use Utopia\Abuse\Abuse; +use Utopia\Abuse\Adapters\Database\TimeLimit; use Utopia\App; use Utopia\CLI\Console; use Utopia\Config\Config; @@ -187,6 +189,35 @@ App::post('/v1/functions') ->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $templateRepository, string $templateOwner, string $templateRootDirectory, string $templateVersion, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Database $dbForPlatform, GitHub $github) use ($redeployVcs) { $functionId = ($functionId == 'unique()') ? ID::unique() : $functionId; + // Temporary abuse check + $abuseCheck = function () use ($project, $dbForProject, $response) { + $abuseKey = "projectId:{projectId},url:{url}"; + $abuseLimit = App::getEnv('_APP_FUNCTIONS_CREATION_ABUSE_LIMIT', 50); + $abuseTime = 86400; // 1 day + + $timeLimit = new TimeLimit($abuseKey, $abuseLimit, $abuseTime, $dbForProject); + $timeLimit + ->setParam('{projectId}', $project->getId()) + ->setParam('{url}', '/v1/functions'); + + $abuse = new Abuse($timeLimit); + $remaining = $timeLimit->remaining(); + $limit = $timeLimit->limit(); + $time = (new \DateTime($timeLimit->time()))->getTimestamp() + $abuseTime; + + $response + ->addHeader('X-RateLimit-Limit', $limit) + ->addHeader('X-RateLimit-Remaining', $remaining) + ->addHeader('X-RateLimit-Reset', $time); + + $enabled = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled'; + if ($enabled && $abuse->check()) { + throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED); + } + }; + + $abuseCheck(); + $allowList = \array_filter(\explode(',', System::getEnv('_APP_FUNCTIONS_RUNTIMES', ''))); if (!empty($allowList) && !\in_array($runtime, $allowList)) { diff --git a/app/controllers/general.php b/app/controllers/general.php index 32cc68fecf..bb60b01ddd 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -91,6 +91,15 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $project = Authorization::skip( fn () => $dbForPlatform->getDocument('projects', $projectId) ); + + if (!$project->isEmpty() && $project->getId() !== 'console') { + $accessedAt = $project->getAttribute('accessedAt', ''); + if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) { + $project->setAttribute('accessedAt', DateTime::now()); + Authorization::skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project)); + } + } + if (array_key_exists('proxy', $project->getAttribute('services', []))) { $status = $project->getAttribute('services', [])['proxy']; if (!$status) { diff --git a/docker-compose.yml b/docker-compose.yml index f9a79ee84d..2fb19f7126 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -195,6 +195,7 @@ services: - _APP_DATABASE_SHARED_TABLES - _APP_DATABASE_SHARED_TABLES_V1 - _APP_DATABASE_SHARED_NAMESPACE + - _APP_FUNCTIONS_CREATION_ABUSE_LIMIT appwrite-console: <<: *x-logging @@ -868,7 +869,7 @@ services: appwrite-assistant: container_name: appwrite-assistant - image: appwrite/assistant:0.5.0 + image: appwrite/assistant:0.7.0 networks: - appwrite environment: diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index 10623e7fa5..dad2db0d9a 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -9,6 +9,7 @@ use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Exception; use Utopia\Database\Query; +use Utopia\Database\Validator\Authorization; use Utopia\Platform\Action; use Utopia\Pools\Group; use Utopia\System\System; @@ -39,6 +40,17 @@ abstract class ScheduleBase extends Action ->callback(fn (Group $pools, Database $dbForPlatform, callable $getProjectDB) => $this->action($pools, $dbForPlatform, $getProjectDB)); } + protected function updateProjectAccess(Document $project, Database $dbForPlatform): void + { + if (!$project->isEmpty() && $project->getId() !== 'console') { + $accessedAt = $project->getAttribute('accessedAt', ''); + if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) { + $project->setAttribute('accessedAt', DateTime::now()); + Authorization::skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project)); + } + } + } + /** * 1. Load all documents from 'schedules' collection to create local copy * 2. Create timer that sync all changes from 'schedules' collection to local copy. Only reading changes thanks to 'resourceUpdatedAt' attribute diff --git a/src/Appwrite/Platform/Tasks/ScheduleExecutions.php b/src/Appwrite/Platform/Tasks/ScheduleExecutions.php index 50beb48e9d..086bad513e 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleExecutions.php +++ b/src/Appwrite/Platform/Tasks/ScheduleExecutions.php @@ -57,6 +57,8 @@ class ScheduleExecutions extends ScheduleBase $delay = $scheduledAt->getTimestamp() - (new \DateTime())->getTimestamp(); + $this->updateProjectAccess($schedule['project'], $dbForPlatform); + \go(function () use ($queueForFunctions, $schedule, $delay, $data) { Co::sleep($delay); diff --git a/src/Appwrite/Platform/Tasks/ScheduleFunctions.php b/src/Appwrite/Platform/Tasks/ScheduleFunctions.php index 156ff1e31d..c443bb6c2d 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleFunctions.php +++ b/src/Appwrite/Platform/Tasks/ScheduleFunctions.php @@ -70,7 +70,7 @@ class ScheduleFunctions extends ScheduleBase } foreach ($delayedExecutions as $delay => $scheduleKeys) { - \go(function () use ($delay, $scheduleKeys, $pools) { + \go(function () use ($delay, $scheduleKeys, $pools, $dbForPlatform) { \sleep($delay); // in seconds $queue = $pools->get('queue')->pop(); @@ -84,6 +84,8 @@ class ScheduleFunctions extends ScheduleBase $schedule = $this->schedules[$scheduleKey]; + $this->updateProjectAccess($schedule['project'], $dbForPlatform); + $queueForFunctions = new Func($connection); $queueForFunctions diff --git a/src/Appwrite/Platform/Tasks/ScheduleMessages.php b/src/Appwrite/Platform/Tasks/ScheduleMessages.php index 2136e62f08..5d997fc5bb 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleMessages.php +++ b/src/Appwrite/Platform/Tasks/ScheduleMessages.php @@ -45,6 +45,8 @@ class ScheduleMessages extends ScheduleBase $connection = $queue->getResource(); $queueForMessaging = new Messaging($connection); + $this->updateProjectAccess($schedule['project'], $dbForPlatform); + $queueForMessaging ->setType(MESSAGE_SEND_TYPE_EXTERNAL) ->setMessageId($schedule['resourceId']) diff --git a/src/Appwrite/Platform/Workers/Usage.php b/src/Appwrite/Platform/Workers/Usage.php index 98b2ea32c7..8199fe73a7 100644 --- a/src/Appwrite/Platform/Workers/Usage.php +++ b/src/Appwrite/Platform/Workers/Usage.php @@ -60,12 +60,13 @@ class Usage extends Action } $document = $payload['project'] ?? []; + $project = new Document($document); - if (empty($document)) { + if (empty($project->getAttribute('database'))) { var_dump($payload); + return; } - $project = new Document($document); $projectId = $project->getInternalId(); foreach ($payload['reduce'] ?? [] as $document) { if (empty($document)) { @@ -80,6 +81,7 @@ class Usage extends Action ); } + $this->stats[$projectId]['project'] = [ '$id' => $project->getId(), '$internalId' => $project->getInternalId(), diff --git a/src/Appwrite/Platform/Workers/UsageDump.php b/src/Appwrite/Platform/Workers/UsageDump.php index 1fe6b742b8..3e50ba0363 100644 --- a/src/Appwrite/Platform/Workers/UsageDump.php +++ b/src/Appwrite/Platform/Workers/UsageDump.php @@ -72,7 +72,7 @@ class UsageDump extends Action $project = new Document($document); if (empty($project->getAttribute('database'))) { - var_dump($stats); + continue; } /**