docs+test(hierarchies): end-to-end review follow-ups (M5/M6/M7)

Findings from a 3-lens end-to-end review (correctness, security, tests).
Security review came back clean (cross-repo authz sound, isolation intact,
M5 gate sound modulo the documented manifest-trust boundary). No behavior
changes here — only accuracy + coverage:

- apply_service Phase B2: correct the descendant-expansion comment. The chain
  is COMMITTED ancestry (ancestors() reads the pool), so a reparent in the same
  apply takes effect next apply — same "one more apply" shape the in-tree path
  and group-create reparenting already have. Authz stays sound: the gate and the
  expansion consume the SAME chain (checked == written).
- doc §4.5: document the two known "one more apply / no data risk" limitations
  — reparent-in-same-apply template resolution, and the `{env}` source split
  (declared apps use --env; cross-repo descendants use apps.environment).
- approval journey: give the app real (`[vars]`) content so the editor member's
  AppVarsWrite is actually exercised — the admin-gate test now proves approval
  is ABOVE editor-write, not just "any non-admin is refused". (Vars cascade with
  the app; scripts are ON DELETE RESTRICT and would break AppGuard teardown.)
- templates journey: assert descendant expansions are idempotent (stable row
  ids on re-apply — no churn), matching the in-tree guarantee.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-28 19:57:21 +02:00
parent 22d6c33d0b
commit aa2831da90
4 changed files with 62 additions and 7 deletions

View File

@@ -1615,12 +1615,20 @@ impl ApplyService {
.iter()
.map(|g| g.id)
.collect();
// The chain is the descendant's COMMITTED ancestry (`ancestors` reads
// the pool, not this tx). A reparent performed in THIS same apply is
// therefore not yet reflected, so a template gained/lost via that
// reparent fans out on the NEXT apply — the same "reparent takes one
// more apply" limitation the in-tree path has (prepare_tree also
// computes app chains pre-tx) and that group-create reparenting
// already documents above (§4.5 known limitation).
//
// Authoritative IN-TX per-recipient authz (§7.4, M7): the actor needs
// the matching write cap on THIS descendant before any expansion is
// written into it. Checked here (post-reparent, already locked) so the
// checked set is by construction the written set — no pre-tx TOCTOU
// and a reparent-into-a-templated-group can't slip the gate. Sound via
// the hierarchy-aware effective_app_role: a group admin/editor is
// written into it. The gate and the expansion below consume the SAME
// `chain`, so the checked set is by construction the written set — no
// pre-tx TOCTOU, the per-app cap can't be slipped. Sound via the
// hierarchy-aware effective_app_role: a group admin/editor is
// implicitly authorized on its subtree; a narrow principal is refused.
let route_tmpls = crate::template_repo::list_for_groups_tx(&mut tx, &chain)
.await