diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 5c5186d677..59a29ac4d9 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -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()); diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 14255ef7a4..cdaf062788 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -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) diff --git a/app/controllers/api/migrations.php b/app/controllers/api/migrations.php index ac149ac8eb..75afc7ed2c 100644 --- a/app/controllers/api/migrations.php +++ b/app/controllers/api/migrations.php @@ -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') diff --git a/app/controllers/general.php b/app/controllers/general.php index b785f0dde9..6db4a8b28d 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -893,7 +893,6 @@ App::error() ->trigger(); } - if ($logger && $publish) { try { /** @var Utopia\Database\Document $user */ diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index bcee2550c2..7f7b73ab0c 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -1,8 +1,7 @@ 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; diff --git a/app/init.php b/app/init.php index 0c6746de3c..25fa9faa85 100644 --- a/app/init.php +++ b/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']); diff --git a/composer.json b/composer.json index b28eec6964..d3ceb8b7a9 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index e83c7a8ce4..11d2ba4c2f 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/src/Appwrite/Auth/Key.php b/src/Appwrite/Auth/Key.php new file mode 100644 index 0000000000..1c40b35f54 --- /dev/null +++ b/src/Appwrite/Auth/Key.php @@ -0,0 +1,158 @@ +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; + } + } +} diff --git a/src/Appwrite/Event/StatsUsage.php b/src/Appwrite/Event/StatsUsage.php index bed25419f6..e259ba5e04 100644 --- a/src/Appwrite/Event/StatsUsage.php +++ b/src/Appwrite/Event/StatsUsage.php @@ -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; + }), ]; } } diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index cd567f6fa3..3bf8e06356 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -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; diff --git a/src/Appwrite/Platform/Workers/StatsUsageDump.php b/src/Appwrite/Platform/Workers/StatsUsageDump.php index 38ebd578a5..5d7240f4b5 100644 --- a/src/Appwrite/Platform/Workers/StatsUsageDump.php +++ b/src/Appwrite/Platform/Workers/StatsUsageDump.php @@ -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...'); diff --git a/tests/e2e/General/UsageTest.php b/tests/e2e/General/UsageTest.php index 74ae1c00bc..c0d0c80eb1 100644 --- a/tests/e2e/General/UsageTest.php +++ b/tests/e2e/General/UsageTest.php @@ -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(); diff --git a/tests/e2e/Services/Migrations/MigrationsBase.php b/tests/e2e/Services/Migrations/MigrationsBase.php index e4c7ba7712..381706f5ee 100644 --- a/tests/e2e/Services/Migrations/MigrationsBase.php +++ b/tests/e2e/Services/Migrations/MigrationsBase.php @@ -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'], ]); } } diff --git a/tests/unit/Auth/KeyTest.php b/tests/unit/Auth/KeyTest.php new file mode 100644 index 0000000000..8ae2114697 --- /dev/null +++ b/tests/unit/Auth/KeyTest.php @@ -0,0 +1,56 @@ + $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; + } +}