gristlabs_grist-core/test/server/lib/GristAuditLogger.ts
George Gevoian 3e22b89fa2 (core) Add audit logging machinery
Summary:
Adds machinery to support audit logging in the backend.

Logging is currently implemented by streaming events to external HTTP
endpoints. All flavors of Grist support a default "grist" payload format,
and Grist Enterprise additionally supports an HEC-compatible payload format.

Logging of all audit events will be added at a later date.

Test Plan: Server tests.

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D4331
2024-09-12 12:13:41 -04:00

118 lines
3.3 KiB
TypeScript

import {IAuditLogger} from 'app/server/lib/AuditLogger';
import {LogMethods} from 'app/server/lib/LogMethods';
import {assert} from 'chai';
import moment from 'moment-timezone';
import nock from 'nock';
import * as sinon from 'sinon';
import {TestServer} from 'test/gen-server/apiUtils';
import * as testUtils from 'test/server/testUtils';
describe('GristAuditLogger', function() {
let auditLogger: IAuditLogger;
let oldEnv: testUtils.EnvironmentSnapshot;
let server: TestServer;
before(async function() {
oldEnv = new testUtils.EnvironmentSnapshot();
process.env.GRIST_AUDIT_HTTP_ENDPOINT = 'https://api.getgrist.com/events';
process.env.GRIST_AUDIT_HTTP_AUTHORIZATION_HEADER = 'Grist bb48d1f8-8f6c-4065-8951-8543a8e70597';
server = new TestServer(this);
await server.start();
auditLogger = server.server.getAuditLogger();
});
after(async function() {
await server.stop();
oldEnv.restore();
});
describe('logEventAsync', function() {
const sandbox: sinon.SinonSandbox = sinon.createSandbox();
let logErrorCallArguments: any[] = [];
before(async function() {
sandbox
.stub(LogMethods.prototype, 'error')
.callsFake((...args) => logErrorCallArguments.push([...args]));
});
after(async function() {
sandbox.restore();
});
afterEach(function() {
logErrorCallArguments = [];
nock.cleanAll();
});
it('logs audit events', async function() {
const timestamp = moment().toISOString();
const scope = nock('https://api.getgrist.com')
.matchHeader('Authorization', 'Grist bb48d1f8-8f6c-4065-8951-8543a8e70597')
.post('/events', {
event: {
name: 'createDocument',
user: null,
details: {
id: 'docId',
},
},
timestamp,
})
.reply(200);
await assert.isFulfilled(
auditLogger.logEventAsync(null, {
event: {
name: 'createDocument',
details: {id: 'docId'},
},
timestamp,
})
);
assert.isTrue(scope.isDone());
});
it('throws on failure to log', async function() {
nock('https://api.getgrist.com')
.post('/events')
.reply(404, 'Not found');
await assert.isRejected(
auditLogger.logEventAsync(null, {
event: {
name: 'createDocument',
details: {id: 'docId'},
},
}),
'received a non-200 response from https://api.getgrist.com/events: 404 Not found'
);
});
it('throws if max pending requests exceeded', async function() {
nock('https://api.getgrist.com')
.persist()
.post('/events')
.delay(2000)
.reply(200);
// Queue up enough pending requests to reach the limit (25).
for (let i = 0; i < 25; i++) {
void auditLogger.logEvent(null, {
event: {
name: 'createDocument',
details: {id: 'docId'},
},
});
}
await assert.isRejected(
auditLogger.logEventAsync(null, {
event: {
name: 'createDocument',
details: {id: 'docId'},
},
}),
'exceeded the maximum number of pending audit event calls (25)'
);
nock.abortPendingRequests();
});
});
});