Lesson 28 — 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 — and run them |
execute |
run fns there (and nothing else) |
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
execute (a writer can run what they may edit — which is why
the org-wide write a new member gets in Lesson 27 is a working
membership), view-impl (you can't edit a fn you can't see) and
the two narrow edit caps (bind-args, append-list). execute
on its own is a run-only grant. The narrow caps exist so you can
hand someone "tune the parameters of my app" without handing them
"restructure my app".
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 fn created outside any namespace needs a root grant — blank namespace); a value-only binding edit passes withbind-args; appending a list item withappend-list. Namespaces are gated too: creating, renaming, moving or deleting one needswriteon its parent namespace — on both the old and the new parent for a move — so a grant onacme.teamcannot pullacme.secretsunder it. Rotating a secret's value needswriteon the namespace of the fn that holds it. - Execute gate — running a fn needs
executeon its namespace (awritegrant there counts). - Request gate — a cheap per-request check: reads pass,
mutations require the subject to hold SOME write-family
capability,
/api/executerequiresexecute(or awrite).
The editor also reads the grant table indirectly: the
X-Graphden-Capabilities header (Lesson 27) that unlocks
tenant-mode UI is computed from these same rows.
You can read your own answer without opening any admin panel: avatar → Settings → Access lists the capabilities you hold in this org, under a tenancy addon — the same fact every response carries in that header. It is a readout, not a control: who may touch what is enforced by the server, and the admin panels stay on the Organization surface.
The panel
Expand Grants on the Organization surface (open it from the
account menu; same gating as Members: 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 seven, plus the require-2fa policy
below), 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 29) 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, the
policy group of the <select>. Granted to a member from the
panel (or, through the API with subject-kind org, to the whole
org) it doesn't allow anything — it requires the subject to
enroll two-factor authentication before any other request passes
(Lesson 36 shows enrollment). It
implies nothing and nothing implies it.
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.
Handing several members the same org-management capability as one row is a role — the next lesson, Lesson 29.
Try it
(An org owner — or a manage-grants holder — on a tenancy-addon
instance.)
- Add
carolto your orgacme(Lesson 27). - 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 27) 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).
Next
lesson 29 — Roles: capabilities as a bundle.