Documentation
¶
Overview ¶
Package authzfilter resolves per-object READ-visibility for kaname's own read surfaces by asking a DIRECT per-object question — never by enumerating the objects a subject may see. The predicate is RelationsFor(objectType): the relation that gates a single-object read of that type.
Why not an enumeration (this was the bug, and the enumeration is now gone) ¶
Every iam read that filtered by visibility USED TO ask the external relations engine "enumerate ALL objects of this type the subject may see" and then match the resource against that set — read-by-id via membership, List via an `id = ANY(...)` push-down.
The engine bounded that enumeration SERVER-side (default 1000) and exposed NO continuation token, so it silently returned an arbitrary 1000-id prefix. The bound applied to the TYPE IN THE STORE (cluster-wide), not to the tenant: on a long-lived store a tenant's own resource fell outside the prefix and became PERMANENTLY invisible — Get → 403/404, List → absent — while the DB row existed, the grant existed, and mutations (which ask a DIRECT per-object question) kept working. Asking for a larger cap did not help: that argument was only a CLIENT-side trim of an already-cut response.
Обе стороны этого разбора — история. Движок снят стадией S6 (эпик #747), а его перечисление снято с контракта тем же изменением: продолжения у ответа не было by construction, и остаток оставался недостижимым при живых правах. Разбор оставлен, потому что ФОРМА ВОПРОСА, к которой он привёл, и есть предмет этого пакета: спрашивать «видит ли субъект ЭТОТ объект», а не «перечисли вселенную».
The cure is not a bigger cap (it is external, and finite either way) but a different SHAPE of question: instead of "enumerate the universe and look for me in it", ask "may this subject see THIS object", for the objects on the page (List) or for the single object being read (Get). Cost then scales with the PAGE, not with the type's population.
The predicate: the relation that gates the read ¶
Bounding the SHAPE of the question was one change; which question is asked is a second, and the two are independent. The enumeration this package replaced asked the union `viewer ∪ v_list`, while the gateway gates a single-object `Get` on `v_get` — different sets in a model that decouples tiers from verbs, so the page and the read disagreed in both directions. A row now belongs on a page exactly when its holder may read it by id; see pageRelations for why that loses no granted access, and for the one type whose read carries no catalog relation at all.
Fail-closed ¶
Any Check error aborts the whole resolution with that error; callers map it to UNAVAILABLE. A partially-resolved set is never returned: a page filtered by an incomplete answer would silently under-report (and, on the deny side, could under-report a deny).
Index ¶
- Constants
- func RelationsFor(objectType string) []string
- func Visible(ctx context.Context, chk ObjectChecker, subject, objectType, id string) (bool, error)
- func VisibleSet(ctx context.Context, chk ObjectChecker, subject, objectType string, ...) (map[string]bool, error)
- type BatchObjectChecker
- type ObjectChecker
Constants ¶
const BatchParallelism = 8
BatchParallelism bounds how many partitions of one page are in flight.
It replaces DefaultParallelism on the batched path and is smaller on purpose: each partition already carries MaxBatchChecksPerRequest questions, which the store resolves with its own internal concurrency, so in-flight partitions multiply against that. Eight partitions is 400 questions the store may be resolving at once — comparable to the pressure the per-object path applied at sixteen in-flight single questions only because that path spread the same work across 125 waves instead of three.
A contract-sized page is 20 partitions per relation, so this bound makes a relation round three waves and the whole page at most six — against 125. The arithmetic is asserted, with both counts, in TestVisibleSet_BatchedWorstCasePageCost.
const DefaultParallelism = 16
DefaultParallelism bounds how many per-object Checks VisibleSet keeps in flight ON THE FALLBACK PATH — the one taken by a checker that cannot answer in batches. Production wiring can (clients.RelationQueries requires the capability), so this bound governs in-process fakes and any future checker that does not offer the batched door.
What a page costs on this path, and what this bound does not do ¶
It bounds DEPTH, not COUNT. Каждый вопрос — читающая транзакция к базе службы. Со снятием движка (стадия S6) сетевого перехода на этом пути больше нет, и прежний довод «клиент в процессе, а хранилище за сетью» истёк вместе с хранилищем; цена вопроса от этого не исчезла, а сменила природу — теперь это запрос к своей базе. A page pays len(RelationsFor(objectType)) questions for every object the first relation does not resolve, so a contract-sized page (validate.MaxPageSize = 1000) costs up to 1000 round-trips on the types gated by a single relation — and up to 2000 on `iam_role`, the one type that still asks two. This bound turns them into sequential waves rather than removing any of them: 63 waves at one relation, 125 at two. At a 10ms answer that is over half a second of wall time on ONE List; at 50ms, over three.
The 200ms budget on a Check belongs to the CALL, not to the page — nothing caps the page as a whole — so those waves accumulate without any per-call deadline firing. Measured, with the arithmetic spelled out, in TestVisibleSet_WorstCasePageCost; the bound itself is locked by TestVisibleSet_MaxPageBoundedFanOut.
Why the bound is nonetheless right ¶
Unbounded is worse, not better: a goroutine per row puts the whole page on the client at once. The bound is what keeps a large page from arriving all at once; it is not an argument that the page is cheap.
The count IS smaller now, on the path production takes ¶
This paragraph used to end "Making the COUNT smaller is a different change — asking the store about many objects in one request — and it is not done here". It is done here now: see MaxBatchChecksPerRequest and visibleSetBatched, which resolve the same 2000 tuples in 40 requests. The sentence is kept, corrected, rather than deleted, because it was true when written and the next reader of DefaultParallelism has to know which path the number above describes.
const MaxBatchChecksPerRequest = 50
MaxBatchChecksPerRequest — how many objects one question about a page may name.
It USED TO BE the store's ceiling rather than ours, and the distinction decided the number: the external relations engine refused an over-cap request outright — a `validation_error`, never a trim — so a partition wider than its bound turned every page into a refusal instead of a faster page. The bound was read off the deployed build rather than assumed: 51 checks were refused by name, 50 were answered.
That engine is gone. The verdict is computed by the relational form in iam's own database, one statement per partition, and nothing outside refuses a wider one. So the number stopped being a foreign boundary and became OUR partition size: the unit a page is split into, and the unit BatchParallelism counts in flight. It is kept where it stood because the page arithmetic below is asserted against it (TestVisibleSet_BatchedWorstCasePageCost) — not because anything still enforces it. Moving it is now a cost decision about one statement, and it belongs with a fresh measurement, not with this comment.
iam still publishes its own `AuthorizeService.BatchCheck` bounded at 100. That is a CONTRACT, not a store limit, and it governs how large a batch a SIBLING service may hand to iam — a different number for a different reason.
Variables ¶
This section is empty.
Functions ¶
func RelationsFor ¶
RelationsFor returns the page-membership predicate for one FGA object type, in the order it is asked. Each relation is queried only for the objects the previous one denied, so the order is a cost decision and never a correctness one.
A type with no entry takes the default, which is narrow: an omission can never silently widen a page.
func Visible ¶
Visible reports whether `subject` may read `<objectType>:<id>`, evaluated as a direct per-object question over RelationsFor(objectType).
Fail-closed: a nil checker, an empty subject or an empty id yields (false, nil); a Check transport error yields (false, err) and the caller MUST propagate it (never treat it as a deny — that would turn an FGA outage into a silent, permanent 404).
func VisibleSet ¶
func VisibleSet(ctx context.Context, chk ObjectChecker, subject, objectType string, ids []string) (map[string]bool, error)
VisibleSet returns the subset of `ids` visible to `subject`, as a set. It is the page-scoped form: callers read a page from their OWN database by cursor FIRST (so page_size/page_token validation keeps running before any authz short-circuit) and then filter that page with this call.
Duplicate ids are resolved once. The result is always non-nil so callers can index it directly. Fail-closed: the FIRST Check error aborts the whole resolution and is returned — a partially-resolved set is never handed back.
Types ¶
type BatchObjectChecker ¶
type BatchObjectChecker interface {
BatchCheckWithContext(ctx context.Context, subject, relation string, objects []string,
condCtx map[string]any) (allowed []bool, err error)
}
BatchObjectChecker — OPTIONAL capability of an ObjectChecker: answer ONE relation question about MANY objects in a single request.
Declared here as a narrow port and satisfied by the decision door, so this package keeps knowing nothing about where the answer comes from — the same leaf discipline as ObjectChecker.
Unlike pagePreparer this capability is NOT best-effort: it decides. An implementation must return one verdict per object, in the order the objects were given, or an error. It must never return a short slice — a caller cannot tell a short answer from a page of denials, and a page of silent denials is exactly the permanent-invisibility defect this package exists to prevent.
type ObjectChecker ¶
type ObjectChecker interface {
CheckWithContext(ctx context.Context, subject, relation, object string, condCtx map[string]any) (allowed bool, err error)
}
ObjectChecker — narrow port: ONE direct per-object relation question. Satisfied by clients.RelationQueries (and by the decision door behind it).
The port is declared here rather than imported so this package stays a leaf (it must not depend on the adapter that answers) — the same discipline as authzguard.RelationChecker.