Command authorisation requirements; relax schema selection to any server-owned identifier - #3
Merged
Conversation
…ver-owned identifier The `dataschema` clause said servers MUST select the validation schema "keyed by the command `type` field". That wording arrived in 617bea5 ("improve versioning", 0.4.8) and replaced the original 0.4.5 rule, which required only that the schema come from a server-maintained catalogue and not from the caller-supplied URI. The SSRF rationale in the surrounding prose only ever supported the original: the control is *don't dereference a caller-supplied URI*, not *which server-owned identifier keys the lookup*. The stricter wording also contradicted the SHOULD two lines below it, which permits rejecting a `dataschema` that doesn't match a catalogue entry — a field you may reject on mismatch is not "informational". In practice it pushed implementations into lossy PascalCase<->kebab-case derivations. Where such a transform is not total over the catalogue, the authorisation key and the validation key silently diverge: a correctly allow-listed command is denied under a policy key the operator never configured, and in the worse direction a caller selects their own contract. Changes: - Reframe `dataschema` as a selector, not a location. Selection MAY be keyed by `type` or by the schema name it carries, provided the entry is server-owned. Both MUST-NOT-fetch rules unchanged. - Add the same-identifier rule: selection, authorisation and dispatch MUST key on one identifier, and any derivation between `type` and `schema` MUST be total or resolved once and reused. - Require fail-closed on unresolvable schemas, and a documented version policy with no partial honouring of a declared version. - New Command Authorisation section, filling a real gap — the Read/Write scope table was the entire authorisation model, which implies any Write-scoped caller may POST every command. Adds deny-by-default per-command policy, deployment-scoped policy, catalogue reflecting the caller's actual permissions, actor binding for payload principal fields, and controls for destructive commands that MUST NOT be self-serviceable. No wire-format change, so 0.9.1. Clients need no change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The
dataschemaclause required servers to select the validation schema "keyed by the commandtypefield". That wording arrived in 617bea5 ("improve versioning", 0.4.8) and replaced the original 0.4.5 rule, which required only that the schema come from a server-maintained catalogue and not from the caller-supplied URI.The SSRF rationale in the surrounding prose only ever supported the original. The control is don't dereference a caller-supplied URI — not which server-owned identifier keys the lookup. The stricter wording also contradicted the SHOULD two lines below it, which permits rejecting a
dataschemathat doesn't match a catalogue entry: a field you may reject on mismatch is not "informational".It also had a cost. It pushed implementations into lossy PascalCase↔kebab-case derivations. Where that transform is not total over the catalogue, the authorisation key and the validation key silently diverge — a correctly allow-listed command is denied under a policy key the operator never configured, and in the worse direction a caller selects their own contract.
Found while auditing a real implementation: 7 of 23 command types in one deployment's test suite hit exactly this, and the spec's stated justification for type-primacy (
typeis the routing key) did not even hold there — that implementation dispatches on the resolved schema, never ontype.What changed
Relaxed.
dataschemais reframed as a selector, not a location. Selection MAY be keyed bytypeor by the schema name it carries, provided the entry is server-owned. Both MUST-NOT-fetch rules are unchanged. A server already keying ontypestays conformant with no action.Added — same identifier, or a total mapping. Selection, authorisation and dispatch MUST key on one identifier; any derivation between
typeandschemaMUST be total over the catalogue, or the identifier MUST be resolved once and reused. Plus fail-closed on unresolvable schemas, and a documented version policy with no partial honouring of a caller-declared version.Added — Command Authorisation. This filled a genuine gap: the Read/Write scope table was the entire authorisation model, which implies any Write-scoped caller may POST every command — while
POST /commandsis one endpoint for operations that do not share a blast radius. New section covers deny-by-default per-command policy (an empty policy MUST NOT mean allow-everything), deployment-scoped policy,GET /commandsreflecting the caller's real permissions, actor binding for payload principal fields (reject-on-mismatch or overwrite, overwrite preferred), unchecked subject references as IDOR, and controls for destructive commands that MUST NOT be self-serviceable.Version
0.9.1 — no wire-format change; clients need no change.
MIGRATION.mdrestructured to carry both releases (the 0.8.x → 0.9.0 guidance is still live per ROADMAP).Verification
validate-schemas.mjsandvalidate-examples.mjs— 12 files, 0 errorsnpm run build— clean prerenderslugify(it collapses-+), updated all 3 inbound links, and verified every internal anchor across all spec pages plus the root docs resolves{{BEST_VERSION}}🤖 Generated with Claude Code