Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/spicy-kangaroos-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/rest-typings": minor
---

Introduces `/v1/audit.settings` endpoint for querying changed settings audit events
1 change: 1 addition & 0 deletions apps/meteor/app/api/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,6 @@ import './v1/voip/omnichannel';
import './v1/voip';
import './v1/federation';
import './v1/moderation';
import './v1/server-events';

export { API, APIClass, defaultRateLimiterOptions } from './api';
53 changes: 53 additions & 0 deletions apps/meteor/app/api/server/v1/server-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { ServerEvents } from '@rocket.chat/models';
import { isServerEventsAuditSettingsProps } from '@rocket.chat/rest-typings';

import { API } from '../api';
import { getPaginationItems } from '../helpers/getPaginationItems';

API.v1.addRoute(
'audit.settings',
{ authRequired: true, validateParams: isServerEventsAuditSettingsProps /* , permissionsRequired: ['can-audit'] */ },
Comment thread
gabriellsh marked this conversation as resolved.
Outdated
{
async get() {
const { start, end, sort, 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 = { ts: sort?.ts ? sort.ts : -1 };

const { cursor, totalCount } = ServerEvents.findPaginated(
{
...(settingId && { 'data.key': 'id', 'data.value': settingId }),
...(actor && { 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,
});
},
},
);
81 changes: 80 additions & 1 deletion apps/meteor/tests/end-to-end/api/settings.ts
Comment thread
gabriellsh marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { LoginServiceConfiguration } from '@rocket.chat/core-typings';
import type { IServerEvents, LoginServiceConfiguration } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { before, describe, it, after } from 'mocha';

Expand Down Expand Up @@ -273,4 +273,83 @@ describe('[Settings]', () => {
.end(done);
});
});

describe('/audit.settings', () => {
const formatDate = (date: Date) => date.toISOString().slice(0, 10).replace(/-/g, '/');

it('should return list of settings changed (no filters)', async () => {
void request
.get(api('audit.settings'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('events').and.to.be.an('array');
});
});

it('should return list of settings between date ranges', async () => {
const startDate = new Date();
const endDate = new Date();
endDate.setDate(startDate.getDate() + 1);

void request
.get(api('audit.settings'))
.query({ start: formatDate(startDate), end: formatDate(endDate) })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('events').and.to.be.an('array');
});
});

it('should return list of settings changed filtered by an actor', async () => {
void request
.get(api('audit.settings'))
.query({ actor: { type: 'user' } })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('events').and.to.be.an('array');
});
});

it('should return list of changes of an specific setting', async () => {
void request
.get(api('audit.settings'))
.query({ settingId: 'Site_Url' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('events').and.to.be.an('array');
res.body.events.find(
(event: IServerEvents['settings.changed']) => event.data[0].key === 'id' && event.data[0].value === 'Site_Url',
);
});
});

it('should return list of changes of an specific setting filtered by an actor between date ranges', async () => {
const startDate = new Date();
const endDate = new Date();
endDate.setDate(startDate.getDate() + 1);

void request
.get(api('audit.settings'))
.query({ actor: { type: 'user' }, start: formatDate(startDate), end: formatDate(endDate) })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('events').and.to.be.an('array');
});
});
});
});
3 changes: 3 additions & 0 deletions packages/rest-typings/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import type { PresenceEndpoints } from './v1/presence';
import type { PushEndpoints } from './v1/push';
import type { RolesEndpoints } from './v1/roles';
import type { RoomsEndpoints } from './v1/rooms';
import type { ServerEventsEndpoints } from './v1/server-events';
import type { SettingsEndpoints } from './v1/settings';
import type { StatisticsEndpoints } from './v1/statistics';
import type { SubscriptionsEndpoints } from './v1/subscriptionsEndpoints';
Expand Down Expand Up @@ -101,6 +102,7 @@ export interface Endpoints
AuthEndpoints,
ImportEndpoints,
VoipFreeSwitchEndpoints,
ServerEventsEndpoints,
DefaultEndpoints {}

type OperationsByPathPatternAndMethod<
Expand Down Expand Up @@ -253,6 +255,7 @@ export * from './v1/users/UsersUpdateParamsPOST';
export * from './v1/users/UsersCheckUsernameAvailabilityParamsGET';
export * from './v1/users/UsersSendConfirmationEmailParamsPOST';
export * from './v1/moderation';
export * from './v1/server-events';

export * from './v1/autotranslate/AutotranslateGetSupportedLanguagesParamsGET';
export * from './v1/autotranslate/AutotranslateSaveSettingsParamsPOST';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { IAuditServerActor } from '@rocket.chat/core-typings';
import Ajv from 'ajv';

import type { PaginatedRequest } from '../../helpers/PaginatedRequest';

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

export type ServerEventsAuditSettingsParamsGET = PaginatedRequest<{
start?: string;
end?: string;
settingId?: string;
actor?: IAuditServerActor;
}>;

const ServerEventsAuditSettingsParamsGetSchema = {
type: 'object',
properties: {
sort: {
type: 'string',
nullable: true,
},
count: {
type: 'number',
nullable: true,
},
offset: {
type: 'number',
nullable: true,
},
start: {
type: 'string',
nullable: true,
},
end: {
type: 'string',
nullable: true,
},
settingId: {
type: 'string',
nullable: true,
},
actor: {
type: 'object',
nullable: true,
properties: {
type: {
type: 'string',
nullable: true,
},
_id: {
type: 'string',
nullable: true,
},
username: {
type: 'string',
nullable: true,
},
ip: {
type: 'string',
nullable: true,
},
useragent: {
type: 'string',
nullable: true,
},
reason: {
type: 'string',
nullable: true,
},
},
},
},
additionalProperties: false,
};

export const isServerEventsAuditSettingsProps = ajv.compile<ServerEventsAuditSettingsParamsGET>(ServerEventsAuditSettingsParamsGetSchema);
2 changes: 2 additions & 0 deletions packages/rest-typings/src/v1/server-events/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './ServerEventsAuditSettingsParamsGET';
export * from './server-events';
12 changes: 12 additions & 0 deletions packages/rest-typings/src/v1/server-events/server-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { IServerEvents } from '@rocket.chat/core-typings';

import type { ServerEventsAuditSettingsParamsGET } from './ServerEventsAuditSettingsParamsGET';
import type { PaginatedResult } from '../../helpers/PaginatedResult';

export type ServerEventsEndpoints = {
'/v1/audit.settings': {
GET: (params: ServerEventsAuditSettingsParamsGET) => PaginatedResult<{
events: IServerEvents['settings.changed'][];
}>;
};
};