Skip to content

fix(audit): attribute admin actions to caller + log old->new diffs (#21 follow-up)#3245

Merged
houko merged 2 commits into
mainfrom
followup/21-audit-attribution-and-diffs
Apr 26, 2026
Merged

fix(audit): attribute admin actions to caller + log old->new diffs (#21 follow-up)#3245
houko merged 2 commits into
mainfrom
followup/21-audit-attribution-and-diffs

Conversation

@houko

@houko houko commented Apr 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Audit follow-up for review item #21 (cross-cutting audit-attribution bugs). Two problems fixed across budget mutation endpoints:

  1. Caller attribution: Rows used to either omit the caller (update_budget, update_agent_budget emitted no audit at all) or, where the row was emitted, lose the diff context.
  2. Old → new diff: update_user_budget previously logged only the forward state (hourly=X daily=Y monthly=Z alert=A). Forensics had to correlate multiple rows to see what was rotated.

Both are now self-describing in a single row.

Call sites changed

File:line Handler Change
crates/librefang-api/src/routes/budget.rs:331 update_budget (global PUT /api/budget) Added audit emission with caller attribution + diff (was emitting nothing)
crates/librefang-api/src/routes/budget.rs:505 update_agent_budget (PUT /api/budget/agents/{id}) Added audit emission with caller attribution + diff (was emitting nothing)
crates/librefang-api/src/routes/budget.rs:849 update_user_budget (PUT /api/budget/users/{user}) Replaced forward-only state string with old→new diff
crates/librefang-api/src/routes/budget.rs:1011 delete_user_budget (DELETE /api/budget/users/{user}) Records the cap that was cleared, not just "cleared"

The require_admin / require_admin_for_user_budget paths in routes/audit.rs, routes/authz.rs, and routes/budget.rs already pass Some(u.user_id) for the user_id field — agent_id="system" there is correct because the action wasn't an agent invocation, it was a request denial. Left those alone per task notes.

Audit detail samples

Before (update_user_budget):

user_budget updated for Alice: hourly=5 daily=10 monthly=100 alert=0.8

Reader cannot tell what changed without correlating with the previous row.

After (update_user_budget, second PUT bumping hourly 1.0 → 5.0):

user_budget updated for Alice: hourly: 1→5 daily: 10→10 monthly: 100→100 alert: 0.8→0.8

After (update_user_budget, first-time configuration):

user_budget updated for Alice: hourly: none→1 daily: none→10 monthly: none→100 alert: none→0.8

After (delete_user_budget):

user_budget cleared for Alice: hourly: 5→none daily: 10→none monthly: 100→none alert: 0.8→none

After (update_budget global):

global_budget updated: hourly: 0→2 daily: 0→20 monthly: 0→200 alert: 0.8→0.8 tokens/h_default: 0→0

After (update_agent_budget):

agent_budget updated for {agent-uuid}: hourly: 1→5 daily: 10→10 monthly: 100→100 tokens/h: none→1000

Implementation notes

  • Diff helpers (fmt_user_budget_diff, fmt_agent_resources_diff, fmt_global_budget_diff) live at the top of crates/librefang-api/src/routes/budget.rs. None renders as the literal none so a missing cap is distinguishable from 0.0 (which means "explicitly unlimited").
  • f64 default Display is used (no rounding); 1.0_f64.to_string() == "1".
  • Hash-chain integrity is preserved — only the detail string changed; the chain re-hashes the entire entry.
  • The agent_id field in update_agent_budget's audit row is the target agent (the one whose cap is being mutated), not the actor. The actor is in user_id. This matches the existing convention used by other agent-scoped writes.

Test plan

  • test_user_budget_put_audit_records_old_new_diff_and_caller (new): two sequential PUTs to /api/budget/users/Alice, then queries /api/audit/query?action=ConfigChange and asserts:
    • first PUT row contains hourly: none→1 and alert: none→0.8
    • second PUT row contains hourly: 1→5
    • user_id field is the calling admin's UUID (Alice)
    • agent_id field is "system" (config mutations are not agent-scoped)
    • detail is prefixed "user_budget updated for " for grep-friendly forensics
  • CI runs cargo build --workspace --lib, cargo test --workspace, cargo clippy --workspace --all-targets -- -D warnings (skipped locally per repo policy)

Linked review item

Follow-up to review item #21 from the audit-attribution review.

… follow-up)

Audit rows for budget mutations now carry the caller's user_id as
attribution and an old->new diff per field, so a single forensics row
is self-describing instead of requiring a multi-row correlation walk.

- update_user_budget: detail now '... hourly: 1->5 daily: 10->10 ...'
  with old=None rendered as 'none' (first-time configuration)
- delete_user_budget: detail now '... hourly: 5->none ...' (records
  what was actually cleared, not just 'cleared')
- update_agent_budget: previously emitted no audit row at all; now
  emits ConfigChange with old->new diff and caller attribution
- update_budget (global): previously emitted no audit row at all; now
  emits ConfigChange with old->new diff and caller attribution

Diff helpers fmt_user_budget_diff / fmt_agent_resources_diff /
fmt_global_budget_diff colocated at the top of routes/budget.rs.

Hash-chain integrity preserved (we only changed the detail string;
the chain re-hashes the entire entry).

Test test_user_budget_put_audit_records_old_new_diff_and_caller pins
the new format end-to-end via /api/audit/query.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@github-actions github-actions Bot added ready-for-review PR is ready for maintainer review size/L 250-999 lines changed labels Apr 26, 2026
@houko
houko merged commit ba5871d into main Apr 26, 2026
20 checks passed
@houko
houko deleted the followup/21-audit-attribution-and-diffs branch April 26, 2026 14:57
@github-actions github-actions Bot removed the ready-for-review PR is ready for maintainer review label Apr 26, 2026
@houko houko mentioned this pull request Apr 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/L 250-999 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant