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>
52 lines
1.5 KiB
TOML
52 lines
1.5 KiB
TOML
[package]
|
|
name = "picloud-cli"
|
|
version.workspace = true
|
|
edition.workspace = true
|
|
rust-version.workspace = true
|
|
license.workspace = true
|
|
repository.workspace = true
|
|
authors.workspace = true
|
|
description = "PiCloud command-line client"
|
|
# Each top-level `tests/*.rs` would otherwise auto-discover as its own
|
|
# test binary, respawning picloud once per file. We want one binary
|
|
# with module sub-files (auth.rs, apps.rs, …) so the LazyLock fixture
|
|
# is genuinely shared.
|
|
autotests = false
|
|
|
|
[[bin]]
|
|
name = "pic"
|
|
path = "src/main.rs"
|
|
|
|
[[test]]
|
|
name = "cli"
|
|
path = "tests/cli.rs"
|
|
|
|
[dependencies]
|
|
picloud-shared.workspace = true
|
|
reqwest = { workspace = true, features = ["json"] }
|
|
percent-encoding.workspace = true
|
|
serde.workspace = true
|
|
serde_json.workspace = true
|
|
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
|
chrono = { workspace = true }
|
|
clap = { version = "4", features = ["derive"] }
|
|
toml = "0.8"
|
|
directories = "5"
|
|
rpassword = "7"
|
|
anyhow = "1"
|
|
# §7 M3: minting the stable project key in `.picloud/project.json`.
|
|
uuid = { version = "1", features = ["v4"] }
|
|
|
|
[dev-dependencies]
|
|
assert_cmd = "2"
|
|
predicates = "3"
|
|
tempfile = "3"
|
|
reqwest = { workspace = true, features = ["json", "blocking"] }
|
|
libc = "0.2"
|
|
# Synchronous Postgres driver used by the dead-letters integration test
|
|
# to inject a row the replay path can act on. Smaller compile cost than
|
|
# pulling sqlx into the CLI tree, and the existing tests are
|
|
# blocking-style anyway.
|
|
postgres = { version = "0.19", features = ["with-uuid-1", "with-serde_json-1"] }
|
|
uuid = { version = "1", features = ["v4"] }
|