Skip to content

Commit

Permalink
feat(audit): New endpoint for listing members of any room (#32916)
Browse files Browse the repository at this point in the history
  • Loading branch information
KevLehman authored Aug 12, 2024
1 parent 08ac2f3 commit 127866c
Show file tree
Hide file tree
Showing 9 changed files with 425 additions and 11 deletions.
6 changes: 6 additions & 0 deletions .changeset/funny-boats-guess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/i18n": minor
---

Added a new Audit endpoint `audit/rooms.members` that allows users with `view-members-list-all-rooms` to fetch a list of the members of any room even if the user is not part of it.
16 changes: 10 additions & 6 deletions apps/meteor/client/views/audit/components/AuditFiltersDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,24 @@ import { useFormatDate } from '../../../hooks/useFormatDate';
type AuditFiltersDisplayProps = {
users?: IUser['username'][];
room?: IRoom['name'];
startDate: Date;
endDate: Date;
startDate?: Date;
endDate?: Date;
filters?: string;
};

const AuditFiltersDisplay = ({ users, room, startDate, endDate }: AuditFiltersDisplayProps): ReactElement => {
const AuditFiltersDisplay = ({ users, room, startDate, endDate, filters }: AuditFiltersDisplayProps): ReactElement => {
const formatDate = useFormatDate();
const t = useTranslation();

return (
<Box display='flex' flexDirection='column' alignItems='stretch' withTruncatedText>
<Box withTruncatedText>{users?.length ? users.map((user) => `@${user}`).join(' : ') : `#${room}`}</Box>
<Box withTruncatedText>
{formatDate(startDate)} {t('Date_to')} {formatDate(endDate)} {/* TODO: fix this translation */}
</Box>
{startDate && endDate ? (
<Box withTruncatedText>
{formatDate(startDate)} {t('Date_to')} {formatDate(endDate)} {/* TODO: fix this translation */}
</Box>
) : null}
{filters ? <Box withTruncatedText>{filters}</Box> : null}
</Box>
);
};
Expand Down
8 changes: 5 additions & 3 deletions apps/meteor/client/views/audit/components/AuditLogEntry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { IAuditLog } from '@rocket.chat/core-typings';
import { Box } from '@rocket.chat/fuselage';
import { useMediaQuery } from '@rocket.chat/fuselage-hooks';
import { UserAvatar } from '@rocket.chat/ui-avatar';
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import React, { memo, useMemo } from 'react';

Expand All @@ -13,10 +14,11 @@ type AuditLogEntryProps = { value: IAuditLog };

const AuditLogEntry = ({ value: { u, results, ts, _id, fields } }: AuditLogEntryProps): ReactElement => {
const formatDateAndTime = useFormatDateAndTime();
const t = useTranslation();

const { username, name, avatarETag } = u;

const { msg, users, room, startDate, endDate } = fields;
const { msg, users, room, startDate, endDate, type, filters } = fields;

const when = useMemo(() => formatDateAndTime(ts), [formatDateAndTime, ts]);

Expand All @@ -43,12 +45,12 @@ const AuditLogEntry = ({ value: { u, results, ts, _id, fields } }: AuditLogEntry
</Box>
</GenericTableCell>
<GenericTableCell fontScale='p2m' color='hint' withTruncatedText>
{msg}
{type === 'room_member_list' ? t('Room_members_list') : msg}
</GenericTableCell>
<GenericTableCell withTruncatedText>{when}</GenericTableCell>
<GenericTableCell withTruncatedText>{results}</GenericTableCell>
<GenericTableCell fontScale='p2' color='hint' withTruncatedText>
<AuditFiltersDisplay users={users} room={room} startDate={startDate} endDate={endDate} />
<AuditFiltersDisplay users={users} room={room} startDate={startDate} endDate={endDate} filters={filters} />
</GenericTableCell>
</GenericTableRow>
);
Expand Down
95 changes: 95 additions & 0 deletions apps/meteor/ee/server/api/audit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import type { IUser, IRoom } from '@rocket.chat/core-typings';
import { Rooms, AuditLog } from '@rocket.chat/models';
import type { PaginatedRequest, PaginatedResult } from '@rocket.chat/rest-typings';
import Ajv from 'ajv';

import { API } from '../../../app/api/server/api';
import { getPaginationItems } from '../../../app/api/server/helpers/getPaginationItems';
import { findUsersOfRoom } from '../../../server/lib/findUsersOfRoom';

const ajv = new Ajv({
coerceTypes: true,
});

type AuditRoomMembersParams = PaginatedRequest<{
roomId: string;
filter: string;
}>;

const auditRoomMembersSchema = {
type: 'object',
properties: {
roomId: { type: 'string', minLength: 1 },
filter: { type: 'string' },
count: { type: 'number' },
offset: { type: 'number' },
sort: { type: 'string' },
},
required: ['roomId'],
additionalProperties: false,
};

export const isAuditRoomMembersProps = ajv.compile<AuditRoomMembersParams>(auditRoomMembersSchema);

declare module '@rocket.chat/rest-typings' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface Endpoints {
'/v1/audit/rooms.members': {
GET: (
params: AuditRoomMembersParams,
) => PaginatedResult<{ members: Pick<IUser, '_id' | 'name' | 'username' | 'status' | '_updatedAt'>[] }>;
};
}
}

API.v1.addRoute(
'audit/rooms.members',
{ authRequired: true, permissionsRequired: ['view-members-list-all-rooms'], validateParams: isAuditRoomMembersProps },
{
async get() {
const { roomId, filter } = this.queryParams;
const { count: limit, offset: skip } = await getPaginationItems(this.queryParams);
const { sort } = await this.parseJsonQuery();

const room = await Rooms.findOneById<Pick<IRoom, '_id' | 'name' | 'fname'>>(roomId, { projection: { _id: 1, name: 1, fname: 1 } });
if (!room) {
return API.v1.notFound();
}

const { cursor, totalCount } = findUsersOfRoom({
rid: room._id,
filter,
skip,
limit,
...(sort?.username && { sort: { username: sort.username } }),
});

const [members, total] = await Promise.all([cursor.toArray(), totalCount]);

await AuditLog.insertOne({
ts: new Date(),
results: total,
u: {
_id: this.user._id,
username: this.user.username,
name: this.user.name,
avatarETag: this.user.avatarETag,
},
fields: {
msg: 'Room_members_list',
rids: [room._id],
type: 'room_member_list',
room: room.name || room.fname,
filters: filter,
},
});

return API.v1.success({
members,
count: members.length,
offset: skip,
total,
});
},
},
);
1 change: 1 addition & 0 deletions apps/meteor/ee/server/lib/audit/startup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const createPermissions = async () => {
const permissions = [
{ _id: 'can-audit', roles: ['admin', 'auditor'] },
{ _id: 'can-audit-log', roles: ['admin', 'auditor-log'] },
{ _id: 'view-members-list-all-rooms', roles: ['admin', 'auditor'] },
];

const defaultRoles = [
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/ee/server/startup/audit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createPermissions } from '../lib/audit/startup';

await License.onLicense('auditing', async () => {
await import('../lib/audit/methods');
await import('../api/audit');

await createPermissions();
});
Loading

0 comments on commit 127866c

Please sign in to comment.