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)) = membersView on GitHub (pinned to f956e6fe06)
Solutions
- Have a channel owner/admin issue the elevated-role grant
- Drop the role tag entirely or request a non-elevated role (member/guest/bot) if that is what you actually need
- 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
- Default invite flows to no role tag; require an explicit elevation step signed by an admin
- Never pre-fill 'admin' in invite forms for member-level users
- Model elevation as a two-step UX (add then promote) so authorization failures are structurally impossible
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
- only owners/admins may change an active member's role
- actor not authorized
- moderator access required
- missing or invalid h tag
- channel not found
AI-assisted analysis of block/buzz@f956e6fe06 (2026-08-16).
Data as JSON: /api/errors/9ca23fa5a22ab90a.
Report an issue: GitHub.