From 6732a1e1df041e2be80610b88a9f85c153e2560e Mon Sep 17 00:00:00 2001 From: Hermes Date: Sat, 22 Aug 2026 16:22:53 -0700 Subject: [PATCH] [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). --- dashcaddy-api/__tests__/admin-invites.test.js | 47 ++++++++++++++++++- dashcaddy-api/routes/auth/admin.js | 5 +- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/dashcaddy-api/__tests__/admin-invites.test.js b/dashcaddy-api/__tests__/admin-invites.test.js index f75aadf..3b8b47a 100644 --- a/dashcaddy-api/__tests__/admin-invites.test.js +++ b/dashcaddy-api/__tests__/admin-invites.test.js @@ -237,4 +237,49 @@ describe('DC-085: link-first admin invites', () => { .send({ email: 'a@x.com', ttlHours: 1 }); expect(res.body.shareText).toContain('expires in 1h'); }); -}); \ No newline at end of file + + 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'); + }); +}); diff --git a/dashcaddy-api/routes/auth/admin.js b/dashcaddy-api/routes/auth/admin.js index 015e00b..cfe6e3e 100644 --- a/dashcaddy-api/routes/auth/admin.js +++ b/dashcaddy-api/routes/auth/admin.js @@ -29,6 +29,7 @@ const platformPaths = require('../../platform-paths'); const { createUserStore } = require('../../src/security/user-store'); const { createInviteStore } = require('../../src/security/invite-store'); 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 { 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. log.warn && log.warn('auth-invite-send', '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'; } } catch (sendErr) { @@ -369,7 +370,7 @@ module.exports = function({ asyncHandler, errorResponse, log, session, dataDir } log.info && log.info('auth', 'invite accepted, user created', { userId: userResult.user.id, - email: userResult.user.email, + email: AuthProvider.maskEmail(userResult.user.email) || '[unmaskable-email]', role: userResult.user.role, inviteId: invite.id, });