pluginsdk/docs/adr/0008-a-plugin-grants-and-revokes-file-access-through-the-files-family.md
Alex Dunmow 6dd745bc34 Let a plugin grant and revoke file access
WO-FL-009. The files family reaches the CMS File access grant store: a plugin
grants one file to a member or an address, lists what a file has handed out,
and revokes what it gave. The response carries the signed unlock link, so a
seller can deliver a file to somebody with no account. Nothing on the wire
names the calling plugin: the host stamps plugin:<name> on what this family
writes and refuses a revoke of anything else. ADR 0008.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 10:47:13 +08:00

55 lines
3.0 KiB
Markdown

# A plugin grants and revokes file access through the files family
Status: accepted (WO-FL-009, 2026-09-17)
The CMS File library gates a published file and hands it to one person at a time
through a File access grant (cms ADRs 0181, 0186, 0187). ADR 0181 named the
storefront case explicitly: selling a single file belongs to a plugin, which
creates a grant after payment and revokes it on a refund. Until now a plugin had
no way to say either thing, because the capability surface carried no file
family at all.
Decision: the ABI gains `files.grant_access`, `files.revoke_access` and
`files.list_grants`, mirrored by `plugin.FileGrants` on `CoreServices`. A grant
names a file by its attachment kind and id and exactly one subject: a public
user, or an email address that receives a signed unlock link instead. The
response carries the stored grant, including that link, so a plugin that sells a
file to somebody with no account can deliver it in its own receipt.
**The source is the host's to write, and so is the revoke rule.** Nothing in
these messages names the calling plugin: the host already authenticates the
caller at the capability boundary, stamps `plugin:<name>` on every grant this
family writes, and refuses a revoke of a grant with any other source. A plugin
therefore cannot attribute a grant to somebody else, and cannot take away access
that an administrator, a workflow or another plugin gave. Passing the plugin
name on the wire was rejected for the same reason the bridge does not: a value a
guest supplies is a value a guest can change.
A grant with no expiry is the ordinary case, because revocation is the control
the CMS relies on; `expires_at` is optional and absent means "until revoked".
Alternatives rejected: a general "write a row in a core table" capability, which
would put the CHECK constraints and the unique indexes of a security table
behind a generic escape hatch; and returning only a grant id, which would force
a second call for the link every seller needs.
Consequences:
- New: `abi/proto/v1/capability.proto` messages `FileAccessGrant`,
`FilesGrantAccessRequest`/`Response`, `FilesRevokeAccessRequest`/`Response`,
`FilesListGrantsRequest`/`Response`; `plugin/wasmguest/caps/files.go` and its
round-trip goldens; `plugin.FileGrants`, `plugin.FileGrantParams` and
`plugin.FileGrant` in `plugin/deps.go`.
- `CoreServices.FileGrants` is nil on a host that does not wire it, exactly like
the other optional members, and the guest stub then fails the call rather than
pretending.
- The host half, the plugin attribution and the revoke guard live in the cms
repo (`backend/plugin/wasmhost/caps/files.go`, ADR 0190 there).
Keywords: files.grant_access, files.revoke_access, files.list_grants,
FileGrants, FileGrantParams, FileGrant, FileAccessGrant, FilesGrantAccessRequest,
FilesRevokeAccessRequest, FilesListGrantsRequest, filesStub, capability.proto,
File access grant, unlock link, unlock_url, plugin grant, grant source,
storefront plugin, File purchase, revoke own grants, WO-FL-009, ADR 0181,
CoreServices, wasm ABI, host_call