Skip to content

Commit 98b2b8e

Browse files
fix(audit): add config flags for IP/User-Agent capture (GDPR) (#3323)
* fix(audit): add config flags for IP/User-Agent capture (GDPR) Closes #3320 * fix(audit): add toHaveBeenCalledTimes assertions, backward-compat test, and MIGRATIONS entry - Add toHaveBeenCalledTimes(1) to all four GDPR flag tests - Add test for undefined captureIp/captureUserAgent (backward compat) - Document audit.captureIp and audit.captureUserAgent in MIGRATIONS.md
1 parent 24a1bec commit 98b2b8e

4 files changed

Lines changed: 83 additions & 6 deletions

File tree

MIGRATIONS.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,31 @@ Breaking changes and upgrade notes for downstream projects.
44

55
---
66

7+
## Audit GDPR Flags (2026-03-26)
8+
9+
New config flags to control IP and User-Agent capture in audit logs for GDPR compliance.
10+
11+
### Configuration
12+
13+
Add to your audit config (e.g. `modules/audit/config/audit.development.config.js`):
14+
15+
```js
16+
audit: {
17+
captureIp: true, // set false to stop storing client IP addresses
18+
captureUserAgent: true, // set false to stop storing User-Agent strings
19+
}
20+
```
21+
22+
Both default to `true` (backward compatible). When set to `false`, the audit log stores an empty string instead of the real value.
23+
24+
### Action for downstream
25+
26+
1. Run `/update-stack` to pull the change
27+
2. Optionally set `captureIp: false` and/or `captureUserAgent: false` in your audit config for GDPR compliance
28+
3. No DB migration needed — existing entries are unaffected
29+
30+
---
31+
732
## Logging & Monitoring (2026-03-26)
833

934
Structured logging, audit trail, Sentry error capture, and enriched health check.

modules/audit/config/audit.development.config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ const config = {
22
audit: {
33
enabled: true,
44
ttlDays: 90,
5+
captureIp: true,
6+
captureUserAgent: true,
57
},
68
};
79

modules/audit/services/audit.service.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ const log = async ({ action, req, targetType, targetId, metadata } = {}) => {
3333
const oid = req.organization?._id || req.organization?.id;
3434
if (uid) entry.userId = String(uid);
3535
if (oid) entry.orgId = String(oid);
36-
entry.ip = req.ip || req.connection?.remoteAddress || '';
37-
entry.userAgent = req.headers?.['user-agent'] || '';
36+
entry.ip = config.audit?.captureIp !== false ? (req.ip || req.connection?.remoteAddress || '') : '';
37+
entry.userAgent = config.audit?.captureUserAgent !== false ? (req.headers?.['user-agent'] || '') : '';
3838
}
3939

4040
try {

modules/audit/tests/audit.service.unit.tests.js

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ jest.unstable_mockModule('../repositories/audit.repository.js', () => ({
1414
},
1515
}));
1616

17-
// Mock config
17+
// Mock config (mutable so individual tests can override flags)
18+
const mockConfig = {
19+
audit: { enabled: true, ttlDays: 90, captureIp: true, captureUserAgent: true },
20+
};
1821
jest.unstable_mockModule('../../../config/index.js', () => ({
19-
default: {
20-
audit: { enabled: true, ttlDays: 90 },
21-
},
22+
default: mockConfig,
2223
}));
2324

2425
// Mock logger to avoid winston config dependency in unit tests
@@ -46,6 +47,8 @@ describe('AuditService unit tests:', () => {
4647

4748
afterEach(() => {
4849
jest.restoreAllMocks();
50+
// Reset config to defaults
51+
mockConfig.audit = { enabled: true, ttlDays: 90, captureIp: true, captureUserAgent: true };
4952
});
5053

5154
test('should log an audit entry with request context', async () => {
@@ -111,4 +114,51 @@ describe('AuditService unit tests:', () => {
111114
await AuditService.list({ action: undefined, userId }, 1, 10);
112115
expect(mockList).toHaveBeenCalledWith({ userId }, 1, 10);
113116
});
117+
118+
// GDPR config flag tests
119+
test('should capture IP when captureIp is true (default)', async () => {
120+
mockConfig.audit = { enabled: true, ttlDays: 90, captureIp: true, captureUserAgent: true };
121+
const req = { ip: '10.0.0.1', headers: { 'user-agent': 'Bot/1.0' } };
122+
await AuditService.log({ action: 'test.ip', req });
123+
expect(mockCreate).toHaveBeenCalledTimes(1);
124+
const arg = mockCreate.mock.calls[0][0];
125+
expect(arg.ip).toBe('10.0.0.1');
126+
});
127+
128+
test('should set IP to empty string when captureIp is false', async () => {
129+
mockConfig.audit = { enabled: true, ttlDays: 90, captureIp: false, captureUserAgent: true };
130+
const req = { ip: '10.0.0.1', headers: { 'user-agent': 'Bot/1.0' } };
131+
await AuditService.log({ action: 'test.ip', req });
132+
expect(mockCreate).toHaveBeenCalledTimes(1);
133+
const arg = mockCreate.mock.calls[0][0];
134+
expect(arg.ip).toBe('');
135+
});
136+
137+
test('should capture User-Agent when captureUserAgent is true (default)', async () => {
138+
mockConfig.audit = { enabled: true, ttlDays: 90, captureIp: true, captureUserAgent: true };
139+
const req = { ip: '10.0.0.1', headers: { 'user-agent': 'Bot/1.0' } };
140+
await AuditService.log({ action: 'test.ua', req });
141+
expect(mockCreate).toHaveBeenCalledTimes(1);
142+
const arg = mockCreate.mock.calls[0][0];
143+
expect(arg.userAgent).toBe('Bot/1.0');
144+
});
145+
146+
test('should set User-Agent to empty string when captureUserAgent is false', async () => {
147+
mockConfig.audit = { enabled: true, ttlDays: 90, captureIp: true, captureUserAgent: false };
148+
const req = { ip: '10.0.0.1', headers: { 'user-agent': 'Bot/1.0' } };
149+
await AuditService.log({ action: 'test.ua', req });
150+
expect(mockCreate).toHaveBeenCalledTimes(1);
151+
const arg = mockCreate.mock.calls[0][0];
152+
expect(arg.userAgent).toBe('');
153+
});
154+
155+
test('should default to capturing IP and User-Agent when config keys are undefined', async () => {
156+
mockConfig.audit = { enabled: true, ttlDays: 90 };
157+
const req = { ip: '192.168.1.1', headers: { 'user-agent': 'DefaultBot/2.0' } };
158+
await AuditService.log({ action: 'test.defaults', req });
159+
expect(mockCreate).toHaveBeenCalledTimes(1);
160+
const arg = mockCreate.mock.calls[0][0];
161+
expect(arg.ip).toBe('192.168.1.1');
162+
expect(arg.userAgent).toBe('DefaultBot/2.0');
163+
});
114164
});

0 commit comments

Comments
 (0)