Absorb the authz wrapper — and settle the ListObjects type:id footgun #2
Labels
No labels
needs-info
needs-triage
ready-for-agent
ready-for-human
wayfinder:grilling
wayfinder:map
wayfinder:prototype
wayfinder:research
wayfinder:task
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
ginjiruu/toolbelt#2
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Question
What shape does the absorbed authorization helper take, and where does the
ListObjectstype:idfootgun get fixed?Both consumers hand-write
internal/authz(~140-160 lines):Scope()callsServer.ListObjectsand re-attaches thetype:prefix infgaScope.Allows();Check()delegates;Enforce()writes 403/500 JSON. The example hand-writes the prefix strip inService.ListBoardIDs. This is duplicated glue the example had to write AND consumers copy — it passes the "example proves it" boundary.Decisions (settled during grilling)
toolbelt/authzpackage (not extendingembeddedfga) —embeddedfgastays the raw transport;authzowns the policy layer, mirroringauthn↔embeddedoidc.type:idstrip lives inauthz.Scope—Scopecalls the backendListObjects, strips theobjectType:prefix once, stores bare ids;Allows(bareID)compares bare.embeddedfgastays faithful to OpenFGA's wire contract.Enforce()absorbed — the byte-identical HTTP 403/500 JSON writer comes intoauthz.ScopeFuncANDListRelationsabsorbed.ScopeFuncis the tiny predicate→Scope adapter;ListRelationsis the BatchCheck primitive ("which of these relations hold for user↔object") that powers HATEOAS action computation. HATEOAS as a pattern is its own ticket (see "HATEOAS actions pattern — show only actions the user can take"), but the two primitives belong inauthz.Scope(ctx, objectType, relation, userRef string)— refs are codegen-owned (fga-generateemitsTypeBoard/BoardRef/UserRef), so the wrapper passes through pre-built strings; it never formats refs itself.authz. Tuple writes already ride onembeddedfga.Server(app/component.go:42exposes it asComponent.Authorizer); the example proves that path via projectors. Write seam stays where it is.authzbinds toCheck(ctx, Tuple)+ListObjects(ctx, objectType, relation, user)(+ BatchCheck forListRelations).*embeddedfga.OpenFGAServersatisfies it today; the Client doesn't (different signatures) — making backends swappable is the separate "Swappable backends" issue.The ListRelations surface needs a BatchCheck seam on the backend. ttrpg's implementation reaches into raw
openfgav1(f.Server.Server.BatchCheck,StoreID,AuthorizationModelID). The absorbed version should hide that behind a cleanauthzAPI — this is a design decision to resolve while implementing this ticket.Resolve with
/grilling+/domain-modeling; link any prototype API sketch as an asset. On close, write the absorption plan todocs/plans/and reshape the relevantexample/surface onto the decided shape.RESOLVED — design settled via grilling.
Answer: a new
toolbelt/authzpackage owns the policy layer; thetype:idstrip lives inScope.Seven decisions locked:
toolbelt/authzpackage (not extendingembeddedfga) —embeddedfgastays the raw OpenFGA transport;authzis the policy layer on top, mirroringauthn↔embeddedoidc.type:idstrip inauthz.Scope—Scopecalls the backendListObjects, stripsobjectType:once, stores bare ids;Allows(bareID)compares bare.embeddedfgastays faithful to OpenFGA's wire contract. (The footgun is solved at the policy layer where every app already lands.)Enforce()absorbed — the byte-identical HTTP 403/500 JSON writer.ScopeFuncANDListRelationsboth absorbed — the predicate→Scope adapter and the BatchCheck primitive ("which relations hold for user↔object") that powers HATEOAS. The HATEOAS pattern itself is ticket #11 ("HATEOAS actions pattern — show only actions the user can take").Scope(ctx, objectType, relation, userRef string)— refs are codegen-owned (fga-generate), so the wrapper passes through pre-built strings; never formats refs.authz— tuple writes ride onembeddedfga.Server(Component.Authorizer, app/component.go:42); the example proves that via projectors.authzbinds toCheck(ctx, Tuple)+ListObjects(ctx, objectType, relation, user)(+ BatchCheck forListRelations).*embeddedfga.OpenFGAServersatisfies it today; making the Client swappable is the separate issue #10 ("Swappable backends").Open design point for the implement pass: the
ListRelationsBatchCheck seam must hide rawopenfgav1(ttrpg reaches intof.Server.Server.BatchChecktoday).Deliverable (next session): write
docs/plans/authz.mdand reshape theexample/kanban surface (the inlineauthorizerinterface +ListBoardIDsprefix strip) onto the absorbedauthzshape.Design settled: new toolbelt/authz policy package, type:id strip in Scope, ScopeFunc + ListRelations absorbed, Enforce absorbed, no write interface. Deliverable (plan + example reshape) taken to a new session. See resolution comment.