fix(plan/C1): close Admin→Owner escalation in enforce_owner_only_elevation

Spot-check of the new H-C1 hook code found the owner-only-elevation gate was
bypassable: it skipped any member ALREADY privileged in the parent, but since
Admin is also "privileged", an Admin→Owner promotion was skipped and accepted —
the exact escalation the gate exists to stop, and a failure of its own paired
test. Gate now skips only UNCHANGED roles (parent role == new role), so every
change into a privileged role (Member→Admin/Owner, Admin→Owner, new privileged
member) requires an owner signer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-06-19 20:00:09 -04:00
parent b655024320
commit f249395644

View File

@@ -4583,12 +4583,14 @@ fn enforce_owner_only_elevation(
if !is_privileged(m.role) {
continue;
}
// Privileged now. Was it already privileged in the parent (no change)?
let already = parent_role(m.member_id.as_str())
.map(is_privileged)
.unwrap_or(false);
if already {
continue; // not an introduction or elevation
// Privileged now. Skip ONLY if the role is unchanged from the parent
// (a no-op same-role entry). Any CHANGE into a privileged role — a new
// privileged member, Member→Admin/Owner, or Admin→Owner — must be
// owner-signed. (A naive "already privileged" skip is WRONG: Admin is
// also privileged, so it would let an Admin be elevated to Owner
// unchecked — the exact escalation this gate exists to stop.)
if parent_role(m.member_id.as_str()) == Some(m.role) {
continue; // unchanged role — not an introduction or elevation
}
// A new owner/admin, or a member elevated to owner/admin → owner-only.
if !signer.role.can_manage_owners() {