feat(hierarchies): multi-repo single-owner ownership + structural prune (M3)
§7 of the groups/project-tool design. A group node is now authoritatively managed by exactly one project-root; `pic apply --dir` claims, conflicts, takes over, and (with --prune) structurally reaps owned nodes. - Migration 0052: `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. - Server: prepare_tree resolves ownership read-only (plan surfaces conflicts + prune candidates; token folds each group's owner key). apply_tree upserts the project in-tx, claims created groups on insert, reconciles existing-group ownership under the per-node advisory lock (first-commit-wins), and prunes owned-but-undeclared groups leaf-first (delete=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. - Tests: tests/ownership.rs (claim → conflict → takeover → flip; non-admin takeover → 403; prune-owned-only); format_conflicts unit test; schema golden reblessed. tree_shape M2 journey updated to reparent in-place (a fresh dir is now a distinct project and would correctly conflict). Closes the M3 milestone; reviewed (4 findings: 1 authz-bypass via --force + key validation + 2 LOW, all fixed). M4 (trigger/route templates) remains. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -83,8 +83,18 @@ fn tree_apply_creates_then_reparents_a_group() {
|
||||
"re-plan of an applied tree must be a no-op:\n{plan_txt}"
|
||||
);
|
||||
|
||||
// --- Reparent: move the child under the instance root. ---
|
||||
let dir2 = group_dir(&child, "root");
|
||||
// --- Reparent: move the child under the instance root. Rewrite the SAME
|
||||
// repo's manifest (M3: a stable `.picloud/` project key per repo — a fresh
|
||||
// dir would be a different project and conflict on the owned group). ---
|
||||
fs::write(
|
||||
dir.path().join("picloud.toml"),
|
||||
format!(
|
||||
"[group]\nslug = \"{child}\"\nname = \"Child Group\"\nparent = \"root\"\n\n\
|
||||
[[scripts]]\nname = \"hello\"\nfile = \"scripts/hello.rhai\"\n"
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
let dir2 = &dir;
|
||||
let out = common::pic_as(&env)
|
||||
.args(["apply", "--dir"])
|
||||
.arg(dir2.path())
|
||||
|
||||
Reference in New Issue
Block a user