Files
upstream-mirror 30810eecd7
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

505 lines
14 KiB
TypeScript

import type { IAuditLog, IMessage, IUser, IRoom } from '@rocket.chat/core-typings';
import { Rooms, AuditLog, ServerEvents } from '@rocket.chat/models';
import {
isServerEventsAuditSettingsProps,
ajv,
ajvQuery,
validateBadRequestErrorResponse,
validateUnauthorizedErrorResponse,
validateForbiddenErrorResponse,
validateNotFoundErrorResponse,
} from '@rocket.chat/rest-typings';
import type { PaginatedRequest, PaginatedResult } from '@rocket.chat/rest-typings';
import { convertSubObjectsIntoPaths } from '@rocket.chat/tools';
import { API } from '../../../server/api/api';
import { getPaginationItems } from '../../../server/api/lib/getPaginationItems';
import { findUsersOfRoom } from '../../../server/lib/findUsersOfRoom';
import { getUsersHiddenFrom, redactHiddenUsers } from '../../../server/lib/statusVisibility/hiddenUsers';
import { auditGetAuditionsMethod, auditGetMessagesMethod, auditGetOmnichannelMessagesMethod } from '../lib/audit/functions';
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 = ajvQuery.compile<AuditRoomMembersParams>(auditRoomMembersSchema);
type AuditAuditionsParams = { startDate: string; endDate: string };
const auditAuditionsSchema = {
type: 'object',
properties: {
startDate: { type: 'string', minLength: 1 },
endDate: { type: 'string', minLength: 1 },
},
required: ['startDate', 'endDate'],
additionalProperties: false,
};
const isAuditAuditionsProps = ajvQuery.compile<AuditAuditionsParams>(auditAuditionsSchema);
type AuditMessagesPayload = {
rid?: string;
startDate: string;
endDate: string;
users: string[];
msg: string;
type: string;
visitor?: string;
agent?: string;
};
const auditMessagesSchema = {
type: 'object',
properties: {
rid: { type: 'string' },
startDate: { type: 'string', minLength: 1 },
endDate: { type: 'string', minLength: 1 },
users: { type: 'array', items: { type: 'string' } },
msg: { type: ['string', 'null'] },
type: { type: 'string' },
visitor: { type: ['string', 'null'] },
agent: { type: ['string', 'null'] },
},
required: ['startDate', 'endDate', 'users', 'msg', 'type'],
additionalProperties: false,
};
const isAuditMessagesProps = ajv.compile<AuditMessagesPayload>(auditMessagesSchema);
type AuditOmnichannelMessagesPayload = {
startDate: string;
endDate: string;
users: string[];
msg: string;
type: string;
visitor?: string;
agent?: string;
};
const auditOmnichannelMessagesSchema = {
type: 'object',
properties: {
startDate: { type: 'string', minLength: 1 },
endDate: { type: 'string', minLength: 1 },
users: { type: 'array', items: { type: 'string' } },
msg: { type: ['string', 'null'] },
type: { type: 'string' },
visitor: { type: ['string', 'null'] },
agent: { type: ['string', 'null'] },
},
required: ['startDate', 'endDate', 'users', 'msg', 'type'],
additionalProperties: false,
};
const isAuditOmnichannelMessagesProps = ajv.compile<AuditOmnichannelMessagesPayload>(auditOmnichannelMessagesSchema);
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'>[] }>;
};
'/v1/audit.auditions': {
GET: (params: AuditAuditionsParams) => { auditions: IAuditLog[] };
};
'/v1/audit.messages': {
POST: (params: AuditMessagesPayload) => { messages: IMessage[] };
};
'/v1/audit.omnichannelMessages': {
POST: (params: AuditOmnichannelMessagesPayload) => { messages: IMessage[] };
};
}
}
const auditRoomMembersResponseSchema = ajv.compile<
PaginatedResult<{ members: Pick<IUser, '_id' | 'name' | 'username' | 'status' | '_updatedAt'>[] }>
>({
type: 'object',
properties: {
members: {
type: 'array',
items: {
type: 'object',
properties: {
_id: { type: 'string' },
name: { type: 'string' },
username: { type: 'string' },
nickname: { type: 'string' },
status: { type: 'string', enum: ['online', 'away', 'offline', 'busy', 'disabled'] },
avatarETag: { type: 'string' },
federated: { type: 'boolean' },
_updatedAt: { type: 'string', format: 'date-time' },
},
required: ['_id'],
additionalProperties: false,
},
},
count: { type: 'number' },
offset: { type: 'number' },
total: { type: 'number' },
success: { type: 'boolean', enum: [true] },
},
required: ['members', 'count', 'offset', 'total', 'success'],
additionalProperties: false,
});
API.v1.get(
'audit/rooms.members',
{
authRequired: true,
permissionsRequired: ['view-members-list-all-rooms'],
query: isAuditRoomMembersProps,
license: ['auditing'],
response: {
200: auditRoomMembersResponseSchema,
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
403: validateForbiddenErrorResponse,
404: validateNotFoundErrorResponse,
},
},
async function action() {
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 hidden = await getUsersHiddenFrom(this.userId);
const { cursor, totalCount } = await findUsersOfRoom({
rid: room._id,
hidden,
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,
...(this.user.avatarETag && { 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: redactHiddenUsers(members, hidden),
count: members.length,
offset: skip,
total,
});
},
);
API.v1.get(
'audit.settings',
{
response: {
200: ajv.compile({
additionalProperties: false,
type: 'object',
properties: {
events: {
type: 'array',
items: {
type: 'object',
},
},
count: {
type: 'number',
description: 'The number of events returned in this response.',
},
offset: {
type: 'number',
description: 'The number of events that were skipped in this response.',
},
total: {
type: 'number',
description: 'The total number of events that match the query.',
},
success: {
type: 'boolean',
description: 'Indicates if the request was successful.',
},
},
required: ['events', 'count', 'offset', 'total', 'success'],
}),
400: ajv.compile({
type: 'object',
properties: {
success: {
type: 'boolean',
enum: [false],
},
error: {
type: 'string',
},
errorType: {
type: 'string',
},
},
required: ['success', 'error'],
}),
},
query: isServerEventsAuditSettingsProps,
authRequired: true,
permissionsRequired: ['can-audit'],
license: ['auditing'],
},
async function action() {
const { start, end, settingId, actor } = this.queryParams;
if (start && isNaN(Date.parse(start))) {
return API.v1.failure('The "start" query parameter must be a valid date.');
}
if (end && isNaN(Date.parse(end))) {
return API.v1.failure('The "end" query parameter must be a valid date.');
}
const { offset, count } = await getPaginationItems(this.queryParams);
const { sort } = await this.parseJsonQuery();
const _sort = { ts: sort?.ts ? sort?.ts : -1 };
const { cursor, totalCount } = ServerEvents.findPaginated(
{
...(settingId && { 'data.key': 'id', 'data.value': settingId }),
...(actor && convertSubObjectsIntoPaths({ actor })),
ts: {
$gte: start ? new Date(start) : new Date(0),
$lte: end ? new Date(end) : new Date(),
},
t: 'settings.changed',
},
{
sort: _sort,
skip: offset,
limit: count,
allowDiskUse: true,
},
);
const [events, total] = await Promise.all([cursor.toArray(), totalCount]);
return API.v1.success({
events,
count: events.length,
offset,
total,
});
},
);
const auditAuditionsResponseSchema = ajv.compile<{ auditions: IAuditLog[] }>({
type: 'object',
properties: {
auditions: {
type: 'array',
items: {
type: 'object',
properties: {
_id: { type: 'string' },
_updatedAt: { type: 'string', format: 'date-time' },
ts: { type: 'string', format: 'date-time' },
results: { type: 'number' },
u: {
type: 'object',
properties: {
_id: { type: 'string' },
username: { type: 'string' },
name: { type: 'string' },
avatarETag: { type: 'string' },
},
required: ['_id'],
additionalProperties: false,
},
fields: {
type: 'object',
properties: {
type: { type: 'string' },
msg: { type: ['string', 'null'] },
startDate: { type: ['string', 'null'], format: 'date-time' },
endDate: { type: ['string', 'null'], format: 'date-time' },
rids: { type: ['array', 'null'], items: { type: 'string' } },
room: { type: ['string', 'null'] },
users: { type: ['array', 'null'], items: { type: 'string' } },
visitor: { type: ['string', 'null'] },
agent: { type: ['string', 'null'] },
filters: { type: ['string', 'null'] },
},
required: ['type'],
additionalProperties: false,
},
},
required: ['_id', 'ts', 'results', 'u', 'fields'],
additionalProperties: false,
},
},
success: { type: 'boolean', enum: [true] },
},
required: ['auditions', 'success'],
additionalProperties: false,
});
// `messages` items are full IMessage documents. A strict `$ref` to IMessage fails response
// validation because its attachment union is a `oneOf` whose branches overlap on file/quote
// attachments (AJV `passingSchemas: 0,1`), so real audit results with attachments would 400.
// Kept relaxed until the IMessage attachment schema gains discriminators.
const auditMessagesResponseSchema = ajv.compile<{ messages: IMessage[] }>({
type: 'object',
properties: {
messages: { type: 'array', items: { type: 'object' } },
success: { type: 'boolean', enum: [true] },
},
required: ['messages', 'success'],
additionalProperties: false,
});
const auditErrorResponseSchema = ajv.compile({
type: 'object',
properties: {
success: { type: 'boolean', enum: [false] },
error: { type: 'string' },
errorType: { type: 'string' },
},
required: ['success', 'error'],
});
const parseDateOrFail = (value: string, name: string): Date => {
const ts = Date.parse(value);
if (Number.isNaN(ts)) {
throw new Error(`The "${name}" parameter must be a valid date.`);
}
return new Date(ts);
};
API.v1.get(
'audit.auditions',
{
authRequired: true,
permissionsRequired: ['can-audit-log'],
query: isAuditAuditionsProps,
license: ['auditing'],
rateLimiterOptions: { numRequestsAllowed: 10, intervalTimeInMS: 60000 },
response: {
200: auditAuditionsResponseSchema,
400: auditErrorResponseSchema,
401: validateUnauthorizedErrorResponse,
403: validateForbiddenErrorResponse,
},
},
async function action() {
const startDate = parseDateOrFail(this.queryParams.startDate, 'startDate');
const endDate = parseDateOrFail(this.queryParams.endDate, 'endDate');
const auditions = await auditGetAuditionsMethod(this.userId, startDate, endDate);
return API.v1.success({ auditions });
},
);
API.v1.post(
'audit.messages',
{
authRequired: true,
permissionsRequired: ['can-audit'],
body: isAuditMessagesProps,
license: ['auditing'],
rateLimiterOptions: { numRequestsAllowed: 10, intervalTimeInMS: 60000 },
response: {
200: auditMessagesResponseSchema,
400: auditErrorResponseSchema,
401: validateUnauthorizedErrorResponse,
403: validateForbiddenErrorResponse,
},
},
async function action() {
const { rid, users, msg, type, visitor, agent } = this.bodyParams;
const startDate = parseDateOrFail(this.bodyParams.startDate, 'startDate');
const endDate = parseDateOrFail(this.bodyParams.endDate, 'endDate');
const messages = await auditGetMessagesMethod(this.userId, {
rid,
startDate,
endDate,
users,
msg,
type,
visitor,
agent,
});
return API.v1.success({ messages });
},
);
API.v1.post(
'audit.omnichannelMessages',
{
authRequired: true,
permissionsRequired: ['can-audit'],
body: isAuditOmnichannelMessagesProps,
license: ['auditing'],
rateLimiterOptions: { numRequestsAllowed: 10, intervalTimeInMS: 60000 },
response: {
200: auditMessagesResponseSchema,
400: auditErrorResponseSchema,
401: validateUnauthorizedErrorResponse,
403: validateForbiddenErrorResponse,
},
},
async function action() {
const { users, msg, type, visitor, agent } = this.bodyParams;
const startDate = parseDateOrFail(this.bodyParams.startDate, 'startDate');
const endDate = parseDateOrFail(this.bodyParams.endDate, 'endDate');
const messages = await auditGetOmnichannelMessagesMethod(this.userId, {
startDate,
endDate,
users,
msg,
type,
visitor,
agent,
});
return API.v1.success({ messages });
},
);