1
0
Fork 0
mirror of synced 2024-06-03 03:14:50 +12:00
appwrite/app/controllers/shared/api.php

742 lines
30 KiB
PHP
Raw Normal View History

2019-05-09 18:54:39 +12:00
<?php
2019-10-01 17:57:41 +13:00
2021-01-12 10:52:05 +13:00
use Appwrite\Auth\Auth;
2024-03-01 15:08:30 +13:00
use Appwrite\Auth\MFA\Type\TOTP;
2022-05-26 23:51:08 +12:00
use Appwrite\Event\Audit;
2024-02-21 00:40:55 +13: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 22:51:49 +12:00
use Appwrite\Event\Database as EventDatabase;
2022-05-26 23:51:08 +12:00
use Appwrite\Event\Delete;
2022-04-04 18:30:07 +12:00
use Appwrite\Event\Event;
2022-11-16 07:13:17 +13:00
use Appwrite\Event\Func;
use Appwrite\Event\Messaging;
2023-10-25 20:39:59 +13:00
use Appwrite\Event\Usage;
2024-03-07 06:34:21 +13:00
use Appwrite\Extend\Exception;
2024-03-08 12:30:23 +13:00
use Appwrite\Extend\Exception as AppwriteException;
2021-06-30 23:36:58 +12:00
use Appwrite\Messaging\Adapter\Realtime;
2022-05-26 23:51:08 +12:00
use Appwrite\Utopia\Request;
2024-03-07 06:34:21 +13:00
use Appwrite\Utopia\Response;
2019-11-30 07:23:29 +13:00
use Utopia\Abuse\Abuse;
use Utopia\Abuse\Adapters\TimeLimit;
2024-03-07 06:34:21 +13:00
use Utopia\App;
use Utopia\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
2024-03-07 06:34:21 +13:00
use Utopia\Config\Config;
2022-05-26 23:51:08 +12:00
use Utopia\Database\Database;
2022-08-31 20:52:55 +12:00
use Utopia\Database\DateTime;
2021-10-08 04:35:17 +13:00
use Utopia\Database\Document;
use Utopia\Database\Helpers\Role;
2024-03-07 06:34:21 +13:00
use Utopia\Database\Validator\Authorization;
2024-04-02 00:02:47 +13:00
use Utopia\System\System;
2024-04-02 00:08:46 +13:00
use Utopia\Validator\WhiteList;
2019-11-30 07:23:29 +13:00
2022-08-17 00:28:30 +12:00
$parseLabel = function (string $label, array $responsePayload, array $requestParams, Document $user) {
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-17 00:28:30 +12:00
}
$namespace = $parts[0] ?? '';
$replace = $parts[1] ?? '';
$params = match ($namespace) {
'user' => (array)$user,
'request' => $requestParams,
default => $responsePayload,
};
if (array_key_exists($replace, $params)) {
$label = \str_replace($find, $params[$replace], $label);
}
}
return $label;
};
2019-11-30 07:23:29 +13:00
2023-10-25 20:39:59 +13:00
$databaseListener = function (string $event, Document $document, Document $project, Usage $queueForUsage, Database $dbForProject) {
$value = 1;
2022-10-30 18:16:51 +13:00
if ($event === Database::EVENT_DOCUMENT_DELETE) {
2023-10-25 20:39:59 +13:00
$value = -1;
2022-10-30 18:14:46 +13:00
}
2023-10-25 20:39:59 +13:00
switch (true) {
case $document->getCollection() === 'teams':
$queueForUsage
->addMetric(METRIC_TEAMS, $value); // per project
2023-05-24 01:43:03 +12:00
break;
2023-10-25 20:39:59 +13:00
case $document->getCollection() === 'users':
$queueForUsage
->addMetric(METRIC_USERS, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForUsage
->addReduce($document);
}
2022-10-30 18:14:46 +13:00
break;
2023-10-25 20:39:59 +13:00
case $document->getCollection() === 'sessions': // sessions
$queueForUsage
->addMetric(METRIC_SESSIONS, $value); //per project
2022-10-30 18:14:46 +13:00
break;
2023-10-25 20:39:59 +13:00
case $document->getCollection() === 'databases': // databases
$queueForUsage
->addMetric(METRIC_DATABASES, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForUsage
->addReduce($document);
}
2023-05-24 01:43:03 +12:00
break;
2023-10-25 20:39:59 +13:00
case str_starts_with($document->getCollection(), 'database_') && !str_contains($document->getCollection(), 'collection'): //collections
$parts = explode('_', $document->getCollection());
$databaseInternalId = $parts[1] ?? 0;
$queueForUsage
->addMetric(METRIC_COLLECTIONS, $value) // per project
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_COLLECTIONS), $value) // per database
;
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForUsage
->addReduce($document);
2023-10-16 06:41:09 +13:00
}
2023-05-24 01:43:03 +12:00
break;
2023-10-25 20:39:59 +13:00
case str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_'): //documents
$parts = explode('_', $document->getCollection());
$databaseInternalId = $parts[1] ?? 0;
$collectionInternalId = $parts[3] ?? 0;
$queueForUsage
->addMetric(METRIC_DOCUMENTS, $value) // per project
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_DOCUMENTS), $value) // per database
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS), $value); // per collection
break;
case $document->getCollection() === 'buckets': //buckets
$queueForUsage
->addMetric(METRIC_BUCKETS, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForUsage
->addReduce($document);
}
break;
case str_starts_with($document->getCollection(), 'bucket_'): // files
$parts = explode('_', $document->getCollection());
$bucketInternalId = $parts[1];
$queueForUsage
->addMetric(METRIC_FILES, $value) // per project
->addMetric(METRIC_FILES_STORAGE, $document->getAttribute('sizeOriginal') * $value) // per project
->addMetric(str_replace('{bucketInternalId}', $bucketInternalId, METRIC_BUCKET_ID_FILES), $value) // per bucket
->addMetric(str_replace('{bucketInternalId}', $bucketInternalId, METRIC_BUCKET_ID_FILES_STORAGE), $document->getAttribute('sizeOriginal') * $value); // per bucket
break;
case $document->getCollection() === 'functions':
$queueForUsage
->addMetric(METRIC_FUNCTIONS, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForUsage
->addReduce($document);
2023-10-16 06:41:09 +13:00
}
2023-05-24 01:43:03 +12:00
break;
2023-10-25 20:39:59 +13:00
case $document->getCollection() === 'deployments':
$queueForUsage
->addMetric(METRIC_DEPLOYMENTS, $value) // per project
->addMetric(METRIC_DEPLOYMENTS_STORAGE, $document->getAttribute('size') * $value) // per project
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getAttribute('resourceType'), $document->getAttribute('resourceInternalId')], METRIC_FUNCTION_ID_DEPLOYMENTS), $value) // per function
2023-10-25 20:39:59 +13:00
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getAttribute('resourceType'), $document->getAttribute('resourceInternalId')], METRIC_FUNCTION_ID_DEPLOYMENTS_STORAGE), $document->getAttribute('size') * $value);
break;
default:
break;
2022-10-30 18:14:46 +13:00
}
};
App::init()
->groups(['api'])
->inject('utopia')
->inject('request')
->inject('dbForConsole')
->inject('project')
->inject('user')
->inject('session')
->inject('servers')
->inject('mode')
->action(function (App $utopia, Request $request, Database $dbForConsole, Document $project, Document $user, ?Document $session, array $servers, string $mode) {
$route = $utopia->getRoute();
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
/**
* ACL Check
*/
$role = ($user->isEmpty())
? Role::guests()->toString()
: Role::users()->toString();
// Add user roles
$memberships = $user->find('teamId', $project->getAttribute('teamId'), 'memberships');
if ($memberships) {
foreach ($memberships->getAttribute('roles', []) as $memberRole) {
switch ($memberRole) {
case 'owner':
$role = Auth::USER_ROLE_OWNER;
break;
case 'admin':
$role = Auth::USER_ROLE_ADMIN;
break;
case 'developer':
$role = Auth::USER_ROLE_DEVELOPER;
break;
}
}
}
$roles = Config::getParam('roles', []);
$scope = $route->getLabel('scope', 'none'); // Allowed scope for chosen route
$scopes = $roles[$role]['scopes']; // Allowed scopes for user role
$authKey = $request->getHeader('x-appwrite-key', '');
if (!empty($authKey)) { // API Key authentication
2024-02-21 00:45:11 +13:00
// Do not allow API key and session to be set at the same time
if (!$user->isEmpty()) {
throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET);
}
// Check if given key match project API keys
$key = $project->find('secret', $authKey, 'keys');
2024-02-21 00:45:11 +13:00
if ($key) {
$user = new Document([
'$id' => '',
'status' => true,
'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(),
'password' => '',
'name' => $project->getAttribute('name', 'Untitled'),
]);
$role = Auth::USER_ROLE_APPS;
$scopes = \array_merge($roles[$role]['scopes'], $key->getAttribute('scopes', []));
$expire = $key->getAttribute('expire');
if (!empty($expire) && $expire < DateTime::formatTz(DateTime::now())) {
throw new Exception(Exception::PROJECT_KEY_EXPIRED);
}
Authorization::setRole(Auth::USER_ROLE_APPS);
Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys.
$accessedAt = $key->getAttribute('accessedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_KEY_ACCCESS)) > $accessedAt) {
$key->setAttribute('accessedAt', DateTime::now());
$dbForConsole->updateDocument('keys', $key->getId(), $key);
$dbForConsole->purgeCachedDocument('projects', $project->getId());
}
$sdkValidator = new WhiteList($servers, true);
$sdk = $request->getHeader('x-sdk-name', 'UNKNOWN');
if ($sdkValidator->isValid($sdk)) {
$sdks = $key->getAttribute('sdks', []);
if (!in_array($sdk, $sdks)) {
array_push($sdks, $sdk);
$key->setAttribute('sdks', $sdks);
/** Update access time as well */
$key->setAttribute('accessedAt', Datetime::now());
$dbForConsole->updateDocument('keys', $key->getId(), $key);
$dbForConsole->purgeCachedDocument('projects', $project->getId());
}
}
}
}
Authorization::setRole($role);
foreach (Auth::getRoles($user) as $authRole) {
Authorization::setRole($authRole);
}
$service = $route->getLabel('sdk.namespace', '');
if (!empty($service)) {
if (
array_key_exists($service, $project->getAttribute('services', []))
&& !$project->getAttribute('services', [])[$service]
&& !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles()))
) {
throw new Exception(Exception::GENERAL_SERVICE_DISABLED);
}
}
if (!\in_array($scope, $scopes)) {
if ($project->isEmpty()) { // Check if permission is denied because project is missing
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, $user->getAttribute('email', 'User') . ' (role: ' . \strtolower($roles[$role]['label']) . ') missing scope (' . $scope . ')');
}
if (false === $user->getAttribute('status')) { // Account is blocked
throw new Exception(Exception::USER_BLOCKED);
}
if ($user->getAttribute('reset')) {
throw new Exception(Exception::USER_PASSWORD_RESET_REQUIRED);
}
2024-04-17 21:10:33 +12: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;
if (!in_array('mfa', $route->getGroups())) {
if ($session && \count($session->getAttribute('factors', [])) < $minimumFactors) {
2024-04-17 21:10:33 +12:00
throw new Exception(Exception::USER_MORE_FACTORS_REQUIRED);
}
}
});
2022-07-22 18:00:42 +12:00
App::init()
2022-08-02 13:10:48 +12:00
->groups(['api'])
2022-07-22 18:00:42 +12:00
->inject('utopia')
->inject('request')
->inject('response')
->inject('project')
->inject('user')
2022-12-21 05:11:30 +13:00
->inject('queueForEvents')
->inject('queueForMessaging')
2022-12-21 05:11:30 +13:00
->inject('queueForAudits')
->inject('queueForDeletes')
->inject('queueForDatabase')
2024-02-21 00:40:55 +13:00
->inject('queueForBuilds')
->inject('queueForUsage')
2022-07-22 18:00:42 +12:00
->inject('dbForProject')
->inject('mode')
2024-02-21 00:40:55 +13:00
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Usage $queueForUsage, Database $dbForProject, string $mode) use ($databaseListener) {
2022-07-22 18:00:42 +12:00
2023-02-20 00:04:12 +13:00
$route = $utopia->getRoute();
2022-07-22 18:00:42 +12:00
2024-03-05 11:12:54 +13:00
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);
}
/*
2022-07-22 18:00:42 +12:00
* Abuse Check
*/
2022-08-12 11:53:52 +12:00
$abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}');
$timeLimitArray = [];
2021-06-07 17:17:29 +12:00
2022-07-22 18:00:42 +12:00
$abuseKeyLabel = (!is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel;
2021-06-07 17:17:29 +12:00
2022-08-12 11:53:52 +12:00
foreach ($abuseKeyLabel as $abuseKey) {
$start = $request->getContentRangeStart();
$end = $request->getContentRangeEnd();
2022-08-12 11:53:52 +12:00
$timeLimit = new TimeLimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600), $dbForProject);
$timeLimit
2024-02-12 14:18:19 +13:00
->setParam('{projectId}', $project->getId())
2023-05-24 01:43:03 +12: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-12 11:53:52 +12:00
$timeLimitArray[] = $timeLimit;
}
2021-06-07 17:17:29 +12:00
2022-07-22 18:00:42 +12:00
$closestLimit = null;
2021-06-07 17:17:29 +12:00
2022-07-22 18:00:42 +12:00
$roles = Authorization::getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
2021-06-07 17:17:29 +12:00
2022-07-22 18:00:42 +12: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 17:17:29 +12:00
2022-07-22 18:00:42 +12:00
$abuse = new Abuse($timeLimit);
$remaining = $timeLimit->remaining();
$limit = $timeLimit->limit();
2022-08-31 20:52:55 +12:00
$time = (new \DateTime($timeLimit->time()))->getTimestamp() + $route->getLabel('abuse-time', 3600);
2022-07-22 18:00:42 +12:00
if ($limit && ($remaining < $closestLimit || is_null($closestLimit))) {
$closestLimit = $remaining;
2022-07-22 18:00:42 +12:00
$response
->addHeader('X-RateLimit-Limit', $limit)
->addHeader('X-RateLimit-Remaining', $remaining)
->addHeader('X-RateLimit-Reset', $time);
2021-03-01 07:36:13 +13:00
}
2021-06-07 17:17:29 +12:00
2024-04-02 00:02:47 +13:00
$enabled = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled';
2022-07-22 18:00:42 +12:00
if (
$enabled // Abuse is enabled
&& !$isAppUser // User is not API key
&& !$isPrivilegedUser // User is not an admin
&& $abuse->check() // Route is rate-limited
) {
2022-08-09 02:44:07 +12:00
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED);
2021-03-01 07:36:13 +13:00
}
}
2021-01-06 01:22:20 +13:00
2022-11-16 18:30:57 +13:00
/*
* Background Jobs
*/
2022-12-21 05:11:30 +13:00
$queueForEvents
2022-07-22 18:00:42 +12:00
->setEvent($route->getLabel('event', ''))
->setProject($project)
2022-08-12 01:47:53 +12:00
->setUser($user);
2022-04-04 18:30:07 +12:00
2022-12-21 05:11:30 +13:00
$queueForAudits
2022-07-22 18:00:42 +12:00
->setMode($mode)
->setUserAgent($request->getUserAgent(''))
->setIP($request->getIP())
2022-09-04 20:23:24 +12:00
->setEvent($route->getLabel('audits.event', ''))
2022-07-22 18:00:42 +12:00
->setProject($project)
2022-08-12 01:47:53 +12:00
->setUser($user);
2021-03-01 07:36:13 +13:00
2022-12-21 05:11:30 +13:00
$queueForDeletes->setProject($project);
$queueForDatabase->setProject($project);
2024-02-21 00:40:55 +13:00
$queueForBuilds->setProject($project);
2024-02-21 01:06:35 +13:00
$queueForMessaging->setProject($project);
2022-08-09 23:57:33 +12:00
2023-10-25 20:39:59 +13:00
$dbForProject
->on(Database::EVENT_DOCUMENT_CREATE, 'calculate-usage', fn ($event, $document) => $databaseListener($event, $document, $project, $queueForUsage, $dbForProject))
->on(Database::EVENT_DOCUMENT_DELETE, 'calculate-usage', fn ($event, $document) => $databaseListener($event, $document, $project, $queueForUsage, $dbForProject));
2022-10-17 20:10:18 +13:00
2022-08-09 23:57:33 +12:00
$useCache = $route->getLabel('cache', false);
if ($useCache) {
2023-09-06 03:29:11 +12:00
$key = md5($request->getURI() . implode('*', $request->getParams()) . '*' . APP_CACHE_BUSTER);
$cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key));
2022-08-16 01:55:11 +12:00
$cache = new Cache(
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId())
);
2022-08-09 23:57:33 +12:00
$timestamp = 60 * 60 * 24 * 30;
$data = $cache->load($key, $timestamp);
if (!empty($data) && !$cacheLog->isEmpty()) {
$parts = explode('/', $cacheLog->getAttribute('resourceType'));
$type = $parts[0] ?? null;
if ($type === 'bucket') {
$bucketId = $parts[1] ?? null;
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
2023-08-17 09:58:25 +12:00
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
$validator = new Authorization(Database::PERMISSION_READ);
$valid = $validator->isValid($bucket->getRead());
if (!$fileSecurity && !$valid) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
$parts = explode('/', $cacheLog->getAttribute('resource'));
$fileId = $parts[1] ?? null;
if ($fileSecurity && !$valid) {
$file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
} else {
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
}
if ($file->isEmpty()) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
}
2022-08-10 01:43:37 +12:00
$response
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $timestamp) . ' GMT')
->addHeader('X-Appwrite-Cache', 'hit')
->setContentType($cacheLog->getAttribute('mimeType'))
->send($data);
2022-08-10 01:43:37 +12:00
} else {
$response->addHeader('X-Appwrite-Cache', 'miss');
}
}
2022-07-22 18:00:42 +12:00
});
App::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-24 01:43:03 +12:00
/**
* Limit user session
*
* Delete older sessions if the number of sessions have crossed
* the session limit set for the project
*/
App::shutdown()
->groups(['session'])
->inject('utopia')
->inject('request')
->inject('response')
->inject('project')
->inject('dbForProject')
->action(function (App $utopia, Request $request, Response $response, Document $project, Database $dbForProject) {
$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-15 02:32:06 +13:00
$dbForProject->purgeCachedDocument('users', $userId);
2023-05-24 01:43:03 +12:00
});
2022-07-22 18:00:42 +12:00
App::shutdown()
2022-08-02 13:10:48 +12:00
->groups(['api'])
2022-07-22 18:00:42 +12:00
->inject('utopia')
->inject('request')
->inject('response')
->inject('project')
->inject('user')
2022-12-21 05:11:30 +13:00
->inject('queueForEvents')
->inject('queueForAudits')
2023-10-25 20:39:59 +13:00
->inject('queueForUsage')
2022-12-21 05:11:30 +13:00
->inject('queueForDeletes')
->inject('queueForDatabase')
2024-02-21 00:40:55 +13:00
->inject('queueForBuilds')
2024-02-21 01:06:35 +13:00
->inject('queueForMessaging')
2022-07-22 18:00:42 +12:00
->inject('dbForProject')
2022-11-16 23:33:11 +13:00
->inject('queueForFunctions')
2022-12-15 20:56:06 +13:00
->inject('mode')
->inject('dbForConsole')
2024-02-21 01:06:35 +13:00
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Audit $queueForAudits, Usage $queueForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Database $dbForProject, Func $queueForFunctions, string $mode, Database $dbForConsole) use ($parseLabel) {
2022-08-09 23:57:33 +12:00
2022-08-08 03:09:37 +12:00
$responsePayload = $response->getPayload();
2022-07-22 18:00:42 +12:00
2022-12-21 05:11:30 +13:00
if (!empty($queueForEvents->getEvent())) {
if (empty($queueForEvents->getPayload())) {
$queueForEvents->setPayload($responsePayload);
2021-03-01 07:36:13 +13:00
}
2023-06-02 15:54:34 +12:00
2022-07-22 18:00:42 +12:00
/**
* Trigger functions.
*/
2022-11-16 23:33:11 +13:00
$queueForFunctions
2022-12-21 05:11:30 +13:00
->from($queueForEvents)
2022-07-22 18:00:42 +12:00
->trigger();
/**
* Trigger webhooks.
*/
2022-12-21 05:11:30 +13:00
$queueForEvents
2022-07-22 18:00:42 +12:00
->setClass(Event::WEBHOOK_CLASS_NAME)
->setQueue(Event::WEBHOOK_QUEUE_NAME)
->trigger();
/**
* Trigger realtime.
*/
if ($project->getId() !== 'console') {
2022-12-21 05:11:30 +13:00
$allEvents = Event::generateEvents($queueForEvents->getEvent(), $queueForEvents->getParams());
$payload = new Document($queueForEvents->getPayload());
2022-07-22 18:00:42 +12:00
2022-12-21 05:11:30 +13:00
$db = $queueForEvents->getContext('database');
$collection = $queueForEvents->getContext('collection');
$bucket = $queueForEvents->getContext('bucket');
2022-07-22 18:00:42 +12:00
$target = Realtime::fromPayload(
2022-08-11 19:02:40 +12:00
// Pass first, most verbose event pattern
2022-07-22 18:00:42 +12:00
event: $allEvents[0],
payload: $payload,
project: $project,
database: $db,
collection: $collection,
bucket: $bucket,
);
Realtime::send(
projectId: $target['projectId'] ?? $project->getId(),
2022-12-21 05:11:30 +13:00
payload: $queueForEvents->getPayload(),
2022-07-22 18:00:42 +12:00
events: $allEvents,
channels: $target['channels'],
roles: $target['roles'],
options: [
'permissionsChanged' => $target['permissionsChanged'],
2022-12-21 05:11:30 +13:00
'userId' => $queueForEvents->getParam('userId')
2022-07-22 18:00:42 +12:00
]
);
2021-03-01 07:36:13 +13:00
}
2022-04-19 04:21:45 +12:00
}
2021-03-01 07:36:13 +13:00
2023-02-20 00:04:12 +13:00
$route = $utopia->getRoute();
2022-08-15 07:27:43 +12:00
$requestParams = $route->getParamsValues();
2022-08-09 00:19:41 +12:00
2022-08-18 02:29:22 +12:00
/**
* Audit labels
*/
2022-08-17 00:28:30 +12:00
$pattern = $route->getLabel('audits.resource', null);
if (!empty($pattern)) {
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user);
if (!empty($resource) && $resource !== $pattern) {
2022-12-21 05:11:30 +13:00
$queueForAudits->setResource($resource);
2022-08-08 02:30:47 +12:00
}
2022-08-08 03:49:30 +12:00
}
2022-08-13 20:02:00 +12:00
if (!$user->isEmpty()) {
2023-10-02 06:39:26 +13:00
$queueForAudits->setUser($user);
2021-03-12 05:28:03 +13:00
}
2021-10-08 04:35:17 +13:00
2022-12-21 05:11:30 +13:00
if (!empty($queueForAudits->getResource()) && !empty($queueForAudits->getUser()->getId())) {
2022-08-17 00:28:30 +12: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-21 05:11:30 +13:00
$queueForAudits->setPayload($responsePayload);
2022-08-17 00:28:30 +12:00
}
2022-12-21 05:11:30 +13:00
foreach ($queueForEvents->getParams() as $key => $value) {
$queueForAudits->setParam($key, $value);
2021-03-01 07:36:13 +13:00
}
2022-12-21 05:11:30 +13:00
$queueForAudits->trigger();
2022-04-19 04:21:45 +12:00
}
2021-10-08 04:35:17 +13:00
2022-12-21 05:11:30 +13:00
if (!empty($queueForDeletes->getType())) {
$queueForDeletes->trigger();
}
2021-10-08 04:35:17 +13:00
2022-12-21 05:11:30 +13:00
if (!empty($queueForDatabase->getType())) {
$queueForDatabase->trigger();
2022-07-22 18:00:42 +12:00
}
2021-10-08 04:35:17 +13:00
2024-02-21 00:40:55 +13:00
if (!empty($queueForBuilds->getType())) {
$queueForBuilds->trigger();
}
2024-02-21 01:06:35 +13:00
if (!empty($queueForMessaging->getType())) {
2024-02-21 02:20:09 +13:00
$queueForMessaging->trigger();
2024-02-21 01:06:35 +13:00
}
2022-08-18 02:29:22 +12:00
/**
* Cache label
*/
2022-08-17 03:02:17 +12:00
$useCache = $route->getLabel('cache', false);
if ($useCache) {
$resource = $resourceType = null;
2022-08-10 01:43:37 +12:00
$data = $response->getPayload();
2022-08-17 03:02:17 +12:00
if (!empty($data['payload'])) {
$pattern = $route->getLabel('cache.resource', null);
if (!empty($pattern)) {
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user);
}
2022-08-15 21:05:41 +12:00
$pattern = $route->getLabel('cache.resourceType', null);
if (!empty($pattern)) {
$resourceType = $parseLabel($pattern, $responsePayload, $requestParams, $user);
}
$key = md5($request->getURI() . '*' . implode('*', $request->getParams())) . '*' . APP_CACHE_BUSTER;
$signature = md5($data['payload']);
$cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key));
2022-08-31 20:52:55 +12:00
$accessedAt = $cacheLog->getAttribute('accessedAt', '');
$now = DateTime::now();
2022-08-17 03:02:17 +12:00
if ($cacheLog->isEmpty()) {
Authorization::skip(fn () => $dbForProject->createDocument('cache', new Document([
2024-03-07 06:34:21 +13:00
'$id' => $key,
'resource' => $resource,
2024-03-08 05:16:39 +13:00
'resourceType' => $resourceType,
'mimeType' => $response->getContentType(),
2024-03-07 06:34:21 +13:00
'accessedAt' => $now,
'signature' => $signature,
2022-08-10 01:43:37 +12:00
])));
2022-09-01 01:11:23 +12:00
} elseif (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_CACHE_UPDATE)) > $accessedAt) {
$cacheLog->setAttribute('accessedAt', $now);
2022-08-10 01:43:37 +12:00
Authorization::skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), $cacheLog));
2022-08-17 03:02:17 +12:00
}
2022-08-15 03:01:34 +12:00
2022-08-17 03:02:17 +12:00
if ($signature !== $cacheLog->getAttribute('signature')) {
$cache = new Cache(
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId())
);
$cache->save($key, $data['payload']);
2022-08-17 03:02:17 +12:00
}
2022-08-16 00:16:32 +12:00
}
}
2023-10-25 20:39:59 +13:00
if ($project->getId() !== 'console') {
2024-04-25 13:23:01 +12:00
if (!Auth::isPrivilegedUser(Authorization::getRoles())) {
2023-10-25 20:39:59 +13:00
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
}
2023-10-25 20:39:59 +13:00
$queueForUsage
->addMetric(METRIC_NETWORK_REQUESTS, 1)
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize)
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize());
2022-08-17 22:55:01 +12:00
}
2023-10-25 20:39:59 +13:00
$queueForUsage
->setProject($project)
->trigger();
2022-07-22 18:00:42 +12:00
}
/**
* Update user last activity
*/
if (!$user->isEmpty()) {
$accessedAt = $user->getAttribute('accessedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCCESS)) > $accessedAt) {
$user->setAttribute('accessedAt', DateTime::now());
if (APP_MODE_ADMIN !== $mode) {
$dbForProject->updateDocument('users', $user->getId(), $user);
} else {
$dbForConsole->updateDocument('users', $user->getId(), $user);
}
}
}
2022-07-22 18:00:42 +12:00
});
App::init()
->groups(['usage'])
->action(function () {
2024-04-02 00:02:47 +13:00
if (System::getEnv('_APP_USAGE_STATS', 'enabled') !== 'enabled') {
throw new Exception(Exception::GENERAL_USAGE_DISABLED);
}
});