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) { if !is_privileged(m.role) {
continue; continue;
} }
// Privileged now. Was it already privileged in the parent (no change)? // Privileged now. Skip ONLY if the role is unchanged from the parent
let already = parent_role(m.member_id.as_str()) // (a no-op same-role entry). Any CHANGE into a privileged role — a new
.map(is_privileged) // privileged member, Member→Admin/Owner, or Admin→Owner — must be
.unwrap_or(false); // owner-signed. (A naive "already privileged" skip is WRONG: Admin is
if already { // also privileged, so it would let an Admin be elevated to Owner
continue; // not an introduction or elevation // 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. // A new owner/admin, or a member elevated to owner/admin → owner-only.
if !signer.role.can_manage_owners() { if !signer.role.can_manage_owners() {