diff --git a/apps/meteor/client/lib/queryKeys.ts b/apps/meteor/client/lib/queryKeys.ts index 31b7930719a46..e8d60dd4a4138 100644 --- a/apps/meteor/client/lib/queryKeys.ts +++ b/apps/meteor/client/lib/queryKeys.ts @@ -120,6 +120,10 @@ export const teamsQueryKeys = { export const ABACQueryKeys = { all: ['abac'] as const, + logs: { + all: () => [...ABACQueryKeys.all, 'logs'] as const, + list: (query?: PaginatedRequest) => [...ABACQueryKeys.logs.all(), 'list', query] as const, + }, roomAttributes: { all: () => [...ABACQueryKeys.all, 'room-attributes'] as const, roomAttributesList: (query?: PaginatedRequest) => [...ABACQueryKeys.roomAttributes.all(), 'room-attributes-list', query] as const, diff --git a/apps/meteor/client/views/admin/ABAC/AdminABACLogs.tsx b/apps/meteor/client/views/admin/ABAC/AdminABACLogs.tsx new file mode 100644 index 0000000000000..f3bce44c2b0dc --- /dev/null +++ b/apps/meteor/client/views/admin/ABAC/AdminABACLogs.tsx @@ -0,0 +1,180 @@ +import type { AbacAttributeDefinitionChangeType, Serialized } 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'; + +import GenericNoResults from '../../../components/GenericNoResults'; +import { + GenericTable, + GenericTableBody, + GenericTableCell, + GenericTableHeader, + GenericTableHeaderCell, + GenericTableRow, +} from '../../../components/GenericTable'; +import { usePagination } from '../../../components/GenericTable/hooks/usePagination'; +import { useFormatDateAndTime } from '../../../hooks/useFormatDateAndTime'; +import { ABACQueryKeys } from '../../../lib/queryKeys'; +import DateRangePicker from '../moderation/helpers/DateRangePicker'; + +const AdminABACLogs = () => { + const { t } = useTranslation(); + + const [startDate, setStartDate] = useState(new Date().toISOString().split('T')[0]); + const [endDate, setEndDate] = useState(new Date().toISOString().split('T')[0]); + + const formatDate = useFormatDateAndTime(); + + const { current, itemsPerPage, setItemsPerPage, setCurrent, ...paginationProps } = usePagination(); + const getLogs = useEndpoint('GET', '/v1/abac/audit'); + const query = useMemo( + () => ({ + ...(startDate && { start: new Date(`${startDate}T00:00:00.000Z`).toISOString() }), + ...(endDate && { end: new Date(`${endDate}T23:59:59.999Z`).toISOString() }), + offset: current, + count: itemsPerPage, + }), + [current, itemsPerPage, startDate, endDate], + ); + + // Whenever the user changes the filter or the text, reset the pagination to the first page + useEffect(() => { + setCurrent(0); + }, [startDate, endDate, setCurrent]); + + const { data, isLoading } = useQuery({ + queryKey: ABACQueryKeys.logs.list(query), + queryFn: () => getLogs(query), + }); + + const getActionLabel = (action?: AbacAttributeDefinitionChangeType | null) => { + switch (action) { + case 'created': + return t('Created'); + case 'updated': + return t('Updated'); + case 'deleted': + return t('Deleted'); + case 'all-deleted': + return t('ABAC_All_Attributes_deleted'); + case 'key-removed': + return t('ABAC_Key_removed'); + case 'key-renamed': + return t('ABAC_Key_renamed'); + case 'value-removed': + return t('ABAC_Value_removed'); + case 'key-added': + return t('ABAC_Key_added'); + case 'key-updated': + return t('ABAC_Key_updated'); + 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), + }; + }; + + return ( + <> + + + setStartDate((e.target as HTMLInputElement).value)} + /> + + setEndDate((e.target as HTMLInputElement).value)} + /> + + + { + setStartDate(range.start); + setEndDate(range.end); + }} + /> + + + + {(!data || data.events?.length === 0) && !isLoading ? ( + + + + ) : ( + <> + + + {t('User')} + {t('Action')} + {t('ABAC_Element')} + {t('ABAC_Element_Name')} + {t('Timestamp')} + + + {data?.events?.map((event) => { + const eventInfo = getEventInfo(event); + return ( + + + + {eventInfo.userAvatar} + + {eventInfo.user} + + {eventInfo.action} + {eventInfo.element} + {eventInfo.name} + {formatDate(eventInfo.timestamp)} + + ); + })} + + + + + )} + + ); +}; + +export default AdminABACLogs; diff --git a/apps/meteor/client/views/admin/ABAC/AdminABACPage.tsx b/apps/meteor/client/views/admin/ABAC/AdminABACPage.tsx index eaa6a5d6d8ce9..b8b84acb77dfb 100644 --- a/apps/meteor/client/views/admin/ABAC/AdminABACPage.tsx +++ b/apps/meteor/client/views/admin/ABAC/AdminABACPage.tsx @@ -1,15 +1,16 @@ import { Box, Button, Callout } from '@rocket.chat/fuselage'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { ContextualbarDialog } from '@rocket.chat/ui-client'; import { useRouteParameter, useRouter } from '@rocket.chat/ui-contexts'; import { Trans, useTranslation } from 'react-i18next'; +import AdminABACLogs from './AdminABACLogs'; import AdminABACRoomAttributes from './AdminABACRoomAttributes'; import AdminABACSettings from './AdminABACSettings'; import AdminABACTabs from './AdminABACTabs'; import RoomAttributesContextualBar from './RoomAttributesContextualBar'; import RoomAttributesContextualBarWithData from './RoomAttributesContextualBarWithData'; import useIsABACAvailable from './hooks/useIsABACAvailable'; -import { ContextualbarDialog } from '../../../components/Contextualbar'; import { Page, PageContent, PageHeader } from '../../../components/Page'; import { useExternalLink } from '../../../hooks/useExternalLink'; import { links } from '../../../lib/links'; @@ -65,6 +66,7 @@ const AdminABACPage = ({ shouldShowWarning }: AdminABACPageProps) => { {tab === 'settings' && } {tab === 'room-attributes' && } + {tab === 'logs' && } {tab === 'room-attributes' && context !== undefined && ( diff --git a/apps/meteor/client/views/admin/ABAC/AdminABACTabs.tsx b/apps/meteor/client/views/admin/ABAC/AdminABACTabs.tsx index e4733b8be5114..88878c05af2e6 100644 --- a/apps/meteor/client/views/admin/ABAC/AdminABACTabs.tsx +++ b/apps/meteor/client/views/admin/ABAC/AdminABACTabs.tsx @@ -20,6 +20,9 @@ const AdminABACTabs = () => { handleTabClick('room-attributes')}> {t('ABAC_Room_Attributes')} + handleTabClick('logs')}> + {t('ABAC_Logs')} + ); }; diff --git a/apps/meteor/client/views/admin/ABAC/RoomAttributesContextualBar.tsx b/apps/meteor/client/views/admin/ABAC/RoomAttributesContextualBar.tsx index ace7a9ea0c022..87feaadde6de6 100644 --- a/apps/meteor/client/views/admin/ABAC/RoomAttributesContextualBar.tsx +++ b/apps/meteor/client/views/admin/ABAC/RoomAttributesContextualBar.tsx @@ -1,4 +1,5 @@ import { ContextualbarTitle } from '@rocket.chat/fuselage'; +import { ContextualbarClose, ContextualbarHeader } from '@rocket.chat/ui-client'; import { useEndpoint, useRouteParameter, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { FormProvider, useForm } from 'react-hook-form'; @@ -6,7 +7,6 @@ import { useTranslation } from 'react-i18next'; import type { AdminABACRoomAttributesFormFormData } from './AdminABACRoomAttributesForm'; import AdminABACRoomAttributesForm from './AdminABACRoomAttributesForm'; -import { ContextualbarClose, ContextualbarHeader } from '../../../components/Contextualbar'; import { ABACQueryKeys } from '../../../lib/queryKeys'; type RoomAttributesContextualBarProps = { diff --git a/apps/meteor/client/views/admin/ABAC/RoomAttributesContextualBarWithData.tsx b/apps/meteor/client/views/admin/ABAC/RoomAttributesContextualBarWithData.tsx index 8b334fdcadceb..5513e0819a4e0 100644 --- a/apps/meteor/client/views/admin/ABAC/RoomAttributesContextualBarWithData.tsx +++ b/apps/meteor/client/views/admin/ABAC/RoomAttributesContextualBarWithData.tsx @@ -1,8 +1,8 @@ +import { ContextualbarSkeletonBody } from '@rocket.chat/ui-client'; import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import RoomAttributesContextualBar from './RoomAttributesContextualBar'; -import { ContextualbarSkeletonBody } from '../../../components/Contextualbar'; import { ABACQueryKeys } from '../../../lib/queryKeys'; type RoomAttributesContextualBarWithDataProps = { diff --git a/apps/meteor/ee/server/api/abac/index.ts b/apps/meteor/ee/server/api/abac/index.ts index 37c0dc13d5363..0ed503678f527 100644 --- a/apps/meteor/ee/server/api/abac/index.ts +++ b/apps/meteor/ee/server/api/abac/index.ts @@ -1,6 +1,6 @@ import { Abac } from '@rocket.chat/core-services'; import type { AbacActor } from '@rocket.chat/core-services'; -import type { IUser } from '@rocket.chat/core-typings'; +import type { IServerEvents, IUser } from '@rocket.chat/core-typings'; import { ServerEvents, Users } from '@rocket.chat/models'; import { validateUnauthorizedErrorResponse } from '@rocket.chat/rest-typings/src/v1/Ajv'; import { convertSubObjectsIntoPaths } from '@rocket.chat/tools'; @@ -400,7 +400,12 @@ const abacEndpoints = API.v1 const [events, total] = await Promise.all([cursor.toArray(), totalCount]); return API.v1.success({ - events, + events: events as ( + | IServerEvents['abac.action.performed'] + | IServerEvents['abac.attribute.changed'] + | IServerEvents['abac.object.attribute.changed'] + | IServerEvents['abac.object.attributes.removed'] + )[], count: events.length, offset, total, diff --git a/apps/meteor/ee/server/api/abac/schemas.ts b/apps/meteor/ee/server/api/abac/schemas.ts index 3e4b76774567c..856c1c9ab1353 100644 --- a/apps/meteor/ee/server/api/abac/schemas.ts +++ b/apps/meteor/ee/server/api/abac/schemas.ts @@ -1,4 +1,4 @@ -import type { IAbacAttribute, IAbacAttributeDefinition, IAuditServerActor, IRoom, IServerEvent } from '@rocket.chat/core-typings'; +import type { IAbacAttribute, IAbacAttributeDefinition, IAuditServerActor, IRoom, IServerEvents } from '@rocket.chat/core-typings'; import type { PaginatedResult, PaginatedRequest } from '@rocket.chat/rest-typings'; import { ajv } from '@rocket.chat/rest-typings'; @@ -226,7 +226,12 @@ const GetAbacAuditEventsResponseSchemaObject = { }; export const GETAbacAuditEventsResponseSchema = ajv.compile<{ - events: IServerEvent[]; + events: ( + | IServerEvents['abac.action.performed'] + | IServerEvents['abac.attribute.changed'] + | IServerEvents['abac.object.attribute.changed'] + | IServerEvents['abac.object.attributes.removed'] + )[]; count: number; offset: number; total: number; diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 347f0c365d679..0d9413b1d39c9 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -25,7 +25,10 @@ "ABAC_Warning_Modal_Content": "You will not be able to automatically or manually manage users in existing ABAC-managed rooms. To restore a room's default access control, it must be removed from ABAC management in <1>ABAC > Rooms.", "ABAC_ShowAttributesInRooms": "Show ABAC attributes in rooms", "ABAC_ShowAttributesInRooms_Description": "Display the ABAC attributes assigned to the room in the contextual bar", - "ABAC_Room_Attributes": "Room Attributes", + "ABAC_Room": "Room", + "ABAC_Room_Attribute": "Room Attribute", + "ABAC_Element": "ABAC Element", + "ABAC_Element_Name": "Element Name", "ABAC_Cannot_delete_attribute": "Cannot delete attribute assigned to rooms", "ABAC_Cannot_delete_attribute_content": "Unassign {{attributeName}} from all rooms before attempting to delete it.", "ABAC_Delete_room_attribute": "Delete attribute", @@ -37,10 +40,22 @@ "ABAC_Edit_attribute": "Edit attribute", "ABAC_Edit_attribute_description": "Attribute values cannot be edited, but can be added or deleted.", "ABAC_New_attribute_description": "Create an attribute that can later be assigned to rooms.", + "ABAC_No_logs": "No logs", + "ABAC_No_logs_description": "ABAC-management related activity will appear here.", "ABAC_Search_attributes": "Search attributes", "ABAC_Remove_attribute": "Delete attribute value", "abac-management": "Manage ABAC configuration", "abac_removed_user_from_the_room": "was removed by ABAC", + "ABAC_No_attributes": "No Attributes", + "ABAC_No_attributes_description": "Create custom characteristics to determine who can access a room.", + "ABAC_No_rooms": "No rooms", + "ABAC_No_rooms_description": "ABAC-managed rooms will appear here.", + "ABAC_All_Attributes_deleted": "All attributes deleted", + "ABAC_Key_removed": "Key removed", + "ABAC_Key_renamed": "Key renamed", + "ABAC_Value_removed": "Value removed", + "ABAC_Key_added": "Key added", + "ABAC_Key_updated": "Key updated", "AI_Actions": "AI actions", "API": "API", "API_Add_Personal_Access_Token": "Add new Personal Access Token", @@ -110,6 +125,7 @@ "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", + "ABAC_Logs": "Logs", "Accept": "Accept", "Accept_Call": "Accept Call", "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Accept incoming omnichannel requests even if there are no online agents", @@ -5479,6 +5495,7 @@ "Update_version": "Update version", "Update_your_RocketChat": "Update your Rocket.Chat", "Updated_at": "Updated at", + "Updated": "Updated", "Upgrade": "Upgrade", "UpgradeToGetMore_Headline": "Upgrade to get more", "UpgradeToGetMore_Subtitle": "Supercharge your workspace with advanced capabilities.",