Enforce scoped permissions (ODRL data windows) from dauth tokens - #25
Open
elffjs wants to merge 2 commits into
Open
Enforce scoped permissions (ODRL data windows) from dauth tokens#25elffjs wants to merge 2 commits into
elffjs wants to merge 2 commits into
Conversation
dauth's exchange can now mint permissions granted only for data recorded within a time window (DIMO ODRL profile v1, dimo:recordedAt). Such permissions arrive in a new scoped_permissions claim, deliberately absent from the flat permissions array, so before this change dq refused windowed tokens everywhere — fail-closed by encoding. This change opens each surface deliberately, with the window enforced: - Possession checks (schema directives, per-signal privilege filter) count scoped permissions as held; the new internal/scope package is the one place that reads them, always alongside their constraints. A constraint dq cannot interpret grants nothing. - Ranged queries — signals, events, segments, dailyActivity, and cloud-event reads (before/after) — REJECT requests whose range exceeds the window, naming the window in the error. Rejection rather than silent clamping: an aggregate computed over a narrower range than requested would be mislabeled as covering the full range. An absent range means all time and only an unbounded grant passes. - Latest-value queries (signalsLatest, signalsSnapshot) are evaluated WITHIN the window: a value recorded outside it is withheld, which is indistinguishable from the vehicle not having transmitted then, and the returned timestamps keep the result self-describing. The derived approximate location is gated separately from the raw coordinates row, since either qualifying permission may allow a timestamp the other does not. - All-time surfaces that cannot yet window their answers stay closed for scoped grants: availableSignals and dataSummary exclude signals behind scoped permissions (their counts and first/last-seen are unwindowable post hoc; the summary's top-level fold is recomputed from what survives), lastSeen is suppressed, and the gRPC fetch surface still reads only the flat claim. Teaching these surfaces window-bounded queries is follow-up work. NOTE: go.mod temporarily replaces github.com/DIMO-Network/dauth with a local path; swap for a tagged dauth release (DIMO-Network/dauth#17) before merge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bug: the signals range check rejected the WHOLE query when a scoped token requested a signal whose permission it does not hold at all — where the design (and main's behavior) leaves possession to the per-field privilege directives. A scoped token mixing one windowed signal with one unheld signal got an opaque window rejection instead of the directive's per-field missing-privilege error. The range check now speaks only for permissions the token actually holds, mirroring the latest path's signalValueVisible split. Also adds tests/scoped_windows_graphql_test.go: end-to-end tests over the REAL executable schema with the REAL auth directives (the existing harnesses stub auth) and a real DuckLake-backed repository, with claims JSON-round-tripped exactly as they ride in a dauth-minted JWT. Covers: in-window ranged queries served, out-of-window ranges rejected naming the window, unconditional signals unaffected, latest served in-window / withheld out-of-window, lastSeen suppression, approximate location served from a raw row the location window withholds, availableSignals/ dataSummary exclusion + count recomputation + event-summary drop, events range rejection, and the unheld-signal-is-a-field-error fix. 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
Companion to DIMO-Network/dauth#17. dauth's exchange can now mint permissions granted only for data recorded within a time window (DIMO ODRL profile v1,
dimo:recordedAt). Those arrive in a newscoped_permissionsclaim:The encoding is fail-closed by construction: windowed permissions are deliberately absent from the flat
permissionsarray, so dq as deployed today already refuses windowed tokens everywhere with zero changes. This PR opens each surface deliberately, with the window enforced. The newinternal/scopepackage is the one place that readsscoped_permissions— always alongside its constraints — and a constraint dq cannot interpret grants nothing.Enforcement model
Ranged queries reject; point queries window-evaluate. The dividing line is whether the response could misrepresent what was computed:
signals,events,segments,dailyActivity, and cloud-event reads (after/before) — are rejected when the requested range exceeds the window, with the window named in the error. Silent clamping would return an aggregate computed over a narrower range than requested, labeled as the full range — a wrong answer presented as right. An absent range means "all time" and only an unbounded grant passes, so integrators must state ranges inside their window.signalsLatest,signalsSnapshot— are evaluated within the window: a value recorded outside it is withheld, which is indistinguishable from the vehicle not having transmitted then, and every returned value carries its timestamp, keeping results self-describing. The derived approximate location is gated separately from the raw coordinates row (either qualifying permission may allow a timestamp the other does not).Surfaces that stay closed for scoped grants (all-time facts that can't be windowed post hoc; follow-up work to teach them window-bounded queries):
availableSignals/dataSummaryexclude signals behind scoped permissions — listing a name or a count/first-seen asserts all-time facts. The summary's top-level first/last-seen fold is recomputed from surviving entries so it can't leak filtered timestamps.lastSeen(computed across every signal) is suppressed for scoped tokens.Files
internal/scope/— possession + window evaluation over the token (Holds,Unscoped,AllowsRange,AllowsAt,Describe), fail-closed on unknown constraint vocabularyinternal/auth/directives.go— directives accept scoped possessioninternal/graph/privilege_filter.go— token-aware per-signal checks: possession, unscoped-only, range, per-value visibilityinternal/graph/arguments.go— per-signal range rejection forsignals; latest-args window hooks +lastSeensuppressioninternal/graph/base.resolvers.go,events.resolvers.go,segments.resolvers.go— surface wiringinternal/graph/auth_helpers.go— cloud-event raw-data path: unscoped fast path, windowed path requires in-windowafter/beforeinternal/repositories/signals.go+internal/graph/model/signalArgs.go— latest-row window filter; approximate location derived from the raw row so it can be served when the raw coordinates are withheldinternal/auth/grpc.go— comment documenting the (already fail-closed) scoped behaviorgo.modtemporarily carriesreplace github.com/DIMO-Network/dauth => ../dauthso CI here will fail until DIMO-Network/dauth#17 merges and a dauth version is tagged; then the replace is dropped and the requirement bumped.Testing
internal/scope: possession/window semantics, exclusive vs inclusive bounds, fail-closed on uninterpretable constraints (a geofence-constrained grant is "held" but allows no data).internal/graph/scoped_windows_test.go: per-signal range checks (windowed location vs unconditional non-location, derived approximate location), per-value visibility, unscoped-only filtering for the all-time surfaces, and the cloud-event path (in-window bounds pass; exceeding or absent bounds rejected; unconditional raw-data unaffected).🤖 Generated with Claude Code