fix(storage): apply all audit log filters on cassandra and couchbase - #802
Open
lakhansamani wants to merge 3 commits into
Open
lakhansamani wants to merge 3 commits into
lakhansamani wants to merge 3 commits into
Conversation
ListAuditLogRequest advertises resource_type, resource_id, from_timestamp and to_timestamp. Cassandra/ScyllaDB and Couchbase built a WHERE clause from action and actor_id only, silently returning unfiltered rows — an auditor narrowing to one resource or one time window read the result as authoritative. - cassandra: index resource_type and resource_id; ALLOW FILTERING only when a created_at bound or a second restriction is present, so indexed-equality queries keep their index-served plan - cassandra: probe every indexed column for readiness, not just actor_id — Scylla builds indexes concurrently, so the last one created is not the last one ready - couchbase: apply all four as named N1QL parameters Verified by running the new subtests against both backends before the fix (all three fail) and after (pass).
Two indexed equalities with no ALLOW FILTERING are rejected outright by Scylla: InvalidRequest: Cannot execute this query as it might involve data filtering [...] use ALLOW FILTERING That is the shape the pre-fix builder emitted, so _audit_logs(action:, actor_id:) already errored on Scylla before this branch — a second latent bug, invisible because no test combined two filters. Single-column equality is unaffected and still index-served.
- cassandra: check scanner.Err() in ListAuditLogs. A scan dying part-way ends Next() normally, so the call returned a truncated audit page with a nil error while Total reported the real count. Newly reachable now that filters can produce ALLOW FILTERING scans. DeleteAuditLogsBefore in the same file already did this. - cassandra: split the index-wait budget per column. One shared deadline let a slow first index consume it all, after which the remaining columns were never probed — turning a race on one column into a race on every other. - cassandra: extract pollUntilQuerySucceeds so the backoff cannot drift between the single- and multi-column waits. - couchbase: add GSIs for resource_type and resource_id. Without them the new predicates fall back to the primary-index scan while cassandra serves them from an index — the cross-provider parity bug AGENTS.md forbids. Verified via EXPLAIN that the planner picks AuditLogResourceIdIndex with no PrimaryScan. - correct two comments that still described a single actor_id probe, and one that sized ALLOW FILTERING for the timestamp range only when it also covers any two equality filters.
This was referenced Oct 2, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
ListAuditLogRequestadvertises six filters. Two backends built theirWHEREclause fromactionandactor_idonly:actionactor_idresource_typeresource_idfrom/to_timestampThe call returns rows and no error, so a caller cannot tell a filter was dropped. An auditor narrowing
_audit_logsto one resource or one time window reads an unfiltered result as authoritative.Second, previously unknown bug. On Scylla, two indexed equalities with no
ALLOW FILTERINGare rejected outright:That is exactly the query the pre-fix builder emitted, so
_audit_logs(action:, actor_id:)— the one filter combination Cassandra did support — already errored on Scylla. It went unnoticed because no test combined two filters.Root cause for both:
admin_audit_rest_test.goandadmin_audit_grpc_test.goonly ever passaction.Changes
resource_typeandresource_id; all six filters applied.ALLOW FILTERINGadded only when acreated_atbound or a second restriction is present — single-column equality keeps its index-served plan (verified directly via cqlsh).waitForCassandraIndexesprobes every indexed column, not justactor_id. Scylla builds secondary indexes concurrently, so the last created is not the last ready; without this the new indexes are a startup race.testAuditLogOperations, so every backend inTEST_DBSis covered.Verification
Run on real containers, not inferred.
resource_idgot 4 rows,resource_type5,timestamp_range2 — all expected 1make test)go build ./...andgo vet ./...clean;gofmtclean on touched files.The
ALLOW FILTERINGrule was verified rather than assumed: narrowing it to timestamp-bounds-only makes the combined-equality subtest fail on Scylla, and restoring it makes it pass.Lint
CI's
Lint (golangci-lint)job passes on this branch, along withGo tests (SQLite),govulncheckandRelease smoke.An earlier revision of this description claimed
make lintwas red onmainwith 7 pre-existing issues. That was wrong and is retracted. The repo pins golangci-lintv2.11.4(Makefile), butGOLANGCI_LINT ?= $(shell command -v golangci-lint)means the pinned version is installed only when none is already on PATH — my local machine hadv2.12.2on go1.27.0, which reports findings the pinned version does not. Those 7 issues are a local-toolchain artifact, not a repo problem.What does survive the correction: the before/after comparison was run with the same binary on both sides, and the issue set is byte-identical except one pre-existing finding shifting line number as added lines pushed it down. This branch introduces no new lint findings on either version.
Notes
ALLOW FILTERINGon the timestamp range is a deliberate trade, marked in the code: the range is on a non-primary-key column so no index can serve it, and this is a rare admin-only query. The upgrade path, if it ever gets hot, is a materialized view keyed on a coarse time bucket.Groundwork for recording before/after state on authorization changes (design: authorizerdev/docs#96) — setting
resource_idon those audit rows is pointless while two backends cannot filter on it.Review round
Both reviews were worked through; two findings were regressions in this branch, one was an AGENTS.md violation.
Fixed
ListAuditLogsnever checkedscanner.Err(). A scan dying part-way endsNext()normally, so it returned a truncated audit page with a nil error whileTotal(a separate query) reported the real count. Pre-existing, but newly reachable: the filters here can now produceALLOW FILTERINGscans, where partial reads are far likelier.DeleteAuditLogsBeforein the same file already did this.returnon expiry, so a slow first index could consume it and the rest were never probed — turning a race on one column into a race on every other. Now split per column.resource_type/resource_id. Cassandra got indexes, Couchbase did not, so the same filter was index-served on one backend and a primary-index scan on the other — the parity bug AGENTS.md explicitly forbids. Added, and verified byEXPLAIN: the planner picksAuditLogResourceIdIndex, noPrimaryScan.pollUntilQuerySucceedsso the backoff cannot drift between the single- and multi-column waits.actor_idprobe, and one that sized theALLOW FILTERINGtrade for the timestamp range only when it also covers any two equality filters.Not fixed, deliberately
resource_typeindex creates a large MV partition per value. True, but the pre-existingactionindex has the same shape and is worse (user.login_successis the hottest value on a high-write table), as doescreated_at. Redesigning the audit indexing strategy is not a bug-fix PR's job. Tracked in Review audit_log secondary-index strategy on Cassandra/Scylla (low-cardinality hotspots) #805.Pre-existing bug found, reported not fixed
waitForCassandraSecondaryIndex(provider.go:646) probes with... ALLOW FILTERING. That hint makes the query succeed whether or not the index exists, so the function returns on its first attempt and never waits. Verified on Scylla against a deliberately non-indexed column:All 13 of its callers therefore have a no-op index wait. The fix is to drop the hint from the probe, but that would make 13 startup waits genuinely block — a real latency change deserving its own PR and testing. Tracked in #803. The new
waitForCassandraIndexesin this PR deliberately omits the hint for exactly this reason, now commented.CI gap
Confirmed: no workflow in
.github/workflows/references any non-SQLite backend. CI runs Lint, govulncheck, Go tests (SQLite) and Release smoke only. AGENTS.md step 4 requires a non-SQL backend run for storage changes, and nothing enforces it — which is the structural reason both bugs in this PR shipped. Everything here was verified locally against real ScyllaDB and Couchbase containers. Tracked in #804.