feat(authz): shared filter derivation, snapshot decision boundary, and adapter conformance suite - #1483
Draft
tlyyxjz wants to merge 3 commits into
Draft
feat(authz): shared filter derivation, snapshot decision boundary, and adapter conformance suite#1483tlyyxjz wants to merge 3 commits into
tlyyxjz wants to merge 3 commits into
Conversation
- extract resolve_resource_filter derivation shared by both providers (fixes latent parent-type drift between the builtin and Casbin copies) - add RelationalAccessRepository.decision_snapshot: revision, active bindings and ownership read in one transaction; providers use it when offered, else bounded revision-check-and-retry, failing closed when no stable read is obtained (AccessRepository protocol unchanged) - one captured evaluation time per decision for all expiry comparisons - conformance suite: deliberately-interleaving regression for the read-consistency gap, single-transaction pin, fail-closed on unstable revision, point/list agreement and cross-provider filter parity, idempotency ledger conflicts across operations and payloads - no persistent Casbin policy store added (rationale in PR)
…annotations * Annotate decision_snapshot row buffers as Sequence[Mapping[str, Any]] so SQLAlchemy Mapping results type-check. * Make the test doubles nominal subtypes (AsyncDatabase / RelationalAccessRepository) instead of duck-typed wrappers, and hide the decision_snapshot capability via __getattribute__ so the bounded-retry path stays exercised. * Assert policy revision is not None before int() conversion.
ty infers RowMapping keys as a union type and Mapping is invariant in its key type, so Sequence[Mapping[str, Any]] rejects the rows returned by mappings().all(). Mapping[Any, Any] is accepted by both ty and pyright.
Author
|
Note on the CI result: the |
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.
feat(authz): shared filter derivation, snapshot decision boundary, and adapter conformance suite
Draft PR promised in #1398 (comment), following the review answers from @Teingi. Target scope as agreed there: shared filter derivation, a defined snapshot/read boundary with the deliberately-interleaving regression, the conformance extensions, and an explicit capability statement for the native-writer question. Branch is a single draft; every claim below is backed by a test in this PR.
What changed
1. Shared filter derivation — one derivation, zero drift risk.
resolve_resource_filterwas duplicated betweenBuiltinAuthorizationProvider(service.py L395-433) andCasbinAuthorizationProvider(casbin.py L125-159) with a latent semantic difference: the Casbin copy's parent test (_is_parent) accepted a SERVER resource as a parent for any requested type, while the built-in copy (_resource_is_parent+_parent_binding_grants) constrains the child type. Both now call one package-internal derivation (_derive_authorized_resource_filterinservice.py), keeping the built-in (stricter) semantics. The private helpers remain package-internal; nothing is promoted to public adapter API.2. Snapshot decision boundary — the read gap is closed two ways.
The read-consistency gap demonstrated on this thread (revision read at 3, grant commits at 4, bindings evaluated from 4 while the decision is labeled
3) came frompolicy_revision(),active_bindings()and ownership reads running in separate transactions. Both providers now read decision inputs through one boundary (read_decision_stateinservice.py):RelationalAccessRepository.decision_snapshot()reads revision + active bindings + artifact ownership (+ owned resources when needed) inside one transaction; backends with snapshot isolation (SQLite read transactions, OceanBase REPEATABLE READ) supply the consistency. One captured evaluation time is used for every expiry comparison in the decision.AccessUnavailableError("policy-snapshot-unstable")) when no stable read is obtained.AccessRepositoryprotocol is unchanged —decision_snapshotis an optional capability detected by the providers, so existing implementations keep working through the fallback.3. Native-writer capability statement (as promised, measured not asserted).
No second policy store is added. With the composition in this repo — canonical five-table schema as the only writable copy, enforcer materialized from committed state at evaluation time — a persistent Casbin policy table would add storage without a demonstrated need, and this PR contains no workload showing adapter translation falling short. The concrete deltas this draft delivers are the three the review identified: shared derivation, reusable conformance tests, and a defined snapshot boundary. If a large-policy-set workload appears, the same single-writer + revision-equality rule from the earlier storage-layout comment remains the agreed shape for adding one.
4. Conformance suite.
New
tests/test_access_snapshot_boundary.py:test_providers_read_decision_inputs_from_one_snapshot— one transaction percheck/resolve_resource_filter(was 3+), both providers;test_interleaved_mutation_cannot_label_decision_with_stale_revision— the controlled interleaving from the review, replayed: a grant committed between the reads can no longer produceallowed=trueunder the pre-mutation revision, and a stable retry labels it with the matching revision;test_unstable_revision_fails_closed— no stable snapshot within the budget →AccessUnavailableError, never a possibly-stale allow;test_providers_agree_on_point_and_list_for_direct_and_inherited_grants— point/list agreement plus cross-provider filter parity;test_idempotency_ledger_conflicts_across_operations_and_payloads— repository-level, shared by both adapters: changed-payload conflict on the same key, cross-operation conflict, replay-after-revocation returning the revoked binding without resurrection, and post-revocation re-grant via a fresh key.Test evidence
Tests were written first and run against the pre-fix tree; they failed with exactly the gap under review, which is the mutation check for this suite:
After the fix (this PR), full authz suite:
ruff checkandruff formatclean on all touched files.What this draft does not claim
DatabaseConfigabstraction and backend-agnostic, but per the review the same interleaving should be exercised on OceanBase in your CI; I cannot run that from this environment and would rather say so than claim a green I did not produce.load_filtered_policy— untouched; it remains an optional optimization and is not load-bearing for decision correctness or pagination safety.