Ports the M3 milestone from the superseded feat/hierarchies branch onto main's evolved apply engine. A group node is authoritatively managed by at most one project-root; `pic apply --dir` claims, conflicts, takes over, and (with --prune) structurally reaps owned nodes. - Migration 0066: `projects(id, key UNIQUE)` + promote the inert `groups.owner_project` (0047) to a real FK (ON DELETE SET NULL) + index. - CLI mints a stable, gitignored project key in `.picloud/project.json` (`pic init`, or lazily on first tree plan/apply) and presents it on every tree request; `pic apply --dir --takeover` flag; `pic plan --dir` surfaces ownership conflicts + structural-prune candidates read-only. - Server: prepare_tree resolves ownership read-only (token folds each group's owner key, so a claim/takeover by another repo between plan and apply trips StateMoved). apply_tree upserts the project in-tx, claims unclaimed declared groups, and takes over owned ones under `--takeover`. --prune reaps owned-but-undeclared groups leaf-first (delete_group_tx RESTRICT — never another repo's or a UI-owned node). - Authz (§7.4, ownership ⟂ RBAC): takeover requires GroupAdmin per contested node — enforced in authz_tree (pre-tx) AND re-verified in-tx at the ownership decision, so --force (which waives the staleness token) can't open a takeover-without-admin window. The attacker-supplied project key is length/charset-validated server-side. Adaptation to main (which lacks the superseded branch's M2 declarative group create/reparent): groups pre-exist, so ownership stamps existing declared nodes rather than claim-on-create; the declarative attach-point is deferred. Review fixes (adversarial pass, 3 findings, all closed): - HIGH: an empty `[group]` node was claimed capability-free — require a baseline GroupScriptsWrite (editor) on every group apply node, so claiming ownership needs write authority (Ownership ⟂ RBAC). - MEDIUM: structural prune would silently CASCADE a group's §11.6 shared collections + secrets (which postdate M3's original design). Guard: a prune candidate holding shared data/secrets is KEPT with a warning (plus its candidate ancestors, so a preserved child never aborts a parent delete). - LOW: a corrupt `.picloud/project.json` silently re-minted a new key (orphaning ownership) — now fails loudly if the file exists but won't parse. Tests: tests/ownership.rs (claim → conflict → takeover → flip; non-admin takeover → 403; prune-owned-only; prune-refuses-to-cascade-shared-data); format_conflicts + validate_project_key unit tests; schema golden reblessed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
40 lines
2.1 KiB
SQL
40 lines
2.1 KiB
SQL
-- Phase 5 / M3 (v1.2 Hierarchies): multi-repo single-owner ownership (§7).
|
|
--
|
|
-- A group node is authoritatively MANAGED by at most one project-root (the
|
|
-- repo whose manifest declares it as something it applies, not merely
|
|
-- references). `groups.owner_project` has carried this seam since 0047 as an
|
|
-- inert, FK-less UUID. M3 makes it a real reference: a `projects` table keyed
|
|
-- by a stable, gitignored project key the CLI mints in `.picloud/`, and a
|
|
-- foreign key from `groups.owner_project` to it.
|
|
--
|
|
-- Ownership is recorded at GROUP granularity; apps inherit their owning project
|
|
-- from their group (apps are never claimed directly). A NULL `owner_project`
|
|
-- means the node is UI/API-owned — no manifest fights it (§7.5). First apply
|
|
-- claims; a second project's apply to an owned node is refused unless
|
|
-- `--takeover` (group-admin gated). Ownership ⟂ RBAC: owning the manifest does
|
|
-- NOT grant write — the actor still needs the usual group capabilities (§7.4).
|
|
--
|
|
-- ON DELETE SET NULL: deleting a project row (not something the platform does
|
|
-- today) reverts its nodes to UI-owned rather than cascading away real groups.
|
|
|
|
CREATE TABLE projects (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
-- Stable, opaque key minted by `pic init` and persisted (gitignored) in the
|
|
-- repo's `.picloud/`. The CLI presents it on every tree apply; the server
|
|
-- maps it to this row (upsert-by-key), so the same repo keeps the same
|
|
-- project identity across clones/CI without committing any server id.
|
|
key TEXT NOT NULL UNIQUE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
-- Promote the inert `groups.owner_project` (0047) to a real FK now that the
|
|
-- referent exists. Existing rows are all NULL (no projects yet), so this adds
|
|
-- no validation burden.
|
|
ALTER TABLE groups
|
|
ADD CONSTRAINT groups_owner_project_fkey
|
|
FOREIGN KEY (owner_project) REFERENCES projects(id) ON DELETE SET NULL;
|
|
|
|
-- The ownership reconcile reads "which groups does project P own?" (to find
|
|
-- owned-but-undeclared nodes for structural prune) and "who owns group G?".
|
|
CREATE INDEX groups_owner_project_idx ON groups (owner_project);
|