block/buzz · error · IngestError::Rejected
moderator access required
Error message
moderator access required
What it means
Thrown by decide_authority() in the relay's moderation authorization seam: the actor holds neither a community-wide 'owner'/'admin' role in relay_members nor a channel 'owner'/'admin' role for the target channel. Buzz v1 has no separate Moderator tier — every moderation capability (Ban, Unban, Timeout, Utimeout, ResolveReport, ViewQueue, plus channel-local DeleteMessage/Kick) routes through authorize_moderation_action, and channel roles only ever authorize DeleteMessage/Kick. Hitting this means no role source matched the actor's signing pubkey in this community (roles are tenant-fenced).
Source
Thrown at crates/buzz-relay/src/handlers/moderation_authz.rs:178
// command handler separately rejects a banned actor on every transport,
// so the reachable case is an unrestricted admin lifting another admin's
// restriction; that remains benign, audited, and owner-reversible.
Some("admin") => {
if matches!(action, ModerationAction::Ban | ModerationAction::Timeout)
&& matches!(target_role, Some("owner") | Some("admin"))
{
anyhow::bail!("an admin cannot ban or time out a community owner or fellow admin");
}
Ok(ModerationAuthority::CommunityAdmin)
}
// Not a community owner/admin: channel owner/admin keep channel-local
// authority for DeleteMessage/Kick only.
_ => match (action, channel_role) {
(
ModerationAction::DeleteMessage | ModerationAction::Kick,
Some("owner") | Some("admin"),
) => Ok(ModerationAuthority::ChannelRole),
_ => anyhow::bail!("moderator access required"),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Every community-wide action a community owner can take. Channel-local
/// actions (DeleteMessage/Kick) are included — the owner holds them too.
const ALL_ACTIONS: [ModerationAction; 8] = [
ModerationAction::DeleteMessage,
ModerationAction::Kick,
ModerationAction::Ban,
ModerationAction::Unban,
ModerationAction::Timeout,
ModerationAction::Untimeout,
ModerationAction::ResolveReport,View on GitHub (pinned to f956e6fe06)
Solutions
- Grant the actor community 'admin' or 'owner' in relay_members (buzz-admin / relay operator CLI) and re-sign with that exact pubkey
- For DeleteMessage/Kick only: ensure the actor is an active 'owner'/'admin' member of that specific channel (kind 39002/9000 role row under tenant.community())
- Verify the client is signing with the pubkey that actually holds the role — dump relay_members for the community and compare hex pubkeys
- Confirm the action is aimed at a channel/user in the same community; cross-community moderation is always denied
Example fix
// before: plain member's key signs a community-wide ban → "moderator access required" await client.publish(banCommandEvent); // Err(moderator access required) // after: grant admin in relay_members, then sign with that key // buzz-admin members set-role --community <id> --pubkey <hex> --role admin await client.publish(banCommandEvent); // Ok(ModerationAuthority::CommunityAdmin)
Defensive patterns
Strategy: validation
Validate before calling
// Before issuing a moderation command, resolve the actor's authority
let role = client.get_relay_member(community, actor_hex).await?;
let is_channel_elevated = matches!(
client.get_channel_role(community, channel_id, actor).await?,
Some(r) if r == "owner" || r == "admin"
);
let action_is_local = matches!(action, ModerationAction::DeleteMessage | ModerationAction::Kick);
anyhow::ensure!(
matches!(role.as_deref(), Some("owner") | Some("admin")) || (action_is_local && is_channel_elevated),
"actor lacks any moderation authority for {action:?}"
); Type guard
fn can_moderate(actor_role: Option<&str>, channel_role: Option<&str>, action: &str) -> bool {
match actor_role {
Some("owner") | Some("admin") => true,
_ => matches!(action, "delete_message" | "kick")
&& matches!(channel_role, Some("owner") | Some("admin")),
}
} Try / catch
match authorize_moderation_action(&tenant, &state, &actor, channel, target, action).await {
Ok(authority) => { /* record authority in audit row */ }
Err(e) if e.to_string().contains("moderator access required") => {
// client-safe: surface as 403-style denial, do NOT retry with the same key
return deny("You need community admin or channel owner/admin rights for this action");
}
Err(e) => return Err(e),
} Prevention
- Gate moderation UI actions on a pre-fetched role check so unauthorized commands never leave the client
- Keep one canonical admin key per community and verify it against relay_members after any key rotation
- Remember v1 has no moderator tier — provisioning docs should say owner/admin only
- Channel roles authorize only DeleteMessage/Kick; never assume they cover bans or timeouts
When it happens
Trigger: A plain community member issues a ban/timeout/unban/resolve-report/view-queue moderation command; a channel owner/admin attempts a community-wide action such as Ban (channel authority only covers DeleteMessage/Kick); a channel admin tries DeleteMessage/Kick against a channel_id where they hold no membership row; the signing pubkey differs from the pubkey that carries the role in relay_members.
Common situations: Teams expect a 'moderator' role that does not exist in v1; the moderator's client signs with a different key (or an agent key) than the one granted admin in relay_members; acting across community boundaries — authority never crosses the tenant fence; role was granted in a different community with the same relay.
Related errors
- only owners/admins may grant elevated roles
- only owners/admins may change an active member's role
- an admin cannot ban or time out a community owner or fellow
- must be event author
- invalid role: {s}
AI-assisted analysis of block/buzz@f956e6fe06 (2026-08-16).
Data as JSON: /api/errors/cabd08338a9078aa.
Report an issue: GitHub.