feat: `audit.settings` endpoint (#35258)

Co-authored-by: Lucas Pelegrino <16467257+lucas-a-pelegrino@users.noreply.github.com>
pull/35531/head
gabriellsh 10 months ago committed by GitHub
parent 38f1c508c9
commit bb4ff0db3d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      .changeset/spicy-kangaroos-hear.md
  2. 3
      apps/meteor/app/api/server/index.ts
  3. 104
      apps/meteor/app/api/server/v1/server-events.ts
  4. 97
      apps/meteor/tests/end-to-end/api/settings.ts
  5. 3
      packages/rest-typings/src/index.ts
  6. 84
      packages/rest-typings/src/v1/server-events/ServerEventsAuditSettingsParamsGET.ts
  7. 2
      packages/rest-typings/src/v1/server-events/index.ts
  8. 12
      packages/rest-typings/src/v1/server-events/server-events.ts

@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/rest-typings": minor
---
Introduces `/v1/audit.settings` endpoint for querying changed settings audit events

@ -48,6 +48,9 @@ import './v1/voip/omnichannel';
import './v1/voip';
import './v1/federation';
import './v1/moderation';
import './v1/server-events';
// This has to come last so all endpoints are registered before generating the OpenAPI documentation
import './default/openApi';
export { API, APIClass, defaultRateLimiterOptions } from './api';

@ -0,0 +1,104 @@
import { ServerEvents } from '@rocket.chat/models';
import { isServerEventsAuditSettingsProps } from '@rocket.chat/rest-typings';
import { ajv } from '@rocket.chat/rest-typings/src/v1/Ajv';
import { API } from '../api';
import { getPaginationItems } from '../helpers/getPaginationItems';
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'],
},
async function action() {
const { start, end, settingId, actor } = this.queryParams;
if (start && isNaN(Date.parse(start as string))) {
return API.v1.failure('The "start" query parameter must be a valid date.');
}
if (end && isNaN(Date.parse(end as string))) {
return API.v1.failure('The "end" query parameter must be a valid date.');
}
const { offset, count } = await getPaginationItems(this.queryParams as Record<string, string | number | null | undefined>);
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 && { actor }),
ts: {
$gte: start ? new Date(start as string) : new Date(0),
$lte: end ? new Date(end as string) : 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,
});
},
);

@ -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';
@ -273,4 +273,99 @@ 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 throw error when sending invalid dates', async () => {
const startDate = new Date();
const endDate = '2025/01';
void request
.get(api('audit.settings'))
.query({ start: formatDate(startDate), end: endDate })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('message', 'The "start" query parameter must be a valid date.');
});
});
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');
});
});
});
});

@ -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';
@ -101,6 +102,7 @@ export interface Endpoints
AuthEndpoints,
ImportEndpoints,
VoipFreeSwitchEndpoints,
ServerEventsEndpoints,
DefaultEndpoints {}
type OperationsByPathPatternAndMethod<
@ -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';

@ -0,0 +1,84 @@
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: 'object',
nullable: true,
properties: {
ts: {
type: 'number',
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);

@ -0,0 +1,2 @@
export * from './ServerEventsAuditSettingsParamsGET';
export * from './server-events';

@ -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'][];
}>;
};
};
Loading…
Cancel
Save