snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
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 1s
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 1s
This commit is contained in:
235
apps/meteor/ee/server/lib/audit/functions.ts
Normal file
235
apps/meteor/ee/server/lib/audit/functions.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
import type { ILivechatAgent, ILivechatVisitor, IMessage, IRoom, IUser, IAuditLog } from '@rocket.chat/core-typings';
|
||||
import { LivechatRooms, Messages, Rooms, Users, AuditLog } from '@rocket.chat/models';
|
||||
import { escapeRegExp, isTruthy } from '@rocket.chat/tools';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import type { Filter } from 'mongodb';
|
||||
|
||||
import { hasPermissionAsync } from '../../../../server/lib/authorization/hasPermission';
|
||||
import { callbacks } from '../../../../server/lib/callbacks';
|
||||
import { i18n } from '../../../../server/lib/i18n';
|
||||
import { updateCounter } from '../../../../server/lib/statistics';
|
||||
|
||||
const getValue = (room: IRoom | null) => room && { rids: [room._id], name: room.name };
|
||||
|
||||
const getUsersIdFromUserName = async (usernames: IUser['username'][]) => {
|
||||
const users = usernames ? await Users.findByUsernames(usernames.filter(isTruthy)).toArray() : undefined;
|
||||
|
||||
return users?.filter(isTruthy).map((userId) => userId._id);
|
||||
};
|
||||
|
||||
const getRoomInfoByAuditParams = async ({
|
||||
type,
|
||||
roomId: rid,
|
||||
users: usernames,
|
||||
visitor,
|
||||
agent,
|
||||
userId,
|
||||
}: {
|
||||
type: string;
|
||||
roomId: IRoom['_id'];
|
||||
users: NonNullable<IUser['username']>[];
|
||||
visitor: ILivechatVisitor['_id'];
|
||||
agent: ILivechatAgent['_id'];
|
||||
userId: string;
|
||||
}) => {
|
||||
if (rid) {
|
||||
return getValue(await Rooms.findOne({ _id: rid, abacAttributes: { $exists: false } }));
|
||||
}
|
||||
|
||||
if (type === 'd') {
|
||||
return getValue(await Rooms.findDirectRoomContainingAllUsernames(usernames));
|
||||
}
|
||||
|
||||
if (type === 'l') {
|
||||
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { userId });
|
||||
const rooms = await LivechatRooms.findByVisitorIdAndAgentId(
|
||||
visitor,
|
||||
agent,
|
||||
{
|
||||
projection: { _id: 1 },
|
||||
},
|
||||
extraQuery,
|
||||
).toArray();
|
||||
return rooms?.length ? { rids: rooms.map(({ _id }) => _id), name: i18n.t('Omnichannel') } : undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const requireAuditor = async (userId: string | null): Promise<IUser> => {
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('Not allowed');
|
||||
}
|
||||
|
||||
const user = await Users.findOneById(userId);
|
||||
if (!user || !(await hasPermissionAsync(user._id, 'can-audit'))) {
|
||||
throw new Meteor.Error('Not allowed');
|
||||
}
|
||||
return user;
|
||||
};
|
||||
|
||||
type AuditMessagesParams = {
|
||||
rid?: IRoom['_id'];
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
users: NonNullable<IUser['username']>[];
|
||||
msg: IMessage['msg'];
|
||||
type: string;
|
||||
visitor?: ILivechatVisitor['_id'];
|
||||
agent?: ILivechatAgent['_id'];
|
||||
};
|
||||
|
||||
export const auditGetMessagesMethod = async (userId: string | null, params: AuditMessagesParams): Promise<IMessage[]> => {
|
||||
const { rid, startDate, endDate, users: usernames, msg, type, visitor, agent } = params;
|
||||
|
||||
const user = await requireAuditor(userId);
|
||||
|
||||
const userFields = {
|
||||
_id: user._id,
|
||||
username: user.username,
|
||||
...(user.name && { name: user.name }),
|
||||
...(user.avatarETag && { avatarETag: user.avatarETag }),
|
||||
};
|
||||
|
||||
let rids;
|
||||
let name;
|
||||
|
||||
const query: Filter<IMessage> = {
|
||||
ts: {
|
||||
$gt: startDate,
|
||||
$lt: endDate,
|
||||
},
|
||||
};
|
||||
|
||||
if (type === 'u') {
|
||||
const usersId = await getUsersIdFromUserName(usernames);
|
||||
query['u._id'] = { $in: usersId };
|
||||
|
||||
const abacRooms = await Rooms.findAllPrivateRoomsWithAbacAttributes({ projection: { _id: 1 } })
|
||||
.map((doc) => doc._id)
|
||||
.toArray();
|
||||
|
||||
query.rid = { $nin: abacRooms };
|
||||
} else {
|
||||
const roomInfo = await getRoomInfoByAuditParams({
|
||||
type,
|
||||
roomId: rid ?? '',
|
||||
users: usernames,
|
||||
visitor: visitor ?? '',
|
||||
agent: agent ?? '',
|
||||
userId: user._id,
|
||||
});
|
||||
if (!roomInfo) {
|
||||
throw new Meteor.Error(`Room doesn't exist`);
|
||||
}
|
||||
|
||||
rids = roomInfo.rids;
|
||||
name = roomInfo.name;
|
||||
query.rid = { $in: rids };
|
||||
}
|
||||
|
||||
if (msg) {
|
||||
const regex = new RegExp(escapeRegExp(msg).trim(), 'i');
|
||||
query.msg = regex;
|
||||
}
|
||||
|
||||
const messages = await Messages.find(query).toArray();
|
||||
|
||||
await AuditLog.insertOne(
|
||||
{
|
||||
ts: new Date(),
|
||||
results: messages.length,
|
||||
u: userFields,
|
||||
fields: { msg, users: usernames, rids, room: name, startDate, endDate, type, visitor, agent },
|
||||
},
|
||||
{ ignoreUndefined: true },
|
||||
);
|
||||
|
||||
updateCounter({ settingsId: 'Message_Auditing_Panel_Load_Count' });
|
||||
|
||||
return messages;
|
||||
};
|
||||
|
||||
type AuditOmnichannelMessagesParams = {
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
users: NonNullable<IUser['username']>[];
|
||||
msg: IMessage['msg'];
|
||||
type: string;
|
||||
visitor?: ILivechatVisitor['_id'];
|
||||
agent?: ILivechatAgent['_id'];
|
||||
};
|
||||
|
||||
export const auditGetOmnichannelMessagesMethod = async (
|
||||
userId: string | null,
|
||||
params: AuditOmnichannelMessagesParams,
|
||||
): Promise<IMessage[]> => {
|
||||
const { startDate, endDate, users: usernames, msg, type, visitor, agent } = params;
|
||||
|
||||
const user = await requireAuditor(userId);
|
||||
|
||||
const userFields = {
|
||||
_id: user._id,
|
||||
username: user.username,
|
||||
...(user.name && { name: user.name }),
|
||||
...(user.avatarETag && { avatarETag: user.avatarETag }),
|
||||
};
|
||||
|
||||
// No livechat.applyRoomRestrictions here (unlike the type 'l' path in getRoomInfoByAuditParams): this
|
||||
// mirrors the original DDP auditGetOmnichannelMessages. Access is gated by the can-audit permission and
|
||||
// audit is intended to span all omnichannel rooms, so unit/visibility restrictions are not applied.
|
||||
const rooms = await LivechatRooms.findByVisitorIdAndAgentId(visitor, agent, {
|
||||
projection: { _id: 1 },
|
||||
}).toArray();
|
||||
// keep rids an array — `$in: undefined` throws "$in needs an array" when no rooms match
|
||||
const rids = rooms.map(({ _id }) => _id);
|
||||
const name = i18n.t('Omnichannel');
|
||||
|
||||
const query: Filter<IMessage> = {
|
||||
rid: { $in: rids },
|
||||
ts: {
|
||||
$gt: startDate,
|
||||
$lt: endDate,
|
||||
},
|
||||
};
|
||||
|
||||
if (msg) {
|
||||
const regex = new RegExp(escapeRegExp(msg).trim(), 'i');
|
||||
query.msg = regex;
|
||||
}
|
||||
|
||||
const messages = await Messages.find(query).toArray();
|
||||
|
||||
await AuditLog.insertOne(
|
||||
{
|
||||
ts: new Date(),
|
||||
results: messages.length,
|
||||
u: userFields,
|
||||
fields: { msg, users: usernames, rids, room: name, startDate, endDate, type, visitor, agent },
|
||||
},
|
||||
{ ignoreUndefined: true },
|
||||
);
|
||||
|
||||
return messages;
|
||||
};
|
||||
|
||||
export const auditGetAuditionsMethod = async (userId: string | null, startDate: Date, endDate: Date): Promise<IAuditLog[]> => {
|
||||
if (!userId || !(await hasPermissionAsync(userId, 'can-audit-log'))) {
|
||||
throw new Meteor.Error('Not allowed');
|
||||
}
|
||||
return AuditLog.find(
|
||||
{
|
||||
ts: {
|
||||
$gt: startDate,
|
||||
$lt: endDate,
|
||||
},
|
||||
},
|
||||
{
|
||||
projection: {
|
||||
'u.services': 0,
|
||||
'u.roles': 0,
|
||||
'u.lastLogin': 0,
|
||||
'u.statusConnection': 0,
|
||||
'u.emails': 0,
|
||||
},
|
||||
},
|
||||
).toArray();
|
||||
};
|
||||
79
apps/meteor/ee/server/lib/audit/methods.ts
Normal file
79
apps/meteor/ee/server/lib/audit/methods.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import type { ILivechatAgent, ILivechatVisitor, IMessage, IRoom, IUser, IAuditLog } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { check } from 'meteor/check';
|
||||
import { DDPRateLimiter } from 'meteor/ddp-rate-limiter';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { auditGetAuditionsMethod, auditGetMessagesMethod, auditGetOmnichannelMessagesMethod } from './functions';
|
||||
import { methodDeprecationLogger } from '../../../../server/lib/deprecationWarningLogger';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
auditGetAuditions: (params: { startDate: Date; endDate: Date }) => IAuditLog[];
|
||||
auditGetMessages: (params: {
|
||||
rid: IRoom['_id'];
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
users: NonNullable<IUser['username']>[];
|
||||
msg: IMessage['msg'];
|
||||
type: string;
|
||||
visitor: ILivechatVisitor['_id'];
|
||||
agent: ILivechatAgent['_id'];
|
||||
}) => IMessage[];
|
||||
auditGetOmnichannelMessages: (params: {
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
users: NonNullable<IUser['username']>[];
|
||||
msg: IMessage['msg'];
|
||||
type: string;
|
||||
visitor?: ILivechatVisitor['_id'];
|
||||
agent?: ILivechatAgent['_id'];
|
||||
}) => IMessage[];
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async auditGetOmnichannelMessages(params) {
|
||||
methodDeprecationLogger.method('auditGetOmnichannelMessages', '9.0.0', '/v1/audit.omnichannelMessages');
|
||||
check(params.startDate, Date);
|
||||
check(params.endDate, Date);
|
||||
return auditGetOmnichannelMessagesMethod(Meteor.userId(), params);
|
||||
},
|
||||
async auditGetMessages(params) {
|
||||
methodDeprecationLogger.method('auditGetMessages', '9.0.0', '/v1/audit.messages');
|
||||
check(params.startDate, Date);
|
||||
check(params.endDate, Date);
|
||||
return auditGetMessagesMethod(Meteor.userId(), params);
|
||||
},
|
||||
async auditGetAuditions({ startDate, endDate }) {
|
||||
methodDeprecationLogger.method('auditGetAuditions', '9.0.0', '/v1/audit.auditions');
|
||||
check(startDate, Date);
|
||||
check(endDate, Date);
|
||||
return auditGetAuditionsMethod(Meteor.userId(), startDate, endDate);
|
||||
},
|
||||
});
|
||||
|
||||
DDPRateLimiter.addRule(
|
||||
{
|
||||
type: 'method',
|
||||
name: 'auditGetAuditions',
|
||||
userId(/* userId*/) {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
10,
|
||||
60000,
|
||||
);
|
||||
|
||||
DDPRateLimiter.addRule(
|
||||
{
|
||||
type: 'method',
|
||||
name: 'auditGetMessages',
|
||||
userId(/* userId*/) {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
10,
|
||||
60000,
|
||||
);
|
||||
22
apps/meteor/ee/server/lib/audit/startup.ts
Normal file
22
apps/meteor/ee/server/lib/audit/startup.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Permissions } from '@rocket.chat/models';
|
||||
|
||||
import { createOrUpdateProtectedRoleAsync } from '../../../../server/lib/roles/createOrUpdateProtectedRole';
|
||||
|
||||
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 = [
|
||||
{ name: 'auditor', scope: 'Users' },
|
||||
{ name: 'auditor-log', scope: 'Users' },
|
||||
] as const;
|
||||
|
||||
for (const permission of permissions) {
|
||||
void Permissions.create(permission._id, permission.roles);
|
||||
}
|
||||
|
||||
return Promise.all(defaultRoles.map((role) => createOrUpdateProtectedRoleAsync(role.name, role)));
|
||||
};
|
||||
Reference in New Issue
Block a user