Repository navigation
Add authentication audit logging and metrics - #1465
Conversation
Log login, logout, password change/reset, token refresh and API token generate/revoke on DigiScript.Audit with sanitised usernames, never including secrets, and count them in a bounded Prometheus counter. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Adds user_create, user_update and user_delete audit events and covers the v2 users and token controllers plus v1 user create/delete. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
0872527 to
68ff3fb
Compare
Tim020
left a comment
There was a problem hiding this comment.
Review round 1
Head SHA: 68ff3fb6a87b0ad318404a60ee3fa3600775c98d
Verdict: Changes needed, but non-blocking — the audit-logging/metrics design itself is solid (fixed-set metric labels, sanitised + JSON-quoted usernames, reST docstrings, strong existing test discipline around secret-leakage). The issues below are accuracy/polish and test-coverage gaps, not correctness bugs that break the feature.
Previous round follow-up: N/A — this is round 1.
SonarQube: Quality gate PASSED. All 5 configured conditions pass (new reliability/security/maintainability ratings, 2.5% duplication vs 3% threshold, 100% security hotspots reviewed — no coverage condition is configured in this gate, so the check-run's "0.0% Coverage on New Code" is informational only and doesn't gate the PR). 4 open MINOR/MAJOR code-smell issues were flagged by Sonar's new-code window (python:S7504 ×2 in server/utils/web/app_metrics.py, python:S5906 and python:S8997 in server/test/digi_server/test_logger.py) — none of these files are touched by this PR's diff (base is the stacked branch feature/obs-monitoring-docs), so they're pre-existing debt from an earlier PR in the stack, surfaced here only by Sonar's new-code period. Not actionable in this PR.
New findings: 6 Important, 9 Suggestion. 9 posted inline; 6 couldn't be anchored to a changed line and are listed below.
Not anchored to a diff line:
- Important — Every
audit()call on a success path sits unguarded between the DB commit andself.finish()(e.g.server/controllers/api/v1/auth/user.py:104,244,334,359,477,token.py:28,68, and the v2 equivalents). Ifaudit()ever raised, the handler would 500 even though the real mutation (login, user create/delete, password change, token issue) already succeeded — and foruser_create/user_deleteit would also skip theGET_USERSbroadcast. Not reachable today (every call site passes a hardcoded literal fromEVENTS/RESULTS, andaudit()'s internals can't currently throw), but it's an unguarded single point of failure that will get copied to future call sites. Consider a narrow try/except insideaudit()itself that logs+swallows rather than propagates. - Suggestion —
AdminPasswordResetControllerdoesn't audit two of its refusal branches: "User not found" (server/controllers/api/v1/auth/user.py:523, 404) and "Cannot reset your own password" (:529, 400) — both pre-existing, unchanged by this PR, hence not inline-commentable here.user_delete'sonly_adminrefusal is audited; for parity, consider auditing these too withreason=not_found/reason=self_reset. - Important — No test asserts that
auth_events_total's label set stays restricted to{event, result}(excludesusernameor other per-request values) — the cardinality-safety claim inauth_audit.py's docstring is never directly checked, e.g. viaauth_events_total._labelnamesor two different usernames collapsing to the same series. - Suggestion — The
only_adminfailure branches foruser_delete(v1user.py:145) anduser_update/user_delete(v2users.py:140,198) have no audit-assertion test (plausibly hard to reach given the admin-count guard, but untested either way). - Suggestion — The "first user, no actor" case (
test_auth.pyandtest_users.pybootstrap-create tests) only asserts the message contains"AUDIT user_create success"; it never assertsusername/user_idare absent. A regression that attributed the bootstrap event to a stale actor would pass. - Suggestion —
password_resetfailure viareason=invalid_password(triggered bychange_passwordraisingValueError,user.py:546) has no audit-assertion test; existing reset tests cover not-found/self-reset/missing-id but not this branch.
Strengths: Thorough existing test coverage pairs HTTP-response assertions with assertLogs + Prometheus counter-delta checks at nearly all ~28 call sites, not just status codes; _assert_no_secrets / _no_secrets helpers give a solid regression guard against accidentally logging credentials; the sanitisation design (control-char stripping + truncation + JSON-quoting for usernames, fixed label sets for metrics) is the right shape for this kind of feature; reST docstrings are properly formatted throughout; test_auth_audit.py correctly stays on plain unittest.TestCase since it touches no DB/settings/temp paths.
🤖 Generated with Claude Code
audit() now logs and swallows failures instead of raising, every (event, result) series is pre-initialised, ip is quoted unless a valid address, reset-password refusals are audited, v1 token controllers use actor(), and docs/tests are tightened. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Review round 1 response (fixes in df45bda). pytest 1216 passed, ruff check and format --check clean. Decision for Tim to confirm: on an Unanchored findings
Inline findings
|
…ing conflicts with dev's login hardening LoginHandler now follows dev's throttled flow (no per-username lock, dummy bcrypt for unknown users) with the audit calls re-applied to each outcome, including a new 'reason=throttled' audit line for lockouts. Test imports are the union of both sides, plus a test for the throttled audit line. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Tim020
left a comment
There was a problem hiding this comment.
Review round 2
Head SHA: 55951a1a2c0a0f6c0b0e1486c4cab87d77f5bb91
Verdict: Changes needed — one new, gate-blocking finding (SonarQube duplication) introduced since round 1. Everything from round 1 was fixed, several fixes going beyond what was asked (e.g. the IP-quoting fix parses the address properly instead of just always quoting it; audit() is now fully non-raising with its own test).
Previous round follow-up: All 9 round-1 findings verified fixed in commit df45bda ("Address audit review: non-raising audit(), preinit labels, more tests") — replied inline on each thread, not resolving (left for Tim to close). 5 of the 6 findings that couldn't be anchored to a diff line in round 1 were also fixed in the same commit: the unguarded-audit()-on-success-path architecture issue (now wrapped in try/except, swallows and logs), the admin-password-reset refusal audit gaps (reason=not_found/reason=self_reset both added, with a new test), the missing label-cardinality test (test_label_names_are_bounded + test_usernames_do_not_create_series added), the weak first-user-no-actor assertion (now explicitly asserts username/user_id are absent), and the untested password_reset invalid_password branch (new test added). Still open: the only_admin failure branches for user_delete/user_update remain untested (v1 user.py and v2 users.py) — unchanged since round 1, still a Suggestion-level gap, not reposted.
SonarQube: Quality gate now FAILS (was passing at round 1). Failing condition: new_duplicated_lines_density at 7.8%, required ≤3%. All other conditions (reliability/security/maintainability ratings, hotspots reviewed) remain OK, and there are no new bugs/vulnerabilities/code-smell issues — this is purely a duplication regression. Root cause (confirmed against source at head): the v1/v2 controller pairs for user-create, user-delete's admin-guard, and api-token-revoke were already structurally parallel, and adding near-identical audit() calls to both copies in lockstep (needed for v1/v2 parity, correctly done) pushed the match over the 3% threshold. Posted as 4 inline comments (2 duplicate pairs + their mirrors) with a suggested fix: extract the shared bodies into helpers (e.g. in services/user_service.py for user create/delete, a small shared function for token revoke) that both API versions call.
New findings: 4 Critical (all SonarQube duplication, posted inline), 0 Important, 0 Suggestion. No other new issues — the only genuinely new application code since round 1 (the audit() call for the login-throttle-rejection branch, now auditable as reason=throttled) is correct and well-tested; the login_throttle mechanism itself predates this PR (merged in from a sibling branch) and is out of this PR's own scope.
Strengths: Round 1's response was unusually thorough — every finding fixed, several improved beyond the letter of what was asked, and new tests added for each. The docs are now precise and honest about the one inherent limitation (a mistyped password in the username field gets logged) rather than overclaiming.
🤖 Generated with Claude Code
Move user create/delete and API token generate/revoke (with their audit calls) into utils/web/user_admin.py, called by both API versions. Pure refactor: responses, status codes, audit lines and broadcasts unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Round 2 (fix in bd30551)SonarQube duplication gate: extracted the shared v1/v2 handler bodies (user create, user delete guard and removal, API token generate and revoke, including their audit calls) into server/utils/web/user_admin.py. Both API versions call these helpers. Pure refactor with no test changes: responses, status codes, audit lines and counters are identical and the existing suites are the safety net. Full pytest 1264 passed, ruff check and format --check clean. Login/throttle flow untouched. A difflib check of matching runs of 6+ lines between the v1 and v2 controllers finds 14 matching lines for user.py/users.py (imports and the unrelated GET list handlers) and 8 for token.py (the GET has_token handlers), none in the flagged ranges. Sonar cannot be run locally. The new helpers have no separate tests since the controller tests cover every branch except the existing untested only_admin guards. |
|
Tim020
left a comment
There was a problem hiding this comment.
Review round 3
Head SHA: 9d233de74765c4522b223fcb8e30267af3563b10
Verdict: Ready to merge, as far as this review can tell. No new findings.
Previous round follow-up: All 4 round-2 findings verified fixed in commit bd30551 ("Share v1/v2 user and API-token handler bodies to remove duplication") — replied inline on each thread and resolved all 4. Checked the extraction itself, not just the Sonar number: create_user(), delete_user(), generate_api_token() and revoke_api_token() now live once in the new server/utils/web/user_admin.py and are called from both v1 and v2 controllers, which are reduced to thin wrappers. Confirmed it's a genuinely pure move — logic is unchanged, the one real v1/v2 behavioral difference (v1's "Could not find user to delete" 400 vs v2's "User not found" 404) is correctly kept in each caller rather than accidentally unified, the per-endpoint decorators (@redact_data_paths, @require_admin, @no_live_session, etc.) are untouched, and no unused imports were left behind in the four trimmed-down files. No other findings remain open from round 1 or 2.
SonarQube: Quality gate PASSES. new_duplicated_lines_density is back to 0.0% (required ≤3%) and there are 0 new issues of any kind.
New findings: None. This round's only change beyond the round-2 fix is the new server/utils/web/user_admin.py file itself, which I read in full — docstrings are accurate reST, no secrets handling changed, and the existing v1/v2 test suites (unchanged this round) continue to exercise these functions through both API versions since the controllers now delegate to them.
Strengths: Clean, minimal extraction — the shared module's docstring explains why it exists in one sentence, each function's docstring states its precondition (e.g. delete_user documents that the caller must have already loaded user_to_delete), and the refactor commit message explicitly calls out that it's a pure move with no behavioral change, which the diff bears out.
🤖 Generated with Claude Code



Records login, logout, password change/reset, token refresh, API token generate/revoke (v1 and v2) and user create/update/delete (v1 and v2) on
DigiScript.Audit(INFO success, WARNING failure) with username, user_id and remote_ip, and counts them indigiscript_auth_events_total{event,result}(fixed event set, bounded labels). Failed logins log unknown_user vs bad_password reasons; HTTP responses are unchanged. User management events log the acting admin plustarget_user_id. Usernames are sanitised (control chars stripped, 64 cap, JSON-quoted); secrets are never logged. Docs updated in user_config.md.Tests: full pytest 1176 passed; full Playwright 225 passed (chromium); audit lines inspected in the server log.
🤖 Generated with Claude Code