HATEOAS actions pattern — show only actions the user can take #11
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#11
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
How should the HATEOAS "show only actions the user can take" pattern look in the example app — and what does it need from the absorbed
toolbelt/authz?The rationale (from the authz deliberation): every app should only surface actions the current user can actually take — gray the ones they can't (with a tooltip), don't show buttons that will 403. It's good UX, and it should be the canonical pattern every example demonstrates.
ListRelations(BatchCheck overopenfgav1) is the primitive that powers it: given(user, object, [relations]), return which relations hold, so the UI renders exactly the enabled actions. ttrpg already implements it for HATEOAS action computation (internal/authz/fga_authorizer.goListRelations+ generated{Type}RelationsForUserhelpers); the example app doesn't yet.Decisions this ticket resolves:
ListRelations/BatchCheck gets a clean home intoolbelt/authz(orembeddedfga) — per the authz ticket,ScopeFuncandListRelationsare both being absorbed.fga.{Type}RelationsForUserhelpers (fromtools/fga-generate) and the HATEOAS computation fit the absorbedauthzsurface.This ticket graduates the fog from the authz ticket ("ListRelations left in fog") into a concrete example pattern.
Resolve with
/grilling+/domain-modeling(prototype the UI if useful). On close, write the plan todocs/plans/. Per the map's deliverable shape, no example code lands with this ticket — implementation is deferred to the post-map clean rewrite.Child of: "Toolbelt glue absorption — map: what the toolbelt absorbs from duplicated consumer glue" (#1).
RESOLVED — design settled via grilling.
Answer: actions are generated from the FGA model via a
can_convention; permission-only; routes and state stay app-side.Eight decisions locked:
can_convention. A relation beginning withcan_is a capability action, 1:1 with an affordance, and must read as "can ".owner/memberare composition roles, never actions. This is what makes codegen viable — no bundles to guess. Advisory to document in the fga-generate docs.tools/fga-generateemits, dependency-free (imports onlycontext):Actions()(allcan_*actions for a type) andActionsForUser(ctx, lister, user, object)— the same set annotated{Rel, Allowed, Reason}from oneListRelationsbatch. No app-sidecomputeActions; no glue.Action = {Rel, Allowed, Reason}— no Href/Method. Routes, labels, and widgets live in the renderer's rel→widget map. Dumb, templ-free.authz.ListRelations(ctx, user, object, relations)hides rawopenfgav1BatchCheck; the generatedRelationListerstays in generated output;authz.Authorizersatisfies it.[user]-typed, seeded so granularity is visible (bob = member without can_delete_board).Plan:
docs/plans/hateoas-actions.md. Implementation deferred to the post-map clean rewrite per the map's deliverable shape. OpenAPI-driven routes left as map fog.Design settled: can_ convention, actions codegen from FGA model, permission-only; plan docs/plans/hateoas-actions.md. State verification graduated to #24.