Files
PiCloud/crates/manager-core/migrations/0067_project_environments.sql
MechaCat02 c06a9e801e feat(apply): make the per-env approval gate hermetic (§3 M3 / Track A M1)
The per-env approval policy was applier-supplied — a hand-crafted request
that omitted `project.environments` was ungated, and flipping a gate to
`confirm = false` in the same request un-gated it. Persist the policy
server-side and enforce against `persisted ∪ declared`.

- Migration 0067: `project_environments (project_id, env_name, confirm)`,
  CASCADE on the project. Written declaratively (delete-then-insert) inside
  `upsert_project_tx`, same tx as the project row + node claim.
- `ProjectRepository::get_environments_by_slug` (read side) +
  `ApplyService::effective_env_policy` union the persisted policy with the
  request's declared one (confirm if EITHER says so — monotonic).
- `env_gate_check` now evaluates the effective policy; the three handlers load
  it before the gate. `plan.approvals_required` is the effective gated set, so
  CI sees a persisted gate even when the manifest omits it.
- The union rule closes both bypasses: an omitted policy still trips a
  persisted gate, and an ungating apply must itself pass the gate (the
  declarative replace then takes effect next time) — TOCTOU-safe.

Pinned by env_approval::{persisted_policy_gates_a_request_that_omits_it,
flipping_a_gate_to_false_in_the_same_request_still_gates} +
projects_repo::get_environments_by_slug_returns_persisted_policy. Schema golden
reblessed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 14:31:15 +02:00

26 lines
1.4 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- §3 M3 (hermetic gate): persist the per-env approval policy server-side.
--
-- Before this table the policy lived ONLY in the apply request
-- (`ProjectDecl.environments`), so a hand-crafted request that omitted it was
-- ungated — the server had nothing authoritative to check against. This makes
-- the gate hermetic: the confirm-required set is stored per project and the
-- server enforces against `persisted declared` (an env is gated if EITHER the
-- stored row OR the request says so), so an omitted or flipped-to-false policy
-- can no longer bypass a gate a prior apply established.
--
-- One owner (the project), a single boolean policy per environment name — no
-- polymorphic owner and no environment_scope (unlike `vars`/`secrets`, which
-- are data the apply targets; this is metadata ABOUT the apply). CASCADE on the
-- project mirrors 0066's philosophy: un-claiming/deleting a project drops its
-- policy, it does not linger to gate a tree the project no longer owns.
CREATE TABLE project_environments (
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
-- The environment name as declared in `[project.environments]` (e.g.
-- "production"); matched against the apply's `--env`.
env_name TEXT NOT NULL,
-- TRUE => applying to this env is gated (needs `--approve <env>` + admin).
confirm BOOLEAN NOT NULL,
PRIMARY KEY (project_id, env_name)
);