block/buzz · error · IngestError::Rejected
an admin cannot ban or time out a community owner or fellow
Error message
an admin cannot ban or time out a community owner or fellow admin
What it means
Moderation authorization in the relay: an actor whose relay_members role is 'admin' holds community-wide capabilities, but the Ban and Timeout actions are explicitly capped — they may never target a member whose role is 'owner' or 'admin'. Only the community owner may action an admin (and nobody but the process owner path actions the owner). The guard keys on the TARGET's role row only: a target with no relay_members row (e.g. a drive-by spammer who left) is still bannable. Unban/Untimeout are intentionally NOT guarded at this seam — lifting another admin's restriction is benign, audited, and owner-reversible — and the command handler separately rejects banned actors on every transport.
Source
Thrown at crates/buzz-relay/src/handlers/moderation_authz.rs:167
channel_role: Option<&str>,
action: ModerationAction,
) -> anyhow::Result<ModerationAuthority> {
match actor_role {
// Owner holds every capability, community-wide, with no guard rail.
Some("owner") => Ok(ModerationAuthority::CommunityOwner),
// Admin holds every capability, but cannot ban/timeout the owner or a
// fellow admin — only the owner may action an admin. The guard trips only
// on a target *role* of owner/admin: a target with no `relay_members` row
// (a drive-by spammer who already left) is bannable. Unban/Untimeout lift
// a restriction and are intentionally unguarded at this role seam. The
// 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::*;View on GitHub (pinned to f956e6fe06)
Solutions
- Route the action through the community owner: the owner's key signs the ban/timeout against the admin target — that path is permitted.
- If the target admin must be stopped urgently and the owner is unavailable, use channel-scope tools an admin still holds (e.g. delete-message) or demote via owner-approved governance; do not attempt direct bans.
- For anti-spam bots, exclude owner/admin pubkeys from automatic ban lists (fetch the kind:39001 admin list first) so the guard is never hit.
- If you intended UNban or un-timeout of a fellow admin, that is intentionally allowed — re-issue as Unban/Untimeout, which is unguarded at this seam.
Example fix
// before (admin key signs ban targeting another admin)
const ev = await createModerationEvent(9000, { p: adminB_pubkey, reason: 'spam' }); // -> 403 guard
// after (community owner's key signs the same action)
await impersonateOwner(); // or have the owner run the moderation
const ev = await createModerationEvent(9000, { p: adminB_pubkey, reason: 'spam' }); Defensive patterns
Strategy: type-guard
Validate before calling
// before issuing ban/timeout, fetch roles and veto elevated targets
const admins = await fetchAdminList(relayUrl, channelId); // kind:39001 p-tags + owner
function canModerate(actorRole: string, targetPubkey: string, action: 'ban'|'timeout'): boolean {
if (actorRole !== 'admin' && actorRole !== 'owner') return false;
if (actorRole === 'admin' && admins.has(targetPubkey)) return false; // guard mirrors relay
return true;
} Type guard
function isProtectedTarget(actorRole: string, targetRole: string | null, action: string): boolean {
const elevated = targetRole === 'owner' || targetRole === 'admin';
return actorRole === 'admin' && elevated && (action === 'ban' || action === 'timeout');
} Try / catch
// relay returns an authz error for kind:900x — surface it as a role issue, not a transport failure
try { await publishModeration(action, targetPubkey); }
catch (e) {
if (String(e).includes('cannot ban or time out'))
throw new Error(`Refused: ${targetPubkey} is owner/admin; ask the community owner to act`);
throw e;
} Prevention
- Mod bots must exclude owner/admin pubkeys (kind:39001 list) from automated bans/timeouts.
- Design admin UIs to disable ban/timeout actions on members shown with owner/admin badges.
- Remember the asymmetry: admins CAN unban/untimeout peers — route remediation through those actions or the owner.
When it happens
Trigger: A community admin sends a kind:900x ban/timeout moderation event (via WebSocket NIP-29 or the equivalent HTTP path) whose p-tag targets the community owner's pubkey or another admin's pubkey. The authz resolution sees actor role 'admin' + target role 'owner'/'admin' + action Ban|Timeout and bails before any state change.
Common situations: Two admins in a heated channel, one tries to timeout the other; automation/bot accounts with admin role applying anti-spam bans that sweep an admin's pubkey; attempts to ban the owner during a dispute. Legitimate path: have the OWNER issue the ban/timeout against the admin.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- moderator access required
- actor not authorized
- only owners/admins may grant elevated roles
- only owners/admins may change an active member's role
- policy:owner_only — only the agent owner can add this agent
AI-assisted analysis of block/buzz@f956e6fe06 (2026-08-16).
Data as JSON: /api/errors/2cc747300acfd301.
Report an issue: GitHub.