Skip to content

Grants

What a caller may do with a context, and how an implementation decides it. The model is deliberately small: durable per-context policy, set-intersection delegation, and enforcement on the server.

Status: descriptive. See ../../GOVERNANCE.md.

Profiles: RFC 6749 (for what a scope is, and is not). Error codes are index.md §5. Contexts are contexts.md.

1. Grant and scope are different things

Conflating them is the mistake this chapter exists to prevent, and it is an easy one — both are strings, and both say something about permission.

Travels in the access token Example
Grant no <context-iri>#read, #write, #manage
Scope (RFC 6749) yes public-read, openid, offline_access

SPS-GRANT-001 — A grant is durable server-side policy on one context. An implementation MUST NOT place a grant inside an access token, and MUST NOT treat a token's contents as the authorization decision for a context.

SPS-GRANT-002 — An implementation MUST resolve a request's grants from durable storage on every request, keyed by the pair of the verified client identifier and the verified subject.

The client belongs in the key, and leaving it out is a privilege escalation between applications rather than an imprecision. A person may delegate broadly to one application and narrowly to another; resolving on the subject alone collapses every application that person uses into one principal, so the second inherits the first's access by presenting a token for the same person. SPS-MCP-008 states the same pair for the agent surface.

For a service token there is no person, and the subject is the client (SPS-AUTH-017) — so the pair degenerates to one identity and the rule still holds without a special case.

SPS-GRANT-003 — A grant that is revoked MUST take effect on the caller's next request. An implementation MUST NOT let an already-issued access token retain access for the remainder of its lifetime.

The reason these are requirements rather than an implementation note: a context grant is named-graph visibility policy, not a capability. A capability scope tells a client which API to call and shapes its behaviour. Here the data API is uniform — the same requests go to the same routes whatever the caller holds, and the server filters what comes back. Row-level security is the right analogy, not an OAuth scope, and policy in a client-facing token was a category error whose visible symptom was tokens that grew with the pod.

The cost is stated rather than hidden: the grant store is on the hot path of every authenticated request, so the token is not self-contained. That is acceptable here because the authorization server and the resource server are the same pod, and it is a trade an implementation splitting them would have to reconsider.

2. Grammar

<context-iri>#read       read the context
<context-iri>#write      write to the context
<context-iri>#manage     manage this context root and its slash-delimited descendants

SPS-GRANT-004 — The separator MUST be the last # in the grant string.

SPS-GRANT-005 — The left side MUST be a canonical context IRI inside the pod's base URL. An implementation MUST reject a grant naming an IRI outside it, and MUST reject the pod base URL itself.

A grant on the pod root would match every context under it under any prefix rule, which is exactly the wildcard the next requirement refuses.

SPS-GRANT-006 — The right side MUST be exactly one of read, write or manage. An implementation MUST reject any other permission, and MUST NOT interpret a wildcard.

3. manage is slash-delimited, not a string prefix

SPS-GRANT-007 — Given R#manage, an implementation MUST authorize a context C if and only if C equals R, or C begins with R followed by /. An implementation MUST NOT authorize a context that merely shares a string prefix with R.

tasks#manage reaches tasks and tasks/inbox. It MUST NOT reach tasks-private.

This is the one place the model deviates from what a naive implementation would write, and it is load-bearing for context-tree isolation: a raw startsWith hands an app every sibling whose name begins with the same letters. It is worth implementing once, in one place, and calling it from every site that asks the question — the read path, the write path and context management all have to give the same answer.

SPS-GRANT-008 — When a #manage grant is expanded into a set of contexts — to answer what a caller may read, to list what they may write, or to sweep delegations on revocation — the expansion MUST cover registered contexts only. An implementation MUST NOT report authority over a context that does not exist.

SPS-GRANT-033 — Expansion is not the same question as authorization of a single target. Where a request names one context, a #manage grant covering it under SPS-GRANT-007 MUST authorize the request whether or not that context is registered yet.

Without this split a manager could never create anything. Creating R/sub names a context that is by definition not registered at the moment of the request, so an implementation applying the expansion rule to the create check would refuse every creation inside the sandbox the grant exists to give — which is the whole mechanism a service client works through (SPS-AUTH-014).

4. What implies what

SPS-GRANT-009#write MUST imply #read, and #manage MUST imply both #write and #read, on the same context.

SPS-GRANT-010 — A client MUST NOT request #read alongside #write for the same context, and an implementation MUST NOT require it.

The second half matters for anyone reading a grant set: there is no such thing as a write-only context here. An implementation that assumes one will build a read path that hides data its own write path can reach.

5. Who holds what

SPS-GRANT-011 — The pod owner MUST hold read, write and manage on every registered context of that pod, implicitly. An implementation MUST NOT require the owner's grants to be stored or granted.

SPS-GRANT-012 — Any other person's grants MUST be explicit, stored per pod and per identity.

SPS-GRANT-013 — What an application receives MUST be the intersection of what it requested with what the authorizing person effectively holds:

granted = requested ∩ person's effective permissions

SPS-GRANT-029 — An implementation MUST NOT grant an application more than the authorizing person effectively holds.

SPS-GRANT-030 — An implementation MUST NOT require an application to be installed or pre-registered with the pod before a person may delegate to it, and MUST NOT require the authorizing person to be the pod owner.

Anyone who holds grants can delegate a subset of them. Making delegation conditional on the owner, or on an installation step, would put the pod between a person and their own access — and there is nothing an install could establish that the intersection does not already enforce.

SPS-GRANT-028 — "Effectively holds" MUST be the person's grants after expanding #manage roots over the registered contexts they cover (SPS-GRANT-007) and after applying the implications of SPS-GRANT-009. An implementation MUST NOT compute the intersection over the stored grant strings.

The distinction is the difference between working and not. A person holding R#manage who is asked for R/sub#write does hold that authority — but the two strings are different, so a literal intersection is empty and the flow answers consent_required for an ordinary descendant. The expansion is not an optimisation; it is what the earlier requirements already promised.

SPS-GRANT-014 — Where that intersection is empty and no public context is available, the authorization flow MUST fail as consent_required, which is recoverable. An implementation MUST NOT answer access_denied, which is reserved for a hard signal such as an invalid identity token or an explicit refusal.

The difference is what a client does next. consent_required says "ask again, differently"; access_denied says "stop". Returning the second for an ordinary empty intersection strands a client that had a perfectly good next move.

6. Revocation

An intersection computed at consent time is a snapshot, and a person who later narrows their own grants would otherwise leave an application holding access they no longer have.

SPS-GRANT-015 — When a person's grants are narrowed or removed, an implementation MUST recompute what they still hold and remove every application delegation no longer covered by it.

SPS-GRANT-016 — That removal MUST be a recomputation, not a string match on the revoked grant.

Because <root>#manage expands into its registered descendants, revoking the root has to sweep derived delegations like <root>/child#write — whose text matches nothing in the grant that was removed. A string match silently leaves them behind, and they keep working.

SPS-GRANT-017 — Deleting a context MUST run the same recomputation, for the same reason.

SPS-GRANT-018 — Consent MUST persist its result first and re-check second, and MUST answer consent_required if nothing survives the re-check.

Time passes between intersecting a request and storing the result. A revocation landing inside that window is invisible to both sides otherwise — the intersection is already stale, and the revocation's sweep runs before the delegation exists to be swept. Both sides writing first and checking second is what makes them unable to miss each other.

SPS-GRANT-019 — Widening a person's grants MUST NOT widen an application retroactively. An application receives more only through a fresh consent.

The asymmetry is deliberate: narrowing propagates, widening does not. A person who regains access they had revoked should not silently re-arm every application that once asked for it.

7. public-read

SPS-GRANT-031 — A caller presenting no credential at all MUST be able to read the pod's public contexts, over every read surface this specification defines. An implementation MUST NOT require authentication for a public read.

SPS-GRANT-032 — An unauthenticated caller's readable set MUST be exactly the pod's public contexts — no more, and not empty because no token was presented.

Anonymous is a supported mode, not a degraded one, and until now that claim lived in the module chapters and in prose rather than in a requirement. Without these two, an implementation reading only SPS-GRANT-020 would conclude that public contexts are unioned in only when a token carries public-read — and a caller with no token has no scope, so it would see nothing. That is the opposite of the model: a pod's public data is Linked Open Data, dereferenceable by anyone, and that is the property the whole design exists to make ordinary.

public-read is what an authenticated caller adds on top of its own grants (SPS-GRANT-020); it is not what makes public data public.

SPS-GRANT-020public-read is a scope, not a grant. It MUST travel in the access token, and it MUST be additive: where present, the pod's currently public contexts are unioned into the caller's readable set on top of the grants resolved from storage.

SPS-GRANT-021 — The set of public contexts MUST be expanded at access time, not frozen at consent time.

SPS-GRANT-022 — Revoking a person's context grants MUST NOT remove public-read. It is not a grant on a context, and removing it would end a session over an unrelated narrowing.

8. Enforcement

SPS-GRANT-023 — An implementation MUST enforce grants on the server. It MUST NOT rely on a client restricting its own requests, and MUST NOT trust a client-supplied statement about which contexts a request should touch.

SPS-GRANT-024 — A read MUST return statements only from contexts the caller may read.

SPS-GRANT-025 — A write MUST name its target context explicitly and MUST land only there. An implementation MUST NOT provide an implicit or default write context.

SPS-GRANT-026 — The statements parsed from a write request MUST concern the target resource only. An implementation MUST reject a request carrying statements about other subjects alongside it.

SPS-GRANT-027 — A valid token whose scope is insufficient for an operation MUST produce 403. A missing or rejected token MUST produce 401. An implementation MUST NOT conflate the two.