Skip to content

fix(api): guard the audit action/entityType vocabulary against drift - #439

Merged
mforce merged 2 commits into
mainfrom
fix/258-audit-vocabulary-coverage
Aug 7, 2026
Merged

mforce merged 2 commits into
mainfrom
fix/258-audit-vocabulary-coverage

Conversation

@mforce

@mforce mforce commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Closes #258.

Problem

The SPA's audit-log action filter and entity-type labels are driven by two hand-maintained lists in web/src/i18n/enums.ts (AUDIT_ACTION_VALUES, ENTITY_TYPE_VALUES). Nothing kept them in sync with what the server actually emits — #247 was the symptom (the client list had fallen 3 codes behind), caught by manual review, not a gate.

Fix

Actions — hoisted every inline action string literal at every audit.WriteAsync(...) call site into a new AuditActions constants class (same shape as #231's validator-errorCode registry), so the vocabulary is reflectable rather than re-derived by grepping call sites — which can't work in general, since IdentityProvider.ResetPasswordAndRevokeAsync takes its action as a parameter (two callers, two different actions), so the literal doesn't live at the WriteAsync call itself.

Entity types — no such indirection exists today (every call site is nameof(Type) or a bare literal), so AuditVocabularyCoverageTests walks every audit.WriteAsync(...) call site directly (found by scanning every .cs file under src/, not a maintained project list).

Three guard facts:

  • Every call site's action argument is a direct AuditActions.X reference or a two-branch ternary of them — never an inline literal — with one explicit, line-pinned exception for the ResetPasswordAndRevokeAsync indirection (moving that call even one line, or a new dynamic call site anywhere, fails the test).
  • AuditActions's reflected fields are a 1:1 match with AUDIT_ACTION_VALUES.
  • The observed entityType values are a 1:1 match with ENTITY_TYPE_VALUES.

No current drift — the 32 actions and 14 entity types found in source already matched the client lists exactly; this is prevention, not a fix to an existing gap.

Verification

  • Build 0 warnings / 0 errors.
  • 117 Cluckwork.Application.Tests green (114 existing + 3 new), 303 Cluckwork.Domain.Tests green, 37 non-Docker Cluckwork.Api.IntegrationTests green (this environment has no Docker for the Testcontainers-backed suite — relies on CI).
  • Mutation-checked: a reverted call site (raw literal), an extra unregistered AuditActions field, a bogus entityType literal, and a removed exception-list entry each independently made the relevant guard fail with a clear message; all four restored to green.
  • One honest note from development: the scanner's own regex initially matched a mention of audit.WriteAsync( inside this file's own doc comments (a real false positive, caught immediately). Fixed by matching against a comment-stripped view rather than relying on that exact phrase never appearing in prose again.

Docs

No GLOSSARY/Help-page change — internal audit-vocabulary tooling, zero product-facing behavior change (no web/ files touched, no audit action/entityType renamed).


Generated by Claude Code

…258)

Root-cause follow-up to #247, which fixed the symptom (the SPA's
AUDIT_ACTION_VALUES/ENTITY_TYPE_VALUES had fallen behind the server)
without adding anything to stop it recurring.

Hoists every audit.WriteAsync(...) action string literal into a new
AuditActions constants class (mirrors #231's validator-errorCode
registry), and adds AuditVocabularyCoverageTests:

- Every audit.WriteAsync( call site under src/ is walked (found by
  scanning every .cs file, not a maintained project list) and its
  action argument is asserted to be a direct AuditActions.X
  reference or a two-branch ternary of them -- never an inline
  literal, with one explicit, line-pinned exception for
  IdentityProvider's ResetPasswordAndRevokeAsync (its action is a
  parameter because it has two callers, each of which now passes an
  AuditActions member).
- AuditActions' reflected fields are asserted to be a 1:1 match with
  web/src/i18n/enums.ts's AUDIT_ACTION_VALUES.
- The observed entityType arguments (nameof(Type) or a bare literal
  at every call site -- no indirection exists for this dimension
  today) are asserted to be a 1:1 match with ENTITY_TYPE_VALUES.

Mutation-checked: a reverted call site, an extra registry entry, a
bogus entityType literal, and a removed exception-list entry each
independently make the guard fail with a clear message; all four
restored to green. A false-positive hit during development is worth
recording -- the scanner's own regex initially matched a mention of
"audit.WriteAsync(" inside this file's doc comments, so call sites
are now found against a comment-stripped view rather than trusting
that phrase never appears in prose again.

No current drift: the 32 actions and 14 entity types found in source
already matched the client lists exactly.
@mforce

mforce commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

@codex review


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0d3999d20d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

- Match every IAuditWriter-typed receiver, not just the literal `audit`
  variable name, when scanning for .WriteAsync(...) call sites — a
  differently-named injection would previously slip past the guard
  undetected. Mutation-tested by renaming a handler's field and reverting
  its call to a raw literal; confirmed caught, then reverted.

- Validate every actual caller of IdentityProvider's
  ResetPasswordAndRevokeAsync (whose action reaches WriteAsync through a
  parameter) instead of trusting the exemption's original manual review of
  its two callers. Distinguishes the method's own declaration from a real
  call site by checking whether '{' follows the argument list.
  Mutation-tested with a fake third caller passing a raw string literal;
  confirmed caught, then reverted.

- Strip comments before parsing the SPA's AUDIT_ACTION_VALUES /
  ENTITY_TYPE_VALUES arrays, so commenting out a removed action doesn't
  still read as present. Mutation-tested by commenting out an entry;
  confirmed the guard reported the drift, then reverted.
@mforce

mforce commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

@codex address that feedback


Generated by Claude Code

@chatgpt-codex-connector

Copy link
Copy Markdown

Summary

Testing

  • ⚠️ dotnet test tests/Cluckwork.Application.Tests/Cluckwork.Application.Tests.csproj --no-restore --filter FullyQualifiedName~AuditVocabularyCoverageTests (the environment does not have the dotnet executable installed).
  • ✅ git status --short --branch (clean working tree on branch work).

View task →

@mforce
mforce merged commit a5603f2 into main Aug 7, 2026
9 checks passed
@mforce
mforce deleted the fix/258-audit-vocabulary-coverage branch August 7, 2026 16:07
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.

Audit vocabulary: validate the client action/entityType list against the server so it can't silently drift (#247 root cause)

2 participants