1
0
Fork 0
mirror of synced 2024-09-30 17:26:48 +13:00

fix: move user logic from general to api hook

This commit is contained in:
Torsten Dittmann 2024-02-16 16:26:50 +00:00
parent ca650fce1b
commit 5193a97ddb
2 changed files with 159 additions and 158 deletions

View file

@ -3,7 +3,6 @@
require_once __DIR__ . '/../init.php'; require_once __DIR__ . '/../init.php';
use Utopia\App; use Utopia\App;
use Utopia\Database\Helpers\Role;
use Utopia\Locale\Locale; use Utopia\Locale\Locale;
use Utopia\Logger\Logger; use Utopia\Logger\Logger;
use Utopia\Logger\Log; use Utopia\Logger\Log;
@ -15,7 +14,6 @@ use Appwrite\Utopia\View;
use Appwrite\Extend\Exception as AppwriteException; use Appwrite\Extend\Exception as AppwriteException;
use Utopia\Config\Config; use Utopia\Config\Config;
use Utopia\Domains\Domain; use Utopia\Domains\Domain;
use Appwrite\Auth\Auth;
use Appwrite\Event\Certificate; use Appwrite\Event\Certificate;
use Appwrite\Network\Validator\Origin; use Appwrite\Network\Validator\Origin;
use Appwrite\Utopia\Response\Filters\V11 as ResponseV11; use Appwrite\Utopia\Response\Filters\V11 as ResponseV11;
@ -27,7 +25,6 @@ use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17; use Appwrite\Utopia\Response\Filters\V17 as ResponseV17;
use Utopia\CLI\Console; use Utopia\CLI\Console;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document; use Utopia\Database\Document;
use Utopia\Database\Query; use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Authorization;
@ -39,7 +36,6 @@ use Appwrite\Utopia\Request\Filters\V15 as RequestV15;
use Appwrite\Utopia\Request\Filters\V16 as RequestV16; use Appwrite\Utopia\Request\Filters\V16 as RequestV16;
use Appwrite\Utopia\Request\Filters\V17 as RequestV17; use Appwrite\Utopia\Request\Filters\V17 as RequestV17;
use Utopia\Validator\Text; use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
Config::setParam('domainVerification', false); Config::setParam('domainVerification', false);
Config::setParam('cookieDomain', 'localhost'); Config::setParam('cookieDomain', 'localhost');
@ -205,15 +201,11 @@ App::init()
->inject('console') ->inject('console')
->inject('project') ->inject('project')
->inject('dbForConsole') ->inject('dbForConsole')
->inject('user')
->inject('locale') ->inject('locale')
->inject('localeCodes') ->inject('localeCodes')
->inject('clients') ->inject('clients')
->inject('servers')
->inject('session')
->inject('mode')
->inject('queueForCertificates') ->inject('queueForCertificates')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, Document $user, Locale $locale, array $localeCodes, array $clients, array $servers, ?Document $session, string $mode, Certificate $queueForCertificates) { ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, Locale $locale, array $localeCodes, array $clients, Certificate $queueForCertificates) {
/* /*
* Appwrite Router * Appwrite Router
*/ */
@ -453,140 +445,6 @@ App::init()
) { ) {
throw new AppwriteException(AppwriteException::GENERAL_UNKNOWN_ORIGIN, $originValidator->getDescription()); throw new AppwriteException(AppwriteException::GENERAL_UNKNOWN_ORIGIN, $originValidator->getDescription());
} }
/*
* 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
// Check if given key match project API keys
$key = $project->find('secret', $authKey, 'keys');
/*
* Try app auth when we have project key and no user
* Mock user to app and grant API key scopes in addition to default app scopes
*/
if ($key && $user->isEmpty()) {
$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 AppwriteException(AppwriteException::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 AppwriteException(AppwriteException::GENERAL_SERVICE_DISABLED);
}
}
if (!\in_array($scope, $scopes)) {
if ($project->isEmpty()) { // Check if permission is denied because project is missing
throw new AppwriteException(AppwriteException::PROJECT_NOT_FOUND);
}
throw new AppwriteException(AppwriteException::GENERAL_UNAUTHORIZED_SCOPE, $user->getAttribute('email', 'User') . ' (role: ' . \strtolower($roles[$role]['label']) . ') missing scope (' . $scope . ')');
}
if (in_array('api', $route->getGroups())) {
if (false === $user->getAttribute('status')) { // Account is blocked
throw new AppwriteException(AppwriteException::USER_BLOCKED);
}
if ($user->getAttribute('reset')) {
throw new AppwriteException(AppwriteException::USER_PASSWORD_RESET_REQUIRED);
}
if ($mode !== APP_MODE_ADMIN) {
$mfaEnabled = $user->getAttribute('mfa', false);
$hasVerifiedAuthenticator = $user->getAttribute('totpVerification', false);
$hasVerifiedEmail = $user->getAttribute('emailVerification', false);
$hasVerifiedPhone = $user->getAttribute('phoneVerification', false);
$hasMoreFactors = $hasVerifiedEmail || $hasVerifiedPhone || $hasVerifiedAuthenticator;
$minimumFactors = ($mfaEnabled && $hasMoreFactors) ? 2 : 1;
if (!in_array('mfa', $route->getGroups())) {
if ($session && \count($session->getAttribute('factors')) < $minimumFactors) {
throw new AppwriteException(AppwriteException::USER_MORE_FACTORS_REQUIRED);
}
}
}
}
}); });
App::options() App::options()

View file

@ -6,7 +6,6 @@ use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete; use Appwrite\Event\Delete;
use Appwrite\Event\Event; use Appwrite\Event\Event;
use Appwrite\Event\Func; use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging; use Appwrite\Event\Messaging;
use Appwrite\Extend\Exception; use Appwrite\Extend\Exception;
use Appwrite\Event\Usage; use Appwrite\Event\Usage;
@ -22,7 +21,9 @@ use Utopia\Database\Database;
use Utopia\Database\DateTime; use Utopia\Database\DateTime;
use Utopia\Database\Document; use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Authorization;
use MaxMind\Db\Reader; use Utopia\Config\Config;
use Utopia\Database\Helpers\Role;
use Utopia\Validator\WhiteList;
$parseLabel = function (string $label, array $responsePayload, array $requestParams, Document $user) { $parseLabel = function (string $label, array $responsePayload, array $requestParams, Document $user) {
preg_match_all('/{(.*?)}/', $label, $matches); preg_match_all('/{(.*?)}/', $label, $matches);
@ -135,7 +136,7 @@ $databaseListener = function (string $event, Document $document, Document $proje
$queueForUsage $queueForUsage
->addMetric(METRIC_DEPLOYMENTS, $value) // per project ->addMetric(METRIC_DEPLOYMENTS, $value) // per project
->addMetric(METRIC_DEPLOYMENTS_STORAGE, $document->getAttribute('size') * $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 ->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getAttribute('resourceType'), $document->getAttribute('resourceInternalId')], METRIC_FUNCTION_ID_DEPLOYMENTS), $value) // per function
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getAttribute('resourceType'), $document->getAttribute('resourceInternalId')], METRIC_FUNCTION_ID_DEPLOYMENTS_STORAGE), $document->getAttribute('size') * $value); ->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getAttribute('resourceType'), $document->getAttribute('resourceInternalId')], METRIC_FUNCTION_ID_DEPLOYMENTS_STORAGE), $document->getAttribute('size') * $value);
break; break;
default: default:
@ -143,6 +144,151 @@ $databaseListener = function (string $event, Document $document, Document $proje
} }
}; };
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();
/**
* 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
// Check if given key match project API keys
$key = $project->find('secret', $authKey, 'keys');
/*
* Try app auth when we have project key and no user
* Mock user to app and grant API key scopes in addition to default app scopes
*/
if ($key && $user->isEmpty()) {
$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);
}
if ($mode !== APP_MODE_ADMIN) {
$mfaEnabled = $user->getAttribute('mfa', false);
$hasVerifiedAuthenticator = $user->getAttribute('totpVerification', false);
$hasVerifiedEmail = $user->getAttribute('emailVerification', false);
$hasVerifiedPhone = $user->getAttribute('phoneVerification', false);
$hasMoreFactors = $hasVerifiedEmail || $hasVerifiedPhone || $hasVerifiedAuthenticator;
$minimumFactors = ($mfaEnabled && $hasMoreFactors) ? 2 : 1;
if (!in_array('mfa', $route->getGroups())) {
if ($session && \count($session->getAttribute('factors')) < $minimumFactors) {
throw new Exception(Exception::USER_MORE_FACTORS_REQUIRED);
}
}
}
});
App::init() App::init()
->groups(['api']) ->groups(['api'])
->inject('utopia') ->inject('utopia')
@ -212,8 +358,7 @@ App::init()
$response $response
->addHeader('X-RateLimit-Limit', $limit) ->addHeader('X-RateLimit-Limit', $limit)
->addHeader('X-RateLimit-Remaining', $remaining) ->addHeader('X-RateLimit-Remaining', $remaining)
->addHeader('X-RateLimit-Reset', $time) ->addHeader('X-RateLimit-Reset', $time);
;
} }
$enabled = App::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled'; $enabled = App::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled';
@ -253,8 +398,7 @@ App::init()
$dbForProject $dbForProject
->on(Database::EVENT_DOCUMENT_CREATE, 'calculate-usage', fn ($event, $document) => $databaseListener($event, $document, $project, $queueForUsage, $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)) ->on(Database::EVENT_DOCUMENT_DELETE, 'calculate-usage', fn ($event, $document) => $databaseListener($event, $document, $project, $queueForUsage, $dbForProject));
;
$useCache = $route->getLabel('cache', false); $useCache = $route->getLabel('cache', false);
@ -296,7 +440,7 @@ App::init()
if ($fileSecurity && !$valid) { if ($fileSecurity && !$valid) {
$file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
} else { } else {
$file = Authorization::skip(fn() => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
} }
if ($file->isEmpty()) { if ($file->isEmpty()) {
@ -308,8 +452,7 @@ App::init()
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $timestamp) . ' GMT') ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $timestamp) . ' GMT')
->addHeader('X-Appwrite-Cache', 'hit') ->addHeader('X-Appwrite-Cache', 'hit')
->setContentType($data['contentType']) ->setContentType($data['contentType'])
->send(base64_decode($data['payload'])) ->send(base64_decode($data['payload']));
;
} else { } else {
$response->addHeader('X-Appwrite-Cache', 'miss'); $response->addHeader('X-Appwrite-Cache', 'miss');
} }
@ -497,7 +640,7 @@ App::shutdown()
'resource' => $resource, 'resource' => $resource,
'contentType' => $response->getContentType(), 'contentType' => $response->getContentType(),
'payload' => base64_encode($data['payload']), 'payload' => base64_encode($data['payload']),
]) ; ]);
$signature = md5($data); $signature = md5($data);
$cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key)); $cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key));
@ -505,10 +648,10 @@ App::shutdown()
$now = DateTime::now(); $now = DateTime::now();
if ($cacheLog->isEmpty()) { if ($cacheLog->isEmpty()) {
Authorization::skip(fn () => $dbForProject->createDocument('cache', new Document([ Authorization::skip(fn () => $dbForProject->createDocument('cache', new Document([
'$id' => $key, '$id' => $key,
'resource' => $resource, 'resource' => $resource,
'accessedAt' => $now, 'accessedAt' => $now,
'signature' => $signature, 'signature' => $signature,
]))); ])));
} elseif (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_CACHE_UPDATE)) > $accessedAt) { } elseif (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_CACHE_UPDATE)) > $accessedAt) {
$cacheLog->setAttribute('accessedAt', $now); $cacheLog->setAttribute('accessedAt', $now);