Accept ODRL SACD grants under DIMO profile v1, with per-permission data windows - #17
Open
elffjs wants to merge 4 commits into
Open
Accept ODRL SACD grants under DIMO profile v1, with per-permission data windows#17elffjs wants to merge 4 commits into
elffjs wants to merge 4 commits into
Conversation
Alongside legacy SACDData documents, the exchange now accepts CloudEvents of type dimo.sacd.odrl whose data is a W3C ODRL 2.2 Agreement restricted to DIMO profile v1 (https://ns.dimo.co/odrl/v1). Dispatch is on the CloudEvent envelope type; legacy documents are untouched. Profile v1 covers exactly the capabilities of legacy permission grants: assigner (grantor), assignee (grantee), target (asset DID), a validity period as conjunctive dateTime constraints, and named permissions using the existing privilege:* vocabulary. The minted claims are identical to those from an equivalent legacy document, so downstream consumers (dq) need no changes. CloudEvent-scoped access is not in profile v1 and such requests are refused when backed by an ODRL grant. The evaluator is fail-closed: unknown fields (prohibitions, duties, refinements), unknown actions, unsupported operators, and any @context other than the fixed canonical forms reject the document in full. No JSON-LD processing is ever performed, so a hostile @context cannot remap term meanings under the grantor's signature. See docs/odrl-profile-v1.md and docs/odrl-profile-v1.schema.json. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DNqJMcyn2p3oKosj3KFdQR
A permission in a profile v1 agreement may now carry constraints on the
profile-defined left operand dimo:recordedAt, bounding the recording
timestamps of the data the permission may read:
{ "action": "privilege:GetLocationHistory",
"constraint": [
{ "leftOperand": "dimo:recordedAt", "operator": "gteq",
"rightOperand": "2026-04-01T00:00:00Z" } ] }
This is distinct from the policy-level dateTime constraints, which bound
when the grant may be exercised and are consumed at exchange time. The
two vocabularies never mix positions: dateTime is rejected on a
permission, dimo:recordedAt at the policy level.
The constraint atom {leftOperand, operator, rightOperand} is now a
shared type in pkg/tokenclaims, used verbatim in both grant documents
and minted tokens: the exchange verifies and consumes the policy
envelope (parties, target, validity, signature) and forwards the
surviving data-level atoms untouched — selection and resolution, never
translation. pkg/tokenclaims also carries the canonical fail-closed
evaluator (RecordedAtWindow, AllowsInterval, AllowsAt) for consumers;
a constraint a consumer cannot interpret denies access.
The claim encoding is fail-closed by construction: a permission granted
under constraints is minted ONLY into the new scoped_permissions claim,
never the flat permissions array or the deprecated privilege_ids. A
consumer reading only the flat claims sees no permission at all, so it
cannot honor a windowed grant while ignoring its window. The gRPC
AccessCheck, whose response cannot express constraints, reports
has_access=false for scoped grants for the same reason.
Enforcement lives in the data services; dq grows it on its
scoped-permissions branch (ranged queries reject ranges outside the
window; latest-value queries are evaluated within it).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Under the ODRL model, two permission entries naming the same action are independent grants — a union, e.g. two disjoint data windows. Profile v1 defines no union semantics, and the evaluator's action-keyed grant map silently honored only the last entry (while dq's claim reader would take the first): exactly the partial honoring the profile promises never to do. Reject the document instead, keeping the union shape unambiguous for a future profile version that defines it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design doc only — nothing implemented; v1 evaluators reject every v2
construct by construction (profile IRI, duplicate actions, unknown
action/operands/operator).
The two additions ship together because the second forces the first: a
grant covering several event tuples needs several permission entries of
the same action, which v1 deliberately rejects. v2 defines the ODRL
reading — same-name entries are alternatives, some-entry-covers
authorizes — which also gives data windows disjoint unions ("the last
two weekends"). dimo:ReadCloudEvents reaches parity with legacy SACD
cloudevent agreements via categorical operands (eventType/Source/ID/Tag,
eq + isAnyOf) minus the magic "*" wildcards, and composes with
dimo:recordedAt in a single entry — something the legacy format cannot
express. Consumer skew under both additions is deny-biased, so v2 can
ship exchange-first.
Co-Authored-By: Claude Fable 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.
What
Alongside legacy
SACDDatadocuments, the exchange now accepts CloudEvents of typedimo.sacd.odrlwhosedatais a W3C ODRL 2.2Agreementrestricted to DIMO profile v1 (https://ns.dimo.co/odrl/v1). Dispatch is on the CloudEvent envelopetype; legacy documents flow through the existing path untouched.ODRL: W3C spec · Wikipedia
Profile v1 covers the capabilities of legacy permission grants plus one extension beyond them: per-permission data windows.
target(ERC721/Ethr DID)assigner(did:ethr:...)assignee(did:ethr:...)constraint(dateTimecomparisons, conjunctive; absent = unbounded)permission[].action(existingprivilege:*names)permission[].constraint(dimo:recordedAtcomparisons, conjunctive)Parties are DIDs rather than legacy
{address}objects. CloudEvent-scoped access is not in profile v1; token requests carrying event filters are refused when backed by an ODRL grant.Data windows (
dimo:recordedAt)A permission may be granted only for data recorded within a time window — "you can watch the car live and read Q2 2026 history, but nothing else":
{ "action": "privilege:GetLocationHistory", "constraint": [ { "leftOperand": "dimo:recordedAt", "operator": "gteq", "rightOperand": "2026-04-01T00:00:00Z" }, { "leftOperand": "dimo:recordedAt", "operator": "lt", "rightOperand": "2026-07-01T00:00:00Z" } ] }The two constraint positions answer different questions and accept disjoint vocabularies. Policy-level
dateTimebounds when the grant may be exercised — evaluated once, at exchange time, never in the token. Per-permissiondimo:recordedAt(profile-defined) bounds the recording timestamps of readable data — not evaluated at exchange time, but forwarded verbatim into the minted token for the data services to enforce.One constraint language end to end: the atom
{leftOperand, operator, rightOperand}is a shared type inpkg/tokenclaims, used identically in grant documents and token claims. The exchange verifies and consumes the policy envelope (parties, target, validity, signature) and forwards the surviving data-level atoms untouched — selection and resolution, never translation.pkg/tokenclaimsalso ships the canonical fail-closed evaluator (RecordedAtWindow,AllowsInterval,AllowsAt) for consumers.The claim encoding is fail-closed by construction
A permission granted under constraints is minted only into the new
scoped_permissionsclaim — never the flatpermissionsarray, never the deprecatedprivilege_ids:A consumer that reads only the flat claims sees no permission at all, so it cannot honor a windowed grant while ignoring its window — every deployed consumer today refuses scoped tokens with zero changes. If a consumer forgets to enforce a future constraint type, that degrades to "windowed tokens don't work there" (a loud functional bug), not a silent authorization hole. The gRPC
AccessCheck, whose response cannot express constraints, reportshas_access: falsefor scoped grants for the same reason.Unconditional grants mint claims identical to an equivalent legacy document — no
scoped_permissionskey at all — so existing consumers are unaffected until a grantor actually authors a windowed agreement.Companion: dq's
scoped-permissionsbranch adds the enforcement: ranged queries (signals/events/segments/cloud events) reject ranges outside the window; latest-value queries are evaluated within it.Strictness rules
prohibition,duty, refinements) reject the document in full.@contextmust be byte-identical to a canonical form and is never dereferenced — a hostile context cannot remap term meanings under the grantor's signature.gteq/gt/lteq/ltwith plain RFC 3339 operands; policy-level constraints accept onlydateTime, per-permission constraints onlydimo:recordedAt— exercise time and data time never mix positions.Later profile versions widen the vocabulary (CloudEvent scoping via refinements,
Settemplates,isAnyOf, purpose constraints, further data-level operands like geofences); they never change the meaning of v1-valid documents, and consumers automatically refuse operands they haven't been taught.Files
pkg/tokenclaims/constraint.go— shared constraint atom + canonical fail-closed window evaluatorpkg/tokenclaims/tokenclaims.go—scoped_permissionsclaiminternal/exchange/models/odrl.go— strict profile parser and structural validation (both constraint positions)internal/exchange/autheval/odrl.go— grant-map evaluation (target, validity window, actions, per-permission constraints)internal/exchange/services/access/access.go— envelope-type dispatch +evaluateODRLDoc;ValidateAccessnow returns aDecisioncarrying scoped grantsinternal/exchange/services/signer.go— fail-closed claim split at mintinginternal/exchange/controllers/rpc/rpc.go—AccessCheckfail-closed on scoped grantsinternal/exchange/services/ipfs_service.go— accept the new envelope typedocs/odrl-profile-v1.md,docs/odrl-profile-v1.schema.json— profile spec, example document, JSON Schemadocs/odrl-profile-v2-draft.md— design draft (nothing implemented) for the next widening: union semantics for duplicate actions + CloudEvent-scoped access (dimo:ReadCloudEvents); v1 evaluators reject every v2 construct by constructionTesting
21 table-driven cases in
odrl_test.goagainstValidateAccessViaSourceDoc: happy path, subset grants, expired / not-yet-effective / unbounded agreements, assignee/target/signature mismatches, strictness rejections (unknown action, prohibition present, wrong@type, tampered@context, unsupported operator, event-filter refusal), scoped-grant decisions (verbatim constraint flow, mixed scoped/unscoped), and per-permission rejections (dateTimeon a permission, unknown operator, non-timestamp operand, refinements). Wire-format tests pin the claim split (scoped_permissionspresent, flatpermissions/privilege_idsexclude scoped names; unconditional tokens byte-identical to before). Unit tests for the window evaluator (bound tightening, inclusivity, fail-closed on unknown vocabulary). Full build, vet, and test suite pass.🤖 Generated with Claude Code
https://claude.ai/code/session_01DNqJMcyn2p3oKosj3KFdQR