mirror of
https://github.com/appwrite/appwrite
synced 2026-05-24 01:18:37 +00:00
Merge pull request #9336 from appwrite/feat-key-segmented-usage
Feat key segmented usage
This commit is contained in:
commit
ae91869e76
15 changed files with 437 additions and 240 deletions
|
|
@ -3138,8 +3138,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents')
|
|||
->inject('user')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('mode')
|
||||
->action(function (string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, Response $response, Database $dbForProject, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage, string $mode) {
|
||||
->action(function (string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, Response $response, Database $dbForProject, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage) {
|
||||
|
||||
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
|
||||
|
||||
|
|
@ -3394,9 +3393,8 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
|
|||
->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('mode')
|
||||
->inject('queueForStatsUsage')
|
||||
->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, string $mode, StatsUsage $queueForStatsUsage) {
|
||||
->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, StatsUsage $queueForStatsUsage) {
|
||||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
|
|
@ -3510,8 +3508,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
|
|||
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_DATABASES_OPERATIONS_READS, $operations)
|
||||
->addMetric(str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_OPERATIONS_READS), $operations)
|
||||
;
|
||||
->addMetric(str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_OPERATIONS_READS), $operations);
|
||||
|
||||
$response->addHeader('X-Debug-Operations', $operations);
|
||||
|
||||
|
|
@ -3573,9 +3570,8 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
|
|||
->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('mode')
|
||||
->inject('queueForStatsUsage')
|
||||
->action(function (string $databaseId, string $collectionId, string $documentId, array $queries, Response $response, Database $dbForProject, string $mode, StatsUsage $queueForStatsUsage) {
|
||||
->action(function (string $databaseId, string $collectionId, string $documentId, array $queries, Response $response, Database $dbForProject, StatsUsage $queueForStatsUsage) {
|
||||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
|
|
@ -3653,8 +3649,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
|
|||
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_DATABASES_OPERATIONS_READS, $operations)
|
||||
->addMetric(str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_OPERATIONS_READS), $operations)
|
||||
;
|
||||
->addMetric(str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_OPERATIONS_READS), $operations);
|
||||
|
||||
$response->addHeader('X-Debug-Operations', $operations);
|
||||
|
||||
|
|
@ -3807,9 +3802,8 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
|
|||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('mode')
|
||||
->inject('queueForStatsUsage')
|
||||
->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents, string $mode, StatsUsage $queueForStatsUsage) {
|
||||
->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage) {
|
||||
|
||||
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
|
||||
|
||||
|
|
@ -3951,8 +3945,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
|
|||
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, $operations)
|
||||
->addMetric(str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_OPERATIONS_WRITES), $operations)
|
||||
;
|
||||
->addMetric(str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_OPERATIONS_WRITES), $operations);
|
||||
|
||||
$response->addHeader('X-Debug-Operations', $operations);
|
||||
|
||||
|
|
@ -4062,8 +4055,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu
|
|||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('mode')
|
||||
->action(function (string $databaseId, string $collectionId, string $documentId, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, string $mode) {
|
||||
->action(function (string $databaseId, string $collectionId, string $documentId, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage) {
|
||||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
|
|
|
|||
|
|
@ -1277,9 +1277,9 @@ App::post('/v1/functions/:functionId/deployments')
|
|||
model: Response::MODEL_DEPLOYMENT,
|
||||
)
|
||||
],
|
||||
requestType: 'multipart/form-data',
|
||||
type: MethodType::UPLOAD,
|
||||
packaging: true,
|
||||
requestType: 'multipart/form-data',
|
||||
))
|
||||
->param('functionId', '', new UID(), 'Function ID.')
|
||||
->param('entrypoint', null, new Text(1028), 'Entrypoint File.', true)
|
||||
|
|
|
|||
|
|
@ -48,9 +48,9 @@ App::post('/v1/migrations/appwrite')
|
|||
]
|
||||
))
|
||||
->param('resources', [], new ArrayList(new WhiteList(Appwrite::getSupportedResources())), 'List of resources to migrate')
|
||||
->param('endpoint', '', new URL(), "Source's Appwrite Endpoint")
|
||||
->param('projectId', '', new UID(), "Source's Project ID")
|
||||
->param('apiKey', '', new Text(512), "Source's API Key")
|
||||
->param('endpoint', '', new URL(), 'Source Appwrite endpoint')
|
||||
->param('projectId', '', new UID(), 'Source Project ID')
|
||||
->param('apiKey', '', new Text(512), 'Source API Key')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
|
|
|
|||
|
|
@ -893,7 +893,6 @@ App::error()
|
|||
->trigger();
|
||||
}
|
||||
|
||||
|
||||
if ($logger && $publish) {
|
||||
try {
|
||||
/** @var Utopia\Database\Document $user */
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
<?php
|
||||
|
||||
use Ahc\Jwt\JWT;
|
||||
use Ahc\Jwt\JWTException;
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Auth\Key;
|
||||
use Appwrite\Auth\MFA\Type\TOTP;
|
||||
use Appwrite\Event\Audit;
|
||||
use Appwrite\Event\Build;
|
||||
|
|
@ -16,6 +15,7 @@ use Appwrite\Event\StatsUsage;
|
|||
use Appwrite\Event\Webhook;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Extend\Exception as AppwriteException;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Abuse\Abuse;
|
||||
|
|
@ -98,28 +98,22 @@ $usageDatabaseListener = function (string $event, Document $document, StatsUsage
|
|||
|
||||
switch (true) {
|
||||
case $document->getCollection() === 'teams':
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_TEAMS, $value); // per project
|
||||
$queueForStatsUsage->addMetric(METRIC_TEAMS, $value); // per project
|
||||
break;
|
||||
case $document->getCollection() === 'users':
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_USERS, $value); // per project
|
||||
$queueForStatsUsage->addMetric(METRIC_USERS, $value); // per project
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$queueForStatsUsage
|
||||
->addReduce($document);
|
||||
$queueForStatsUsage->addReduce($document);
|
||||
}
|
||||
break;
|
||||
case $document->getCollection() === 'sessions': // sessions
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_SESSIONS, $value); //per project
|
||||
$queueForStatsUsage->addMetric(METRIC_SESSIONS, $value); //per project
|
||||
break;
|
||||
case $document->getCollection() === 'databases': // databases
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_DATABASES, $value); // per project
|
||||
$queueForStatsUsage->addMetric(METRIC_DATABASES, $value); // per project
|
||||
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$queueForStatsUsage
|
||||
->addReduce($document);
|
||||
$queueForStatsUsage->addReduce($document);
|
||||
}
|
||||
break;
|
||||
case str_starts_with($document->getCollection(), 'database_') && !str_contains($document->getCollection(), 'collection'): //collections
|
||||
|
|
@ -127,12 +121,10 @@ $usageDatabaseListener = function (string $event, Document $document, StatsUsage
|
|||
$databaseInternalId = $parts[1] ?? 0;
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_COLLECTIONS, $value) // per project
|
||||
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_COLLECTIONS), $value)
|
||||
;
|
||||
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_COLLECTIONS), $value);
|
||||
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$queueForStatsUsage
|
||||
->addReduce($document);
|
||||
$queueForStatsUsage->addReduce($document);
|
||||
}
|
||||
break;
|
||||
case str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_'): //documents
|
||||
|
|
@ -195,117 +187,79 @@ App::init()
|
|||
->inject('servers')
|
||||
->inject('mode')
|
||||
->inject('team')
|
||||
->action(function (App $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, Document $user, ?Document $session, array $servers, string $mode, Document $team) {
|
||||
->inject('apiKey')
|
||||
->action(function (App $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, Document $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey) {
|
||||
$route = $utopia->getRoute();
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
/** Default role */
|
||||
$roles = Config::getParam('roles', []);
|
||||
$role = ($user->isEmpty())
|
||||
|
||||
$role = $user->isEmpty()
|
||||
? Role::guests()->toString()
|
||||
: Role::users()->toString();
|
||||
|
||||
/** Allowed Scopes for the role */
|
||||
$scopes = $roles[$role]['scopes'];
|
||||
|
||||
$apiKey = $request->getHeader('x-appwrite-key', '');
|
||||
|
||||
// API Key authentication
|
||||
if (!empty($apiKey)) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Remove after migration
|
||||
if (!\str_contains($apiKey, '_')) {
|
||||
$keyType = API_KEY_STANDARD;
|
||||
$authKey = $apiKey;
|
||||
} else {
|
||||
[ $keyType, $authKey ] = \explode('_', $apiKey, 2);
|
||||
if ($apiKey->isExpired()) {
|
||||
throw new Exception(Exception::PROJECT_KEY_EXPIRED);
|
||||
}
|
||||
|
||||
if ($keyType === API_KEY_DYNAMIC) {
|
||||
// Dynamic key
|
||||
$role = $apiKey->getRole();
|
||||
$scopes = $apiKey->getScopes();
|
||||
|
||||
$jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 86400, 0);
|
||||
// Disable authorization checks for API keys
|
||||
Authorization::setDefaultStatus(false);
|
||||
|
||||
try {
|
||||
$payload = $jwtObj->decode($authKey);
|
||||
} catch (JWTException $error) {
|
||||
throw new Exception(Exception::API_KEY_EXPIRED);
|
||||
}
|
||||
if ($apiKey->getRole() === Auth::USER_ROLE_APPS) {
|
||||
$user = new Document([
|
||||
'$id' => '',
|
||||
'status' => true,
|
||||
'type' => Auth::ACTIVITY_TYPE_APP,
|
||||
'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(),
|
||||
'password' => '',
|
||||
'name' => $apiKey->getName(),
|
||||
]);
|
||||
|
||||
$projectId = $payload['projectId'] ?? '';
|
||||
$tokenScopes = $payload['scopes'] ?? [];
|
||||
$queueForAudits->setUser($user);
|
||||
}
|
||||
|
||||
// JWT includes project ID for better security
|
||||
if ($projectId === $project->getId()) {
|
||||
$user = new Document([
|
||||
'$id' => '',
|
||||
'status' => true,
|
||||
'type' => Auth::ACTIVITY_TYPE_APP,
|
||||
'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(),
|
||||
'password' => '',
|
||||
'name' => 'Dynamic Key',
|
||||
]);
|
||||
if ($apiKey->getType() === API_KEY_STANDARD) {
|
||||
$dbKey = $project->find(
|
||||
key: 'secret',
|
||||
find: $request->getHeader('x-appwrite-key', ''),
|
||||
subject: 'keys'
|
||||
);
|
||||
|
||||
$role = Auth::USER_ROLE_APPS;
|
||||
$scopes = \array_merge($roles[$role]['scopes'], $tokenScopes);
|
||||
if ($dbKey) {
|
||||
$accessedAt = $dbKey->getAttribute('accessedAt', '');
|
||||
|
||||
Authorization::setRole(Auth::USER_ROLE_APPS);
|
||||
Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys.
|
||||
|
||||
$queueForAudits->setUser($user);
|
||||
}
|
||||
} elseif ($keyType === API_KEY_STANDARD) {
|
||||
// No underline means no prefix. Backwards compatibility.
|
||||
// Regular key
|
||||
|
||||
// Check if given key match project API keys
|
||||
$key = $project->find('secret', $apiKey, 'keys');
|
||||
if ($key) {
|
||||
$user = new Document([
|
||||
'$id' => '',
|
||||
'status' => true,
|
||||
'type' => Auth::ACTIVITY_TYPE_APP,
|
||||
'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(),
|
||||
'password' => '',
|
||||
'name' => $key->getAttribute('name', 'UNKNOWN'),
|
||||
]);
|
||||
|
||||
$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_ACCESS)) > $accessedAt) {
|
||||
$key->setAttribute('accessedAt', DateTime::now());
|
||||
$dbForPlatform->updateDocument('keys', $key->getId(), $key);
|
||||
$dbKey->setAttribute('accessedAt', DateTime::now());
|
||||
$dbForPlatform->updateDocument('keys', $dbKey->getId(), $dbKey);
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
}
|
||||
|
||||
$sdkValidator = new WhiteList($servers, true);
|
||||
$sdk = $request->getHeader('x-sdk-name', 'UNKNOWN');
|
||||
|
||||
if ($sdkValidator->isValid($sdk)) {
|
||||
$sdks = $key->getAttribute('sdks', []);
|
||||
$sdks = $dbKey->getAttribute('sdks', []);
|
||||
|
||||
if (!in_array($sdk, $sdks)) {
|
||||
array_push($sdks, $sdk);
|
||||
$key->setAttribute('sdks', $sdks);
|
||||
$sdks[] = $sdk;
|
||||
$dbKey->setAttribute('sdks', $sdks);
|
||||
|
||||
/** Update access time as well */
|
||||
$key->setAttribute('accessedAt', Datetime::now());
|
||||
$dbForPlatform->updateDocument('keys', $key->getId(), $key);
|
||||
$dbKey->setAttribute('accessedAt', Datetime::now());
|
||||
$dbForPlatform->updateDocument('keys', $dbKey->getId(), $dbKey);
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
}
|
||||
}
|
||||
|
|
@ -313,8 +267,7 @@ App::init()
|
|||
$queueForAudits->setUser($user);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Admin User Authentication
|
||||
} // Admin User Authentication
|
||||
elseif (($project->getId() === 'console' && !$team->isEmpty() && !$user->isEmpty()) || ($project->getId() !== 'console' && !$user->isEmpty() && $mode === APP_MODE_ADMIN)) {
|
||||
$teamId = $team->getId();
|
||||
$adminRoles = [];
|
||||
|
|
@ -330,7 +283,7 @@ App::init()
|
|||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
$scopes = []; // reset scope if admin
|
||||
$scopes = []; // Reset scope if admin
|
||||
foreach ($adminRoles as $role) {
|
||||
$scopes = \array_merge($scopes, $roles[$role]['scopes']);
|
||||
}
|
||||
|
|
@ -345,9 +298,7 @@ App::init()
|
|||
Authorization::setRole($authRole);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update project last activity
|
||||
*/
|
||||
// Update project last activity
|
||||
if (!$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$accessedAt = $project->getAttribute('accessedAt', '');
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
|
||||
|
|
@ -356,9 +307,7 @@ App::init()
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user last activity
|
||||
*/
|
||||
// Update user last activity
|
||||
if (!empty($user->getId())) {
|
||||
$accessedAt = $user->getAttribute('accessedAt', '');
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCESS)) > $accessedAt) {
|
||||
|
|
@ -372,18 +321,18 @@ App::init()
|
|||
}
|
||||
}
|
||||
|
||||
/** Do not allow access to disabled services */
|
||||
/**
|
||||
* @var ?\Appwrite\SDK\Method $method
|
||||
* @var ?Method $method
|
||||
*/
|
||||
$method = $route->getLabel('sdk', false);
|
||||
|
||||
if (is_array($method)) {
|
||||
if (\is_array($method)) {
|
||||
$method = $method[0];
|
||||
}
|
||||
|
||||
if (!empty($method)) {
|
||||
$namespace = $method->getNamespace();
|
||||
|
||||
if (
|
||||
array_key_exists($namespace, $project->getAttribute('services', []))
|
||||
&& !$project->getAttribute('services', [])[$namespace]
|
||||
|
|
@ -393,13 +342,13 @@ App::init()
|
|||
}
|
||||
}
|
||||
|
||||
/** Do now allow access if scope is not allowed */
|
||||
// Do now allow access if scope is not allowed
|
||||
$scope = $route->getLabel('scope', 'none');
|
||||
if (!\in_array($scope, $scopes)) {
|
||||
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, $user->getAttribute('email', 'User') . ' (role: ' . \strtolower($roles[$role]['label']) . ') missing scope (' . $scope . ')');
|
||||
}
|
||||
|
||||
/** Do not allow access to blocked accounts */
|
||||
// Do not allow access to blocked accounts
|
||||
if (false === $user->getAttribute('status')) { // Account is blocked
|
||||
throw new Exception(Exception::USER_BLOCKED);
|
||||
}
|
||||
|
|
@ -440,7 +389,8 @@ App::init()
|
|||
->inject('dbForProject')
|
||||
->inject('timelimit')
|
||||
->inject('mode')
|
||||
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Publisher $publisher, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, StatsUsage $queueForStatsUsage, Database $dbForProject, callable $timelimit, string $mode) use ($usageDatabaseListener, $eventDatabaseListener) {
|
||||
->inject('apiKey')
|
||||
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Publisher $publisher, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, StatsUsage $queueForStatsUsage, Database $dbForProject, callable $timelimit, string $mode, ?Key $apiKey) use ($usageDatabaseListener, $eventDatabaseListener) {
|
||||
|
||||
$route = $utopia->getRoute();
|
||||
|
||||
|
|
@ -471,7 +421,7 @@ App::init()
|
|||
->setParam('{ip}', $request->getIP())
|
||||
->setParam('{url}', $request->getHostname() . $route->getPath())
|
||||
->setParam('{method}', $request->getMethod())
|
||||
->setParam('{chunkId}', (int) ($start / ($end + 1 - $start)));
|
||||
->setParam('{chunkId}', (int)($start / ($end + 1 - $start)));
|
||||
$timeLimitArray[] = $timeLimit;
|
||||
}
|
||||
|
||||
|
|
@ -537,6 +487,12 @@ App::init()
|
|||
$queueForAudits->setUser($userClone);
|
||||
}
|
||||
|
||||
if (!empty($apiKey) && !empty($apiKey->getDisabledMetrics())) {
|
||||
foreach ($apiKey->getDisabledMetrics() as $key) {
|
||||
$queueForStatsUsage->disableMetric($key);
|
||||
}
|
||||
}
|
||||
|
||||
$queueForDeletes->setProject($project);
|
||||
$queueForDatabase->setProject($project);
|
||||
$queueForBuilds->setProject($project);
|
||||
|
|
@ -580,10 +536,7 @@ App::init()
|
|||
$bucketId = $parts[1] ?? null;
|
||||
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAppUser && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
}
|
||||
|
||||
|
|
@ -625,8 +578,7 @@ App::init()
|
|||
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
|
||||
->addHeader('Pragma', 'no-cache')
|
||||
->addHeader('Expires', '0')
|
||||
->addHeader('X-Appwrite-Cache', 'miss')
|
||||
;
|
||||
->addHeader('X-Appwrite-Cache', 'miss');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -787,6 +739,7 @@ App::shutdown()
|
|||
foreach ($queueForEvents->getParams() as $key => $value) {
|
||||
$queueForAudits->setParam($key, $value);
|
||||
}
|
||||
|
||||
$queueForAudits->trigger();
|
||||
}
|
||||
|
||||
|
|
@ -806,9 +759,7 @@ App::shutdown()
|
|||
$queueForMessaging->trigger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache label
|
||||
*/
|
||||
// Cache label
|
||||
$useCache = $route->getLabel('cache', false);
|
||||
if ($useCache) {
|
||||
$resource = $resourceType = null;
|
||||
|
|
|
|||
11
app/init.php
11
app/init.php
|
|
@ -21,6 +21,7 @@ if (\file_exists(__DIR__ . '/../vendor/autoload.php')) {
|
|||
use Ahc\Jwt\JWT;
|
||||
use Ahc\Jwt\JWTException;
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Auth\Key;
|
||||
use Appwrite\Event\Audit;
|
||||
use Appwrite\Event\Build;
|
||||
use Appwrite\Event\Certificate;
|
||||
|
|
@ -1942,3 +1943,13 @@ App::setResource('previewHostname', function (Request $request) {
|
|||
|
||||
return '';
|
||||
}, ['request']);
|
||||
|
||||
App::setResource('apiKey', function (Request $request, Document $project): ?Key {
|
||||
$key = $request->getHeader('x-appwrite-key');
|
||||
|
||||
if (empty($key)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Key::decode($project, $key);
|
||||
}, ['request', 'project']);
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@
|
|||
},
|
||||
"require-dev": {
|
||||
"ext-fileinfo": "*",
|
||||
"appwrite/sdk-generator": "0.39.32",
|
||||
"appwrite/sdk-generator": "0.40.*",
|
||||
"phpunit/phpunit": "9.5.20",
|
||||
"swoole/ide-helper": "5.1.2",
|
||||
"textalk/websocket": "1.5.7",
|
||||
|
|
|
|||
14
composer.lock
generated
14
composer.lock
generated
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "ed36bf1392e79d1b1bb07fb2a81f03bf",
|
||||
"content-hash": "b17c58729c4380afcba7714e9bced863",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/jwt",
|
||||
|
|
@ -5051,16 +5051,16 @@
|
|||
"packages-dev": [
|
||||
{
|
||||
"name": "appwrite/sdk-generator",
|
||||
"version": "0.39.32",
|
||||
"version": "0.40.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appwrite/sdk-generator.git",
|
||||
"reference": "2d02e1305ea5004fb0aec6b2618d6c597659b75c"
|
||||
"reference": "d2880132c900f64108d3e4484a6c1ed1bed2303c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/2d02e1305ea5004fb0aec6b2618d6c597659b75c",
|
||||
"reference": "2d02e1305ea5004fb0aec6b2618d6c597659b75c",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/d2880132c900f64108d3e4484a6c1ed1bed2303c",
|
||||
"reference": "d2880132c900f64108d3e4484a6c1ed1bed2303c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -5096,9 +5096,9 @@
|
|||
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
|
||||
"support": {
|
||||
"issues": "https://github.com/appwrite/sdk-generator/issues",
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/0.39.32"
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/0.40.0"
|
||||
},
|
||||
"time": "2025-01-29T04:04:19+00:00"
|
||||
"time": "2025-02-04T12:47:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/annotations",
|
||||
|
|
|
|||
158
src/Appwrite/Auth/Key.php
Normal file
158
src/Appwrite/Auth/Key.php
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Auth;
|
||||
|
||||
use Ahc\Jwt\JWT;
|
||||
use Ahc\Jwt\JWTException;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\System\System;
|
||||
|
||||
class Key
|
||||
{
|
||||
public function __construct(
|
||||
protected string $projectId,
|
||||
protected string $type,
|
||||
protected string $role,
|
||||
protected array $scopes,
|
||||
protected string $name,
|
||||
protected bool $expired = false,
|
||||
protected array $disabledMetrics = [],
|
||||
) {
|
||||
}
|
||||
|
||||
public function getProjectId(): string
|
||||
{
|
||||
return $this->projectId;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function getRole(): string
|
||||
{
|
||||
return $this->role;
|
||||
}
|
||||
|
||||
public function getScopes(): array
|
||||
{
|
||||
return $this->scopes;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function isExpired(): bool
|
||||
{
|
||||
return $this->expired;
|
||||
}
|
||||
|
||||
public function getDisabledMetrics(): array
|
||||
{
|
||||
return $this->disabledMetrics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the given secret key into a Key object, containing the project ID, type, role, scopes, and name.
|
||||
* Can be a stored API key or a dynamic key (JWT).
|
||||
*
|
||||
* @param Document $project
|
||||
* @param string $key
|
||||
* @return Key
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function decode(
|
||||
Document $project,
|
||||
string $key
|
||||
): Key {
|
||||
if (\str_contains($key, '_')) {
|
||||
[$type, $secret] = \explode('_', $key, 2);
|
||||
} else {
|
||||
$type = API_KEY_STANDARD;
|
||||
$secret = $key;
|
||||
}
|
||||
|
||||
$role = Auth::USER_ROLE_APPS;
|
||||
$roles = Config::getParam('roles', []);
|
||||
$scopes = $roles[Auth::USER_ROLE_APPS]['scopes'] ?? [];
|
||||
$expired = false;
|
||||
|
||||
$guestKey = new Key(
|
||||
$project->getId(),
|
||||
$type,
|
||||
Auth::USER_ROLE_GUESTS,
|
||||
$roles[Auth::USER_ROLE_GUESTS]['scopes'] ?? [],
|
||||
'UNKNOWN'
|
||||
);
|
||||
|
||||
switch ($type) {
|
||||
case API_KEY_DYNAMIC:
|
||||
$jwtObj = new JWT(
|
||||
key: System::getEnv('_APP_OPENSSL_KEY_V1'),
|
||||
algo: 'HS256',
|
||||
maxAge: 86400,
|
||||
leeway: 0
|
||||
);
|
||||
|
||||
try {
|
||||
$payload = $jwtObj->decode($secret);
|
||||
} catch (JWTException) {
|
||||
$expired = true;
|
||||
}
|
||||
|
||||
$name = $payload['name'] ?? 'Dynamic Key';
|
||||
$projectId = $payload['projectId'] ?? '';
|
||||
$disabledMetrics = $payload['disabledMetrics'] ?? [];
|
||||
$scopes = \array_merge($payload['scopes'] ?? [], $scopes);
|
||||
|
||||
if ($projectId !== $project->getId()) {
|
||||
return $guestKey;
|
||||
}
|
||||
|
||||
return new Key(
|
||||
$projectId,
|
||||
$type,
|
||||
$role,
|
||||
$scopes,
|
||||
$name,
|
||||
$expired,
|
||||
$disabledMetrics
|
||||
);
|
||||
case API_KEY_STANDARD:
|
||||
$key = $project->find(
|
||||
key: 'secret',
|
||||
find: $key,
|
||||
subject: 'keys'
|
||||
);
|
||||
|
||||
if (!$key) {
|
||||
return $guestKey;
|
||||
}
|
||||
|
||||
$expire = $key->getAttribute('expire');
|
||||
if (!empty($expire) && $expire < DateTime::formatTz(DateTime::now())) {
|
||||
$expired = true;
|
||||
}
|
||||
|
||||
$name = $key->getAttribute('name', 'UNKNOWN');
|
||||
$scopes = \array_merge($key->getAttribute('scopes', []), $scopes);
|
||||
|
||||
return new Key(
|
||||
$project->getId(),
|
||||
$type,
|
||||
$role,
|
||||
$scopes,
|
||||
$name,
|
||||
$expired
|
||||
);
|
||||
default:
|
||||
return $guestKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,8 @@ use Utopia\Queue\Publisher;
|
|||
class StatsUsage extends Event
|
||||
{
|
||||
protected array $metrics = [];
|
||||
protected array $reduce = [];
|
||||
protected array $reduce = [];
|
||||
protected array $disabled = [];
|
||||
|
||||
public function __construct(protected Publisher $publisher)
|
||||
{
|
||||
|
|
@ -49,6 +50,19 @@ class StatsUsage extends Event
|
|||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set disabled metrics.
|
||||
*
|
||||
* @param string $key
|
||||
* @return self
|
||||
*/
|
||||
public function disableMetric(string $key): self
|
||||
{
|
||||
$this->disabled[] = $key;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the payload for the event
|
||||
*
|
||||
|
|
@ -58,8 +72,15 @@ class StatsUsage extends Event
|
|||
{
|
||||
return [
|
||||
'project' => $this->getProject(),
|
||||
'reduce' => $this->reduce,
|
||||
'metrics' => $this->metrics,
|
||||
'reduce' => $this->reduce,
|
||||
'metrics' => \array_filter($this->metrics, function ($metric) {
|
||||
foreach ($this->disabled as $disabledMetric) {
|
||||
if (\str_ends_with($metric['key'], $disabledMetric)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -188,28 +188,21 @@ class Migrations extends Action
|
|||
}
|
||||
|
||||
/**
|
||||
* @throws \Utopia\Database\Exception
|
||||
* @throws Authorization
|
||||
* @throws Conflict
|
||||
* @throws Restricted
|
||||
* @throws Structure
|
||||
*/
|
||||
protected function removeAPIKey(Document $apiKey): void
|
||||
{
|
||||
$this->dbForPlatform->deleteDocument('keys', $apiKey->getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Authorization
|
||||
* @throws Structure
|
||||
* @throws \Utopia\Database\Exception
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function generateAPIKey(Document $project): string
|
||||
{
|
||||
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 86400, 0);
|
||||
|
||||
$apiKey = $jwt->encode([
|
||||
'projectId' => $project->getId(),
|
||||
'disabledMetrics' => [
|
||||
METRIC_DATABASES_OPERATIONS_READS,
|
||||
METRIC_DATABASES_OPERATIONS_WRITES,
|
||||
METRIC_NETWORK_REQUESTS,
|
||||
METRIC_NETWORK_INBOUND,
|
||||
METRIC_NETWORK_OUTBOUND,
|
||||
],
|
||||
'scopes' => [
|
||||
'users.read',
|
||||
'users.write',
|
||||
|
|
@ -222,12 +215,9 @@ class Migrations extends Action
|
|||
'functions.read',
|
||||
'functions.write',
|
||||
'databases.read',
|
||||
'databases.write',
|
||||
'collections.read',
|
||||
'collections.write',
|
||||
'documents.read',
|
||||
'documents.write'
|
||||
]
|
||||
],
|
||||
]);
|
||||
|
||||
return API_KEY_DYNAMIC . '_' . $apiKey;
|
||||
|
|
|
|||
|
|
@ -98,8 +98,10 @@ class StatsUsageDump extends Action
|
|||
* @param Message $message
|
||||
* @param callable $getProjectDB
|
||||
* @param callable $getLogsDB
|
||||
* @param Registry $register
|
||||
* @return void
|
||||
* @throws Exception
|
||||
* @throws \Throwable
|
||||
* @throws \Utopia\Database\Exception
|
||||
*/
|
||||
public function action(Message $message, callable $getProjectDB, callable $getLogsDB, Registry $register): void
|
||||
|
|
@ -111,7 +113,6 @@ class StatsUsageDump extends Action
|
|||
throw new Exception('Missing payload');
|
||||
}
|
||||
|
||||
|
||||
foreach ($payload['stats'] ?? [] as $stats) {
|
||||
$project = new Document($stats['project'] ?? []);
|
||||
|
||||
|
|
@ -152,7 +153,9 @@ class StatsUsageDump extends Action
|
|||
'value' => $value,
|
||||
'region' => System::getEnv('_APP_REGION', 'default'),
|
||||
]);
|
||||
|
||||
$documentClone = new Document($document->getArrayCopy());
|
||||
|
||||
$dbForProject->createOrUpdateDocumentsWithIncrease(
|
||||
'stats',
|
||||
'value',
|
||||
|
|
@ -315,7 +318,7 @@ class StatsUsageDump extends Action
|
|||
console::log('[' . DateTime::now() . '] DB Storage Calculation [' . $key . '] took ' . (($end - $start) * 1000) . ' milliseconds');
|
||||
}
|
||||
|
||||
protected function writeToLogsDB(Document $project, Document $document)
|
||||
protected function writeToLogsDB(Document $project, Document $document): void
|
||||
{
|
||||
if (!System::getEnv('_APP_STATS_USAGE_DUAL_WRITING', false)) {
|
||||
Console::log('Dual Writing is disabled. Skipping...');
|
||||
|
|
|
|||
|
|
@ -52,6 +52,13 @@ class UsageTest extends Scope
|
|||
}
|
||||
}
|
||||
|
||||
public static function getYesterday(): string
|
||||
{
|
||||
$date = new DateTime();
|
||||
$date->modify('-1 day');
|
||||
return $date->format(self::$formatTz);
|
||||
}
|
||||
|
||||
public static function getToday(): string
|
||||
{
|
||||
$date = new DateTime();
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ namespace Tests\E2E\Services\Migrations;
|
|||
|
||||
use CURLFile;
|
||||
use Tests\E2E\Client;
|
||||
use Tests\E2E\General\UsageTest;
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Services\Functions\FunctionsBase;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
|
|
@ -20,13 +21,13 @@ trait MigrationsBase
|
|||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected static $destinationProject = [];
|
||||
protected static array $destinationProject = [];
|
||||
|
||||
/**
|
||||
* @param bool $fresh
|
||||
* @return array
|
||||
*/
|
||||
public function getDesintationProject(bool $fresh = false): array
|
||||
public function getDestinationProject(bool $fresh = false): array
|
||||
{
|
||||
if (!empty(self::$destinationProject) && !$fresh) {
|
||||
return self::$destinationProject;
|
||||
|
|
@ -40,13 +41,12 @@ trait MigrationsBase
|
|||
return self::$destinationProject;
|
||||
}
|
||||
|
||||
public function performMigrationSync(
|
||||
array $body,
|
||||
): array {
|
||||
public function performMigrationSync(array $body): array
|
||||
{
|
||||
$migration = $this->client->call(Client::METHOD_POST, '/migrations/appwrite', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getDesintationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
|
||||
'x-appwrite-project' => $this->getDestinationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
|
||||
], $body);
|
||||
|
||||
$this->assertEquals(202, $migration['headers']['status-code']);
|
||||
|
|
@ -57,8 +57,8 @@ trait MigrationsBase
|
|||
while ($attempts < 5) {
|
||||
$response = $this->client->call(Client::METHOD_GET, '/migrations/' . $migration['body']['$id'], [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getDesintationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
|
||||
'x-appwrite-project' => $this->getDestinationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
|
@ -82,12 +82,14 @@ trait MigrationsBase
|
|||
$attempts++;
|
||||
sleep(5);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Appwrite E2E Migration Tests
|
||||
*/
|
||||
public function testCreateAppwriteMigration()
|
||||
public function testCreateAppwriteMigration(): void
|
||||
{
|
||||
$response = $this->performMigrationSync([
|
||||
'resources' => Appwrite::getSupportedResources(),
|
||||
|
|
@ -105,7 +107,7 @@ trait MigrationsBase
|
|||
/**
|
||||
* Auth
|
||||
*/
|
||||
public function testAppwriteMigrationAuthUserPassword()
|
||||
public function testAppwriteMigrationAuthUserPassword(): void
|
||||
{
|
||||
$response = $this->client->call(Client::METHOD_POST, '/users', [
|
||||
'content-type' => 'application/json',
|
||||
|
|
@ -144,8 +146,8 @@ trait MigrationsBase
|
|||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/users/' . $user['$id'], [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getDesintationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
|
||||
'x-appwrite-project' => $this->getDestinationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
|
@ -157,8 +159,8 @@ trait MigrationsBase
|
|||
// Cleanup
|
||||
$this->client->call(Client::METHOD_DELETE, '/users/' . $user['$id'], [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getDesintationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
|
||||
'x-appwrite-project' => $this->getDestinationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->client->call(Client::METHOD_DELETE, '/users/' . $user['$id'], [
|
||||
|
|
@ -168,7 +170,7 @@ trait MigrationsBase
|
|||
]);
|
||||
}
|
||||
|
||||
public function testAppwriteMigrationAuthUserPhone()
|
||||
public function testAppwriteMigrationAuthUserPhone(): void
|
||||
{
|
||||
$response = $this->client->call(Client::METHOD_POST, '/users', [
|
||||
'content-type' => 'application/json',
|
||||
|
|
@ -206,8 +208,8 @@ trait MigrationsBase
|
|||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/users/' . $user['$id'], [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getDesintationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
|
||||
'x-appwrite-project' => $this->getDestinationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
|
@ -224,12 +226,12 @@ trait MigrationsBase
|
|||
|
||||
$this->client->call(Client::METHOD_DELETE, '/users/' . $user['$id'], [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getDesintationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
|
||||
'x-appwrite-project' => $this->getDestinationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function testAppwriteMigrationAuthTeam()
|
||||
public function testAppwriteMigrationAuthTeam(): void
|
||||
{
|
||||
$user = $this->client->call(Client::METHOD_POST, '/users', [
|
||||
'content-type' => 'application/json',
|
||||
|
|
@ -309,8 +311,8 @@ trait MigrationsBase
|
|||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/teams/' . $team['body']['$id'], [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getDesintationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
|
||||
'x-appwrite-project' => $this->getDestinationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
|
@ -320,8 +322,8 @@ trait MigrationsBase
|
|||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/teams/' . $team['body']['$id'] . '/memberships', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getDesintationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
|
||||
'x-appwrite-project' => $this->getDestinationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
|
@ -342,8 +344,8 @@ trait MigrationsBase
|
|||
|
||||
$this->client->call(Client::METHOD_DELETE, '/teams/' . $team['body']['$id'], [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getDesintationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
|
||||
'x-appwrite-project' => $this->getDestinationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->client->call(Client::METHOD_DELETE, '/users/' . $user['body']['$id'], [
|
||||
|
|
@ -354,8 +356,8 @@ trait MigrationsBase
|
|||
|
||||
$this->client->call(Client::METHOD_DELETE, '/users/' . $user['body']['$id'], [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getDesintationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
|
||||
'x-appwrite-project' => $this->getDestinationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->client->call(Client::METHOD_DELETE, '/teams/' . $team['body']['$id'], [
|
||||
|
|
@ -366,15 +368,15 @@ trait MigrationsBase
|
|||
|
||||
$this->client->call(Client::METHOD_DELETE, '/teams/' . $team['body']['$id'], [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getDesintationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
|
||||
'x-appwrite-project' => $this->getDestinationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Databases
|
||||
*/
|
||||
public function testAppwriteMigrationDatabase()
|
||||
public function testAppwriteMigrationDatabase(): array
|
||||
{
|
||||
$response = $this->client->call(Client::METHOD_POST, '/databases', [
|
||||
'content-type' => 'application/json',
|
||||
|
|
@ -400,7 +402,6 @@ trait MigrationsBase
|
|||
'apiKey' => $this->getProject()['apiKey'],
|
||||
]);
|
||||
|
||||
|
||||
$this->assertEquals('completed', $result['status']);
|
||||
$this->assertEquals([Resource::TYPE_DATABASE], $result['resources']);
|
||||
$this->assertArrayHasKey(Resource::TYPE_DATABASE, $result['statusCounters']);
|
||||
|
|
@ -412,8 +413,8 @@ trait MigrationsBase
|
|||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getDesintationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
|
||||
'x-appwrite-project' => $this->getDestinationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
|
@ -426,8 +427,8 @@ trait MigrationsBase
|
|||
// Cleanup on destination
|
||||
$this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getDesintationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
|
||||
'x-appwrite-project' => $this->getDestinationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
|
||||
]);
|
||||
|
||||
return [
|
||||
|
|
@ -438,7 +439,7 @@ trait MigrationsBase
|
|||
/**
|
||||
* @depends testAppwriteMigrationDatabase
|
||||
*/
|
||||
public function testAppwriteMigrationDatabasesCollection(array $data)
|
||||
public function testAppwriteMigrationDatabasesCollection(array $data): array
|
||||
{
|
||||
$databaseId = $data['databaseId'];
|
||||
|
||||
|
|
@ -506,8 +507,8 @@ trait MigrationsBase
|
|||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getDesintationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
|
||||
'x-appwrite-project' => $this->getDestinationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
|
@ -518,8 +519,8 @@ trait MigrationsBase
|
|||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/name', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getDesintationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
|
||||
'x-appwrite-project' => $this->getDestinationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
|
@ -532,8 +533,8 @@ trait MigrationsBase
|
|||
// Cleanup
|
||||
$this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getDesintationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
|
||||
'x-appwrite-project' => $this->getDestinationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
|
||||
]);
|
||||
|
||||
return [
|
||||
|
|
@ -545,7 +546,7 @@ trait MigrationsBase
|
|||
/**
|
||||
* @depends testAppwriteMigrationDatabasesCollection
|
||||
*/
|
||||
public function testAppwriteMigrationDatabasesDocument(array $data)
|
||||
public function testAppwriteMigrationDatabasesDocument(array $data): void
|
||||
{
|
||||
$databaseId = $data['databaseId'];
|
||||
$collectionId = $data['collectionId'];
|
||||
|
|
@ -579,6 +580,14 @@ trait MigrationsBase
|
|||
'apiKey' => $this->getProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$finalStats = $this->client->call(Client::METHOD_GET, '/project/usage', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'startDate' => UsageTest::getYesterday(),
|
||||
'endDate' => UsageTest::getTomorrow(),
|
||||
]);
|
||||
|
||||
$this->assertEquals('completed', $result['status']);
|
||||
$this->assertEquals([Resource::TYPE_DATABASE, Resource::TYPE_COLLECTION, Resource::TYPE_ATTRIBUTE, Resource::TYPE_DOCUMENT], $result['resources']);
|
||||
|
||||
|
|
@ -594,8 +603,8 @@ trait MigrationsBase
|
|||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getDesintationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
|
||||
'x-appwrite-project' => $this->getDestinationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
|
@ -607,15 +616,15 @@ trait MigrationsBase
|
|||
// Cleanup
|
||||
$this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getDesintationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
|
||||
'x-appwrite-project' => $this->getDestinationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Storage
|
||||
*/
|
||||
public function testAppwriteMigrationStorageBucket()
|
||||
public function testAppwriteMigrationStorageBucket(): void
|
||||
{
|
||||
$bucket = $this->client->call(Client::METHOD_POST, '/storage/buckets', [
|
||||
'content-type' => 'application/json',
|
||||
|
|
@ -663,8 +672,8 @@ trait MigrationsBase
|
|||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucket['body']['$id'], [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getDesintationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
|
||||
'x-appwrite-project' => $this->getDestinationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
|
@ -683,8 +692,8 @@ trait MigrationsBase
|
|||
// Cleanup
|
||||
$this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $bucket['body']['$id'], [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getDesintationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
|
||||
'x-appwrite-project' => $this->getDestinationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $bucket['body']['$id'], [
|
||||
|
|
@ -694,7 +703,7 @@ trait MigrationsBase
|
|||
]);
|
||||
}
|
||||
|
||||
public function testAppwriteMigrationStorageFiles()
|
||||
public function testAppwriteMigrationStorageFiles(): void
|
||||
{
|
||||
$bucket = $this->client->call(Client::METHOD_POST, '/storage/buckets', [
|
||||
'content-type' => 'application/json',
|
||||
|
|
@ -767,8 +776,8 @@ trait MigrationsBase
|
|||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $fileId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getDesintationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
|
||||
'x-appwrite-project' => $this->getDestinationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
|
@ -786,15 +795,15 @@ trait MigrationsBase
|
|||
|
||||
$this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $bucketId . '/files/' . $fileId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getDesintationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
|
||||
'x-appwrite-project' => $this->getDestinationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Functions
|
||||
*/
|
||||
public function testAppwriteMigrationFunction()
|
||||
public function testAppwriteMigrationFunction(): void
|
||||
{
|
||||
$functionId = $this->setupFunction([
|
||||
'functionId' => ID::unique(),
|
||||
|
|
@ -839,8 +848,8 @@ trait MigrationsBase
|
|||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getDesintationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
|
||||
'x-appwrite-project' => $this->getDestinationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
|
@ -856,8 +865,8 @@ trait MigrationsBase
|
|||
$this->assertEventually(function () use ($functionId) {
|
||||
$deployments = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments/', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getDesintationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
|
||||
'x-appwrite-project' => $this->getDestinationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
|
||||
]));
|
||||
|
||||
$this->assertEquals(200, $deployments['headers']['status-code']);
|
||||
|
|
@ -870,8 +879,8 @@ trait MigrationsBase
|
|||
// Attempt execution
|
||||
$execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getDesintationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
|
||||
'x-appwrite-project' => $this->getDestinationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
|
||||
], [
|
||||
'body' => 'test'
|
||||
]);
|
||||
|
|
@ -888,8 +897,8 @@ trait MigrationsBase
|
|||
|
||||
$this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getDesintationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
|
||||
'x-appwrite-project' => $this->getDestinationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
56
tests/unit/Auth/KeyTest.php
Normal file
56
tests/unit/Auth/KeyTest.php
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Unit\Auth;
|
||||
|
||||
use Ahc\Jwt\JWT;
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Auth\Key;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\System\System;
|
||||
|
||||
class KeyTest extends TestCase
|
||||
{
|
||||
public function testDecode(): void
|
||||
{
|
||||
$projectId = 'test';
|
||||
$usage = false;
|
||||
$scopes = [
|
||||
'databases.read',
|
||||
'collections.read',
|
||||
'documents.read',
|
||||
];
|
||||
$roleScopes = Config::getParam('roles', [])[Auth::USER_ROLE_APPS]['scopes'];
|
||||
|
||||
$key = static::generateKey($projectId, $usage, $scopes);
|
||||
$project = new Document(['$id' => $projectId,]);
|
||||
$decoded = Key::decode($project, $key);
|
||||
|
||||
$this->assertEquals($projectId, $decoded->getProjectId());
|
||||
$this->assertEquals(API_KEY_DYNAMIC, $decoded->getType());
|
||||
$this->assertEquals(Auth::USER_ROLE_APPS, $decoded->getRole());
|
||||
$this->assertEquals(\array_merge($scopes, $roleScopes), $decoded->getScopes());
|
||||
}
|
||||
|
||||
private static function generateKey(
|
||||
string $projectId,
|
||||
bool $usage,
|
||||
array $scopes,
|
||||
): string {
|
||||
$jwt = new JWT(
|
||||
key: System::getEnv('_APP_OPENSSL_KEY_V1'),
|
||||
algo: 'HS256',
|
||||
maxAge: 86400,
|
||||
leeway: 0,
|
||||
);
|
||||
|
||||
$apiKey = $jwt->encode([
|
||||
'projectId' => $projectId,
|
||||
'usage' => $usage,
|
||||
'scopes' => $scopes,
|
||||
]);
|
||||
|
||||
return API_KEY_DYNAMIC . '_' . $apiKey;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue