Skip to content

feat(audit): record permission denials centrally in the auth middleware - #413

Open
clau1902 wants to merge 10 commits into
gotempsh:mainfrom
clau1902:feat/audit-permission-denied
Open

feat(audit): record permission denials centrally in the auth middleware#413
clau1902 wants to merge 10 commits into
gotempsh:mainfrom
clau1902:feat/audit-permission-denied

Conversation

@clau1902

@clau1902 clau1902 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Records PERMISSION_DENIED centrally in AuthMiddleware so authorization-guard 403 responses leave an audit trail without duplicating audit calls across guard macros. Events capture normalized request and credential metadata, support actors without a user ID, and are exposed through the audit-log UI.

This branch is rebased onto the latest main; the previous migration conflict is resolved.

Security and review hardening

  • Bounds attacker-controlled audit User-Agent metadata to 512 UTF-8-safe bytes and stores it only in the dedicated audit column.
  • Builds the retention index concurrently after migrations, with lock/statement timeouts, invalid-index recovery, idempotent startup behavior, and no boot-time write-blocking DDL.
  • Enforces server-side result budgets against real PostgreSQL, MariaDB, MongoDB, and Redis backends while preserving supported PostgreSQL arrays.
  • Makes Redis large collections pageable by offset and cursor, and admits list/set/zset/hash values atomically in Lua before any aggregate payload crosses the wire.
  • Serializes PostgreSQL session timeout changes to prevent concurrent query/count timeout races.
  • Preserves trusted CLI colors after sanitizing untrusted terminal content and fixes the CLI OpenAPI/type drift found during review.
  • Tests the real audit-filter builder and renders the permission-denial row through the React component dispatch.

Load justification

Permission-denial audit writes occur only for rejected control-plane requests. Allowed traffic adds no audit write. The partial retention index is built with CREATE INDEX CONCURRENTLY after application boot (or explicitly via temps migrate) so existing audit writes remain available.

Evidence

Permission-denial metadata is UTF-8 safely bounded

cargo test -p temps-auth denial_user_agent_is_utf8_safely_byte_bounded -- --nocapture
cargo test: 1 passed, 378 filtered out (5 suites, 0.00s)

Real PostgreSQL migration/index path is idempotent and creates the partial index

cargo test -p temps-database test_establish_connection_with_migrations -- --nocapture
cargo test: 1 passed, 25 filtered out (1 suite, 4.99s)

Real backend wire budgets and compatibility

cargo test -p temps-query-postgres oversized_first_row_is_rejected_by_real_postgres_query -- --nocapture
cargo test -p temps-query-redis large_set_remains_pageable_by_offset -- --nocapture
cargo test -p temps-query-mongodb real_mongodb_enforces_wire_budget_and_preserves_documents -- --nocapture
cargo test -p temps-providers real_mariadb_enforces_wire_budget_and_preserves_rows -- --nocapture
PostgreSQL: cargo test: 1 passed, 67 filtered out (1 suite, 1.19s)
Redis:      cargo test: 5 passed (1 suite, 0.78s)
MongoDB:    cargo test: 1 passed, 9 filtered out (1 suite, 2.18s)
MariaDB:    cargo test: 1 passed, 462 filtered out (4 suites, 7.02s)

The PostgreSQL test also creates and reads a text[] row unchanged. The complete Redis crate run verifies a collection larger than the default cell budget returns disjoint offset/cursor pages; incrementally admits list/zset entries; preflights compact set/hash encodings; uses COUNT 1 for scan encodings; rejects oversized aggregate members inside Redis with size-only metadata; enforces the smaller of page/cell budgets; caps page/offset work; and avoids unusable continuation cursors.

Permission-denial recorder aggregation paths

cargo test --lib -p temps-auth permission_denial_recorder::tests -- --nocapture
cargo test: 7 passed, 341 filtered out (1 suite, 0.13s)

These tests inspect User-Agent aggregation through the dedicated audit-column accessor, while serialized audit JSON remains intentionally free of duplicate attacker-controlled origin metadata.

The retained permission-denial migration is intentionally a no-op compatibility identity for early PR builds; both up() and down() make no schema change. The real PostgreSQL test applies that identity, runs post-migration index maintenance twice, and verifies the resulting partial index. temps migrate now runs the same maintenance even when no schema migrations are pending, while the server retries transient failures with capped backoff.

CLI terminal rendering and command helpers

cd apps/temps-cli
bun run typecheck
bun test src/ui/table.test.ts src/ui/terminal.test.ts src/commands/data/index.test.ts
$ tsc --noEmit
20 pass
0 fail
39 expect() calls
Ran 20 tests across 3 files. [113.00ms]

The table test passes malicious OSC content through the real createTable path and proves it is removed while the trusted color callback remains.

Audit-log UI wiring and rendered component dispatch

cd web
bunx tsc --noEmit
bun test src/pages/AuditLogs.test.ts src/components/audit/AuditLogItem.test.ts
5 pass
0 fail
7 expect() calls
Ran 5 tests across 2 files. [265.00ms]

The runtime render test dispatches PERMISSION_DENIED through AuditLogItemRow and asserts its rendered permission, operation, and Authentication category; the filter test calls the same builder used by AuditLogs.

Workspace compile and formatting

cargo fmt --all -- --check
cargo check --lib
git diff --check
cargo fmt: clean
cargo build: 0 errors; no changed-code warnings
git diff --check: clean

@dviejokfs
dviejokfs force-pushed the feat/audit-permission-denied branch from ba3aad4 to 9437e89 Compare August 6, 2026 14:42
@dviejokfs
dviejokfs force-pushed the feat/audit-permission-denied branch from 9437e89 to 5258a22 Compare August 7, 2026 12:28
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.

2 participants