Skip to content

Commit 1b0ed33

Browse files
authored
feat(medic#9885): adds new meta audit database (medic#9909)
Adds new mechanism to audit document changes, through a new shared library, audit. The library is injected into every call that handles requests (PouchDb fetch and couch-request). When a monitored API is called (POST to create a doc, PUT to update a doc or _bulk_docs), the response is analyzed and entries are added into a new database medic-audit. In medic-audit, each document has an audit doc which contains the last 10 entries of received changes. Each entry includes the rev of the change in question, the user that made the change, the request_id (in case this was a direct client request), the service that made the change (api or sentinel) and the date when the change was registered. When an audit doc has 10 history entries, a rotation mechanism will save the old audit entry in a new doc, and the existing document trail will only contain the new change. Moves getDeployInfo into its own shared library to avoid circular dependencies. closes medic#9885
1 parent 6af6906 commit 1b0ed33

File tree

34 files changed

+1663
-464
lines changed

34 files changed

+1663
-464
lines changed

api/src/controllers/bulk-docs.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,7 @@ const invalidRequest = req => {
2626
};
2727

2828
const interceptResponse = (requestDocs, req, res, response) => {
29-
response = JSON.parse(response);
30-
const formattedResults = bulkDocs.formatResults(requestDocs, req.body.docs, response);
31-
res.json(formattedResults);
29+
return bulkDocs.formatResults(requestDocs, req.body.docs, response);
3230
};
3331

3432
module.exports = {

api/src/db.js

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ const PouchDB = require('pouchdb-core');
22
const logger = require('@medic/logger');
33
const environment = require('@medic/environment');
44
const request = require('@medic/couch-request');
5+
56
PouchDB.plugin(require('pouchdb-adapter-http'));
67
PouchDB.plugin(require('pouchdb-session-authentication'));
78
PouchDB.plugin(require('pouchdb-find'));
89
PouchDB.plugin(require('pouchdb-mapreduce'));
910
const asyncLocalStorage = require('./services/async-storage');
11+
const audit = require('@medic/audit');
1012
const { REQUEST_ID_HEADER } = require('./server-utils');
1113

1214
const { UNIT_TEST_ENV } = process.env;
@@ -73,31 +75,37 @@ if (UNIT_TEST_ENV) {
7375
module.exports[fn] = () => notStubbed(fn);
7476
});
7577
} else {
76-
const fetch = (url, opts) => {
78+
const service = 'api';
79+
environment.setService(service);
80+
81+
const fetchFn = (url, opts) => {
7782
// Adding audit flag (haproxy) Service that made the request initially.
78-
opts.headers.set('X-Medic-Service', 'api');
79-
const requestId = asyncLocalStorage.getRequestId();
80-
if (requestId) {
81-
opts.headers.set(REQUEST_ID_HEADER, requestId);
83+
opts.headers.set('X-Medic-Service', service);
84+
const requestMetadata = asyncLocalStorage.getRequest();
85+
if (requestMetadata.requestId) {
86+
opts.headers.set(REQUEST_ID_HEADER, requestMetadata.requestId);
8287
}
83-
return PouchDB.fetch(url, opts);
88+
return PouchDB.fetch(url, opts).then(response => {
89+
void audit.fetchCallback(url, opts, response, requestMetadata);
90+
return response;
91+
});
8492
};
8593

86-
const DB = new PouchDB(environment.couchUrl, { fetch });
94+
const DB = new PouchDB(environment.couchUrl, { fetch: fetchFn });
8795
const getDbUrl = name => `${environment.serverUrl}/${name}`;
8896

8997
DB.setMaxListeners(0);
9098
module.exports.medic = DB;
91-
module.exports.medicUsersMeta = new PouchDB(`${environment.couchUrl}-users-meta`, { fetch });
92-
module.exports.medicLogs = new PouchDB(`${environment.couchUrl}-logs`, { fetch });
93-
module.exports.sentinel = new PouchDB(`${environment.couchUrl}-sentinel`, { fetch });
94-
module.exports.vault = new PouchDB(`${environment.couchUrl}-vault`, { fetch });
99+
module.exports.medicUsersMeta = new PouchDB(`${environment.couchUrl}-users-meta`, { fetch: fetchFn });
100+
module.exports.medicLogs = new PouchDB(`${environment.couchUrl}-logs`, { fetch: fetchFn });
101+
module.exports.sentinel = new PouchDB(`${environment.couchUrl}-sentinel`, { fetch: fetchFn });
102+
module.exports.vault = new PouchDB(`${environment.couchUrl}-vault`, { fetch: fetchFn });
95103
module.exports.createVault = () => module.exports.vault.info();
96-
module.exports.users = new PouchDB(getDbUrl('_users'), { fetch });
104+
module.exports.users = new PouchDB(getDbUrl('_users'), { fetch: fetchFn });
97105
module.exports.builds = new PouchDB(environment.buildsUrl);
98106

99107
// Get the DB with the given name
100-
module.exports.get = name => new PouchDB(getDbUrl(name), { fetch });
108+
module.exports.get = name => new PouchDB(getDbUrl(name), { fetch: fetchFn });
101109
module.exports.close = db => {
102110
if (!db || db._destroyed || db._closed) {
103111
return;
@@ -112,7 +120,7 @@ if (UNIT_TEST_ENV) {
112120

113121
// Resolves with the PouchDB object if the DB with the given name exists
114122
module.exports.exists = name => {
115-
const db = new PouchDB(getDbUrl(name), { skip_setup: true, fetch });
123+
const db = new PouchDB(getDbUrl(name), { skip_setup: true, fetch: fetchFn });
116124
return db
117125
.info()
118126
.then(result => {

api/src/routing.js

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const auth = require('./auth');
1313
const prometheusMiddleware = require('prometheus-api-metrics');
1414
const rateLimiterMiddleware = require('./middleware/rate-limiter');
1515
const logger = require('@medic/logger');
16+
const audit = require('@medic/audit');
1617
const isClientHuman = require('./is-client-human');
1718

1819
const port = typeof environment.port !== 'undefined' && environment.port !== null ? `:${environment.port}` : '';
@@ -849,12 +850,12 @@ const canEdit = (req, res) => {
849850
.check(req, 'can_edit')
850851
.then(userCtx => {
851852
if (!userCtx || !userCtx.name) {
852-
serverUtils.serverError('not-authorized', req, res);
853+
serverUtils.serverError({ code: 401, message: 'not-authorized' }, req, res);
853854
return;
854855
}
855856
proxyForAuth.web(req, res);
856857
})
857-
.catch(() => serverUtils.serverError('not-authorized', req, res));
858+
.catch((err) => serverUtils.error(err, req, res));
858859
};
859860

860861
const editPath = routePrefix + '*';
@@ -895,14 +896,18 @@ proxyForAuth.on('proxyRes', (proxyRes, req, res) => {
895896
}
896897

897898
copyProxyHeaders(proxyRes, res);
899+
let body = Buffer.from('');
900+
proxyRes.on('data', data => (body = Buffer.concat([body, data])));
898901

899-
if (res.interceptResponse) {
900-
let body = Buffer.from('');
901-
proxyRes.on('data', data => (body = Buffer.concat([body, data])));
902-
proxyRes.on('end', () => res.interceptResponse(req, res, body.toString()));
903-
} else {
904-
proxyRes.pipe(res);
905-
}
902+
proxyRes.on('end', () => {
903+
body = JSON.parse(body.toString());
904+
if (res.interceptResponse) {
905+
body = res.interceptResponse(req, res, body);
906+
}
907+
res.json(body);
908+
909+
audit.expressCallback(req, body, asyncLocalStorage.getRequest());
910+
});
906911
});
907912

908913
proxyForAuth.on('proxyRes', infodoc.update);

api/src/services/async-storage.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ module.exports = {
1212
const localStorage = asyncLocalStorage.getStore();
1313
return localStorage?.clientRequest?.id;
1414
},
15+
getRequest: () => {
16+
const localStorage = asyncLocalStorage.getStore();
17+
return {
18+
user: localStorage?.clientRequest?.userCtx?.name,
19+
requestId: localStorage?.clientRequest?.id,
20+
};
21+
}
1522
};
1623

17-
request.initialize(module.exports, REQUEST_ID_HEADER);
24+
request.setStore(module.exports, REQUEST_ID_HEADER);

api/src/services/deploy-info.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
const environment = require('@medic/environment');
1+
const serverInfo = require('@medic/server-info');
22
const fs = require('fs');
33
const path = require('path');
44
const resources = require('../resources');
55

66
const webappPath = resources.webappPath;
77
const DEPLOY_INFO_OUTPUT_PATH = path.join(webappPath, 'deploy-info.json');
88

9-
const getDeployInfo = () => environment.getDeployInfo();
9+
const getDeployInfo = () => serverInfo.getDeployInfo();
1010

1111
const store = async () => {
1212
const deployInfo = await getDeployInfo();

api/src/services/setup/upgrade-steps.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const viewIndexerProgress = require('./view-indexer-progress');
33
const upgradeLogService = require('./upgrade-log');
44
const viewIndexer = require('./view-indexer');
55
const logger = require('@medic/logger');
6-
const environment = require('@medic/environment');
6+
const serverInfo = require('@medic/server-info');
77
const startupLog = require('./startup-log');
88

99
/**
@@ -21,7 +21,7 @@ const finalize = async () => {
2121
await upgradeUtils.deleteStagedDdocs();
2222
await upgradeLogService.setFinalized();
2323
await upgradeUtils.cleanup();
24-
await environment.getDeployInfo(true);
24+
await serverInfo.getDeployInfo(true);
2525
};
2626

2727
/**

api/tests/mocha/controllers/bulk-docs.spec.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,16 +81,16 @@ describe('Bulk Docs controller', () => {
8181
docs: ['filteredDocs'],
8282
new_edits: 'something'
8383
};
84-
controller._interceptResponse(['requestDocs'], testReq, testRes, JSON.stringify(['results']));
84+
const newResponse = controller._interceptResponse(['requestDocs'], testReq, testRes, ['results']);
8585
service.formatResults.callCount.should.equal(1);
8686
service.formatResults.args[0].should.deep.equal([
8787
['requestDocs'],
8888
['filteredDocs'],
8989
['results']
9090
]);
9191

92-
testRes.json.callCount.should.equal(1);
93-
testRes.json.args[0].should.deep.equal([['formatted', 'results']]);
92+
newResponse.should.deep.equal(['formatted', 'results']);
93+
testRes.json.callCount.should.equal(0);
9494
});
9595
});
9696

api/tests/mocha/db.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@ describe('db', () => {
413413
json: sinon.stub().resolves({ result: true }),
414414
ok: true,
415415
});
416-
sinon.stub(asyncLocalStorage, 'getRequestId').returns('the_id');
416+
sinon.stub(asyncLocalStorage, 'getRequest').returns({ requestId: 'the_id' });
417417
db = rewire('../../src/db');
418418

419419
await db.medic.info();

api/tests/mocha/services/async-storage.spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ describe('async-storage', () => {
88
let service;
99

1010
beforeEach(() => {
11-
sinon.stub(request, 'initialize');
11+
sinon.stub(request, 'setStore');
1212
});
1313

1414
afterEach(() => {
@@ -17,7 +17,7 @@ describe('async-storage', () => {
1717

1818
it('should initialize async storage and initialize couch-request', async () => {
1919
service = rewire('../../../src/services/async-storage');
20-
expect(request.initialize.args).to.deep.equal([[
20+
expect(request.setStore.args).to.deep.equal([[
2121
service,
2222
serverUtils.REQUEST_ID_HEADER
2323
]]);

api/tests/mocha/services/deploy-info.spec.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const rewire = require('rewire');
44

55
const fs = require('fs');
66

7-
const environment = require('@medic/environment');
7+
const serverInfo = require('@medic/server-info');
88
const resources = require('../../../src/resources');
99

1010
let service;
@@ -29,25 +29,25 @@ describe('deploy info', () => {
2929
time: 'human readable',
3030
};
3131

32-
sinon.stub(environment, 'getDeployInfo').resolves(deployInfo);
32+
sinon.stub(serverInfo, 'getDeployInfo').resolves(deployInfo);
3333

3434
const result = await service.get();
3535

3636
expect(result).to.deep.equal(deployInfo);
37-
expect(environment.getDeployInfo.callCount).to.equal(1);
37+
expect(serverInfo.getDeployInfo.callCount).to.equal(1);
3838
});
3939

4040
it('should work with undefined deploy info and build info', async () => {
4141
const deployInfo = { version: '20000' };
4242

43-
sinon.stub(environment, 'getDeployInfo').resolves(deployInfo);
43+
sinon.stub(serverInfo, 'getDeployInfo').resolves(deployInfo);
4444

4545
expect(await service.get()).to.deep.equal(deployInfo);
4646
});
4747

4848
it('should throw error when getDeployInfo fails', async () => {
4949
const error = { error: 'whatever' };
50-
sinon.stub(environment, 'getDeployInfo').rejects(error);
50+
sinon.stub(serverInfo, 'getDeployInfo').rejects(error);
5151

5252
try {
5353
await service.get();
@@ -71,7 +71,7 @@ describe('deploy info', () => {
7171
version: '4.10.0-beta.1',
7272
};
7373

74-
sinon.stub(environment, 'getDeployInfo').resolves(deployInfo);
74+
sinon.stub(serverInfo, 'getDeployInfo').resolves(deployInfo);
7575

7676
await service.store();
7777

0 commit comments

Comments
 (0)