Skip to content

Commit 0d3aa70

Browse files
aspiersclaude
andcommitted
docs(design): add permission-sets design for hypercerts & certified data
Design for two record-collection CRUD permission sets — org.hypercerts.permissions.crud and app.certified.permissions.crud — that any AT Protocol app can request via a single include:<nsid> OAuth scope instead of enumerating every repo: scope by hand. Holistic: covers direct OAuth clients (the broadest case), the Certified group service via OAuth proxying, and CGS API keys — all as consumers of the same published Lexicon artifact, which is the single source of truth. Records the spec constraints the sets follow: own-namespace authority (hence two sets, not one combined), no wildcards inside a set (collections enumerated), and repo: scopes carrying no aud. Collection lists derived from the type:"record" defs under lexicons/{org/hypercerts,app/certified}. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c654639 commit 0d3aa70

1 file changed

Lines changed: 261 additions & 0 deletions

File tree

docs/design/permission-sets.md

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
# Design: Permission Sets for Hypercerts & Certified data
2+
3+
Status: **Proposed**
4+
5+
This document designs **permission sets** for the `org.hypercerts.*` and
6+
`app.certified.*` record collections defined in this repository — published,
7+
reusable bundles of AT Protocol OAuth scopes that any application can request
8+
with a single `include:<nsid>` scope, instead of enumerating every underlying
9+
`repo:` scope by hand.
10+
11+
## Background: what a permission set is
12+
13+
AT Protocol clients act on a user's data with **OAuth scopes**. Writing records
14+
of a given collection requires a `repo:<collection>?action=…` scope; calling a
15+
service method requires an `rpc:<lxm>?aud=…` scope. (See the
16+
[AT Protocol permission spec](https://atproto.com/specs/permission).)
17+
18+
A **permission set** ([spec](https://atproto.com/specs/permission#permission-sets))
19+
is a named, published Lexicon schema that bundles several such scopes under one
20+
NSID. A client requests the whole bundle with one scope string —
21+
`include:<nsid>` — and the user's Authorization Server (their PDS) resolves the
22+
set and expands it into the underlying scopes when granting access. Per the
23+
spec, permission sets are _"published publicly and can be used by any client
24+
developer"_, and _"Authorization Servers resolve, authenticate, and process
25+
permission-sets dynamically."_
26+
27+
The motivation is plain: an application that does CRUD over a family of
28+
collections — say, all the records that make up a hypercert — would otherwise
29+
have to list every `repo:org.hypercerts.…?action=…` scope itself, keep that list
30+
correct, and re-derive it independently from every other integrator. A published
31+
set replaces that with one curated, versioned name.
32+
33+
## Who uses these sets
34+
35+
These are general AT Protocol artifacts, not specific to any one service. The
36+
expected consumers, in rough order of generality:
37+
38+
1. **Any atproto app working with Hypercerts / Certified data directly.** An
39+
OAuth client requests `include:org.hypercerts.permissions.crud` (and/or the
40+
`app.certified.*` set) in its authorization request; the user's PDS expands it
41+
and the app reads/writes those collections in the user's own repo. No
42+
intermediary is involved. This is the broadest case.
43+
2. **The Certified group service (CGS), via OAuth.** CGS is used as a standard
44+
OAuth resource (reached through AT Protocol service proxying). A client
45+
calling CGS on a user's behalf requests the same sets; the user's PDS expands
46+
them the same way. CGS is a likely-popular consumer, but it consumes the sets
47+
like any other client — it does not own them.
48+
3. **CGS API keys.** CGS also issues long-lived API keys whose scopes can be
49+
specified as `include:<nsid>`; CGS resolves the **same** published set and
50+
expands it to concrete scopes on the key. (Detailed in CGS's own design doc;
51+
summarised under _Consumption by CGS_ below.)
52+
53+
The common thread: the **published Lexicon record is the single source of
54+
truth.** Whatever resolves a set — a PDS during an OAuth grant, or CGS at
55+
key-creation — reads the same published definition.
56+
57+
## The sets
58+
59+
We define two **record-collection CRUD** sets — one per data namespace in this
60+
repo. Each grants create/update/delete over all of that namespace's record
61+
collections.
62+
63+
| Set NSID | Grants CRUD on |
64+
| --------------------------------- | ----------------------------------------- |
65+
| `org.hypercerts.permissions.crud` | all `org.hypercerts.*` record collections |
66+
| `app.certified.permissions.crud` | all `app.certified.*` record collections |
67+
68+
Both are authored and published from **this repository**, which is the namespace
69+
authority for both — see _Namespace authority_ below.
70+
71+
> **Naming.** The spec prescribes no naming convention: a permission set is
72+
> identified only by `type: "permission-set"` in its `main` def, and its sole
73+
> examples are illustrative (`com.example.authFull`). The `permissions.crud`
74+
> leaf is a house choice for this repo; rename freely.
75+
76+
### `org.hypercerts.permissions.crud`
77+
78+
Enumerates every `type: "record"` collection under `org.hypercerts.*`:
79+
80+
```jsonc
81+
{
82+
"lexicon": 1,
83+
"id": "org.hypercerts.permissions.crud",
84+
"defs": {
85+
"main": {
86+
"type": "permission-set",
87+
"title": "Full CRUD on Hypercerts record collections",
88+
"permissions": [
89+
{
90+
"type": "permission",
91+
"resource": "repo",
92+
"collection": [
93+
"org.hypercerts.claim.activity",
94+
"org.hypercerts.claim.contribution",
95+
"org.hypercerts.claim.contributorInformation",
96+
"org.hypercerts.claim.rights",
97+
"org.hypercerts.collection",
98+
"org.hypercerts.context.acknowledgement",
99+
"org.hypercerts.context.attachment",
100+
"org.hypercerts.context.evaluation",
101+
"org.hypercerts.context.measurement",
102+
"org.hypercerts.funding.receipt",
103+
"org.hypercerts.workscope.tag",
104+
],
105+
"action": ["create", "update", "delete"],
106+
},
107+
],
108+
},
109+
},
110+
}
111+
```
112+
113+
### `app.certified.permissions.crud`
114+
115+
Enumerates every `type: "record"` collection under `app.certified.*`:
116+
117+
```jsonc
118+
{
119+
"lexicon": 1,
120+
"id": "app.certified.permissions.crud",
121+
"defs": {
122+
"main": {
123+
"type": "permission-set",
124+
"title": "Full CRUD on Certified record collections",
125+
"permissions": [
126+
{
127+
"type": "permission",
128+
"resource": "repo",
129+
"collection": [
130+
"app.certified.actor.organization",
131+
"app.certified.actor.profile",
132+
"app.certified.badge.award",
133+
"app.certified.badge.definition",
134+
"app.certified.badge.response",
135+
"app.certified.graph.follow",
136+
"app.certified.link.evm",
137+
"app.certified.location",
138+
"app.certified.signature.proof",
139+
],
140+
"action": ["create", "update", "delete"],
141+
},
142+
],
143+
},
144+
},
145+
}
146+
```
147+
148+
> The collection lists are derived from this repo's `lexicons/org/hypercerts/`
149+
> and `lexicons/app/certified/` (`type: "record"` defs at time of writing). They
150+
> must be **kept in sync** as record types are added — see _Maintenance_.
151+
152+
## Design rules these sets follow
153+
154+
### Namespace authority
155+
156+
A permission set may only contain permissions under its **own** namespace
157+
authority. Spec, verbatim: _"Permission sets are limited to expressing
158+
permissions that reference resources under the same NSID namespace as the set
159+
itself"_ — it may address its own NSID group and children, never siblings or
160+
parents. The rule applies uniformly to `repo:` permissions (by collection NSID)
161+
and `rpc:` permissions (by lxm NSID).
162+
163+
Consequences here:
164+
165+
- A set under `org.hypercerts.*` may grant `repo:org.hypercerts.*` collections,
166+
and **only** those — it cannot reference `app.certified.*` collections, or any
167+
third party's, or any service's `rpc:` methods. Likewise the `app.certified.*`
168+
set is confined to `app.certified.*`.
169+
- This is why there are **two** sets rather than one combined set: the two
170+
namespaces are distinct authorities and cannot legally be mixed in a single
171+
set. An app that needs both requests both `include:` scopes.
172+
- The authority confinement is also what makes a published set **safe for any
173+
client to resolve**: an `include:` can only ever widen access to the set's own
174+
namespace, never smuggle in foreign permissions.
175+
176+
### Collections are enumerated, never wildcarded
177+
178+
Inside a permission set, a `repo:` permission names collections by **exact
179+
NSID**. Spec, verbatim: _"Wildcards are not supported in permissions within a
180+
permission set."_ So a CRUD set cannot say "all `org.hypercerts.*` collections" —
181+
it lists each one. The enumerated list **is** the grant, and is the security
182+
boundary: a newly-added collection is not covered until it is added to the set
183+
and the set is re-published. (Contrast a standalone scope outside a set, where
184+
`repo:*` is permitted — but that is "all collections", not "a namespace", and is
185+
not what these sets are for.)
186+
187+
### `repo:` permissions carry no `aud`
188+
189+
A `repo:` scope targets the **user's own repo**, so it has no audience. Both sets
190+
here are `repo:`-only, so an `include:` for them is requested **without** any
191+
`?aud=` parameter, and expands to plain
192+
`repo:<collection>?action=create&action=update&action=delete` scopes.
193+
194+
(For completeness: an `rpc:` permission _does_ take an `aud` naming the target
195+
service, and a set can let the `include:`'s `?aud=` flow into `rpc:` permissions
196+
marked `inheritAud: true`. Neither CRUD set uses `rpc:`, so this does not apply —
197+
it is noted only because a future Hypercerts/Certified _service-method_ set
198+
would.)
199+
200+
## Publication and resolution
201+
202+
Both sets are published like any other Lexicon schema in this repo (see
203+
[`PUBLISHING.md`](../PUBLISHING.md)) and resolved at runtime via the standard
204+
**Lexicon resolution system**, to which the permission spec defers:
205+
_"Permission sets are Lexicon schemas and are published and fetched using the
206+
Lexicon resolution system, which includes cryptographic authentication."_
207+
208+
The resolution chain a consumer follows (per
209+
[`/specs/lexicon`](https://atproto.com/specs/lexicon), core protocol):
210+
211+
1. **NSID → authority DID via DNS.** Reverse the NSID's authority portion (all
212+
but the final name), prepend `_lexicon.`, query that domain for a `TXT` record
213+
`did=<DID>`.
214+
2. **DID → PDS endpoint** via standard DID resolution.
215+
3. **Fetch the schema record**: `com.atproto.repo.getRecord` with
216+
`repo=<authority-DID>`, `collection=com.atproto.lexicon.schema`,
217+
`rkey=<full-NSID>` (the rkey **is** the NSID).
218+
4. Validate the record's `main` def is `type: "permission-set"`, then expand.
219+
220+
For a direct OAuth client this is done by the **user's PDS** during the grant.
221+
The publication of the `_lexicon` DNS records and schema records for these
222+
namespaces is a maintenance responsibility of this repo's operators.
223+
224+
## Consumption by CGS
225+
226+
CGS is expected to be a prominent consumer, in two ways. Neither changes the
227+
sets — they are recorded here so the sets are designed with these uses in mind;
228+
full detail lives in the CGS design doc
229+
([`api-key-permission-sets.md`](https://github.com/hypercerts-org/certified-group-service/blob/main/docs/design/api-key-permission-sets.md)).
230+
231+
- **Via OAuth + service proxying.** A client calls CGS on a user's behalf; the
232+
user's PDS has already expanded the requested `include:` set into the grant.
233+
CGS sees ordinary `repo:` scopes. Identical to any other OAuth resource.
234+
- **Via CGS API keys.** CGS lets an owner specify an API key's scopes as
235+
`include:<nsid>`. CGS resolves the published set (same chain as above) and
236+
expands it to concrete scopes stored on the key, **once at key-creation time**
237+
(a snapshot — a later change to the set does not retroactively alter issued
238+
keys until they are re-issued). CGS then gates its own write methods
239+
(`app.certified.group.repo.*`, which proxy to the group's PDS) by matching the
240+
request's collection against those `repo:` scopes.
241+
242+
The point for _this_ repo: the sets must be **self-contained and
243+
deployment-agnostic** — no CGS-specific assumptions baked into the definitions.
244+
The two CRUD sets satisfy this trivially (pure `repo:` collection grants).
245+
246+
## Maintenance
247+
248+
The enumerated `collection` lists are the one ongoing obligation. Whenever a
249+
`type: "record"` lexicon is added to or removed from `org.hypercerts.*` or
250+
`app.certified.*`, the corresponding set must be updated and re-published, or the
251+
new collection will be silently uncovered by the set. Worth a check in the
252+
release process (and ideally an automated test asserting each set lists exactly
253+
the `type: "record"` defs in its namespace).
254+
255+
## References
256+
257+
- AT Protocol permission spec — permission sets:
258+
https://atproto.com/specs/permission#permission-sets
259+
- AT Protocol lexicon resolution: https://atproto.com/specs/lexicon
260+
- CGS consumption design:
261+
https://github.com/hypercerts-org/certified-group-service/blob/main/docs/design/api-key-permission-sets.md

0 commit comments

Comments
 (0)