block/buzz · error · IngestError::Rejected

only owners/admins may grant elevated roles

Error message

only owners/admins may grant elevated roles

What it means

On a private channel, the requested role is elevated (owner or admin — MemberRole::is_elevated()) but the actor's own channel role is not. Granting elevated roles on private channels is restricted to existing owners/admins, so a plain member (or a member with guest/bot role) cannot mint new admins or owners even though they may add ordinary members, guests, and bots.

Source

Thrown at crates/buzz-relay/src/handlers/side_effects.rs:373

                .iter()
                .find(|m| m.pubkey == actor_bytes)
                .and_then(|m| m.role.parse().ok());
            let target_pubkey =
                extract_p_tag(event).ok_or_else(|| anyhow::anyhow!("missing p tag"))?;

            // PUT_USER: open channels allow any authenticated user; private channels
            // require the actor to be an existing active member. Any active member may
            // add an ordinary member, guest, or bot, but only owners/admins may grant
            // an elevated role.
            if channel.visibility == "private" {
                if actor_role.is_none() {
                    return Err(anyhow::anyhow!("actor not authorized"));
                }

                if requested_role.is_some_and(|role| role.is_elevated())
                    && !actor_role.is_some_and(|role| role.is_elevated())
                {
                    return Err(anyhow::anyhow!(
                        "only owners/admins may grant elevated roles"
                    ));
                }
            }

            // Changing an ACTIVE existing member's role is privileged in both
            // directions, on every visibility. `get_members` filters
            // `removed_at IS NULL`, so a soft-removed row is deliberately not an
            // "existing member" here: its stored role is history, not live
            // authority, and reactivation is governed by the elevated-granter
            // check above rather than by the role the row remembers.
            //
            // `add_member` is the authority (it also covers the desktop/admin
            // callers that skip this validator); rejecting here too means the
            // client gets a real error instead of an OK for an event whose side
            // effect then fails. Re-adding at the same role stays idempotent —
            // the huddle bot-add path relies on that.
            if let Some((target, role)) = members

View on GitHub (pinned to f956e6fe06)

Solutions

  1. Have a channel owner/admin issue the elevated-role grant
  2. Drop the role tag entirely or request a non-elevated role (member/guest/bot) if that is what you actually need
  3. For self-joins to private channels, add yourself without a role tag and ask an admin for promotion

Example fix

// before: plain member grants admin on a private channel
put_user(private_channel, target).with_role("admin").publish() // → only owners/admins may grant elevated roles

// after: member adds plainly; owner elevates separately
put_user(private_channel, target).publish(); // member: no role tag
put_user(private_channel, target).with_role("admin").publish_as(channel_admin_keys);
Defensive patterns

Strategy: validation

Validate before calling

// Block elevated grants unless the actor is elevated
let elevated_request = requested_role.is_some_and(|r| r.is_elevated());
let actor_elevated = actor_role.is_some_and(|r| r.is_elevated());
if channel.visibility == "private" {
    anyhow::ensure!(
        !elevated_request || actor_elevated,
        "only owners/admins grant owner/admin roles on private channels"
    );
}

Type guard

const isElevated = (r: string): boolean => r === "owner" || r === "admin";
const canGrant = (actorRole: string | null, requested: string | null): boolean =>
  requested === null || !isElevated(requested) || (actorRole !== null && isElevated(actorRole));

Try / catch

match validate_admin_event(&tenant, 9000, &event, &state).await {
    Err(e) if e.to_string().contains("only owners/admins may grant elevated roles") => {
        // split the operation: member adds plainly, owner promotes afterwards
        split_add_and_promote(channel, target, requested_role).await
    }
    other => other,
}

Prevention

When it happens

Trigger: A member-level user publishes 9000 with ["role","admin"] or ["role","owner"] on a private channel; a bot-role member tries to promote itself; self-add with an elevated role (target == actor still passes through this check because it runs before the self-add shortcut).

Common situations: Invite flows that default the role tag to 'admin'; members assuming community-level admin status carries over (it does not at this seam); privilege-escalation probes where a member adds their own second key as channel admin.

Related errors


AI-assisted analysis of block/buzz@f956e6fe06 (2026-08-16). Data as JSON: /api/errors/9ca23fa5a22ab90a. Report an issue: GitHub.