appwrite/app/controllers/shared/api.php

909 lines
38 KiB
PHP
Raw Normal View History

2019-05-09 06:54:39 +00:00
<?php
2019-10-01 04:57:41 +00:00
use Appwrite\Auth\Key;
2024-03-01 02:08:30 +00:00
use Appwrite\Auth\MFA\Type\TOTP;
2022-05-26 11:51:08 +00:00
use Appwrite\Event\Audit;
2024-02-20 11:40:55 +00:00
use Appwrite\Event\Build;
Database layer (#3338) * database response model * database collection config * new database scopes * database service update * database execption codes * remove read write permission from database model * updating tests and fixing some bugs * server side tests are now passing * databases api * tests for database endpoint * composer update * fix error * formatting * formatting fixes * get database test * more updates to events and usage * more usage updates * fix delete type * fix test * delete database * more fixes * databaseId in attributes and indexes * more fixes * fix issues * fix index subquery * fix console scope and index query * updating tests as required * fix phpcs errors and warnings * updates to review suggestions * UI progress * ui updates and cleaning up * fix type * rework database events * update tests * update types * event generation fixed * events config updated * updating context to support multiple * realtime updates * fix ids * update context * validator updates * fix naming conflict * fix tests * fix lint errors * fix wprler and realtime tests * fix webhooks test * fix event validator and other tests * formatting fixes * removing leftover var_dumps * remove leftover comment * update usage params * usage metrics updates * update database usage * fix usage * specs update * updates to usage * fix UI and usage * fix lints * internal id fixes * fixes for internal Id * renaming services and related files * rename tests * rename doc link * rename readme * fix test name * tests: fixes for 0.15.x sync Co-authored-by: Torsten Dittmann <torsten.dittmann@googlemail.com>
2022-06-22 10:51:49 +00:00
use Appwrite\Event\Database as EventDatabase;
2022-05-26 11:51:08 +00:00
use Appwrite\Event\Delete;
2022-04-04 06:30:07 +00:00
use Appwrite\Event\Event;
2022-11-15 18:13:17 +00:00
use Appwrite\Event\Func;
2025-12-18 06:30:43 +00:00
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
2024-11-04 15:20:43 +00:00
use Appwrite\Event\Realtime;
2025-01-30 04:53:53 +00:00
use Appwrite\Event\StatsUsage;
2024-11-04 15:05:54 +00:00
use Appwrite\Event\Webhook;
2024-03-06 17:34:21 +00:00
use Appwrite\Extend\Exception;
2024-03-07 23:30:23 +00:00
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Functions\EventProcessor;
use Appwrite\SDK\Method;
2025-11-04 06:08:35 +00:00
use Appwrite\Utopia\Database\Documents\User;
2022-05-26 11:51:08 +00:00
use Appwrite\Utopia\Request;
2024-03-06 17:34:21 +00:00
use Appwrite\Utopia\Response;
2019-11-29 18:23:29 +00:00
use Utopia\Abuse\Abuse;
use Utopia\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
2024-03-06 17:34:21 +00:00
use Utopia\Config\Config;
2022-05-26 11:51:08 +00:00
use Utopia\Database\Database;
2022-08-31 08:52:55 +00:00
use Utopia\Database\DateTime;
2021-10-07 15:35:17 +00:00
use Utopia\Database\Document;
use Utopia\Database\Helpers\Role;
2024-03-06 17:34:21 +00:00
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Authorization\Input;
use Utopia\Http\Http;
2024-04-01 11:02:47 +00:00
use Utopia\System\System;
2025-08-01 10:22:27 +00:00
use Utopia\Telemetry\Adapter as Telemetry;
2024-10-08 07:54:40 +00:00
use Utopia\Validator\WhiteList;
2019-11-29 18:23:29 +00:00
2025-11-04 06:08:35 +00:00
$parseLabel = function (string $label, array $responsePayload, array $requestParams, User $user) {
2022-08-16 12:28:30 +00:00
preg_match_all('/{(.*?)}/', $label, $matches);
foreach ($matches[1] ?? [] as $pos => $match) {
$find = $matches[0][$pos];
$parts = explode('.', $match);
if (count($parts) !== 2) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, "The server encountered an error while parsing the label: $label. Please create an issue on GitHub to allow us to investigate further https://github.com/appwrite/appwrite/issues/new/choose");
2022-08-16 12:28:30 +00:00
}
$namespace = $parts[0] ?? '';
$replace = $parts[1] ?? '';
$params = match ($namespace) {
'user' => (array)$user,
'request' => $requestParams,
default => $responsePayload,
};
if (array_key_exists($replace, $params)) {
$replacement = $params[$replace];
// Convert to string if it's not already a string
if (!is_string($replacement)) {
2025-10-30 02:07:15 +00:00
if (is_array($replacement)) {
$replacement = json_encode($replacement);
} elseif (is_object($replacement) && method_exists($replacement, '__toString')) {
$replacement = (string)$replacement;
} elseif (is_scalar($replacement)) {
$replacement = (string)$replacement;
} else {
throw new Exception(Exception::GENERAL_SERVER_ERROR, "The server encountered an error while parsing the label: $label. Please create an issue on GitHub to allow us to investigate further https://github.com/appwrite/appwrite/issues/new/choose");
}
}
$label = \str_replace($find, $replacement, $label);
2022-08-16 12:28:30 +00:00
}
}
return $label;
};
2019-11-29 18:23:29 +00:00
2026-02-04 05:30:22 +00:00
Http::init()
->groups(['api'])
2024-10-08 07:54:40 +00:00
->inject('utopia')
->inject('request')
->inject('dbForPlatform')
2024-11-20 04:29:57 +00:00
->inject('dbForProject')
->inject('queueForAudits')
->inject('project')
->inject('user')
->inject('session')
->inject('servers')
->inject('mode')
->inject('team')
->inject('apiKey')
->inject('authorization')
2026-02-04 05:30:22 +00:00
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, Document $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
2024-10-08 07:54:40 +00:00
$route = $utopia->getRoute();
2025-11-04 06:08:35 +00:00
/**
* Handle user authentication and session validation.
*
* This function follows a series of steps to determine the appropriate user session
* based on cookies, headers, and JWT tokens.
*
* Process:
*
* Project & Role Validation:
* 1. Check if the project is empty. If so, throw an exception.
* 2. Get the roles configuration.
* 3. Determine the role for the user based on the user document.
* 4. Get the scopes for the role.
*
* API Key Authentication:
* 5. If there is an API key:
* - Verify no user session exists simultaneously
* - Check if key is expired
* - Set role and scopes from API key
* - Handle special app role case
* - For standard keys, update last accessed time
*
* User Activity:
* 6. If the project is not the console and user is not admin:
* - Update user's last activity timestamp
*
* Access Control:
* 7. Get the method from the route
* 8. Validate namespace permissions
* 9. Validate scope permissions
* 10. Check if user is blocked
*
* Security Checks:
* 11. Verify password status (check if reset required)
* 12. Validate MFA requirements:
* - Check if MFA is enabled
* - Verify email status
* - Verify phone status
* - Verify authenticator status
* 13. Handle Multi-Factor Authentication:
* - Check remaining required factors
* - Validate factor completion
* - Throw exception if factors incomplete
*/
// Step 1: Check if project is empty
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
2025-11-04 06:08:35 +00:00
// Step 2: Get roles configuration
2024-07-23 22:47:46 +00:00
$roles = Config::getParam('roles', []);
2025-11-04 06:08:35 +00:00
// Step 3: Determine role for user
// TODO get scopes from the identity instead of the user roles config. The identity will containn the scopes the user authorized for the access token.
$role = $user->isEmpty()
? Role::guests()->toString()
: Role::users()->toString();
2025-11-04 06:08:35 +00:00
// Step 4: Get scopes for the role
2024-07-23 19:49:11 +00:00
$scopes = $roles[$role]['scopes'];
2025-11-04 06:08:35 +00:00
// Step 5: API Key Authentication
2024-05-14 11:58:31 +00:00
if (!empty($apiKey)) {
2025-11-04 06:08:35 +00:00
// Check if key is expired
2025-02-12 08:22:08 +00:00
if ($apiKey->isExpired()) {
throw new Exception(Exception::PROJECT_KEY_EXPIRED);
2024-05-16 08:39:15 +00:00
}
2024-05-14 11:58:31 +00:00
2025-11-04 06:08:35 +00:00
// Set role and scopes from API key
2025-02-12 07:34:38 +00:00
$role = $apiKey->getRole();
$scopes = $apiKey->getScopes();
2025-11-04 06:08:35 +00:00
// Handle special app role case
if ($apiKey->getRole() === User::ROLE_APPS) {
2026-02-16 15:14:43 +00:00
// Disable authorization checks for project API keys
2026-02-16 15:24:33 +00:00
if ($apiKey->getType() === API_KEY_STANDARD && $apiKey->getProjectId() === $project->getId()) {
2026-02-16 15:14:43 +00:00
$authorization->setDefaultStatus(false);
}
2025-09-13 22:58:21 +00:00
2025-11-04 06:08:35 +00:00
$user = new User([
2025-02-12 08:25:53 +00:00
'$id' => '',
'status' => true,
2025-11-04 06:08:35 +00:00
'type' => ACTIVITY_TYPE_APP,
2025-02-12 08:25:53 +00:00
'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(),
'password' => '',
'name' => $apiKey->getName(),
]);
2025-02-12 07:34:38 +00:00
$queueForAudits->setUser($user);
}
2025-11-04 06:08:35 +00:00
// For standard keys, update last accessed time
2025-12-27 18:08:12 +00:00
if (\in_array($apiKey->getType(), [API_KEY_STANDARD, API_KEY_ORGANIZATION, API_KEY_ACCOUNT])) {
2025-12-29 12:24:26 +00:00
$dbKey = null;
2025-12-27 17:44:01 +00:00
if (!empty($apiKey->getProjectId())) {
$dbKey = $project->find(
key: 'secret',
find: $request->getHeader('x-appwrite-key', ''),
subject: 'keys'
);
} elseif (!empty($apiKey->getUserId())) {
$dbKey = $user->find(
key: 'secret',
find: $request->getHeader('x-appwrite-key', ''),
subject: 'keys'
);
} elseif (!empty($apiKey->getTeamId())) {
$dbKey = $team->find(
key: 'secret',
find: $request->getHeader('x-appwrite-key', ''),
subject: 'keys'
);
}
2025-04-15 22:50:53 +00:00
if (!$dbKey) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
2025-12-27 17:44:01 +00:00
$updates = new Document();
2025-04-25 10:18:04 +00:00
$accessedAt = $dbKey->getAttribute('accessedAt', 0);
2025-04-25 10:57:45 +00:00
2025-04-15 22:50:53 +00:00
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) {
2025-12-27 17:44:01 +00:00
$updates->setAttribute('accessedAt', DateTime::now());
2025-04-15 22:50:53 +00:00
}
2024-05-06 09:55:59 +00:00
2025-04-15 22:50:53 +00:00
$sdkValidator = new WhiteList($servers, true);
$sdk = $request->getHeader('x-sdk-name', 'UNKNOWN');
2024-05-06 08:35:27 +00:00
2025-04-25 10:18:04 +00:00
if ($sdk !== 'UNKNOWN' && $sdkValidator->isValid($sdk)) {
2025-04-15 22:50:53 +00:00
$sdks = $dbKey->getAttribute('sdks', []);
2025-04-15 22:50:53 +00:00
if (!in_array($sdk, $sdks)) {
$sdks[] = $sdk;
2024-05-06 09:55:59 +00:00
2025-12-27 17:44:01 +00:00
$updates->setAttribute('sdks', $sdks);
$updates->setAttribute('accessedAt', Datetime::now());
2024-05-06 09:55:59 +00:00
}
}
2025-04-15 22:50:53 +00:00
2025-12-27 17:44:01 +00:00
if (!$updates->isEmpty()) {
2026-01-21 15:05:43 +00:00
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->updateDocument('keys', $dbKey->getId(), $updates));
2026-01-15 15:16:09 +00:00
if (!empty($apiKey->getProjectId())) {
2026-01-21 15:05:43 +00:00
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
2026-01-15 15:16:09 +00:00
} elseif (!empty($apiKey->getUserId())) {
2026-01-21 15:05:43 +00:00
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('users', $user->getId()));
2026-01-15 15:16:09 +00:00
} elseif (!empty($apiKey->getTeamId())) {
2026-01-21 15:05:43 +00:00
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('teams', $team->getId()));
2026-01-15 15:16:09 +00:00
}
2025-12-27 17:44:01 +00:00
}
2026-02-16 15:14:43 +00:00
$authorization->addRole(Role::team($team->getId())->toString());
$authorization->addRole(Role::team($team->getId(), 'owner')->toString());
$authorization->addRole(Role::member($team->getId())->toString());
2025-04-15 22:50:53 +00:00
$queueForAudits->setUser($user);
}
2025-02-12 07:34:38 +00:00
} // Admin User Authentication
2024-09-24 01:19:05 +00:00
elseif (($project->getId() === 'console' && !$team->isEmpty() && !$user->isEmpty()) || ($project->getId() !== 'console' && !$user->isEmpty() && $mode === APP_MODE_ADMIN)) {
$teamId = $team->getId();
2024-07-23 19:49:11 +00:00
$adminRoles = [];
$memberships = $user->getAttribute('memberships', []);
foreach ($memberships as $membership) {
2024-07-23 22:47:46 +00:00
if ($membership->getAttribute('confirm', false) === true && $membership->getAttribute('teamId') === $teamId) {
2024-07-23 19:49:11 +00:00
$adminRoles = $membership->getAttribute('roles', []);
break;
}
}
if (empty($adminRoles)) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
$scopes = []; // Reset scope if admin
2024-08-21 05:51:37 +00:00
foreach ($adminRoles as $role) {
2024-07-23 22:47:46 +00:00
$scopes = \array_merge($scopes, $roles[$role]['scopes']);
}
2024-10-08 07:54:40 +00:00
$authorization->setDefaultStatus(false); // Cancel security segmentation for admin users.
2024-07-23 19:49:11 +00:00
}
2024-07-23 19:54:52 +00:00
2024-07-23 19:49:11 +00:00
$scopes = \array_unique($scopes);
$authorization->addRole($role);
foreach ($user->getRoles($authorization) as $authRole) {
$authorization->addRole($authRole);
}
2025-11-04 06:08:35 +00:00
// Step 6: Update project and user last activity
2024-11-20 04:29:57 +00:00
if (!$project->isEmpty() && $project->getId() !== 'console') {
2025-04-17 14:13:09 +00:00
$accessedAt = $project->getAttribute('accessedAt', 0);
2024-11-20 04:29:57 +00:00
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));
2024-11-20 04:29:57 +00:00
}
}
2024-11-26 08:59:03 +00:00
if (!empty($user->getId())) {
2025-04-17 14:13:09 +00:00
$accessedAt = $user->getAttribute('accessedAt', 0);
2024-11-20 04:29:57 +00:00
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCESS)) > $accessedAt) {
$user->setAttribute('accessedAt', DateTime::now());
if (APP_MODE_ADMIN !== $mode) {
$dbForProject->updateDocument('users', $user->getId(), $user);
} else {
$dbForPlatform->updateDocument('users', $user->getId(), $user);
2024-11-20 04:29:57 +00:00
}
}
}
2025-11-04 06:08:35 +00:00
// Steps 7-9: Access Control - Method, Namespace and Scope Validation
2025-01-17 04:31:39 +00:00
/**
* @var ?Method $method
2025-01-17 04:31:39 +00:00
*/
$method = $route->getLabel('sdk', false);
2025-03-27 08:03:28 +00:00
// Take the first method if there's more than one,
// namespace can not differ between methods on the same route
if (\is_array($method)) {
2025-01-17 04:31:39 +00:00
$method = $method[0];
}
if (!empty($method)) {
$namespace = $method->getNamespace();
if (
2025-01-17 04:31:39 +00:00
array_key_exists($namespace, $project->getAttribute('services', []))
&& !$project->getAttribute('services', [])[$namespace]
&& !(User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
) {
throw new Exception(Exception::GENERAL_SERVICE_DISABLED);
}
}
2025-11-04 06:08:35 +00:00
// Step 9: Validate scope permissions
2025-09-11 06:51:01 +00:00
$allowed = (array)$route->getLabel('scope', 'none');
if (empty(\array_intersect($allowed, $scopes))) {
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, $user->getAttribute('email', 'User') . ' (role: ' . \strtolower($roles[$role]['label']) . ') missing scopes (' . \json_encode($allowed) . ')');
}
2025-11-04 06:08:35 +00:00
// Step 10: Check if user is blocked
if (false === $user->getAttribute('status')) { // Account is blocked
throw new Exception(Exception::USER_BLOCKED);
}
2025-11-04 06:08:35 +00:00
// Step 11: Verify password status
if ($user->getAttribute('reset')) {
throw new Exception(Exception::USER_PASSWORD_RESET_REQUIRED);
}
2025-11-04 06:08:35 +00:00
// Step 12: Validate MFA requirements
2024-04-17 09:10:33 +00:00
$mfaEnabled = $user->getAttribute('mfa', false);
$hasVerifiedEmail = $user->getAttribute('emailVerification', false);
$hasVerifiedPhone = $user->getAttribute('phoneVerification', false);
$hasVerifiedAuthenticator = TOTP::getAuthenticatorFromUser($user)?->getAttribute('verified') ?? false;
$hasMoreFactors = $hasVerifiedEmail || $hasVerifiedPhone || $hasVerifiedAuthenticator;
$minimumFactors = ($mfaEnabled && $hasMoreFactors) ? 2 : 1;
2025-11-04 06:08:35 +00:00
// Step 13: Handle Multi-Factor Authentication
2024-04-17 09:10:33 +00:00
if (!in_array('mfa', $route->getGroups())) {
if ($session && \count($session->getAttribute('factors', [])) < $minimumFactors) {
2024-04-17 09:10:33 +00:00
throw new Exception(Exception::USER_MORE_FACTORS_REQUIRED);
}
}
});
2026-02-04 05:30:22 +00:00
Http::init()
2022-08-02 01:10:48 +00:00
->groups(['api'])
2024-10-08 07:54:40 +00:00
->inject('utopia')
2022-07-22 06:00:42 +00:00
->inject('request')
->inject('response')
->inject('project')
->inject('user')
2022-12-20 16:11:30 +00:00
->inject('queueForEvents')
->inject('queueForMessaging')
2022-12-20 16:11:30 +00:00
->inject('queueForAudits')
->inject('queueForDeletes')
->inject('queueForDatabase')
2024-02-20 11:40:55 +00:00
->inject('queueForBuilds')
2025-01-30 04:53:53 +00:00
->inject('queueForStatsUsage')
2025-12-07 20:29:45 +00:00
->inject('queueForFunctions')
->inject('queueForMails')
2022-07-22 06:00:42 +00:00
->inject('dbForProject')
2024-12-20 14:44:50 +00:00
->inject('timelimit')
2024-01-04 06:53:22 +00:00
->inject('resourceToken')
2022-07-22 06:00:42 +00:00
->inject('mode')
->inject('apiKey')
2025-04-03 02:44:29 +00:00
->inject('plan')
2024-11-22 04:21:03 +00:00
->inject('devKey')
2025-08-01 10:22:27 +00:00
->inject('telemetry')
2025-12-07 20:29:45 +00:00
->inject('platform')
->inject('authorization')
2026-02-04 05:30:22 +00:00
->action(function (Http $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Mail $queueForMails, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization) {
2024-10-08 07:54:40 +00:00
$route = $utopia->getRoute();
2024-03-04 22:12:54 +00:00
if (
array_key_exists('rest', $project->getAttribute('apis', []))
&& !$project->getAttribute('apis', [])['rest']
&& !(User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
2024-03-04 22:12:54 +00:00
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
/*
2022-07-22 06:00:42 +00:00
* Abuse Check
*/
2022-08-11 23:53:52 +00:00
$abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}');
$timeLimitArray = [];
2021-06-07 05:17:29 +00:00
2022-07-22 06:00:42 +00:00
$abuseKeyLabel = (!is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel;
2021-06-07 05:17:29 +00:00
2022-08-11 23:53:52 +00:00
foreach ($abuseKeyLabel as $abuseKey) {
$start = $request->getContentRangeStart();
$end = $request->getContentRangeEnd();
2024-12-20 14:44:50 +00:00
$timeLimit = $timelimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600));
2022-08-11 23:53:52 +00:00
$timeLimit
2024-02-12 01:18:19 +00:00
->setParam('{projectId}', $project->getId())
2023-05-23 13:43:03 +00:00
->setParam('{userId}', $user->getId())
->setParam('{userAgent}', $request->getUserAgent(''))
->setParam('{ip}', $request->getIP())
->setParam('{url}', $request->getHostname() . $route->getPath())
->setParam('{method}', $request->getMethod())
->setParam('{chunkId}', (int)($start / ($end + 1 - $start)));
2022-08-11 23:53:52 +00:00
$timeLimitArray[] = $timeLimit;
}
2021-06-07 05:17:29 +00:00
2022-07-22 06:00:42 +00:00
$closestLimit = null;
2021-06-07 05:17:29 +00:00
$roles = $authorization->getRoles();
2025-11-04 06:08:35 +00:00
$isPrivilegedUser = User::isPrivileged($roles);
$isAppUser = User::isApp($roles);
2021-06-07 05:17:29 +00:00
2022-07-22 06:00:42 +00:00
foreach ($timeLimitArray as $timeLimit) {
foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys
if (!empty($value)) {
$timeLimit->setParam('{param-' . $key . '}', (\is_array($value)) ? \json_encode($value) : $value);
}
}
2021-06-07 05:17:29 +00:00
2022-07-22 06:00:42 +00:00
$abuse = new Abuse($timeLimit);
$remaining = $timeLimit->remaining();
$limit = $timeLimit->limit();
2024-12-20 14:44:50 +00:00
$time = $timeLimit->time() + $route->getLabel('abuse-time', 3600);
2022-07-22 06:00:42 +00:00
if ($limit && ($remaining < $closestLimit || is_null($closestLimit))) {
$closestLimit = $remaining;
2022-07-22 06:00:42 +00:00
$response
->addHeader('X-RateLimit-Limit', $limit)
->addHeader('X-RateLimit-Remaining', $remaining)
->addHeader('X-RateLimit-Reset', $time);
2021-02-28 18:36:13 +00:00
}
2021-06-07 05:17:29 +00:00
2024-04-01 11:02:47 +00:00
$enabled = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled';
2022-07-22 06:00:42 +00:00
if (
$enabled // Abuse is enabled
&& !$isAppUser // User is not API key
&& !$isPrivilegedUser // User is not an admin
2024-11-22 04:21:03 +00:00
&& $devKey->isEmpty() // request doesn't not contain development key
&& $abuse->check() // Route is rate-limited
) {
2022-08-08 14:44:07 +00:00
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED);
2021-02-28 18:36:13 +00:00
}
}
2021-01-05 12:22:20 +00:00
2025-12-07 20:29:45 +00:00
/**
* TODO: (@loks0n)
* Avoid mutating the message across file boundaries - it's difficult to reason about at scale.
*/
2022-11-16 05:30:57 +00:00
/*
* Background Jobs
*/
2022-12-20 16:11:30 +00:00
$queueForEvents
2022-07-22 06:00:42 +00:00
->setEvent($route->getLabel('event', ''))
->setProject($project)
2022-08-11 13:47:53 +00:00
->setUser($user);
2022-04-04 06:30:07 +00:00
2022-12-20 16:11:30 +00:00
$queueForAudits
2022-07-22 06:00:42 +00:00
->setMode($mode)
->setUserAgent($request->getUserAgent(''))
->setIP($request->getIP())
2025-01-03 10:00:55 +00:00
->setHostname($request->getHostname())
2022-09-04 08:23:24 +00:00
->setEvent($route->getLabel('audits.event', ''))
->setProject($project);
2025-01-21 06:37:03 +00:00
/* If a session exists, use the user associated with the session */
if (!$user->isEmpty()) {
2025-01-21 06:41:20 +00:00
$userClone = clone $user;
2025-01-14 08:17:01 +00:00
// $user doesn't support `type` and can cause unintended effects.
2025-11-04 06:08:35 +00:00
$userClone->setAttribute('type', ACTIVITY_TYPE_USER);
2025-01-21 06:41:20 +00:00
$queueForAudits->setUser($userClone);
}
2021-02-28 18:36:13 +00:00
if (!empty($apiKey) && !empty($apiKey->getDisabledMetrics())) {
foreach ($apiKey->getDisabledMetrics() as $key) {
$queueForStatsUsage->disableMetric($key);
}
}
2025-12-07 20:29:45 +00:00
/* Auto-set projects */
2022-12-20 16:11:30 +00:00
$queueForDeletes->setProject($project);
$queueForDatabase->setProject($project);
2024-02-20 12:06:35 +00:00
$queueForMessaging->setProject($project);
2025-12-07 20:29:45 +00:00
$queueForFunctions->setProject($project);
$queueForBuilds->setProject($project);
$queueForMails->setProject($project);
2025-12-07 20:29:45 +00:00
/* Auto-set platforms */
$queueForFunctions->setPlatform($platform);
$queueForBuilds->setPlatform($platform);
$queueForMails->setPlatform($platform);
2022-08-09 11:57:33 +00:00
2022-10-17 07:10:18 +00:00
2022-08-09 11:57:33 +00:00
$useCache = $route->getLabel('cache', false);
2025-08-01 10:22:27 +00:00
$storageCacheOperationsCounter = $telemetry->createCounter('storage.cache.operations.load');
2022-08-09 11:57:33 +00:00
if ($useCache) {
2025-04-03 02:44:29 +00:00
$route = $utopia->match($request);
$isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview';
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && !User::isPrivileged($authorization->getRoles());
2025-04-03 02:44:29 +00:00
$key = $request->cacheIdentifier();
$cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key));
2022-08-15 13:55:11 +00:00
$cache = new Cache(
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId())
);
$timestamp = 60 * 60 * 24 * 180; // Temporarily increase the TTL to 180 day to ensure files in the cache are still fetched.
2022-08-09 11:57:33 +00:00
$data = $cache->load($key, $timestamp);
if (!empty($data) && !$cacheLog->isEmpty()) {
$usageMetric = $route->getLabel('usage.metric', null);
if ($usageMetric === METRIC_AVATARS_SCREENSHOTS_GENERATED) {
$queueForStatsUsage->disableMetric(METRIC_AVATARS_SCREENSHOTS_GENERATED);
}
2025-04-11 14:52:19 +00:00
$parts = explode('/', $cacheLog->getAttribute('resourceType', ''));
$type = $parts[0] ?? null;
2025-04-03 02:44:29 +00:00
if ($type === 'bucket' && (!$isImageTransformation || !$isDisabled)) {
$bucketId = $parts[1] ?? null;
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isToken = !$resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence();
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
2023-08-16 21:58:25 +00:00
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAppUser && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
if (!$bucket->getAttribute('transformations', true) && !$isAppUser && !$isPrivilegedUser) {
throw new Exception(Exception::STORAGE_BUCKET_TRANSFORMATIONS_DISABLED);
}
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
$valid = $authorization->isValid(new Input(Database::PERMISSION_READ, $bucket->getRead()));
2024-01-04 06:53:22 +00:00
if (!$fileSecurity && !$valid && !$isToken) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
$parts = explode('/', $cacheLog->getAttribute('resource'));
$fileId = $parts[1] ?? null;
2024-01-04 06:53:22 +00:00
if ($fileSecurity && !$valid && !$isToken) {
2025-05-26 05:42:11 +00:00
$file = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId);
} else {
$file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
}
if (!$resourceToken->isEmpty() && $resourceToken->getAttribute('fileInternalId') !== $file->getSequence()) {
2024-01-04 06:53:22 +00:00
throw new Exception(Exception::USER_UNAUTHORIZED);
}
if ($file->isEmpty()) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
//Do not update transformedAt if it's a console user
if (!User::isPrivileged($authorization->getRoles())) {
$transformedAt = $file->getAttribute('transformedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) {
$file->setAttribute('transformedAt', DateTime::now());
$authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $file->getAttribute('bucketInternalId'), $file->getId(), $file));
}
2025-01-28 14:49:55 +00:00
}
}
2022-08-09 13:43:37 +00:00
$response
->addHeader('Cache-Control', sprintf('private, max-age=%d', $timestamp))
2022-08-09 13:43:37 +00:00
->addHeader('X-Appwrite-Cache', 'hit')
2025-04-03 02:44:29 +00:00
->setContentType($cacheLog->getAttribute('mimeType'));
2025-08-01 10:22:27 +00:00
$storageCacheOperationsCounter->add(1, ['result' => 'hit']);
2025-04-03 02:44:29 +00:00
if (!$isImageTransformation || !$isDisabled) {
$response->send($data);
}
2022-08-09 13:43:37 +00:00
} else {
2025-08-01 10:22:27 +00:00
$storageCacheOperationsCounter->add(1, ['result' => 'miss']);
$response
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
->addHeader('Pragma', 'no-cache')
->addHeader('Expires', '0')
2025-02-12 07:34:38 +00:00
->addHeader('X-Appwrite-Cache', 'miss');
2022-08-09 13:43:37 +00:00
}
}
2022-07-22 06:00:42 +00:00
});
2026-02-04 05:30:22 +00:00
Http::init()
->groups(['session'])
->inject('user')
->inject('request')
->action(function (Document $user, Request $request) {
if (\str_contains($request->getURI(), 'oauth2')) {
return;
}
if (!$user->isEmpty()) {
throw new Exception(Exception::USER_SESSION_ALREADY_EXISTS);
}
});
2023-05-23 13:43:03 +00:00
/**
* Limit user session
*
* Delete older sessions if the number of sessions have crossed
* the session limit set for the project
*/
2026-02-04 05:30:22 +00:00
Http::shutdown()
2023-05-23 13:43:03 +00:00
->groups(['session'])
2024-10-08 07:54:40 +00:00
->inject('utopia')
->inject('request')
2023-05-23 13:43:03 +00:00
->inject('response')
->inject('project')
->inject('dbForProject')
2026-02-04 05:30:22 +00:00
->action(function (Http $utopia, Request $request, Response $response, Document $project, Database $dbForProject) {
2023-05-23 13:43:03 +00:00
$sessionLimit = $project->getAttribute('auths', [])['maxSessions'] ?? APP_LIMIT_USER_SESSIONS_DEFAULT;
$session = $response->getPayload();
$userId = $session['userId'] ?? '';
if (empty($userId)) {
return;
}
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
return;
}
$sessions = $user->getAttribute('sessions', []);
$count = \count($sessions);
if ($count <= $sessionLimit) {
return;
}
for ($i = 0; $i < ($count - $sessionLimit); $i++) {
$session = array_shift($sessions);
$dbForProject->deleteDocument('sessions', $session->getId());
}
2023-12-14 13:32:06 +00:00
$dbForProject->purgeCachedDocument('users', $userId);
2023-05-23 13:43:03 +00:00
});
2026-02-04 05:30:22 +00:00
Http::shutdown()
2022-08-02 01:10:48 +00:00
->groups(['api'])
2024-10-08 07:54:40 +00:00
->inject('utopia')
2022-07-22 06:00:42 +00:00
->inject('request')
->inject('response')
->inject('project')
->inject('user')
2022-12-20 16:11:30 +00:00
->inject('queueForEvents')
->inject('queueForAudits')
2025-01-30 04:53:53 +00:00
->inject('queueForStatsUsage')
2022-12-20 16:11:30 +00:00
->inject('queueForDeletes')
->inject('queueForDatabase')
2024-02-20 11:40:55 +00:00
->inject('queueForBuilds')
2024-02-20 12:06:35 +00:00
->inject('queueForMessaging')
2022-11-16 10:33:11 +00:00
->inject('queueForFunctions')
2024-11-04 15:05:54 +00:00
->inject('queueForWebhooks')
->inject('queueForRealtime')
->inject('dbForProject')
->inject('authorization')
2026-01-05 21:05:00 +00:00
->inject('timelimit')
->inject('eventProcessor')
2026-02-04 05:30:22 +00:00
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor) use ($parseLabel) {
2022-08-09 11:57:33 +00:00
2022-08-07 15:09:37 +00:00
$responsePayload = $response->getPayload();
2022-07-22 06:00:42 +00:00
2022-12-20 16:11:30 +00:00
if (!empty($queueForEvents->getEvent())) {
if (empty($queueForEvents->getPayload())) {
$queueForEvents->setPayload($responsePayload);
2021-02-28 18:36:13 +00:00
}
2023-06-02 03:54:34 +00:00
// Get project and function/webhook events (cached)
$functionsEvents = $eventProcessor->getFunctionsEvents($project, $dbForProject);
$webhooksEvents = $eventProcessor->getWebhooksEvents($project);
// Generate events for this operation
$generatedEvents = Event::generateEvents(
$queueForEvents->getEvent(),
$queueForEvents->getParams()
);
2022-07-22 06:00:42 +00:00
if ($project->getId() !== 'console') {
2024-11-04 15:20:43 +00:00
$queueForRealtime
->from($queueForEvents)
->trigger();
2021-02-28 18:36:13 +00:00
}
// Only trigger functions if there are matching function events
if (!empty($functionsEvents)) {
foreach ($generatedEvents as $event) {
if (isset($functionsEvents[$event])) {
$queueForFunctions
->from($queueForEvents)
->trigger();
break;
}
}
}
// Only trigger webhooks if there are matching webhook events
if (!empty($webhooksEvents)) {
foreach ($generatedEvents as $event) {
if (isset($webhooksEvents[$event])) {
$queueForWebhooks
->from($queueForEvents)
->trigger();
break;
}
}
}
2022-04-18 16:21:45 +00:00
}
2021-02-28 18:36:13 +00:00
2024-10-08 07:54:40 +00:00
$route = $utopia->getRoute();
2022-08-14 19:27:43 +00:00
$requestParams = $route->getParamsValues();
2022-08-08 12:19:41 +00:00
2026-01-05 21:05:00 +00:00
/**
* Abuse labels
*/
$abuseEnabled = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled';
$abuseResetCode = $route->getLabel('abuse-reset', []);
$abuseResetCode = \is_array($abuseResetCode) ? $abuseResetCode : [$abuseResetCode];
if ($abuseEnabled && \count($abuseResetCode) > 0 && \in_array($response->getStatusCode(), $abuseResetCode)) {
$abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}');
$abuseKeyLabel = (!is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel;
foreach ($abuseKeyLabel as $abuseKey) {
$start = $request->getContentRangeStart();
$end = $request->getContentRangeEnd();
$timeLimit = $timelimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600));
$timeLimit
->setParam('{projectId}', $project->getId())
->setParam('{userId}', $user->getId())
->setParam('{userAgent}', $request->getUserAgent(''))
->setParam('{ip}', $request->getIP())
->setParam('{url}', $request->getHostname() . $route->getPath())
->setParam('{method}', $request->getMethod())
->setParam('{chunkId}', (int)($start / ($end + 1 - $start)));
foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys
if (!empty($value)) {
$timeLimit->setParam('{param-' . $key . '}', (\is_array($value)) ? \json_encode($value) : $value);
}
}
$abuse = new Abuse($timeLimit);
$abuse->reset();
}
}
2022-08-17 14:29:22 +00:00
/**
* Audit labels
*/
2022-08-16 12:28:30 +00:00
$pattern = $route->getLabel('audits.resource', null);
if (!empty($pattern)) {
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user);
if (!empty($resource) && $resource !== $pattern) {
2022-12-20 16:11:30 +00:00
$queueForAudits->setResource($resource);
2022-08-07 14:30:47 +00:00
}
2022-08-07 15:49:30 +00:00
}
2022-08-13 08:02:00 +00:00
if (!$user->isEmpty()) {
2025-01-21 06:41:20 +00:00
$userClone = clone $user;
2025-01-14 08:17:01 +00:00
// $user doesn't support `type` and can cause unintended effects.
2025-11-04 06:08:35 +00:00
$userClone->setAttribute('type', ACTIVITY_TYPE_USER);
2025-01-21 06:41:20 +00:00
$queueForAudits->setUser($userClone);
} elseif ($queueForAudits->getUser() === null || $queueForAudits->getUser()->isEmpty()) {
/**
* User in the request is empty, and no user was set for auditing previously.
* This indicates:
* - No API Key was used.
* - No active session exists.
*
* Therefore, we consider this an anonymous request and create a relevant user.
*/
2025-11-27 11:48:32 +00:00
$user = new User([
'$id' => '',
'status' => true,
2025-11-04 06:08:35 +00:00
'type' => ACTIVITY_TYPE_GUEST,
2025-01-14 12:15:49 +00:00
'email' => 'guest.' . $project->getId() . '@service.' . $request->getHostname(),
'password' => '',
2025-01-14 12:15:49 +00:00
'name' => 'Guest',
]);
2023-10-01 17:39:26 +00:00
$queueForAudits->setUser($user);
2021-03-11 16:28:03 +00:00
}
2021-10-07 15:35:17 +00:00
if (!empty($queueForAudits->getResource()) && !$queueForAudits->getUser()->isEmpty()) {
2022-08-16 12:28:30 +00:00
/**
* audits.payload is switched to default true
* in order to auto audit payload for all endpoints
*/
$pattern = $route->getLabel('audits.payload', true);
if (!empty($pattern)) {
2022-12-20 16:11:30 +00:00
$queueForAudits->setPayload($responsePayload);
2022-08-16 12:28:30 +00:00
}
2022-12-20 16:11:30 +00:00
foreach ($queueForEvents->getParams() as $key => $value) {
$queueForAudits->setParam($key, $value);
2021-02-28 18:36:13 +00:00
}
2022-12-20 16:11:30 +00:00
$queueForAudits->trigger();
2022-04-18 16:21:45 +00:00
}
2021-10-07 15:35:17 +00:00
2022-12-20 16:11:30 +00:00
if (!empty($queueForDeletes->getType())) {
$queueForDeletes->trigger();
}
2021-10-07 15:35:17 +00:00
2022-12-20 16:11:30 +00:00
if (!empty($queueForDatabase->getType())) {
$queueForDatabase->trigger();
2022-07-22 06:00:42 +00:00
}
2021-10-07 15:35:17 +00:00
2024-02-20 11:40:55 +00:00
if (!empty($queueForBuilds->getType())) {
$queueForBuilds->trigger();
}
2024-02-20 12:06:35 +00:00
if (!empty($queueForMessaging->getType())) {
2024-02-20 13:20:09 +00:00
$queueForMessaging->trigger();
2024-02-20 12:06:35 +00:00
}
// Cache label
2022-08-16 15:02:17 +00:00
$useCache = $route->getLabel('cache', false);
if ($useCache) {
$resource = $resourceType = null;
2022-08-09 13:43:37 +00:00
$data = $response->getPayload();
2022-08-16 15:02:17 +00:00
if (!empty($data['payload'])) {
$pattern = $route->getLabel('cache.resource', null);
if (!empty($pattern)) {
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user);
}
2022-08-15 09:05:41 +00:00
$pattern = $route->getLabel('cache.resourceType', null);
if (!empty($pattern)) {
$resourceType = $parseLabel($pattern, $responsePayload, $requestParams, $user);
}
$cache = new Cache(
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId())
);
$key = $request->cacheIdentifier();
$signature = md5($data['payload']);
$cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key));
2025-04-17 14:13:09 +00:00
$accessedAt = $cacheLog->getAttribute('accessedAt', 0);
$now = DateTime::now();
2022-08-16 15:02:17 +00:00
if ($cacheLog->isEmpty()) {
$authorization->skip(fn () => $dbForProject->createDocument('cache', new Document([
2024-03-06 17:34:21 +00:00
'$id' => $key,
'resource' => $resource,
2024-03-07 16:16:39 +00:00
'resourceType' => $resourceType,
'mimeType' => $response->getContentType(),
2024-03-06 17:34:21 +00:00
'accessedAt' => $now,
'signature' => $signature,
2022-08-09 13:43:37 +00:00
])));
2022-08-31 13:11:23 +00:00
} elseif (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_CACHE_UPDATE)) > $accessedAt) {
$cacheLog->setAttribute('accessedAt', $now);
$authorization->skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), $cacheLog));
// Overwrite the file every APP_CACHE_UPDATE seconds to update the file modified time that is used in the TTL checks in cache->load()
$cache->save($key, $data['payload']);
2022-08-16 15:02:17 +00:00
}
2022-08-14 15:01:34 +00:00
2022-08-16 15:02:17 +00:00
if ($signature !== $cacheLog->getAttribute('signature')) {
$cache->save($key, $data['payload']);
2022-08-16 15:02:17 +00:00
}
2022-08-15 12:16:32 +00:00
}
}
2023-10-25 07:39:59 +00:00
if ($project->getId() !== 'console') {
if (!User::isPrivileged($authorization->getRoles())) {
2023-10-25 07:39:59 +00:00
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
}
$queueForStatsUsage
2023-10-25 07:39:59 +00:00
->addMetric(METRIC_NETWORK_REQUESTS, 1)
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize)
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize());
2022-08-17 10:55:01 +00:00
}
2025-01-30 04:53:53 +00:00
$queueForStatsUsage
2023-10-25 07:39:59 +00:00
->setProject($project)
->trigger();
2022-07-22 06:00:42 +00:00
}
});
2026-02-04 05:30:22 +00:00
Http::init()
->groups(['usage'])
->action(function () {
2024-04-01 11:02:47 +00:00
if (System::getEnv('_APP_USAGE_STATS', 'enabled') !== 'enabled') {
throw new Exception(Exception::GENERAL_USAGE_DISABLED);
}
});