fix(auth): aggregate bulk constraint denials into one audit event; ValueTask constraint checks

A bulk command with N denied items wrote N audit events into the bounded,
drop-on-full audit channel, so a low-privilege key could evict every other
pending audit record. Bulk filters now accumulate a ConstraintDenialSummary
(count, constraint names, <=20 sample targets) and write one aggregated
constraint-denied event per command.

The per-item constraint checks are synchronous, so IConstraintEnforcer's
Check* methods now return ValueTask instead of allocating a Task per item.
This commit is contained in:
Joseph Doherty
2026-10-22 06:12:02 -04:00
parent 488649ce99
commit ca0cf19a1f
10 changed files with 400 additions and 62 deletions
+11 -3
View File
@@ -262,9 +262,17 @@ the gateway fails closed.
Non-bulk constraint failures return gRPC `PermissionDenied`. Bulk read
commands preserve input order and return a failed `SubscribeResult` for each
denied item while still forwarding allowed items to the worker. Every denial
adds an `api_key_audit` entry with the key id, command kind, target, and
blocking constraint; secured values and raw credentials are never logged.
denied item while still forwarding allowed items to the worker. A non-bulk
denial adds one `constraint-denied` audit event (canonical `audit_event` table)
with the key id, command kind, target, and blocking constraint. A bulk command
adds **one aggregated** `constraint-denied` event per command, not one per
denied item: its target is `<commandKind>:bulk` and its details carry the
denied and total item counts, the distinct blocking constraint names, a sample
of at most 20 denied targets (`sampleTruncated` says whether more were
denied), and the client correlation id. The audit writer is a bounded channel
that drops on full, so per-item events let a single low-privilege key flush
every other pending audit record with one large bulk command. Secured values
and raw credentials are never logged.
## Scope Catalog