feat(audit): attribute MP writes to acting user via session context#64
Merged
Conversation
MP write APIs were not receiving $userId, so every Contact_Log and Contacts
write was recorded in MP's audit log under the OAuth integration account
rather than the user who performed the action. The MPHelper layer supported
$userId on create/update/delete; the gap was at the call sites.
- Resolve MP User_ID in customSession from session.user.userGuid via a
process-wide cache, attach to session.user.userId. Failures are logged
but never block session creation.
- New SessionContextService.getActingUserIdForWrite({ table, operation })
returns the userId or null. When null, emits a structured
mp.write.non_user warn so anonymous/system writes are visible in
production logs without hard-failing the request (the app may serve
unauthenticated users in the future).
- ContactLogService.createContactLog / updateContactLog / deleteContactLog
and ContactService.updateContact now resolve and forward $userId.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
createTableRecords/updateTableRecords/deleteTableRecords) were not receiving$userId, so everyContact_LogandContactswrite was attributed to the OAuth integration account in MP's audit log rather than the user who performed the action. The MPHelper layer supported$userId; the gap was at the call sites.Changes
Session enrichment (
src/lib/auth.ts)customSessionnow resolves the acting user's MPUser_IDfromsession.user.userGuidvia a process-wideuserGuid → User_IDcache and attaches it assession.user.userId. Lookup failures are logged but never block session creation — the JWT cookie cache means the resolution happens at most once per (user × container).New
SessionContextService(src/services/sessionContextService.ts)getCurrentUserId()— pure read.getActingUserIdForWrite({ table, operation })— returns the resolvedUser_IDornull, and whennullemits a structuredmp.write.non_userwarn to logs so anonymous / system writes are surfaced in production observability (Vercel, log aggregators) without hard-failing the request.Write call sites wired up
ContactLogService.createContactLog/updateContactLog/deleteContactLogContactService.updateContactEach resolves
$userIdviaSessionContextService.getActingUserIdForWrite(...)and forwards it to MPHelper only when non-null.Design note: graceful + log, not hard fail
Anonymous writes are treated as legitimate (the app may serve unauthenticated users in the future) but are always observable. The structured log key
event: "mp.write.non_user"is stable on purpose so anyone can grep / alert on unattributed writes.Test plan
npm run lint— cleannpm run test:run— 272 / 272 passing (43 in the touched / new files)sessionContextService.test.tscovers: resolved user → returns id and no warn; no session → null + structured warn with correct table/operation; getSession throws → null + warn; pure-read path never warns.contactLogService.test.tsandcontactService.test.tsupdated to assert{ $userId }is forwarded on the authenticated path and omitted on the anonymous path.dp_Audit_Logthat aContact_Logcreate from a signed-in session shows the user'sUser_IDrather than the integration account (post-deploy).🤖 Generated with Claude Code