Merge pull request #9336 from appwrite/feat-key-segmented-usage

Feat key segmented usage
This commit is contained in:
Jake Barnby 2025-02-18 18:47:01 +13:00 committed by GitHub
commit ae91869e76
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 437 additions and 240 deletions

View file

@ -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());

View file

@ -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)

View file

@ -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')

View file

@ -893,7 +893,6 @@ App::error()
->trigger();
}
if ($logger && $publish) {
try {
/** @var Utopia\Database\Document $user */

View file

@ -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;

View file

@ -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']);

View file

@ -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
View file

@ -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
View 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;
}
}
}

View file

@ -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;
}),
];
}
}

View file

@ -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;

View file

@ -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...');

View file

@ -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();

View file

@ -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'],
]);
}
}

View 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;
}
}