Lesson 25 — Grants: who may touch what
Goal: by the end of this lesson you can read the Grants panel, add and revoke a grant, and predict which editor operations a given user can perform — because you'll know the seven capabilities, how namespace scope works, and where the checks actually fire.
Concepts introduced: the :grant entity, the capability
vocabulary, capability implication, namespace-scoped
authorization, default-deny, personal namespaces.
The model in one paragraph
A grant is a row: subject (a member's email, stored as their stable account id) + capability +
namespace. Authorization is default-deny: an operation is
allowed iff some grant covers it. "Covers" is generous in two
directions — a grant on a parent namespace covers all its
descendants (a grant on acme covers acme.billing.invoices;
a blank namespace is a root grant covering everything), and
some capabilities imply others.
The seven capabilities
The closed vocabulary (anything else is rejected at create time with a 400):
| Capability | Lets the subject… |
|---|---|
read |
discover a fn + see its SIGNATURE (slots, types, return) |
view-impl |
see a fn's INTERNAL COMPOSITION (parent chain + bindings) — withhold it and the fn stays executable but its impl is hidden |
write |
create / move / edit fns there |
execute |
run fns there |
admin |
everything above, within the scope |
bind-args |
edit only a binding's VALUE (not structure) |
append-list |
append items to list-typed bindings |
Implication: admin implies all of them; write implies
view-impl (you can't edit a fn you can't see) plus the two
narrow edit caps (bind-args, append-list). The narrow
caps exist so you can hand someone "tune the parameters of my
app" without handing them "restructure my app".
(The panel's capability <select> deliberately offers only six
of these — every one except view-impl, which is set through the
graph-read filter rather than picked by hand — but the
POST /api/grants API accepts all seven.)
One freebie needs no grant row at all: every user implicitly
holds admin over their personal namespace
(<prefix>.<account-id> — built from the stable id, so it survives an email change).
Where the checks fire
Three enforcement layers read the same grant table:
- Write gate — creating a fn or moving it into a namespace
needs
writethere; a value-only binding edit passes withbind-args; appending a list item withappend-list. - Execute gate — running a fn needs
executeon its namespace. - Request gate — a cheap per-request check: reads pass,
mutations require the subject to hold SOME write-family
capability,
/api/executerequiresexecute.
The editor also reads the grant table indirectly: the
X-Graphden-Capabilities header (Lesson 24) that unlocks
tenant-mode UI is computed from these same rows.
The panel
Expand Grants on the Organization surface (open it from the
account menu; same gating as Users: signed in + tenancy addon). The partial (GET /partials/grants-admin)
renders:
Subject | Capability | Namespace |
alice | write | acme.billing | ×
bob | execute | acme | ×
Below it, the add form: a subject input (a member's email, or a
role name — see below; with type-ahead over the org's members),
a capability <select> (the six pickable values — every capability
except view-impl), a namespace input, and + Add grant
(POST /api/grants).
One subtlety worth knowing: the form takes an email, but enforcement keys on the account's stable id — the create handler resolves the email to that id at write time and stores only the id (the email is re-joined from the account row for display). Typo the email and you get a "dead" grant that matches no one — its subject cell renders the raw id — but it doesn't throw.
A team as the subject. Type the name of one of the org's roles
(Lesson 26) instead of an email and the grant is
stored with subject-kind role: everyone who is a member of that
role holds it, and adding someone to the role (or removing them) is
the only thing you edit later. That is how two teams sharing one
org keep their namespaces apart — backend gets admin on
svc-a, frontend gets admin on svc-b, and the contract
namespace both read stays under whichever team owns it. The row
shows the role's name in the subject cell. Resolution order for a
typed subject: a known member's email → a role name → a pending
email (a dormant grant, claimed on first sign-in).
One special capability rides the same rows: require-2fa.
Granted to a user (or, with subject-kind org, to a whole org)
it doesn't allow anything — it requires the subject to enroll
two-factor authentication before any other request passes
(Lesson 33 shows enrollment).
Revoke is the row's × (confirm: "Delete this grant?") —
this one goes through the generic entity endpoint
(DELETE /api/entities/grant/:id), and the row just drops out
of the table.
Who may manage grants
Grants are org-scoped RBAC, administered from WITHIN the org —
not by the platform operator. The Grants panel mounts, and its
create / revoke endpoints authorize, only for a user who may hand
out grants in their own org: the org owner, or a holder of the
manage-grants org-management capability. Every other member sees
no panel. (The operator, on the public platform org, holds no
manage-grants and does not administer a tenant's grants.) Tenants
are both the subjects AND the administrators of grants within
their org.
Roles — bundling capabilities
A role is a named bundle of grants an org owner (or a
manage-roles holder) defines once and assigns to members, so you
don't re-issue the same capability set per person. It has its own
Roles sidebar panel (GET /partials/roles-admin, gated on
manage-roles), parallel to Grants. Lesson 24's "delegate a
capability via a role" points here. (A full roles walk-through is
still a planned lesson; for now, know the panel exists and is
org-scoped like Grants.)
Try it
(An org owner — or a manage-grants holder — on a tenancy-addon
instance.)
- Add
carolto your orgacme(Lesson 24). - In Grants, grant
carol/bind-args/acme.settings. As carol: editing a binding VALUE underacme.settingsworks; renaming the fn or changing its parent is denied — that needswrite. - Replace it with
writeonacme— now structural edits pass anywhere underacme, includingacme.settings(parent-path coverage). - Remove carol (Lesson 24) and watch her grant rows vanish with her — the cascade from the other side.
What we glossed over
- How
can?composes implication + scope — the pure decision function and its default-deny shape (graphden.tenancy.grant, in the privategraphden-tenancyrepo). - The two-layer tenant effect gate — grants say who may, the effect gate says what kinds of side effects cloud code may perform at all (docs/TENANCY_SEAM.md § Effect gate).
- Groups — the design sketch allows group subjects; the panel today handles user subjects only.
Next
lesson 34 — Plans & tiers: what the cloud grants each account, and how an operator changes an org's tier.