From 8b3d3c6f8b855c946b6a1673d5b07510c2dc84f7 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Thu, 19 Mar 2026 03:55:22 +0000 Subject: [PATCH 1/4] feat: distinguish user types in audit logs Introduce granular audit user types to differentiate between regular users, console admins, guests, and the various API key scopes. Co-Authored-By: Claude Opus 4.6 (1M context) --- app/controllers/shared/api.php | 18 +++++++++++++----- app/init/constants.php | 5 ++++- .../Collections/Documents/Logs/XList.php | 1 + .../Http/Databases/Collections/Logs/XList.php | 1 + .../Databases/Http/Databases/Logs/XList.php | 1 + .../Databases/Http/TablesDB/Logs/XList.php | 1 + .../Platform/Modules/Teams/Http/Logs/XList.php | 1 + src/Appwrite/Utopia/Response/Model/Log.php | 6 ++++++ 8 files changed, 28 insertions(+), 6 deletions(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index acc9c2dc9a..eb6a4be69f 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -183,7 +183,7 @@ Http::init() $user = new User([ '$id' => '', 'status' => true, - 'type' => ACTIVITY_TYPE_APP, + 'type' => ACTIVITY_TYPE_KEY_PROJECT, 'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(), 'password' => '', 'name' => $apiKey->getName(), @@ -253,7 +253,14 @@ Http::init() } } - $queueForAudits->setUser($user); + $userClone = clone $user; + $userClone->setAttribute('type', match ($apiKey->getType()) { + API_KEY_STANDARD => ACTIVITY_TYPE_KEY_PROJECT, + API_KEY_ACCOUNT => ACTIVITY_TYPE_KEY_ACCOUNT, + API_KEY_ORGANIZATION => ACTIVITY_TYPE_KEY_ORGANIZATION, + default => ACTIVITY_TYPE_USER, + }); + $queueForAudits->setUser($userClone); } // Apply permission @@ -567,7 +574,7 @@ Http::init() if (! $user->isEmpty()) { $userClone = clone $user; // $user doesn't support `type` and can cause unintended effects. - $userClone->setAttribute('type', ACTIVITY_TYPE_USER); + $userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER); $queueForAudits->setUser($userClone); } @@ -749,7 +756,8 @@ Http::shutdown() ->inject('eventProcessor') ->inject('bus') ->inject('apiKey') - ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey) use ($parseLabel) { + ->inject('mode') + ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) { $responsePayload = $response->getPayload(); @@ -851,7 +859,7 @@ Http::shutdown() if (! $user->isEmpty()) { $userClone = clone $user; // $user doesn't support `type` and can cause unintended effects. - $userClone->setAttribute('type', ACTIVITY_TYPE_USER); + $userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER); $queueForAudits->setUser($userClone); } elseif ($queueForAudits->getUser() === null || $queueForAudits->getUser()->isEmpty()) { /** diff --git a/app/init/constants.php b/app/init/constants.php index 7a484c7f4e..895b202099 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -155,9 +155,12 @@ const SESSION_PROVIDER_SERVER = 'server'; /** * Activity associated with user or the app. */ -const ACTIVITY_TYPE_APP = 'app'; const ACTIVITY_TYPE_USER = 'user'; +const ACTIVITY_TYPE_ADMIN = 'admin'; const ACTIVITY_TYPE_GUEST = 'guest'; +const ACTIVITY_TYPE_KEY_PROJECT = 'keyProject'; +const ACTIVITY_TYPE_KEY_ACCOUNT = 'keyAccount'; +const ACTIVITY_TYPE_KEY_ORGANIZATION = 'keyOrganization'; /** * MFA diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Logs/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Logs/XList.php index 2e838329cb..5d75c67462 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Logs/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Logs/XList.php @@ -129,6 +129,7 @@ class XList extends Action 'userEmail' => $log['data']['userEmail'] ?? null, 'userName' => $log['data']['userName'] ?? null, 'mode' => $log['data']['mode'] ?? null, + 'userType' => $log['data']['userType'] ?? null, 'ip' => $log['ip'], 'time' => $log['time'], 'osCode' => $os['osCode'], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Logs/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Logs/XList.php index 19b1cbbde1..cc082532f9 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Logs/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Logs/XList.php @@ -123,6 +123,7 @@ class XList extends Action 'userEmail' => $log['data']['userEmail'] ?? null, 'userName' => $log['data']['userName'] ?? null, 'mode' => $log['data']['mode'] ?? null, + 'userType' => $log['data']['userType'] ?? null, 'ip' => $log['ip'] ?? null, 'time' => $log['time'] ?? null, 'osCode' => $os['osCode'] ?? null, diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php index 0c0f5f1273..8bf40d23bd 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php @@ -110,6 +110,7 @@ class XList extends Action 'userEmail' => $log['data']['userEmail'] ?? null, 'userName' => $log['data']['userName'] ?? null, 'mode' => $log['data']['mode'] ?? null, + 'userType' => $log['data']['userType'] ?? null, 'ip' => $log['ip'], 'time' => $log['time'], 'osCode' => $os['osCode'], diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Logs/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Logs/XList.php index 6754179425..5d485b386f 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Logs/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Logs/XList.php @@ -104,6 +104,7 @@ class XList extends Action 'userEmail' => $log['data']['userEmail'] ?? null, 'userName' => $log['data']['userName'] ?? null, 'mode' => $log['data']['mode'] ?? null, + 'userType' => $log['data']['userType'] ?? null, 'ip' => $log['ip'], 'time' => $log['time'], 'osCode' => $os['osCode'], diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Logs/XList.php b/src/Appwrite/Platform/Modules/Teams/Http/Logs/XList.php index 486807d5f9..acdb6defc7 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Logs/XList.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Logs/XList.php @@ -103,6 +103,7 @@ class XList extends Action 'userEmail' => $log['data']['userEmail'] ?? null, 'userName' => $log['data']['userName'] ?? null, 'mode' => $log['data']['mode'] ?? null, + 'userType' => $log['data']['userType'] ?? null, 'ip' => $log['ip'], 'time' => $log['time'], 'osCode' => $os['osCode'], diff --git a/src/Appwrite/Utopia/Response/Model/Log.php b/src/Appwrite/Utopia/Response/Model/Log.php index bc2c923494..4fe341abe0 100644 --- a/src/Appwrite/Utopia/Response/Model/Log.php +++ b/src/Appwrite/Utopia/Response/Model/Log.php @@ -40,6 +40,12 @@ class Log extends Model 'default' => '', 'example' => 'admin', ]) + ->addRule('userType', [ + 'type' => self::TYPE_STRING, + 'description' => 'User type who triggered the audit log. Possible values: user, admin, guest, keyProject, keyAccount, keyOrganization.', + 'default' => '', + 'example' => 'user', + ]) ->addRule('ip', [ 'type' => self::TYPE_STRING, 'description' => 'IP session in use when the session was created.', From fe988f4489506424a8e01321004fcd9c6ebad953 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Thu, 19 Mar 2026 17:44:35 +0545 Subject: [PATCH 2/4] Update app/controllers/shared/api.php Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- app/controllers/shared/api.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index eb6a4be69f..94a44da016 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -258,7 +258,7 @@ Http::init() API_KEY_STANDARD => ACTIVITY_TYPE_KEY_PROJECT, API_KEY_ACCOUNT => ACTIVITY_TYPE_KEY_ACCOUNT, API_KEY_ORGANIZATION => ACTIVITY_TYPE_KEY_ORGANIZATION, - default => ACTIVITY_TYPE_USER, + default => ACTIVITY_TYPE_KEY_PROJECT, }); $queueForAudits->setUser($userClone); } From 343e352b17f60a62c3ac89fb9c48dfa16c513d8c Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 22 Mar 2026 02:31:46 +0000 Subject: [PATCH 3/4] fix: prevent overwriting user type in audit queue if already set --- app/controllers/shared/api.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 974300f4c3..622f7a8678 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -590,7 +590,9 @@ Http::init() if (! $user->isEmpty()) { $userClone = clone $user; // $user doesn't support `type` and can cause unintended effects. - $userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER); + if (empty($user->getAttribute('type'))) { + $userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER); + } $queueForAudits->setUser($userClone); } @@ -875,7 +877,9 @@ Http::shutdown() if (! $user->isEmpty()) { $userClone = clone $user; // $user doesn't support `type` and can cause unintended effects. - $userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER); + if (empty($user->getAttribute('type'))) { + $userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER); + } $queueForAudits->setUser($userClone); } elseif ($queueForAudits->getUser() === null || $queueForAudits->getUser()->isEmpty()) { /** From 2d34301834f564c821382192adcbe6c6f1913572 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Apr 2026 01:13:01 +0000 Subject: [PATCH 4/4] fix: add missing userType field to legacy log endpoints The users.php and messaging.php legacy controllers were missing the userType field in their log output, creating an inconsistency with the new audit user type distinction feature. Also adds missing mode field to users.php logs endpoint. https://claude.ai/code/session_01J9gKXwbHoLggsGwJi6KUnM --- app/controllers/api/messaging.php | 4 ++++ app/controllers/api/users.php | 2 ++ 2 files changed, 6 insertions(+) diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index 1ba5eb1119..025e1d6aa1 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -1180,6 +1180,7 @@ Http::get('/v1/messaging/providers/:providerId/logs') 'userEmail' => $log['data']['userEmail'] ?? null, 'userName' => $log['data']['userName'] ?? null, 'mode' => $log['data']['mode'] ?? null, + 'userType' => $log['data']['userType'] ?? null, 'ip' => $log['ip'], 'time' => $log['time'], 'osCode' => $os['osCode'], @@ -2585,6 +2586,7 @@ Http::get('/v1/messaging/topics/:topicId/logs') 'userEmail' => $log['data']['userEmail'] ?? null, 'userName' => $log['data']['userName'] ?? null, 'mode' => $log['data']['mode'] ?? null, + 'userType' => $log['data']['userType'] ?? null, 'ip' => $log['ip'], 'time' => $log['time'], 'osCode' => $os['osCode'], @@ -3000,6 +3002,7 @@ Http::get('/v1/messaging/subscribers/:subscriberId/logs') 'userEmail' => $log['data']['userEmail'] ?? null, 'userName' => $log['data']['userName'] ?? null, 'mode' => $log['data']['mode'] ?? null, + 'userType' => $log['data']['userType'] ?? null, 'ip' => $log['ip'], 'time' => $log['time'], 'osCode' => $os['osCode'], @@ -3813,6 +3816,7 @@ Http::get('/v1/messaging/messages/:messageId/logs') 'userEmail' => $log['data']['userEmail'] ?? null, 'userName' => $log['data']['userName'] ?? null, 'mode' => $log['data']['mode'] ?? null, + 'userType' => $log['data']['userType'] ?? null, 'ip' => $log['ip'], 'time' => $log['time'], 'osCode' => $os['osCode'], diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 3b21d4797d..f0b15b4274 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -972,6 +972,8 @@ Http::get('/v1/users/:userId/logs') 'userId' => ID::custom($log['data']['userId']), 'userEmail' => $log['data']['userEmail'] ?? null, 'userName' => $log['data']['userName'] ?? null, + 'mode' => $log['data']['mode'] ?? null, + 'userType' => $log['data']['userType'] ?? null, 'ip' => $log['ip'], 'time' => $log['time'], 'osCode' => $os['osCode'],