{"record":{"id":"9ca23fa5a22ab90a","repo":"block/buzz","slug":"only-owners-admins-may-grant-elevated-roles","errorCode":null,"errorMessage":"only owners/admins may grant elevated roles","messagePattern":"only owners/admins may grant elevated roles","errorType":"validation","errorClass":"IngestError::Rejected","httpStatus":null,"severity":"error","filePath":"crates/buzz-relay/src/handlers/side_effects.rs","lineNumber":373,"sourceCode":"                .iter()\n                .find(|m| m.pubkey == actor_bytes)\n                .and_then(|m| m.role.parse().ok());\n            let target_pubkey =\n                extract_p_tag(event).ok_or_else(|| anyhow::anyhow!(\"missing p tag\"))?;\n\n            // PUT_USER: open channels allow any authenticated user; private channels\n            // require the actor to be an existing active member. Any active member may\n            // add an ordinary member, guest, or bot, but only owners/admins may grant\n            // an elevated role.\n            if channel.visibility == \"private\" {\n                if actor_role.is_none() {\n                    return Err(anyhow::anyhow!(\"actor not authorized\"));\n                }\n\n                if requested_role.is_some_and(|role| role.is_elevated())\n                    && !actor_role.is_some_and(|role| role.is_elevated())\n                {\n                    return Err(anyhow::anyhow!(\n                        \"only owners/admins may grant elevated roles\"\n                    ));\n                }\n            }\n\n            // Changing an ACTIVE existing member's role is privileged in both\n            // directions, on every visibility. `get_members` filters\n            // `removed_at IS NULL`, so a soft-removed row is deliberately not an\n            // \"existing member\" here: its stored role is history, not live\n            // authority, and reactivation is governed by the elevated-granter\n            // check above rather than by the role the row remembers.\n            //\n            // `add_member` is the authority (it also covers the desktop/admin\n            // callers that skip this validator); rejecting here too means the\n            // client gets a real error instead of an OK for an event whose side\n            // effect then fails. Re-adding at the same role stays idempotent —\n            // the huddle bot-add path relies on that.\n            if let Some((target, role)) = members","sourceCodeStart":355,"sourceCodeEnd":391,"githubUrl":"https://github.com/block/buzz/blob/f956e6fe06a76e50cbd8fba1a162482e752e7f1a/crates/buzz-relay/src/handlers/side_effects.rs#L355-L391","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"// before: plain member grants admin on a private channel\nput_user(private_channel, target).with_role(\"admin\").publish() // → only owners/admins may grant elevated roles\n\n// after: member adds plainly; owner elevates separately\nput_user(private_channel, target).publish(); // member: no role tag\nput_user(private_channel, target).with_role(\"admin\").publish_as(channel_admin_keys);","handlingStrategy":"validation","validationCode":"// Block elevated grants unless the actor is elevated\nlet elevated_request = requested_role.is_some_and(|r| r.is_elevated());\nlet actor_elevated = actor_role.is_some_and(|r| r.is_elevated());\nif channel.visibility == \"private\" {\n    anyhow::ensure!(\n        !elevated_request || actor_elevated,\n        \"only owners/admins grant owner/admin roles on private channels\"\n    );\n}","typeGuard":"const isElevated = (r: string): boolean => r === \"owner\" || r === \"admin\";\nconst canGrant = (actorRole: string | null, requested: string | null): boolean =>\n  requested === null || !isElevated(requested) || (actorRole !== null && isElevated(actorRole));","tryCatchPattern":"match validate_admin_event(&tenant, 9000, &event, &state).await {\n    Err(e) if e.to_string().contains(\"only owners/admins may grant elevated roles\") => {\n        // split the operation: member adds plainly, owner promotes afterwards\n        split_add_and_promote(channel, target, requested_role).await\n    }\n    other => other,\n}","preventionTips":["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"],"tags":["nostr","nip-29","group-admin","authorization","roles","privilege-escalation"],"backgroundTag":"insufficient-permissions","analyzedSha":"f956e6fe06a76e50cbd8fba1a162482e752e7f1a","analyzedAt":"2026-08-16T22:11:40.750Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}