Skip to content

fix(admin): attribute email-plane audit rows to an id the column can hold - #92

Open
42-v wants to merge 1 commit into
mainfrom
fix/admin-email-audit-actor
Open

fix(admin): attribute email-plane audit rows to an id the column can hold#92
42-v wants to merge 1 commit into
mainfrom
fix/admin-email-audit-actor

Conversation

@42-v

@42-v 42-v commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Every write to the admin email plane produced no audit row at all.

The four audit calls in email.go passed h.actor(r), which returns a username. audit.audit_log.user_id is UUID, and the bootstrap admin's username is literally "admin" — so the insert failed 22P02. The admin gateway wires a zero flush interval, so the logger runs synchronously and the error came straight back, where every call site discards it as best-effort under #nosec G104. The handler answered 200 and the row simply did not exist.

Of the audit call sites in this package these four were the only ones not passing an id, which is why nothing else in the admin plane showed the symptom. It matters most here: email:write rewrites the body of password-reset and verification mail for every user of an app — the route in this API a phisher would want, and the one whose trail was empty.

actorID returns a.ID and is used at those four sites only. email.go's UpdatedBy and CreatedBy still take actor(): those are display VARCHAR columns that correctly want a username.

The gate that should have caught this could not

TestPrivilegedMutations_AuditAttributeTheActor used wantActor = "adm-actor-1"not UUID-shaped either. So it compared two strings the column cannot hold, and a mock that accepts any string kept it green.

It now uses a real UUID and asserts the shape, which is what makes it a statement about the column rather than about equality, and it covers all four email routes. Restoring the username fails it by name:

audit admin:email_branding_set: actor = "actor" is not UUID-shaped, and
audit.audit_log.user_id is UUID -- this row cannot be stored

Severity note

As of #75 this is degraded attribution rather than silent loss: actorColumns now rescues such a row into metadata as actor_user_id_raw. The column is still blank, so the admin audit view and CountByUser cannot find it by actor — that is the half this fixes.

Verified: go build ./..., go vet, go test -race ./internal/adminapi/, golangci-lint run ./... 0 issues on a cleaned cache, misspell clean.

fix(admin): attribute email-plane audit rows to an id the column can hold

…hold

Every write to the admin email plane produced no audit row at all.

The four audit calls in email.go passed h.actor(r), which returns a username.
audit.audit_log.user_id is UUID, and the bootstrap admin's username is literally
"admin", so the insert failed 22P02. The admin gateway wires a zero flush
interval, so the logger runs synchronously and the error came straight back --
where every call site discards it as best-effort under #nosec G104. The handler
answered 200 and the row simply did not exist.

Of the audit call sites in this package these four were the only ones not
passing an id, which is why nothing else in the admin plane showed the symptom.
It matters most here: email:write rewrites the body of password-reset and
verification mail for every user of an app, which makes it the route in this API
a phisher would want and the one whose trail was empty.

actorID returns a.ID and is used at those four sites only. email.go's UpdatedBy
and CreatedBy still take actor(): those are display VARCHAR columns that
correctly want a username.

The gate that should have caught this could not. Its actor was "adm-actor-1" --
not UUID-shaped either -- so it was comparing two strings the column cannot
hold and a mock that accepts any string kept it green. It now uses a real UUID
and asserts the shape, which is what makes it a statement about the column
rather than about equality, and it covers all four email routes. Restoring the
username fails it by name.

Note this is degraded attribution rather than silent loss as of #75:
actorColumns now rescues such a row into metadata as actor_user_id_raw. The
column is still blank, so the admin audit view and CountByUser cannot find it by
actor, which is the half this fixes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant