diff --git a/apps/meteor/ee/server/lib/audit/methods.ts b/apps/meteor/ee/server/lib/audit/methods.ts index 40d1b06e316cf..c6b56d741de69 100644 --- a/apps/meteor/ee/server/lib/audit/methods.ts +++ b/apps/meteor/ee/server/lib/audit/methods.ts @@ -163,6 +163,12 @@ Meteor.methods({ 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, agent, userId: user._id }); if (!roomInfo) { diff --git a/apps/meteor/tests/end-to-end/api/abac.ts b/apps/meteor/tests/end-to-end/api/abac.ts index 4ca7233284761..18f522d396652 100644 --- a/apps/meteor/tests/end-to-end/api/abac.ts +++ b/apps/meteor/tests/end-to-end/api/abac.ts @@ -416,6 +416,167 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I }); }); + describe('Audit messages by user and ABAC-managed rooms', () => { + let auditUser: IUser; + let auditUserCreds: Credentials; + let auditRoom: IRoom; + const auditAttrKey = `audit_attr_${Date.now()}`; + let startDate: Date; + let endDate: Date; + + before(async () => { + startDate = new Date(); + endDate = new Date(startDate.getTime() + 1000 * 60); + + auditUser = await createUser(); + auditUserCreds = await login(auditUser.username, password); + + await request + .post(`${v1}/abac/attributes`) + .set(credentials) + .send({ key: auditAttrKey, values: ['v1'] }) + .expect(200); + + await addAbacAttributesToUserDirectly(auditUser._id, [{ key: auditAttrKey, values: ['v1'] }]); + await addAbacAttributesToUserDirectly(credentials['X-User-Id'], [{ key: auditAttrKey, values: ['v1'] }]); + + const roomRes = await createRoom({ type: 'p', name: `abac-audit-user-room-${Date.now()}`, members: [auditUser.username!] }); + auditRoom = roomRes.body.group as IRoom; + + await request + .post(`${v1}/abac/rooms/${auditRoom._id}/attributes/${auditAttrKey}`) + .set(credentials) + .send({ values: ['v1'] }) + .expect(200); + }); + + after(async () => { + await deleteRoom({ type: 'p', roomId: auditRoom._id }); + await deleteUser(auditUser); + }); + + it("should return no messages when auditing a user that's part of an ABAC-managed room", async () => { + await request + .post(`${v1}/chat.sendMessage`) + .set(auditUserCreds) + .send({ message: { rid: auditRoom._id, msg: 'audit message in abac room' } }) + .expect(200); + + await request + .post(methodCall('auditGetMessages')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'auditGetMessages', + params: [ + { + type: 'u', + msg: 'audit message in abac room', + startDate: { $date: startDate }, + endDate: { $date: endDate }, + rid: '', + users: [auditUser.username], + visitor: '', + agent: '', + }, + ], + id: 'abac-audit-1', + msg: 'method', + }), + }) + .expect(200) + .expect((res) => { + const parsed = JSON.parse(res.body.message); + expect(parsed).to.have.property('result'); + expect(parsed.result).to.be.an('array').that.is.empty; + }); + }); + + it('should return no messages when auditing a user that WAS part of an ABAC-managed room', async () => { + await request + .post(`${v1}/chat.sendMessage`) + .set(auditUserCreds) + .send({ message: { rid: auditRoom._id, msg: 'audit message before removal' } }) + .expect(200); + + await request.post(`${v1}/groups.kick`).set(credentials).send({ roomId: auditRoom._id, username: auditUser.username }).expect(200); + + await request + .post(methodCall('auditGetMessages')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'auditGetMessages', + params: [ + { + type: 'u', + msg: 'audit message before removal', + startDate: { $date: startDate }, + endDate: { $date: endDate }, + rid: '', + users: [auditUser.username], + visitor: '', + agent: '', + }, + ], + id: 'abac-audit-2', + msg: 'method', + }), + }) + .expect(200) + .expect((res) => { + const parsed = JSON.parse(res.body.message); + expect(parsed).to.have.property('result'); + expect(parsed.result).to.be.an('array').that.is.empty; + }); + }); + + it("should return messages when auditing a user that is part of a room that's no longer ABAC-managed", async () => { + await request + .post(`${v1}/groups.invite`) + .set(credentials) + .send({ roomId: auditRoom._id, usernames: [auditUser.username] }) + .expect(200); + + await request.delete(`${v1}/abac/rooms/${auditRoom._id}/attributes/${auditAttrKey}`).set(credentials).expect(200); + + await request + .post(`${v1}/chat.sendMessage`) + .set(auditUserCreds) + .send({ message: { rid: auditRoom._id, msg: 'audit message after room no longer abac' } }) + .expect(200); + + await request + .post(methodCall('auditGetMessages')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'auditGetMessages', + params: [ + { + type: 'u', + msg: 'audit message after room no longer abac', + startDate: { $date: startDate }, + endDate: { $date: endDate }, + rid: '', + users: [auditUser.username], + visitor: '', + agent: '', + }, + ], + id: 'abac-audit-3', + msg: 'method', + }), + }) + .expect(200) + .expect((res) => { + const parsed = JSON.parse(res.body.message); + expect(parsed).to.have.property('result'); + expect(parsed.result).to.be.an('array').with.lengthOf.greaterThan(0); + }); + }); + }); + it('PUT room attribute should replace values and keep inUse=true', async () => { await request .put(`${v1}/abac/rooms/${testRoom._id}/attributes/${updatedKey}`) diff --git a/apps/meteor/tests/end-to-end/api/audit.ts b/apps/meteor/tests/end-to-end/api/audit.ts index ba62caf621b89..c71ae4a58552d 100644 --- a/apps/meteor/tests/end-to-end/api/audit.ts +++ b/apps/meteor/tests/end-to-end/api/audit.ts @@ -136,7 +136,7 @@ import { IS_EE } from '../../e2e/config/constants'; expect(message.result).to.be.an('array').with.lengthOf.greaterThan(1); const entry = message.result.find((audition: any) => { - return audition.fields.rids.includes(testChannel._id); + return audition.fields?.rids?.includes(testChannel._id); }); expect(entry).to.have.property('u').that.is.an('object').deep.equal({ _id: 'rocketchat.internal.admin.test', diff --git a/packages/model-typings/src/models/IRoomsModel.ts b/packages/model-typings/src/models/IRoomsModel.ts index 6de80181b2b34..0c95c30f460f3 100644 --- a/packages/model-typings/src/models/IRoomsModel.ts +++ b/packages/model-typings/src/models/IRoomsModel.ts @@ -234,6 +234,7 @@ export interface IRoomsModel extends IBaseModel { findByType(type: IRoom['t'], options?: FindOptions): FindCursor; findByTypeInIds(type: IRoom['t'], ids: string[], options?: FindOptions): FindCursor; findPrivateRoomsByIdsWithAbacAttributes(ids: string[], options?: FindOptions): FindCursor; + findAllPrivateRoomsWithAbacAttributes(options?: FindOptions): FindCursor; findBySubscriptionUserId(userId: string, options?: FindOptions): Promise>; findBySubscriptionUserIdUpdatedAfter(userId: string, updatedAfter: Date, options?: FindOptions): Promise>; findByNameAndTypeNotDefault( diff --git a/packages/models/src/models/Rooms.ts b/packages/models/src/models/Rooms.ts index 9b78b5ac9774b..bcaf7fcd2aa33 100644 --- a/packages/models/src/models/Rooms.ts +++ b/packages/models/src/models/Rooms.ts @@ -1311,6 +1311,15 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { return this.find(query, options); } + findAllPrivateRoomsWithAbacAttributes(options: FindOptions = {}): FindCursor { + const query: Filter = { + t: 'p', + abacAttributes: { $exists: true, $ne: [] }, + }; + + return this.find(query, options); + } + async findBySubscriptionUserId(userId: IUser['_id'], options: FindOptions = {}): Promise> { const data = (await Subscriptions.findByUserId(userId, { projection: { rid: 1 } }).toArray()).map((item) => item.rid);