Skip to content

graphql: separate prefix authorization from user filters - #3304

Draft
GregorShear wants to merge 3 commits into
masterfrom
greg/prefix-filter-refactor
Draft

graphql: separate prefix authorization from user filters#3304
GregorShear wants to merge 3 commits into
masterfrom
greg/prefix-filter-refactor

Conversation

@GregorShear

@GregorShear GregorShear commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Prefix-scoped GraphQL list queries resolved the caller's authorized prefixes and applied the caller's PrefixFilter in the same pass, so the set handed to SQL was partly a function of caller input. This separates the two.

Authorization from grants alone

authorized_prefixes now answers only the authorization question. It takes required_capabilities — a CapabilitySet whose bits must all be held, replacing the ordered min_capability threshold — and no longer accepts a prefix filter. Parent-pruning is extracted as prune_covered.

filtered_authorized_prefixes returns the grant-derived set alongside the filter's decomposed startsWith/in parts. The two stay separate all the way into SQL, where each resolver ANDs an unconditional ^@ ANY($authorized) scope check against the filter's own clause. A bug in filter handling can therefore only return too few rows, never widen visibility. PrefixFilter::narrow_to_overlap and narrow_to_exact_set are removed.

One shared prefix cap

The four per-resolver MAX_PREFIXES constants (alert_configs, invite_links, service_accounts, storage_mappings) collapse into a single MAX_PREFIXES: usize = 100 in authorized_prefixes. The bound exists because the authorized set is bound into SQL as a text[], and 100 matches the ceiling already enforced on in.

Without an in-memory narrowing pass, the cap applies to the grant-derived set whether or not a filter is present, so the alert-configs regression test now asserts that a caller over the cap is refused both with and without a filter. Error messages name the limit.

Fully attenuated prefixes

UserGrant::reachable_prefixes recorded every reachable node, including ones where delegation attenuation removed all capability bits along the path. Those destinations landed in the prefix closure with an empty capability set. They are now omitted, with a regression test covering an Editor grant that delegates across a TeamAdmin role edge — Editor carries Delegate so the edge is walked, but the two bundles share no bits, so nothing survives the path.

Base of #3292.

@GregorShear
GregorShear force-pushed the greg/prefix-filter-refactor branch from 2d8729d to 565502a Compare August 3, 2026 23:44
authorized_prefixes now answers only the authorization question: min_capability becomes required_capabilities (a CapabilitySet whose bits must all be held, not an ordered threshold), the parent-pruning step is extracted as prune_covered, and the startsWith overlap moves out of the function into PrefixFilter::narrow_to_overlap — the counterpart of narrow_to_exact_set. Both filter modes are approximate narrowing post-passes over the authorized set, chained by filtered_authorized_prefixes, with exactness enforced by each resolver's SQL. Behavior-preserving: overlap filtering commutes with parent-pruning, since any prefix overlapping the filter has all of its ancestors overlapping it too.
@GregorShear
GregorShear force-pushed the greg/prefix-filter-refactor branch from 565502a to f9a9d90 Compare August 3, 2026 23:46
Filters no longer narrow the caller's authorized prefix set in memory. `authorized_prefixes` is built from grants, and `filtered_authorized_prefixes` returns the decomposed `startsWith`/`in` parts for the resolver to bind alongside that set, where SQL ANDs an unconditional `^@ ANY($authorized)` scope check against the filter's own clause. Authorization is never a function of caller input, so a bug in filter handling can only return too few rows, never widen visibility.

`PrefixFilter::narrow_to_overlap` and `narrow_to_exact_set` are removed along with their unit tests. The four per-resolver `MAX_PREFIXES` constants are replaced by a single shared bound of 100 in `authorized_prefixes`: without a narrowing pass the cap applies to the grant-derived set whether or not a filter is present, so it has to accommodate callers who legitimately hold many prefixes. The bound exists because that set is bound into SQL as a `text[]`, and it matches the 100-entry ceiling already enforced on `in`. Error messages now name the limit.

The alert-configs regression test is inverted to match: a caller over the cap is refused both with and without a filter.
@GregorShear GregorShear changed the title graphql: refactor prefix filtering into narrowing post-passes graphql: separate prefix authorization from user filters Aug 5, 2026
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.

1 participant