[glm-grade=B] fix(auth): DC-089 mask invite/user emails in server logs
Two log sites in routes/auth/admin.js wrote raw email PII to the server log: the SMTP-unconfigured 'auth-invite-send' warn and the 'invite accepted, user created' info. Both now route through AuthProvider.maskEmail() with a '[unmaskable-email]' sentinel fallback (never the raw address). Two regression tests assert the raw address is absent from log meta and the masked form present. Response contract unchanged (full email still returned to the authenticated admin). Judge: GLM-5.3 cold read (deleg_f0896de3), grade B / ship / zero blockers; polish notes folded in. Verdict URN: urn:ump:jd2htpwq76ni6bapj3vxjpvypfdnoc4argqbzpcerutmh7u5khea Full suite: 2610/2610 (109 suites).
This commit is contained in:
@@ -237,4 +237,49 @@ describe('DC-085: link-first admin invites', () => {
|
|||||||
.send({ email: 'a@x.com', ttlHours: 1 });
|
.send({ email: 'a@x.com', ttlHours: 1 });
|
||||||
expect(res.body.shareText).toContain('expires in 1h');
|
expect(res.body.shareText).toContain('expires in 1h');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('DC-089: SMTP-unconfigured warn log masks the invite email (no raw PII)', async () => {
|
||||||
|
mockEmailSender.isConfigured.mockReturnValueOnce(false);
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/api/v1/auth/admin/invites')
|
||||||
|
.send({ email: 'friend@example.com', role: 'operator', sendEmail: true });
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.deliveredVia).toBe('failed');
|
||||||
|
const warn = logCalls.find(c =>
|
||||||
|
c.level === 'warn' && c.topic === 'auth-invite-send'
|
||||||
|
);
|
||||||
|
expect(warn).toBeDefined();
|
||||||
|
// The raw address must not appear; the masked form must.
|
||||||
|
expect(JSON.stringify(warn.meta)).not.toContain('friend@example.com');
|
||||||
|
expect(warn.meta.email).toBe('fr****@example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('DC-089: invite-accepted info log masks the created user email (no raw PII)', async () => {
|
||||||
|
// Pre-authorize the email (POST /admin/users) so userStore.login doesn't
|
||||||
|
// reject with not_authorized — bootstrap already happened in beforeEach.
|
||||||
|
const preauth = await request(app)
|
||||||
|
.post('/api/v1/auth/admin/users')
|
||||||
|
.send({ email: 'newfriend@example.com' });
|
||||||
|
expect(preauth.status).toBe(200);
|
||||||
|
|
||||||
|
const issue = await request(app)
|
||||||
|
.post('/api/v1/auth/admin/invites')
|
||||||
|
.send({ email: 'newfriend@example.com', role: 'viewer' });
|
||||||
|
expect(issue.status).toBe(200);
|
||||||
|
const token = issue.body.acceptUrl.match(/invites\/([^/]+)\/accept/)[1];
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post(`/api/v1/auth/invites/${token}/accept`)
|
||||||
|
.send({});
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
const info = logCalls.find(c =>
|
||||||
|
c.level === 'info' && c.msg === 'invite accepted, user created'
|
||||||
|
);
|
||||||
|
expect(info).toBeDefined();
|
||||||
|
expect(JSON.stringify(info.meta)).not.toContain('newfriend@example.com');
|
||||||
|
expect(info.meta.email).toBe('ne****@example.com');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -29,6 +29,7 @@ const platformPaths = require('../../platform-paths');
|
|||||||
const { createUserStore } = require('../../src/security/user-store');
|
const { createUserStore } = require('../../src/security/user-store');
|
||||||
const { createInviteStore } = require('../../src/security/invite-store');
|
const { createInviteStore } = require('../../src/security/invite-store');
|
||||||
const emailSender = require('../../src/auth/providers/email-sender');
|
const emailSender = require('../../src/auth/providers/email-sender');
|
||||||
|
const AuthProvider = require('../../src/auth/providers/base');
|
||||||
const { ValidationError, NotFoundError, ForbiddenError, PaymentRequiredError } = require('../../src/utilities/errors');
|
const { ValidationError, NotFoundError, ForbiddenError, PaymentRequiredError } = require('../../src/utilities/errors');
|
||||||
const { ok, successMessage } = require('../../src/utils/responses');
|
const { ok, successMessage } = require('../../src/utils/responses');
|
||||||
|
|
||||||
@@ -254,7 +255,7 @@ module.exports = function({ asyncHandler, errorResponse, log, session, dataDir }
|
|||||||
// server log on every unconfigured-install invite.
|
// server log on every unconfigured-install invite.
|
||||||
log.warn && log.warn('auth-invite-send',
|
log.warn && log.warn('auth-invite-send',
|
||||||
'invite send skipped: SMTP not configured (operator opted in)',
|
'invite send skipped: SMTP not configured (operator opted in)',
|
||||||
{ inviteId: issued.id, email: issued.email });
|
{ inviteId: issued.id, email: AuthProvider.maskEmail(issued.email) || '[unmaskable-email]' });
|
||||||
deliveredVia = 'failed';
|
deliveredVia = 'failed';
|
||||||
}
|
}
|
||||||
} catch (sendErr) {
|
} catch (sendErr) {
|
||||||
@@ -369,7 +370,7 @@ module.exports = function({ asyncHandler, errorResponse, log, session, dataDir }
|
|||||||
|
|
||||||
log.info && log.info('auth', 'invite accepted, user created', {
|
log.info && log.info('auth', 'invite accepted, user created', {
|
||||||
userId: userResult.user.id,
|
userId: userResult.user.id,
|
||||||
email: userResult.user.email,
|
email: AuthProvider.maskEmail(userResult.user.email) || '[unmaskable-email]',
|
||||||
role: userResult.user.role,
|
role: userResult.user.role,
|
||||||
inviteId: invite.id,
|
inviteId: invite.id,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user