From 3e95741af49eb24cea9ad40f29e1304cdffb0465 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 26 Jul 2024 11:18:07 -0600 Subject: [PATCH 01/17] New endpoint for auditing room members --- apps/meteor/ee/server/lib/audit/endpoints.ts | 74 +++++++ apps/meteor/ee/server/lib/audit/startup.ts | 1 + apps/meteor/ee/server/startup/audit.ts | 1 + apps/meteor/tests/end-to-end/api/audit.ts | 214 +++++++++++++++++++ 4 files changed, 290 insertions(+) create mode 100644 apps/meteor/ee/server/lib/audit/endpoints.ts create mode 100644 apps/meteor/tests/end-to-end/api/audit.ts diff --git a/apps/meteor/ee/server/lib/audit/endpoints.ts b/apps/meteor/ee/server/lib/audit/endpoints.ts new file mode 100644 index 000000000000..466e2ab4395d --- /dev/null +++ b/apps/meteor/ee/server/lib/audit/endpoints.ts @@ -0,0 +1,74 @@ +import type { IUser } from '@rocket.chat/core-typings'; +import { Rooms } 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' }, + filter: { type: 'string', nullable: true }, + count: { type: 'number', nullable: true }, + offset: { type: 'number', nullable: true }, + sort: { type: 'string', nullable: true }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isAuditRoomMembersProps = ajv.compile(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: IUser[] }>; + }; + } +} + +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(roomId, { projection: { _id: 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]); + return API.v1.success({ + members, + count: members.length, + offset: skip, + total, + }); + }, + }, +); diff --git a/apps/meteor/ee/server/lib/audit/startup.ts b/apps/meteor/ee/server/lib/audit/startup.ts index ba50eb48e244..076336b50fe6 100644 --- a/apps/meteor/ee/server/lib/audit/startup.ts +++ b/apps/meteor/ee/server/lib/audit/startup.ts @@ -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 = [ diff --git a/apps/meteor/ee/server/startup/audit.ts b/apps/meteor/ee/server/startup/audit.ts index c38794a7582e..7b1e5000648c 100644 --- a/apps/meteor/ee/server/startup/audit.ts +++ b/apps/meteor/ee/server/startup/audit.ts @@ -4,6 +4,7 @@ import { createPermissions } from '../lib/audit/startup'; await License.onLicense('auditing', async () => { await import('../lib/audit/methods'); + await import('../lib/audit/endpoints'); await createPermissions(); }); diff --git a/apps/meteor/tests/end-to-end/api/audit.ts b/apps/meteor/tests/end-to-end/api/audit.ts new file mode 100644 index 000000000000..2ef94977ee8f --- /dev/null +++ b/apps/meteor/tests/end-to-end/api/audit.ts @@ -0,0 +1,214 @@ +import type { Credentials } from '@rocket.chat/api-client'; +import type { IRoom, IUser } from '@rocket.chat/core-typings'; +import { Random } from '@rocket.chat/random'; +import { expect } from 'chai'; +import { before, describe, it, after } from 'mocha'; + +import { getCredentials, api, request, credentials, methodCall } from '../../data/api-data'; +import { updatePermission } from '../../data/permissions.helper'; +import { createRoom, deleteRoom } from '../../data/rooms.helper'; +import { password } from '../../data/user'; +import { createUser, deleteUser, login } from '../../data/users.helper'; +import { IS_EE } from '../../e2e/config/constants'; + +(IS_EE ? describe : describe.skip)('Audit Panel', () => { + let testChannel: IRoom; + let dummyUser: IUser; + let auditor: IUser; + let auditorCredentials: Credentials; + before((done) => getCredentials(done)); + before(async () => { + testChannel = (await createRoom({ type: 'c', name: `chat.api-test-${Date.now()}` })).body.channel; + dummyUser = await createUser(); + auditor = await createUser({ roles: ['user', 'auditor'] }); + + auditorCredentials = await login(auditor.username, password); + }); + after(() => deleteRoom({ type: 'c', roomId: testChannel._id })); + after(() => deleteUser({ _id: dummyUser._id })); + after(() => deleteUser({ _id: auditor._id })); + + describe('audit/rooms.members', () => { + it('should fail if user is not logged in', async () => { + await request + .get(api('audit/rooms.members')) + .query({ + roomId: 'GENERAL', + }) + .expect(401); + }); + it('should fail if user does not have view-members-list-all-rooms permission', async () => { + await updatePermission('view-members-list-all-rooms', []); + await request + .get(api('audit/rooms.members')) + .set(credentials) + .query({ + roomId: 'GENERAL', + }) + .expect(403); + await request + .get(api('audit/rooms.members')) + .set(auditorCredentials) + .query({ + roomId: 'GENERAL', + }) + .expect(403); + + await updatePermission('view-members-list-all-rooms', ['admin', 'auditor']); + }); + it('should fail if roomId is invalid', async () => { + await request + .get(api('audit/rooms.members')) + .set(credentials) + .query({ + roomId: Random.id(), + }) + .expect(404); + }); + it('should fetch the members of a room', async () => { + await request + .get(api('audit/rooms.members')) + .set(credentials) + .query({ + roomId: testChannel._id, + }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body.members).to.be.an('array'); + expect(res.body.members).to.have.lengthOf(1); + }); + }); + it('should fetch the members of a room with offset and count', async () => { + await request + .post(methodCall('addUsersToRoom')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'addUsersToRoom', + params: [{ rid: testChannel._id, users: [dummyUser.username] }], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }); + + await request + .get(api('audit/rooms.members')) + .set(credentials) + .query({ + roomId: testChannel._id, + offset: 1, + count: 1, + }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body.members).to.be.an('array'); + expect(res.body.members).to.have.lengthOf(1); + expect(res.body.members[0].username).to.be.equal(dummyUser.username); + expect(res.body.total).to.be.equal(2); + expect(res.body.offset).to.be.equal(1); + expect(res.body.count).to.be.equal(1); + }); + }); + + it('should filter by username', async () => { + await request + .get(api('audit/rooms.members')) + .set(credentials) + .query({ + roomId: testChannel._id, + filter: dummyUser.username, + }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body.members).to.be.an('array'); + expect(res.body.members).to.have.lengthOf(1); + expect(res.body.members[0].username).to.be.equal(dummyUser.username); + }); + }); + + it('should filter by user name', async () => { + await request + .get(api('audit/rooms.members')) + .set(credentials) + .query({ + roomId: testChannel._id, + filter: dummyUser.name, + }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body.members).to.be.an('array'); + expect(res.body.members).to.have.lengthOf(1); + expect(res.body.members[0].name).to.be.equal(dummyUser.name); + }); + }); + + it('should sort by username', async () => { + await request + .get(api('audit/rooms.members')) + .set(credentials) + .query({ + roomId: testChannel._id, + sort: '{ "username": -1 }', + }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body.members).to.be.an('array'); + expect(res.body.members).to.have.lengthOf(2); + expect(res.body.members[0].username).to.be.equal('rocketchat.internal.admin.test'); + expect(res.body.members[1].username).to.be.equal(dummyUser.username); + }); + }); + + it('should not allow nosqlinjection on filter param', async () => { + await request + .get(api('audit/rooms.members')) + .set(credentials) + .query({ + roomId: testChannel._id, + filter: '{ "$ne": "rocketchat.internal.admin.test" }', + }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body.members).to.be.an('array'); + expect(res.body.members).to.have.lengthOf(0); + }); + + await request + .get(api('audit/rooms.members')) + .set(credentials) + .query({ + roomId: testChannel._id, + filter: { username: 'rocketchat.internal.admin.test' }, + }) + .expect(400); + }); + + it('should allow to fetch info even if user is not in the room', async () => { + await request + .get(api('audit/rooms.members')) + .set(auditorCredentials) + .query({ + roomId: testChannel._id, + }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body.members).to.be.an('array'); + expect(res.body.members[0].username).to.be.equal('rocketchat.internal.admin.test'); + expect(res.body.members[1].username).to.be.equal(dummyUser.username); + expect(res.body.total).to.be.equal(2); + }); + }); + }); +}); From b672a3d7c331ee5cf12d05fd1bf9c49c7f740658 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 26 Jul 2024 11:22:06 -0600 Subject: [PATCH 02/17] right type --- apps/meteor/ee/server/lib/audit/endpoints.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/ee/server/lib/audit/endpoints.ts b/apps/meteor/ee/server/lib/audit/endpoints.ts index 466e2ab4395d..14220921a5a6 100644 --- a/apps/meteor/ee/server/lib/audit/endpoints.ts +++ b/apps/meteor/ee/server/lib/audit/endpoints.ts @@ -35,7 +35,7 @@ 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: IUser[] }>; + GET: (params: AuditRoomMembersParams) => PaginatedResult<{ members: Pick[] }>; }; } } From bb26adcf29d4c717a7723f3170dce2c2af362abf Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 26 Jul 2024 11:34:34 -0600 Subject: [PATCH 03/17] test --- apps/meteor/tests/end-to-end/api/audit.ts | 25 ++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/audit.ts b/apps/meteor/tests/end-to-end/api/audit.ts index 2ef94977ee8f..dea5a308e75e 100644 --- a/apps/meteor/tests/end-to-end/api/audit.ts +++ b/apps/meteor/tests/end-to-end/api/audit.ts @@ -11,14 +11,16 @@ import { password } from '../../data/user'; import { createUser, deleteUser, login } from '../../data/users.helper'; import { IS_EE } from '../../e2e/config/constants'; -(IS_EE ? describe : describe.skip)('Audit Panel', () => { +(IS_EE ? describe.only: describe.skip)('Audit Panel', () => { let testChannel: IRoom; + let testPrivateChannel: IRoom; let dummyUser: IUser; let auditor: IUser; let auditorCredentials: Credentials; before((done) => getCredentials(done)); before(async () => { testChannel = (await createRoom({ type: 'c', name: `chat.api-test-${Date.now()}` })).body.channel; + testPrivateChannel = (await createRoom({ type: 'p', name: `chat.api-test-${Date.now()}` })).body.group; dummyUser = await createUser(); auditor = await createUser({ roles: ['user', 'auditor'] }); @@ -27,6 +29,7 @@ import { IS_EE } from '../../e2e/config/constants'; after(() => deleteRoom({ type: 'c', roomId: testChannel._id })); after(() => deleteUser({ _id: dummyUser._id })); after(() => deleteUser({ _id: auditor._id })); + after(() => deleteRoom({ type: 'p', roomId: testPrivateChannel._id })); describe('audit/rooms.members', () => { it('should fail if user is not logged in', async () => { @@ -164,8 +167,8 @@ import { IS_EE } from '../../e2e/config/constants'; expect(res.body).to.have.property('success', true); expect(res.body.members).to.be.an('array'); expect(res.body.members).to.have.lengthOf(2); - expect(res.body.members[0].username).to.be.equal('rocketchat.internal.admin.test'); - expect(res.body.members[1].username).to.be.equal(dummyUser.username); + expect(res.body.members[1].username).to.be.equal('rocketchat.internal.admin.test'); + expect(res.body.members[0].username).to.be.equal(dummyUser.username); }); }); @@ -210,5 +213,21 @@ import { IS_EE } from '../../e2e/config/constants'; expect(res.body.total).to.be.equal(2); }); }); + + it('should allow to fetch info from private rooms', async () => { + await request + .get(api('audit/rooms.members')) + .set(auditorCredentials) + .query({ + roomId: testPrivateChannel._id, + }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body.members).to.be.an('array'); + expect(res.body.members[0].username).to.be.equal('rocketchat.internal.admin.test'); + expect(res.body.total).to.be.equal(1); + }); + }); }); }); From 432ccaacec1578e2f3eb666fe8a26b7eb862b0be Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 26 Jul 2024 11:42:16 -0600 Subject: [PATCH 04/17] first translation --- packages/i18n/src/locales/en.i18n.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 523888f0912a..fe127d816bdf 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -5792,6 +5792,8 @@ "view-all-teams_description": "Permission to view all teams", "view-all-team-channels": "View All Team Channels", "view-all-team-channels_description": "Permission to view all team's channels", + "view-members-list-all-rooms": "View Member's list for all rooms", + "view-members-list-all-rooms_description": "Permission to view member's list for all rooms, even the ones user is not part of", "view-broadcast-member-list": "View Members List in Broadcast Room", "view-broadcast-member-list_description": "Permission to view list of users in broadcast channel", "view-c-room": "View Public Channel", From 9b29799c45723d55237e7ae4fb05432d8327ba58 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 26 Jul 2024 12:17:01 -0600 Subject: [PATCH 05/17] .only --- apps/meteor/tests/end-to-end/api/audit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/tests/end-to-end/api/audit.ts b/apps/meteor/tests/end-to-end/api/audit.ts index dea5a308e75e..f230c940e30e 100644 --- a/apps/meteor/tests/end-to-end/api/audit.ts +++ b/apps/meteor/tests/end-to-end/api/audit.ts @@ -11,7 +11,7 @@ import { password } from '../../data/user'; import { createUser, deleteUser, login } from '../../data/users.helper'; import { IS_EE } from '../../e2e/config/constants'; -(IS_EE ? describe.only: describe.skip)('Audit Panel', () => { +(IS_EE ? describe : describe.skip)('Audit Panel', () => { let testChannel: IRoom; let testPrivateChannel: IRoom; let dummyUser: IUser; From d0f7df07f5164bbb2b794c5b22c9c551d0d0276b Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 26 Jul 2024 12:18:34 -0600 Subject: [PATCH 06/17] Create funny-boats-guess.md --- .changeset/funny-boats-guess.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/funny-boats-guess.md diff --git a/.changeset/funny-boats-guess.md b/.changeset/funny-boats-guess.md new file mode 100644 index 000000000000..e2a70d162d74 --- /dev/null +++ b/.changeset/funny-boats-guess.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/i18n": patch +--- + +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. From 9962230cb00be2f68b4ee1f2995fcbb1bf9b4c26 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 26 Jul 2024 12:32:40 -0600 Subject: [PATCH 07/17] Update audit.ts --- apps/meteor/tests/end-to-end/api/audit.ts | 27 ++++++++++++++--------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/audit.ts b/apps/meteor/tests/end-to-end/api/audit.ts index f230c940e30e..f72d388c03c6 100644 --- a/apps/meteor/tests/end-to-end/api/audit.ts +++ b/apps/meteor/tests/end-to-end/api/audit.ts @@ -31,17 +31,14 @@ import { IS_EE } from '../../e2e/config/constants'; after(() => deleteUser({ _id: auditor._id })); after(() => deleteRoom({ type: 'p', roomId: testPrivateChannel._id })); - describe('audit/rooms.members', () => { - it('should fail if user is not logged in', async () => { - await request - .get(api('audit/rooms.members')) - .query({ - roomId: 'GENERAL', - }) - .expect(401); + describe('audit/rooms.members [no permissions]', () => { + before(async () => { + await updatePermission('view-members-list-all-rooms', []); + }); + after(async () => { + await updatePermission('view-members-list-all-rooms', ['admin', 'auditor']); }); it('should fail if user does not have view-members-list-all-rooms permission', async () => { - await updatePermission('view-members-list-all-rooms', []); await request .get(api('audit/rooms.members')) .set(credentials) @@ -56,9 +53,19 @@ import { IS_EE } from '../../e2e/config/constants'; roomId: 'GENERAL', }) .expect(403); + }); + }); - await updatePermission('view-members-list-all-rooms', ['admin', 'auditor']); + describe('audit/rooms.members', () => { + it('should fail if user is not logged in', async () => { + await request + .get(api('audit/rooms.members')) + .query({ + roomId: 'GENERAL', + }) + .expect(401); }); + it('should fail if roomId is invalid', async () => { await request .get(api('audit/rooms.members')) From 240ccbd05afc349b00bacdd914651f98e9a7ee3d Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 26 Jul 2024 12:48:03 -0600 Subject: [PATCH 08/17] im sick --- apps/meteor/ee/server/lib/audit/endpoints.ts | 4 +++- apps/meteor/tests/end-to-end/api/audit.ts | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/meteor/ee/server/lib/audit/endpoints.ts b/apps/meteor/ee/server/lib/audit/endpoints.ts index 14220921a5a6..f6f3dec09012 100644 --- a/apps/meteor/ee/server/lib/audit/endpoints.ts +++ b/apps/meteor/ee/server/lib/audit/endpoints.ts @@ -35,7 +35,9 @@ 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[] }>; + GET: ( + params: AuditRoomMembersParams, + ) => PaginatedResult<{ members: Pick[] }>; }; } } diff --git a/apps/meteor/tests/end-to-end/api/audit.ts b/apps/meteor/tests/end-to-end/api/audit.ts index f72d388c03c6..40762a12b715 100644 --- a/apps/meteor/tests/end-to-end/api/audit.ts +++ b/apps/meteor/tests/end-to-end/api/audit.ts @@ -65,7 +65,6 @@ import { IS_EE } from '../../e2e/config/constants'; }) .expect(401); }); - it('should fail if roomId is invalid', async () => { await request .get(api('audit/rooms.members')) From 80088f62cff7ef4e8f59f105e0d5bc42499d802c Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 26 Jul 2024 12:49:36 -0600 Subject: [PATCH 09/17] of eslint --- packages/i18n/src/locales/en.i18n.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index fe127d816bdf..33d1b3d4753a 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -5792,8 +5792,8 @@ "view-all-teams_description": "Permission to view all teams", "view-all-team-channels": "View All Team Channels", "view-all-team-channels_description": "Permission to view all team's channels", - "view-members-list-all-rooms": "View Member's list for all rooms", - "view-members-list-all-rooms_description": "Permission to view member's list for all rooms, even the ones user is not part of", + "view-members-list-all-rooms": "Can view members in all rooms", + "view-members-list-all-rooms_description": "Gives the ability to see the members list in all rooms, even those the user is not part of", "view-broadcast-member-list": "View Members List in Broadcast Room", "view-broadcast-member-list_description": "Permission to view list of users in broadcast channel", "view-c-room": "View Public Channel", From 8d0d0166c869595da1d55992c330912cbb25efe7 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 26 Jul 2024 15:17:30 -0600 Subject: [PATCH 10/17] Apply suggestions from code review Co-authored-by: Matheus Barbosa Silva <36537004+matheusbsilva137@users.noreply.github.com> --- .changeset/funny-boats-guess.md | 4 ++-- apps/meteor/ee/server/lib/audit/endpoints.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/funny-boats-guess.md b/.changeset/funny-boats-guess.md index e2a70d162d74..076acff98329 100644 --- a/.changeset/funny-boats-guess.md +++ b/.changeset/funny-boats-guess.md @@ -1,6 +1,6 @@ --- -"@rocket.chat/meteor": patch -"@rocket.chat/i18n": patch +"@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. diff --git a/apps/meteor/ee/server/lib/audit/endpoints.ts b/apps/meteor/ee/server/lib/audit/endpoints.ts index f6f3dec09012..446de022e7b1 100644 --- a/apps/meteor/ee/server/lib/audit/endpoints.ts +++ b/apps/meteor/ee/server/lib/audit/endpoints.ts @@ -19,7 +19,7 @@ type AuditRoomMembersParams = PaginatedRequest<{ const auditRoomMembersSchema = { type: 'object', properties: { - roomId: { type: 'string' }, + roomId: { type: 'string', minLength: 1 }, filter: { type: 'string', nullable: true }, count: { type: 'number', nullable: true }, offset: { type: 'number', nullable: true }, From 428e12e5b0220df49a4b03582ca991786c8fd80b Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 29 Jul 2024 08:56:12 -0600 Subject: [PATCH 11/17] show auditlog on screen --- .../audit/components/AuditFiltersDisplay.tsx | 8 ++++--- .../views/audit/components/AuditLogEntry.tsx | 6 +++-- apps/meteor/ee/server/lib/audit/endpoints.ts | 24 ++++++++++++++++--- packages/core-typings/src/ee/IAuditLog.ts | 4 ++-- packages/i18n/src/locales/en.i18n.json | 1 + 5 files changed, 33 insertions(+), 10 deletions(-) diff --git a/apps/meteor/client/views/audit/components/AuditFiltersDisplay.tsx b/apps/meteor/client/views/audit/components/AuditFiltersDisplay.tsx index dadb59b04777..019c288c71ce 100644 --- a/apps/meteor/client/views/audit/components/AuditFiltersDisplay.tsx +++ b/apps/meteor/client/views/audit/components/AuditFiltersDisplay.tsx @@ -20,9 +20,11 @@ const AuditFiltersDisplay = ({ users, room, startDate, endDate }: AuditFiltersDi return ( {users?.length ? users.map((user) => `@${user}`).join(' : ') : `#${room}`} - - {formatDate(startDate)} {t('Date_to')} {formatDate(endDate)} {/* TODO: fix this translation */} - + {startDate && endDate ? ( + + {formatDate(startDate)} {t('Date_to')} {formatDate(endDate)} {/* TODO: fix this translation */} + + ) : null} ); }; diff --git a/apps/meteor/client/views/audit/components/AuditLogEntry.tsx b/apps/meteor/client/views/audit/components/AuditLogEntry.tsx index 0ec56c1e5652..74060ec32e83 100644 --- a/apps/meteor/client/views/audit/components/AuditLogEntry.tsx +++ b/apps/meteor/client/views/audit/components/AuditLogEntry.tsx @@ -4,6 +4,7 @@ import { useMediaQuery } from '@rocket.chat/fuselage-hooks'; import { UserAvatar } from '@rocket.chat/ui-avatar'; import type { ReactElement } from 'react'; import React, { memo, useMemo } from 'react'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import { GenericTableRow, GenericTableCell } from '../../../components/GenericTable'; import { useFormatDateAndTime } from '../../../hooks/useFormatDateAndTime'; @@ -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 } = fields; const when = useMemo(() => formatDateAndTime(ts), [formatDateAndTime, ts]); @@ -43,7 +45,7 @@ const AuditLogEntry = ({ value: { u, results, ts, _id, fields } }: AuditLogEntry - {msg} + {type === 'room_member_list' ? t('Room_members_list') : msg } {when} {results} diff --git a/apps/meteor/ee/server/lib/audit/endpoints.ts b/apps/meteor/ee/server/lib/audit/endpoints.ts index 446de022e7b1..c318cdb1b653 100644 --- a/apps/meteor/ee/server/lib/audit/endpoints.ts +++ b/apps/meteor/ee/server/lib/audit/endpoints.ts @@ -1,5 +1,5 @@ -import type { IUser } from '@rocket.chat/core-typings'; -import { Rooms } from '@rocket.chat/models'; +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'; @@ -51,7 +51,7 @@ API.v1.addRoute( const { count: limit, offset: skip } = await getPaginationItems(this.queryParams); const { sort } = await this.parseJsonQuery(); - const room = await Rooms.findOneById(roomId, { projection: { _id: 1 } }); + const room = await Rooms.findOneById>(roomId, { projection: { _id: 1, name: 1, fname: 1 } }); if (!room) { return API.v1.notFound(); } @@ -65,6 +65,24 @@ API.v1.addRoute( }); 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, + }, + }); + return API.v1.success({ members, count: members.length, diff --git a/packages/core-typings/src/ee/IAuditLog.ts b/packages/core-typings/src/ee/IAuditLog.ts index de1c6ff5213f..3c3e6fa60777 100644 --- a/packages/core-typings/src/ee/IAuditLog.ts +++ b/packages/core-typings/src/ee/IAuditLog.ts @@ -12,8 +12,8 @@ export interface IAuditLog extends IRocketChatRecord { fields: { type: string; msg: IMessage['msg']; - startDate: Date; - endDate: Date; + startDate?: Date; + endDate?: Date; rids?: IRoom['_id'][]; room: IRoom['name']; users?: IUser['username'][]; diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 33d1b3d4753a..d64322dc89d4 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -5794,6 +5794,7 @@ "view-all-team-channels_description": "Permission to view all team's channels", "view-members-list-all-rooms": "Can view members in all rooms", "view-members-list-all-rooms_description": "Gives the ability to see the members list in all rooms, even those the user is not part of", + "Room_members_list": "Member's list", "view-broadcast-member-list": "View Members List in Broadcast Room", "view-broadcast-member-list_description": "Permission to view list of users in broadcast channel", "view-c-room": "View Public Channel", From 34f2b4976b0cad6a7066fa2fab0b291f400658a5 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 29 Jul 2024 09:19:08 -0600 Subject: [PATCH 12/17] show filters applied to members search --- .../client/views/audit/components/AuditFiltersDisplay.tsx | 8 +++++--- .../client/views/audit/components/AuditLogEntry.tsx | 8 ++++---- apps/meteor/ee/server/lib/audit/endpoints.ts | 1 + packages/core-typings/src/ee/IAuditLog.ts | 1 + 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/apps/meteor/client/views/audit/components/AuditFiltersDisplay.tsx b/apps/meteor/client/views/audit/components/AuditFiltersDisplay.tsx index 019c288c71ce..276d8c355c9b 100644 --- a/apps/meteor/client/views/audit/components/AuditFiltersDisplay.tsx +++ b/apps/meteor/client/views/audit/components/AuditFiltersDisplay.tsx @@ -9,11 +9,12 @@ 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(); @@ -25,6 +26,7 @@ const AuditFiltersDisplay = ({ users, room, startDate, endDate }: AuditFiltersDi {formatDate(startDate)} {t('Date_to')} {formatDate(endDate)} {/* TODO: fix this translation */} ) : null} + {filters ? {filters} : null} ); }; diff --git a/apps/meteor/client/views/audit/components/AuditLogEntry.tsx b/apps/meteor/client/views/audit/components/AuditLogEntry.tsx index 74060ec32e83..8e3f9788880b 100644 --- a/apps/meteor/client/views/audit/components/AuditLogEntry.tsx +++ b/apps/meteor/client/views/audit/components/AuditLogEntry.tsx @@ -2,9 +2,9 @@ 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'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import { GenericTableRow, GenericTableCell } from '../../../components/GenericTable'; import { useFormatDateAndTime } from '../../../hooks/useFormatDateAndTime'; @@ -18,7 +18,7 @@ const AuditLogEntry = ({ value: { u, results, ts, _id, fields } }: AuditLogEntry const { username, name, avatarETag } = u; - const { msg, users, room, startDate, endDate, type } = fields; + const { msg, users, room, startDate, endDate, type, filters } = fields; const when = useMemo(() => formatDateAndTime(ts), [formatDateAndTime, ts]); @@ -45,12 +45,12 @@ const AuditLogEntry = ({ value: { u, results, ts, _id, fields } }: AuditLogEntry - {type === 'room_member_list' ? t('Room_members_list') : msg } + {type === 'room_member_list' ? t('Room_members_list') : msg} {when} {results} - + ); diff --git a/apps/meteor/ee/server/lib/audit/endpoints.ts b/apps/meteor/ee/server/lib/audit/endpoints.ts index c318cdb1b653..e8a817e15f8c 100644 --- a/apps/meteor/ee/server/lib/audit/endpoints.ts +++ b/apps/meteor/ee/server/lib/audit/endpoints.ts @@ -80,6 +80,7 @@ API.v1.addRoute( rids: [room._id], type: 'room_member_list', room: room.name || room.fname, + filters: filter, }, }); diff --git a/packages/core-typings/src/ee/IAuditLog.ts b/packages/core-typings/src/ee/IAuditLog.ts index 3c3e6fa60777..4618400d8017 100644 --- a/packages/core-typings/src/ee/IAuditLog.ts +++ b/packages/core-typings/src/ee/IAuditLog.ts @@ -19,5 +19,6 @@ export interface IAuditLog extends IRocketChatRecord { users?: IUser['username'][]; visitor?: ILivechatVisitor['_id']; agent?: ILivechatAgent['_id']; + filters?: string; }; } From 727f6c113687c3d506f7903236e36a7369fc8906 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 29 Jul 2024 09:38:09 -0600 Subject: [PATCH 13/17] more tests --- apps/meteor/tests/end-to-end/api/audit.ts | 60 +++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/apps/meteor/tests/end-to-end/api/audit.ts b/apps/meteor/tests/end-to-end/api/audit.ts index 40762a12b715..b470f481fb59 100644 --- a/apps/meteor/tests/end-to-end/api/audit.ts +++ b/apps/meteor/tests/end-to-end/api/audit.ts @@ -2,6 +2,7 @@ import type { Credentials } from '@rocket.chat/api-client'; import type { IRoom, IUser } from '@rocket.chat/core-typings'; import { Random } from '@rocket.chat/random'; import { expect } from 'chai'; +import EJSON from 'ejson'; import { before, describe, it, after } from 'mocha'; import { getCredentials, api, request, credentials, methodCall } from '../../data/api-data'; @@ -74,6 +75,18 @@ import { IS_EE } from '../../e2e/config/constants'; }) .expect(404); }); + it('should fail if roomId is not present', async () => { + await request.get(api('audit/rooms.members')).set(credentials).query({}).expect(400); + }); + it('should fail if roomId is an empty string', async () => { + await request + .get(api('audit/rooms.members')) + .set(credentials) + .query({ + roomId: '', + }) + .expect(400); + }); it('should fetch the members of a room', async () => { await request .get(api('audit/rooms.members')) @@ -88,6 +101,53 @@ import { IS_EE } from '../../e2e/config/constants'; expect(res.body.members).to.have.lengthOf(1); }); }); + it('should persist a log entry', async () => { + await request + .get(api('audit/rooms.members')) + .set(credentials) + .query({ + roomId: testChannel._id, + }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body.members).to.be.an('array'); + expect(res.body.members).to.have.lengthOf(1); + }); + + await request + .post(methodCall('auditGetAuditions')) + .set(credentials) + .send({ + message: EJSON.stringify({ + method: 'auditGetAuditions', + params: [{ startDate: new Date(Date.now() - 86400000), endDate: new Date() }], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + const message = JSON.parse(res.body.message); + + 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); + }); + expect(entry).to.have.property('u').that.is.an('object').deep.equal({ + _id: 'rocketchat.internal.admin.test', + username: 'rocketchat.internal.admin.test', + name: 'RocketChat Internal Admin Test', + }); + expect(entry).to.have.property('fields').that.is.an('object'); + const { fields } = entry; + + expect(fields).to.have.property('msg', 'Room_members_list'); + expect(fields).to.have.property('rids').that.is.an('array').with.lengthOf(1); + }); + }); it('should fetch the members of a room with offset and count', async () => { await request .post(methodCall('addUsersToRoom')) From 34bcf10a77ede0c8be4250d4a1708adbdefabce4 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 29 Jul 2024 10:02:55 -0600 Subject: [PATCH 14/17] Update audit.ts --- apps/meteor/tests/end-to-end/api/audit.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/audit.ts b/apps/meteor/tests/end-to-end/api/audit.ts index b470f481fb59..ba62caf621b8 100644 --- a/apps/meteor/tests/end-to-end/api/audit.ts +++ b/apps/meteor/tests/end-to-end/api/audit.ts @@ -27,10 +27,12 @@ import { IS_EE } from '../../e2e/config/constants'; auditorCredentials = await login(auditor.username, password); }); - after(() => deleteRoom({ type: 'c', roomId: testChannel._id })); - after(() => deleteUser({ _id: dummyUser._id })); - after(() => deleteUser({ _id: auditor._id })); - after(() => deleteRoom({ type: 'p', roomId: testPrivateChannel._id })); + after(async () => { + await deleteRoom({ type: 'c', roomId: testChannel._id }); + await deleteUser({ _id: dummyUser._id }); + await deleteUser({ _id: auditor._id }); + await deleteRoom({ type: 'p', roomId: testPrivateChannel._id }); + }); describe('audit/rooms.members [no permissions]', () => { before(async () => { From e8a09f89bfb2c5a8661d963bee06e4afb6408edb Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 29 Jul 2024 15:25:03 -0600 Subject: [PATCH 15/17] Update packages/i18n/src/locales/en.i18n.json Co-authored-by: Matheus Barbosa Silva <36537004+matheusbsilva137@users.noreply.github.com> --- packages/i18n/src/locales/en.i18n.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index d64322dc89d4..fd7aa4732397 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -5794,7 +5794,7 @@ "view-all-team-channels_description": "Permission to view all team's channels", "view-members-list-all-rooms": "Can view members in all rooms", "view-members-list-all-rooms_description": "Gives the ability to see the members list in all rooms, even those the user is not part of", - "Room_members_list": "Member's list", + "Room_members_list": "Members list", "view-broadcast-member-list": "View Members List in Broadcast Room", "view-broadcast-member-list_description": "Permission to view list of users in broadcast channel", "view-c-room": "View Public Channel", From 7f5ba67bad51e65700fd0ddbaae535a26f9a66c8 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 29 Jul 2024 15:51:29 -0600 Subject: [PATCH 16/17] Apply suggestions from code review --- apps/meteor/ee/server/lib/audit/endpoints.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/meteor/ee/server/lib/audit/endpoints.ts b/apps/meteor/ee/server/lib/audit/endpoints.ts index e8a817e15f8c..bb41c133a4d3 100644 --- a/apps/meteor/ee/server/lib/audit/endpoints.ts +++ b/apps/meteor/ee/server/lib/audit/endpoints.ts @@ -20,10 +20,10 @@ const auditRoomMembersSchema = { type: 'object', properties: { roomId: { type: 'string', minLength: 1 }, - filter: { type: 'string', nullable: true }, - count: { type: 'number', nullable: true }, - offset: { type: 'number', nullable: true }, - sort: { type: 'string', nullable: true }, + filter: { type: 'string' }, + count: { type: 'number' }, + offset: { type: 'number' }, + sort: { type: 'string' }, }, required: ['roomId'], additionalProperties: false, From 137a25b1c84af952e9d5075ba1f7ca645140cc89 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Thu, 1 Aug 2024 08:04:47 -0600 Subject: [PATCH 17/17] fix --- .../ee/server/{lib/audit/endpoints.ts => api/audit.ts} | 6 +++--- apps/meteor/ee/server/startup/audit.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) rename apps/meteor/ee/server/{lib/audit/endpoints.ts => api/audit.ts} (91%) diff --git a/apps/meteor/ee/server/lib/audit/endpoints.ts b/apps/meteor/ee/server/api/audit.ts similarity index 91% rename from apps/meteor/ee/server/lib/audit/endpoints.ts rename to apps/meteor/ee/server/api/audit.ts index bb41c133a4d3..748368f0d569 100644 --- a/apps/meteor/ee/server/lib/audit/endpoints.ts +++ b/apps/meteor/ee/server/api/audit.ts @@ -3,9 +3,9 @@ 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'; +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, diff --git a/apps/meteor/ee/server/startup/audit.ts b/apps/meteor/ee/server/startup/audit.ts index 7b1e5000648c..9f8135a16a65 100644 --- a/apps/meteor/ee/server/startup/audit.ts +++ b/apps/meteor/ee/server/startup/audit.ts @@ -4,7 +4,7 @@ import { createPermissions } from '../lib/audit/startup'; await License.onLicense('auditing', async () => { await import('../lib/audit/methods'); - await import('../lib/audit/endpoints'); + await import('../api/audit'); await createPermissions(); });