Merge branch '1.6.x' into feat-usage-dump-multi-tenant-batch

This commit is contained in:
Damodar Lohani 2025-04-07 09:21:32 +05:45 committed by GitHub
commit 57bc76edc7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 111 additions and 59 deletions

View file

@ -1034,7 +1034,6 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
->param('membershipId', '', new UID(), 'Membership ID.')
->param('roles', [], function (Document $project) {
if ($project->getId() === 'console') {
;
$roles = array_keys(Config::getParam('roles', []));
array_filter($roles, function ($role) {
return !in_array($role, [Auth::USER_ROLE_APPS, Auth::USER_ROLE_GUESTS, Auth::USER_ROLE_USERS]);
@ -1046,9 +1045,10 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
->inject('request')
->inject('response')
->inject('user')
->inject('project')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) {
->action(function (string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents) {
$team = $dbForProject->getDocument('teams', $teamId);
if ($team->isEmpty()) {
@ -1069,6 +1069,21 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
$isAppUser = Auth::isAppUser(Authorization::getRoles());
$isOwner = Authorization::isRole('team:' . $team->getId() . '/owner');
if ($project->getId() === 'console') {
// Quick check: fetch up to 2 owners to determine if only one exists
$ownersCount = $dbForProject->count(
collection: 'memberships',
queries: [Query::contains('roles', ['owner'])],
max: 2
);
// Prevent role change if there's only one owner left,
// the requester is that owner, and the new `$roles` no longer include 'owner'!
if ($ownersCount === 1 && $isOwner && !\in_array('owner', $roles)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'There must be at least one owner in the organization.');
}
}
if (!$isOwner && !$isPrivilegedUser && !$isAppUser) { // Not owner, not admin, not app (server)
throw new Exception(Exception::USER_UNAUTHORIZED, 'User is not allowed to modify roles');
}

View file

@ -871,17 +871,18 @@ App::error()
if (!empty($providerConfig) && $error->getCode() >= 400 && $error->getCode() < 500) {
// Register error logger
try {
$loggingProvider = new DSN($providerConfig ?? '');
$loggingProvider = new DSN($providerConfig);
$providerName = $loggingProvider->getScheme();
if (!empty($providerName) && $providerName === 'sentry') {
$key = $loggingProvider->getPassword();
$projectId = $loggingProvider->getUser() ?? '';
$host = 'https://' . $loggingProvider->getHost();
$sampleRate = $loggingProvider->getParam('sample', 0.01);
$adapter = new Sentry($projectId, $key, $host);
$logger = new Logger($adapter);
$logger->setSample(0.01);
$logger->setSample($sampleRate);
$publish = true;
} else {
throw new \Exception('Invalid experimental logging provider');

View file

@ -390,7 +390,8 @@ App::init()
->inject('timelimit')
->inject('mode')
->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) {
->inject('plan')
->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, array $plan) use ($usageDatabaseListener, $eventDatabaseListener) {
$route = $utopia->getRoute();
@ -520,6 +521,10 @@ App::init()
$useCache = $route->getLabel('cache', false);
if ($useCache) {
$route = $utopia->match($request);
$isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview';
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && !Auth::isPrivilegedUser(Authorization::getRoles());
$key = md5($request->getURI() . '*' . implode('*', $request->getParams()) . '*' . APP_CACHE_BUSTER);
$cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key));
$cache = new Cache(
@ -532,7 +537,7 @@ App::init()
$parts = explode('/', $cacheLog->getAttribute('resourceType'));
$type = $parts[0] ?? null;
if ($type === 'bucket') {
if ($type === 'bucket' && (!$isImageTransformation || !$isDisabled)) {
$bucketId = $parts[1] ?? null;
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
@ -573,8 +578,10 @@ App::init()
$response
->addHeader('Cache-Control', sprintf('private, max-age=%d', $timestamp))
->addHeader('X-Appwrite-Cache', 'hit')
->setContentType($cacheLog->getAttribute('mimeType'))
->send($data);
->setContentType($cacheLog->getAttribute('mimeType'));
if (!$isImageTransformation || !$isDisabled) {
$response->send($data);
}
} else {
$response
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')

View file

@ -51,7 +51,7 @@
"utopia-php/cache": "0.12.*",
"utopia-php/cli": "0.15.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "0.63.*",
"utopia-php/database": "0.64.*",
"utopia-php/domains": "0.5.*",
"utopia-php/dsn": "0.2.1",
"utopia-php/framework": "0.33.*",
@ -63,7 +63,7 @@
"utopia-php/migration": "0.8.*",
"utopia-php/orchestration": "0.9.*",
"utopia-php/platform": "0.7.*",
"utopia-php/pools": "0.7.*",
"utopia-php/pools": "0.8.*",
"utopia-php/preloader": "0.2.*",
"utopia-php/queue": "0.9.*",
"utopia-php/registry": "0.5.*",
@ -72,7 +72,7 @@
"utopia-php/system": "0.9.*",
"utopia-php/telemetry": "0.1.*",
"utopia-php/vcs": "0.9.*",
"utopia-php/websocket": "0.1.*",
"utopia-php/websocket": "0.3.*",
"matomo/device-detector": "6.1.*",
"dragonmantank/cron-expression": "3.3.2",
"phpmailer/phpmailer": "6.9.1",

82
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": "c99b4733669c17013e211c7dc54a86f6",
"content-hash": "6a54c8bc4f9f14cd3883f55880864630",
"packages": [
{
"name": "adhocore/jwt",
@ -2965,16 +2965,16 @@
},
{
"name": "tbachert/spi",
"version": "v1.0.2",
"version": "v1.0.3",
"source": {
"type": "git",
"url": "https://github.com/Nevay/spi.git",
"reference": "2ddfaf815dafb45791a61b08170de8d583c16062"
"reference": "506a79c98e1a51522e76ee921ccb6c62d52faf3a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Nevay/spi/zipball/2ddfaf815dafb45791a61b08170de8d583c16062",
"reference": "2ddfaf815dafb45791a61b08170de8d583c16062",
"url": "https://api.github.com/repos/Nevay/spi/zipball/506a79c98e1a51522e76ee921ccb6c62d52faf3a",
"reference": "506a79c98e1a51522e76ee921ccb6c62d52faf3a",
"shasum": ""
},
"require": {
@ -3011,9 +3011,9 @@
],
"support": {
"issues": "https://github.com/Nevay/spi/issues",
"source": "https://github.com/Nevay/spi/tree/v1.0.2"
"source": "https://github.com/Nevay/spi/tree/v1.0.3"
},
"time": "2024-10-04T16:36:12+00:00"
"time": "2025-04-02T19:38:14+00:00"
},
{
"name": "thecodingmachine/safe",
@ -3497,16 +3497,16 @@
},
{
"name": "utopia-php/database",
"version": "0.63.1",
"version": "0.64.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "ad191bf34151815f716f553796a363ff2b6ef7d3"
"reference": "6530a8a6d3c1fe92d0f9a92f0f05eda698d92e0b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/ad191bf34151815f716f553796a363ff2b6ef7d3",
"reference": "ad191bf34151815f716f553796a363ff2b6ef7d3",
"url": "https://api.github.com/repos/utopia-php/database/zipball/6530a8a6d3c1fe92d0f9a92f0f05eda698d92e0b",
"reference": "6530a8a6d3c1fe92d0f9a92f0f05eda698d92e0b",
"shasum": ""
},
"require": {
@ -3514,7 +3514,8 @@
"ext-pdo": "*",
"php": ">=8.1",
"utopia-php/cache": "0.12.*",
"utopia-php/framework": "0.33.*"
"utopia-php/framework": "0.33.*",
"utopia-php/pools": "0.8.*"
},
"require-dev": {
"fakerphp/faker": "1.23.*",
@ -3546,9 +3547,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/0.63.1"
"source": "https://github.com/utopia-php/database/tree/0.64.1"
},
"time": "2025-03-27T04:58:07+00:00"
"time": "2025-04-02T00:35:29+00:00"
},
{
"name": "utopia-php/domains",
@ -3745,16 +3746,16 @@
},
{
"name": "utopia-php/image",
"version": "0.8.0",
"version": "0.8.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/image.git",
"reference": "dcae5b1c6deb3ff6865f4e68f012b3709c289bca"
"reference": "e8cc7dd14f423270a1b7570ec0dae88a66195b63"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/image/zipball/dcae5b1c6deb3ff6865f4e68f012b3709c289bca",
"reference": "dcae5b1c6deb3ff6865f4e68f012b3709c289bca",
"url": "https://api.github.com/repos/utopia-php/image/zipball/e8cc7dd14f423270a1b7570ec0dae88a66195b63",
"reference": "e8cc7dd14f423270a1b7570ec0dae88a66195b63",
"shasum": ""
},
"require": {
@ -3788,9 +3789,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/image/issues",
"source": "https://github.com/utopia-php/image/tree/0.8.0"
"source": "https://github.com/utopia-php/image/tree/0.8.1"
},
"time": "2025-02-20T11:49:03+00:00"
"time": "2025-04-04T18:55:20+00:00"
},
{
"name": "utopia-php/locale",
@ -4106,16 +4107,16 @@
},
{
"name": "utopia-php/pools",
"version": "0.7.0",
"version": "0.8.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/pools.git",
"reference": "ad64d45afda08ec8b29e2642a8d18075964d40bf"
"reference": "60733929dc328e7ea47e800579c8bbf0d49df5ba"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/pools/zipball/ad64d45afda08ec8b29e2642a8d18075964d40bf",
"reference": "ad64d45afda08ec8b29e2642a8d18075964d40bf",
"url": "https://api.github.com/repos/utopia-php/pools/zipball/60733929dc328e7ea47e800579c8bbf0d49df5ba",
"reference": "60733929dc328e7ea47e800579c8bbf0d49df5ba",
"shasum": ""
},
"require": {
@ -4152,9 +4153,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/pools/issues",
"source": "https://github.com/utopia-php/pools/tree/0.7.0"
"source": "https://github.com/utopia-php/pools/tree/0.8.0"
},
"time": "2025-03-18T03:55:33+00:00"
"time": "2025-03-19T10:22:03+00:00"
},
{
"name": "utopia-php/preloader",
@ -4592,27 +4593,28 @@
},
{
"name": "utopia-php/websocket",
"version": "0.1.0",
"version": "0.3.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/websocket.git",
"reference": "51fcb86171400d8aa40d76c54593481fd273dab5"
"reference": "629e53640b108eab43c7cc9ab375efade8622d43"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/websocket/zipball/51fcb86171400d8aa40d76c54593481fd273dab5",
"reference": "51fcb86171400d8aa40d76c54593481fd273dab5",
"url": "https://api.github.com/repos/utopia-php/websocket/zipball/629e53640b108eab43c7cc9ab375efade8622d43",
"reference": "629e53640b108eab43c7cc9ab375efade8622d43",
"shasum": ""
},
"require": {
"php": ">=8.0"
},
"require-dev": {
"laravel/pint": "^1.15",
"phpstan/phpstan": "^1.12",
"phpunit/phpunit": "^9.5.5",
"swoole/ide-helper": "4.6.6",
"swoole/ide-helper": "5.1.2",
"textalk/websocket": "1.5.2",
"vimeo/psalm": "^4.8.1",
"workerman/workerman": "^4.0"
"workerman/workerman": "4.1.*"
},
"type": "library",
"autoload": {
@ -4624,16 +4626,6 @@
"license": [
"MIT"
],
"authors": [
{
"name": "Eldad Fux",
"email": "eldad@appwrite.io"
},
{
"name": "Torsten Dittmann",
"email": "torsten@appwrite.io"
}
],
"description": "A simple abstraction for WebSocket servers.",
"keywords": [
"framework",
@ -4644,9 +4636,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/websocket/issues",
"source": "https://github.com/utopia-php/websocket/tree/0.1.0"
"source": "https://github.com/utopia-php/websocket/tree/0.3.0"
},
"time": "2021-12-20T10:50:09+00:00"
"time": "2025-03-28T01:11:13+00:00"
},
{
"name": "webmozart/assert",

View file

@ -74,7 +74,11 @@ class Audits extends Action
Console::info('Aggregating audit logs');
$event = $payload['event'] ?? '';
$auditPayload = $payload['payload'] ?? '';
$auditPayload = '';
if ($project->getId() === 'console') {
$auditPayload = $payload['payload'] ?? '';
}
$mode = $payload['mode'] ?? '';
$resource = $payload['resource'] ?? '';
$userAgent = $payload['userAgent'] ?? '';

View file

@ -565,7 +565,8 @@ class Databases extends Action
try {
$documents = $database->deleteDocuments($collectionId, $queries);
} catch (\Throwable $th) {
Console::error('Failed to delete documents for collection ' . $collectionId . ': ' . $th->getMessage());
$tenant = $database->getSharedTables() ? 'Tenant:'.$database->getTenant() : '';
Console::error("Failed to delete documents for collection:{$database->getNamespace()}_{$collectionId} {$tenant} :{$th->getMessage()}");
return;
}

View file

@ -1050,7 +1050,8 @@ class Deletes extends Action
try {
$documents = $database->deleteDocuments($collection, $queries);
} catch (Throwable $th) {
Console::error('Failed to delete documents for collection ' . $collection . ': ' . $th->getMessage());
$tenant = $database->getSharedTables() ? 'Tenant:'.$database->getTenant() : '';
Console::error("Failed to delete documents for collection:{$database->getNamespace()}_{$collection} {$tenant} :{$th->getMessage()}");
return;
}

View file

@ -37,6 +37,37 @@ trait TeamsBase
$teamUid = $response1['body']['$id'];
$teamName = $response1['body']['name'];
/**
* Test: Attempt to downgrade the only OWNER in an organization (should fail)
*/
if ($this->getProject()['$id'] === 'console') {
// Step 1: Fetch all team memberships — only one exists at this point
$response = $this->client->call(Client::METHOD_GET, '/teams/' . $teamUid . '/memberships', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::limit(1)->toString(),
],
]);
// Step 2: Extract the membership ID of the only member (also the only OWNER)
$membershipID = $response['body']['memberships'][0]['$id'];
// Step 3: Attempt to downgrade the member's role to 'developer'
$response = $this->client->call(Client::METHOD_PATCH, '/teams/' . $teamUid . '/memberships/' . $membershipID, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'roles' => ['developer']
]);
// Step 4: Assert failure — cannot remove the only OWNER from a team
$this->assertEquals(400, $response['headers']['status-code']);
$this->assertEquals('general_argument_invalid', $response['body']['type']);
$this->assertEquals('There must be at least one owner in the organization.', $response['body']['message']);
}
$teamId = ID::unique();
$response2 = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
'content-type' => 'application/json',