Skip to content

Command authorisation requirements; relax schema selection to any server-owned identifier - #3

Merged
riccardone merged 1 commit into
mainfrom
command-authz-and-dataschema-selector
Jul 30, 2026
Merged

Command authorisation requirements; relax schema selection to any server-owned identifier#3
riccardone merged 1 commit into
mainfrom
command-authz-and-dataschema-selector

Conversation

@riccardone

Copy link
Copy Markdown
Contributor

Why

The dataschema clause required servers to 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".

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 (type is the routing key) did not even hold there — that implementation dispatches on the resolved schema, never on type.

What changed

Relaxed. dataschema is reframed 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 are unchanged. A server already keying on type stays conformant with no action.

Added — same identifier, or a total mapping. Selection, authorisation and dispatch MUST key on one identifier; any derivation between type and schema MUST 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 /commands is 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 /commands reflecting 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.md restructured to carry both releases (the 0.8.x → 0.9.0 guidance is still live per ROADMAP).

Verification

  • validate-schemas.mjs and validate-examples.mjs — 12 files, 0 errors
  • npm run build — clean prerender
  • Renaming the section changed its anchor; checked the site's own slugify (it collapses -+), updated all 3 inbound links, and verified every internal anchor across all spec pages plus the root docs resolves
  • Version stamping confirmed in the build: 0.9.1 present, no stale 0.9.0, no unstamped {{BEST_VERSION}}

🤖 Generated with Claude Code

…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>
@riccardone
riccardone merged commit 96a081c into main Jul 30, 2026
6 checks passed
@riccardone
riccardone deleted the command-authz-and-dataschema-selector branch July 30, 2026 09:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant