diff --git a/apps/meteor/client/views/admin/ABAC/ABACLogsTab/LogsPage.tsx b/apps/meteor/client/views/admin/ABAC/ABACLogsTab/LogsPage.tsx index 609d48cdc8b96..f4df9d2e10abb 100644 --- a/apps/meteor/client/views/admin/ABAC/ABACLogsTab/LogsPage.tsx +++ b/apps/meteor/client/views/admin/ABAC/ABACLogsTab/LogsPage.tsx @@ -1,10 +1,8 @@ -import type { AbacAttributeDefinitionChangeType, Serialized } from '@rocket.chat/core-typings'; +import type { AbacAttributeDefinitionChangeType, AbacActionPerformed } from '@rocket.chat/core-typings'; import { Box, InputBox, Margins, Pagination } from '@rocket.chat/fuselage'; -import type { OperationResult } from '@rocket.chat/rest-typings'; import { UserAvatar } from '@rocket.chat/ui-avatar'; import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; -import type { ReactNode } from 'react'; import { useMemo, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -47,12 +45,7 @@ const LogsPage = () => { setCurrent(0); }, [startDate, endDate, setCurrent]); - const { data, isLoading } = useQuery({ - queryKey: ABACQueryKeys.logs.list(query), - queryFn: () => getLogs(query), - }); - - const getActionLabel = (action?: AbacAttributeDefinitionChangeType | null) => { + const getActionLabel = (action?: AbacAttributeDefinitionChangeType | AbacActionPerformed | null) => { switch (action) { case 'created': return t('Created'); @@ -72,33 +65,64 @@ const LogsPage = () => { return t('ABAC_Key_added'); case 'key-updated': return t('ABAC_Key_updated'); + case 'revoked-object-access': + return t('ABAC_Revoked_Object_Access'); + case 'granted-object-access': + return t('ABAC_Granted_Object_Access'); default: return ''; } }; - const getEventInfo = ( - event: Serialized>['events'][number], - ): { element: string; userAvatar: ReactNode; user: string; name: string; action: string; timestamp: Date } => { - if (event.t === 'abac.attribute.changed') { - return { - element: t('ABAC_Room_Attribute'), - userAvatar: event.actor?.type === 'user' ? : null, - user: event.actor?.type === 'user' ? event.actor.username : t('System'), - name: event.data?.find((item) => item.key === 'attributeKey')?.value ?? '', - action: getActionLabel(event.data?.find((item) => item.key === 'change')?.value), - timestamp: new Date(event.ts), - }; - } - return { - element: t('ABAC_Room'), - userAvatar: event.actor?.type === 'user' ? : null, - user: event.actor?.type === 'user' ? event.actor.username : t('System'), - name: event.data?.find((item) => item.key === 'room')?.value?.name ?? '', - action: getActionLabel(event.data?.find((item) => item.key === 'change')?.value), - timestamp: new Date(event.ts), - }; - }; + const { data, isLoading } = useQuery({ + queryKey: ABACQueryKeys.logs.list(query), + queryFn: () => getLogs(query), + select: (data) => ({ + events: data.events.map((event) => { + const eventInfo = { + id: event._id, + user: event.actor?.type === 'user' ? event.actor.username : t('System'), + ...(event.actor?.type === 'user' && { userAvatar: }), + timestamp: new Date(event.ts), + element: t('ABAC_Room'), + action: getActionLabel(event.data?.find((item) => item.key === 'change')?.value), + room: undefined, + }; + switch (event.t) { + case 'abac.attribute.changed': + return { + ...eventInfo, + element: t('ABAC_Room_Attribute'), + name: event.data?.find((item) => item.key === 'attributeKey')?.value ?? '', + }; + case 'abac.action.performed': + return { + ...eventInfo, + name: event.data?.find((item) => item.key === 'subject')?.value?.username ?? '', + action: getActionLabel(event.data?.find((item) => item.key === 'action')?.value), + room: event.data?.find((item) => item.key === 'object')?.value?.name ?? '', + element: t('ABAC_room_membership'), + }; + case 'abac.object.attribute.changed': + case 'abac.object.attributes.removed': + return { + ...eventInfo, + name: + event.data + ?.find((item) => item.key === 'current') + ?.value?.map((item) => item.key) + .join(', ') ?? t('Empty'), + room: event.data?.find((item) => item.key === 'room')?.value?.name ?? '', + }; + default: + return null; + } + }), + count: data.count, + offset: data.offset, + total: data.total, + }), + }); return ( <> @@ -139,24 +163,32 @@ const LogsPage = () => { {t('User')} {t('Action')} + {t('Room')} {t('ABAC_Element')} {t('ABAC_Element_Name')} {t('Timestamp')} - {data?.events?.map((event) => { - const eventInfo = getEventInfo(event); + {data?.events.map((eventInfo) => { + if (!eventInfo) { + return null; + } return ( - + - - {eventInfo.userAvatar} - + {eventInfo.userAvatar && ( + + {eventInfo.userAvatar} + + )} {eventInfo.user} {eventInfo.action} + {eventInfo.room} {eventInfo.element} - {eventInfo.name} + + {eventInfo.name} + {formatDate(eventInfo.timestamp)} ); diff --git a/ee/packages/abac/src/index.ts b/ee/packages/abac/src/index.ts index 0b58931ba8796..ffa8297cf05e7 100644 --- a/ee/packages/abac/src/index.ts +++ b/ee/packages/abac/src/index.ts @@ -488,6 +488,7 @@ export class AbacService extends ServiceClass implements IAbacService { } usernames.forEach((username) => { + // TODO: Add room name void Audit.actionPerformed({ username }, { _id: objectId }, 'system', 'granted-object-access'); }); } @@ -609,7 +610,7 @@ export class AbacService extends ServiceClass implements IAbacService { skipAppPreEvents: true, customSystemMessage: 'abac-removed-user-from-room' as const, }) - .then(() => void Audit.actionPerformed({ _id: user._id, username: user.username }, { _id: room._id }, reason)) + .then(() => void Audit.actionPerformed({ _id: user._id, username: user.username }, { _id: room._id, name: room.name }, reason)) .catch((err) => { this.logger.error({ msg: 'Failed to remove user from ABAC room', diff --git a/packages/core-typings/src/ServerAudit/IAuditServerAbacAction.ts b/packages/core-typings/src/ServerAudit/IAuditServerAbacAction.ts index f080168446c9f..d7f5bb8e02ecc 100644 --- a/packages/core-typings/src/ServerAudit/IAuditServerAbacAction.ts +++ b/packages/core-typings/src/ServerAudit/IAuditServerAbacAction.ts @@ -64,13 +64,23 @@ interface IServerEventAbacActionPerformed t: 'abac.action.performed'; } +interface IServerEventAbacObjectAttributesRemoved + extends IAuditServerEventType< + | { key: 'room'; value: MinimalRoom } + | { key: 'reason'; value: AbacAuditReason } + | { key: 'previous'; value: IAbacAttributeDefinition[] } + | { key: 'current'; value: IAbacAttributeDefinition[] | null } + | { key: 'change'; value: AbacAttributeDefinitionChangeType } + > { + t: 'abac.object.attributes.removed'; +} declare module '../IServerEvent' { interface IServerEvents { 'abac.subject.attribute.changed': IServerEventAbacSubjectAttributeChanged; 'abac.object.attribute.changed': IServerEventAbacObjectAttributeChanged; 'abac.attribute.changed': IServerEventAbacAttributeChanged; 'abac.action.performed': IServerEventAbacActionPerformed; - 'abac.object.attributes.removed': IServerEventAbacObjectAttributeChanged; + 'abac.object.attributes.removed': IServerEventAbacObjectAttributesRemoved; } } diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 78586f4751c82..5608963e542dd 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -75,6 +75,9 @@ "ABAC_Value_removed": "Value removed", "ABAC_Key_added": "Key added", "ABAC_Key_updated": "Key updated", + "ABAC_Revoked_Object_Access": "Automatic removal", + "ABAC_Granted_Object_Access": "Manual add approved", + "ABAC_room_membership": "Room membership", "ABAC_Managed": "ABAC Managed", "ABAC_Managed_description": "Only compliant users have access to attribute-based access controlled rooms. Attributes determine room access.", "ABAC_Room_Attributes": "Room Attributes",