Enforce persisted curtailment authorization envelopes - #957
Merged
negarn merged 2 commits intoAug 24, 2026
Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR shifts curtailment event and response-profile authorization from “current topology-derived scope” to a strictly-validated, persisted authorization envelope captured at write-time, ensuring read/mutate/recovery checks remain anchored to the durable snapshot and fail closed on malformed persisted data.
Changes:
- Add strict parsing/validation for persisted curtailment authorization envelopes and enforce envelope-based permission checks (including
site:readfor facility-fan coverage). - Update curtailment event/profile handlers to filter/list and gate single-record operations using persisted envelope-derived resource contexts with per-request permission caching.
- Make response-profile deletion concurrency-safe with respect to authorization changes by including the expected authorization envelope in the SQL delete predicate (plus corresponding store/service/interface changes and tests).
Reviewed changes
Copilot reviewed 18 out of 19 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| server/sqlc/queries/curtailment_response_profile.sql | Adds authorization_envelope_jsonb to the optimistic delete predicate for response profiles. |
| server/internal/handlers/curtailment/response_profiles.go | Enforces persisted-envelope authorization for response profile get/list/update/delete, with request-scoped permission caching. |
| server/internal/handlers/curtailment/handler.go | Enforces persisted-envelope authorization for curtailment events (read/manage/recovery), adds envelope permission caches, and hydrates target-site coverage post-filtering. |
| server/internal/handlers/curtailment/automation.go | Adapts to the updated permission-cache shape in list filtering logic. |
| server/internal/handlers/curtailment/handler_update_test.go | Updates update-handler tests to include persisted authorization envelopes and new site-scoped permission expectations. |
| server/internal/handlers/curtailment/handler_test.go | Adds helper utilities and unit tests for envelope requirement derivation and fail-closed behavior. |
| server/internal/handlers/curtailment/handler_stop_test.go | Updates stop-handler tests to ensure events include persisted envelopes. |
| server/internal/handlers/curtailment/handler_start_test.go | Updates start-handler replay path tests to ensure replay events include persisted envelopes. |
| server/internal/handlers/curtailment/handler_response_profiles_test.go | Updates response-profile handler tests for persisted facility-fan site enforcement and delete-envelope snapshotting. |
| server/internal/handlers/curtailment/handler_list_test.go | Updates list tests to filter by persisted envelope authorization (including whole-org behavior and facility-fan requirements). |
| server/internal/handlers/curtailment/handler_admin_terminate_test.go | Updates admin terminate / force-release tests to rely on persisted envelopes and allow matching site-only manage. |
| server/internal/domain/stores/sqlstores/curtailment.go | Threads expected authorization envelope through response-profile deletion and passes it into sqlc params. |
| server/internal/domain/stores/interfaces/curtailment.go | Updates ResponseProfileStore.DeleteResponseProfile signature to include expected authorization envelope JSON. |
| server/internal/domain/curtailment/response_profile.go | Updates service delete API to include expected authorization envelope JSON. |
| server/internal/domain/curtailment/response_profile_test.go | Updates fake-store/service tests for the updated delete signature. |
| server/internal/domain/curtailment/authorization_envelope.go | Introduces strict persisted-authorization-envelope parsing/validation (fail-closed). |
| server/internal/domain/curtailment/authorization_envelope_test.go | Adds unit tests for strict parsing and rejection cases. |
| server/internal/domain/stores/sqlstores/response_profile_facility_fans_integration_test.go | Extends integration coverage for stale-envelope delete behavior (FailedPrecondition). |
| server/generated/sqlc/curtailment_response_profile.sql.go | Regenerated sqlc bindings for the updated delete query (generated — skip). |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
rl-block
approved these changes
Aug 21, 2026
negarn
deleted the
negar/gh-909-enforce-persisted-curtailment-authorization-envelopes
branch
August 24, 2026 12:49
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.
Reviewable diff: +299/-448 across 8 files (excludes generated, test, and story files).
Summary
Curtailment events and response profiles now authorize reads, mutations, and recovery operations from the durable authorization envelope captured when each record is written. This lets site-scoped operators retain access to the records they were authorized to create even if topology membership changes later, while malformed or incomplete persisted envelopes fail closed. This is the next backend slice of issue #909; topology execution and the remaining lifecycle work stay tracked there.
How it works
Handlers first require that the caller has the relevant curtailment capability at some supported scope, load the organization-owned event or profile, and strictly parse its persisted authorization envelope. Miner and facility-fan coverage are converted into site resource contexts; unbounded coverage requires an organization-wide grant, and facility-fan sites additionally require
site:read.Single-record operations enforce every required context before returning or mutating the record. List operations apply the same checks with per-request permission caches, filter unauthorized records, and only then hydrate display-oriented target coverage. Response-profile deletion also includes the authorized envelope in its SQL optimistic predicate, so a concurrent envelope change cannot invalidate the permission decision before deletion.
Diagrams
flowchart TD A["Event or profile RPC"] --> B["Require scoped curtailment capability"] B --> C["Load organization-owned record"] C --> D["Strictly parse persisted authorization envelope"] D --> E["Check curtailment permission for miner and fan coverage"] E --> F["Check site:read for facility-fan coverage"] F --> G["Filter or continue the lifecycle operation"] G --> H["Hydrate display-only target coverage"]sequenceDiagram participant H as Handler participant S as Response profile service participant DB as PostgreSQL H->>S: Delete with authorized selector, fan, and envelope snapshots S->>DB: DELETE where every expected snapshot still matches DB-->>S: One row or zero rows S-->>H: Success or FailedPreconditionAreas of the code involved
server/internal/domain/curtailment/server/internal/handlers/curtailment/handler.goserver/internal/handlers/curtailment/response_profiles.goserver/internal/domain/stores/andserver/sqlc/queries/server/generated/sqlc/Key technical decisions & trade-offs
site:readfor every captured fan site.Testing & validation
just gencd server && just lintcd server && go test ./internal/domain/curtailment ./internal/handlers/curtailment -count=1cd server && go test ./internal/domain/stores/sqlstores -run '^(TestBuildAuthorizationEnvelopeJSON|TestLockTopologyScopeCoverage)' -count=1git diff --check origin/main...HEADfleetpassword; focused in-memory SQL-store authorization tests passed, and CI remains responsible for the database integration run.Refs #909