Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 70 additions & 38 deletions apps/meteor/client/views/admin/ABAC/ABACLogsTab/LogsPage.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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');
Expand All @@ -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<OperationResult<'GET', '/v1/abac/audit'>>['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' ? <UserAvatar size='x28' userId={event.actor._id} /> : 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' ? <UserAvatar size='x28' userId={event.actor._id} /> : 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: <UserAvatar size='x28' userId={event.actor._id} /> }),
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 (
<>
Expand Down Expand Up @@ -139,24 +163,32 @@ const LogsPage = () => {
<GenericTableHeader>
<GenericTableHeaderCell>{t('User')}</GenericTableHeaderCell>
<GenericTableHeaderCell>{t('Action')}</GenericTableHeaderCell>
<GenericTableHeaderCell>{t('Room')}</GenericTableHeaderCell>
<GenericTableHeaderCell>{t('ABAC_Element')}</GenericTableHeaderCell>
<GenericTableHeaderCell>{t('ABAC_Element_Name')}</GenericTableHeaderCell>
<GenericTableHeaderCell>{t('Timestamp')}</GenericTableHeaderCell>
</GenericTableHeader>
<GenericTableBody>
{data?.events?.map((event) => {
const eventInfo = getEventInfo(event);
{data?.events.map((eventInfo) => {
if (!eventInfo) {
return null;
}
return (
<GenericTableRow key={event._id}>
<GenericTableRow key={eventInfo.id}>
<GenericTableCell withTruncatedText>
<Box is='span' mie={4}>
{eventInfo.userAvatar}
</Box>
{eventInfo.userAvatar && (
<Box is='span' mie={4}>
{eventInfo.userAvatar}
</Box>
)}
{eventInfo.user}
</GenericTableCell>
<GenericTableCell withTruncatedText>{eventInfo.action}</GenericTableCell>
<GenericTableCell withTruncatedText>{eventInfo.room}</GenericTableCell>
<GenericTableCell withTruncatedText>{eventInfo.element}</GenericTableCell>
<GenericTableCell withTruncatedText>{eventInfo.name}</GenericTableCell>
<GenericTableCell withTruncatedText title={eventInfo.name}>
{eventInfo.name}
</GenericTableCell>
<GenericTableCell withTruncatedText>{formatDate(eventInfo.timestamp)}</GenericTableCell>
</GenericTableRow>
);
Expand Down
3 changes: 2 additions & 1 deletion ee/packages/abac/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
}
Expand Down Expand Up @@ -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',
Expand Down
12 changes: 11 additions & 1 deletion packages/core-typings/src/ServerAudit/IAuditServerAbacAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down
3 changes: 3 additions & 0 deletions packages/i18n/src/locales/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading