Repository navigation
feat(conformance-fixtures): pin the grantee arms and the evaluation-time refusals - #320
Merged
Merged
Conversation
…ime refusals Two fixtures covered a _grant with no grantee and a group arm with no role, and both stated in prose that the other arms "are refused the same way" — a rule argued in a fixture description and checked by nothing. Nine fixtures now stand behind it. Seven at the write: each incomplete arm, an empty entityId as a separate input class from an absent one, the listing-only role 'any' that a listGrants() query may ask for and a stored grantee may never carry, an unrecognized kind, a null grantee reported as the missing field it is rather than read as absent-then-defaulted, and an `anyone` element labelled anything but `read`. That last one is load-bearing past the element: stored, it would read as world read *and* satisfy the `read` a `write` element needs beside it, so one malformed element would silence write-implies-read for every grantee on the record. Two more pin the half no request shape can show — that a grant reaching storage unvetted confers nothing when it is read back. A grant naming _app@1 still answers 403 on a create, and a mutate verb with no read companion in the same grant conveys neither the write nor a read, so it answers 404 rather than 403. Both are states grant() refuses and a server mapping a request body, an import, or a foreign server's response can still produce, which is the case the two-sided rule exists for. Every expected response was observed against the reference server rather than derived from the spec, details included — adapter-api's error dispatch had no arm for the permissions endpoint, which the `anyone` fixture needed. No behavior change: each rule is already in docs/spec/access-control.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01St5i1czsAp26y8H56pmANC
🦋 Changeset detectedLatest commit: f99ab6b The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Merged
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.
Summary
Two fixtures covered a
_grantwith no grantee at all and agrouparm with no role. Both said in prose that the other arms "are refused the same way" — a rule argued in a fixture description and checked by nothing. This adds the nine fixtures that stand behind it.Seven pin the write:
…grant-entity-grantee-without-entity-identityarm's half of the rule the existing group-without-role fixture pins…grant-entity-grantee-with-empty-entity-id…grant-group-grantee-without-group-id…grant-group-grantee-with-listing-only-rolerole: 'any'is a widening alistGrants()query may ask for and a stored grantee may never carry…grant-grantee-with-unknown-kind…grant-null-granteenullis the missing field it is, not absent-then-defaulted…anyone-element-labelled-writereadis the whole of what the tier can sayThe last is load-bearing past the element itself: stored, an
anyoneelement carryingwritewould read as world read and satisfy thereadawriteelement needs beside it, so one malformed element would silence write-implies-read for every grantee on the record.Two more pin the half no request shape can show — that a grant reaching storage unvetted confers nothing when it is read back:
error-permission-grant-on-an-ungrantable-family-confers-nothing— a stored_grantnaming_app@1still answers403on a create.error-not-found-mutate-grant-with-no-read-companion— a grant carryingupdate-anywith noread-anybeside it conveys neither the write nor a read, so it answers404, not403.Both are states
grant()refuses and that a server mapping a request body, an import, or a foreign server's response can still produce — which is the case the two-sided rule exists for, and the one a second implementation is most likely to get half right.Spec
No behavior change. Every rule here is already stated in
docs/spec/access-control.md— these fixtures pin what the spec says rather than changing it. Each fixture cites the section it comes from:Verification
All six, clean:
Every expected response was observed against the reference server, not derived from the spec —
detailspaths and messages included, sinceadapter-api's error dispatch checks only status and code and the details would otherwise be free to drift. The two evaluation-time fixtures were confirmed the same way: a stored grant on_app@1answering 403 on a create, and a companion-lessupdate-anyanswering 404.One test change fell out of that:
adapter-api's error-fixture dispatch had no arm for the permissions endpoint, so theanyonefixture had nowhere to route. Added one that callsassociate(), which picks the endpoint from the element's own kind.Notes for reviewers
This will fail
haverstack/server's CI once released, by design. That repo holdserrorResponseFixturesto a coverage gate, so the nine new names have to be dispatched or explicitly skipped there. The server-side dispatch is a follow-up PR that has to wait on this release — it cannot pass CI against the registry until then. Theminorbump is for the same reason: a consumer with a gate can observe new fixtures.On the
detailsfields.adapter-apicompares status anderror.codeonly, and the server's conformance test does the same, sodetailsis normative documentation for other implementations rather than an asserted contract. That makes it the part most able to rot quietly. Worth deciding separately whether either consumer should start asserting it; I left both as they are.Deliberately left out, as sequence fixtures rather than single pairs, and worth filing if wanted:
_groupwithdrawing every permission and grant that reached through its roster_grantstill conferring while excluded from enumerationpermissionskey the reshare gate reads as inert being dropped rather than forwardedAll three are overhaul rules worth pinning across implementations; each needs prior state and an ordering that a single request/response pair cannot express.
These came out of a verification pass on the server repo, driven by a wire-only client against a running server — 126 assertions over the permission surface. Enforcement held throughout; these are the rules that turned out to be pinned by prose alone. Server-side findings are in haverstack/server#127.
🤖 Generated with Claude Code
https://claude.ai/code/session_01St5i1czsAp26y8H56pmANC
Generated by Claude Code