diff --git a/crates/buzz-relay/src/handlers/channel_authz.rs b/crates/buzz-relay/src/handlers/channel_authz.rs index c7f3fca55a..99dabad20e 100644 --- a/crates/buzz-relay/src/handlers/channel_authz.rs +++ b/crates/buzz-relay/src/handlers/channel_authz.rs @@ -77,11 +77,11 @@ pub fn is_sole_owner(members: &[MemberRecord], pubkey: &[u8]) -> bool { pub fn can_edit_privileged_metadata( actor_role: Option<&str>, actor_owns_owner_agent: bool, - actor_is_owned_by_channel_owner: bool, + actor_is_owned_by_elevated_member: bool, ) -> bool { matches!(actor_role, Some("owner" | "admin")) || actor_owns_owner_agent - || (actor_role.is_some() && actor_is_owned_by_channel_owner) + || (actor_role.is_some() && actor_is_owned_by_elevated_member) } /// Decide whether `actor` may remove themselves from the channel. diff --git a/crates/buzz-relay/src/handlers/side_effects.rs b/crates/buzz-relay/src/handlers/side_effects.rs index 394cf65782..c7912dd2bd 100644 --- a/crates/buzz-relay/src/handlers/side_effects.rs +++ b/crates/buzz-relay/src/handlers/side_effects.rs @@ -441,16 +441,25 @@ async fn actor_owns_any_owner_agent( Ok(false) } +/// Whether a channel role may delegate privileged metadata authority to one of +/// its managed agents. +fn role_delegates_privileged_metadata(role: &str) -> bool { + role == "owner" || role == "admin" +} + /// Returns `true` if `actor_bytes` is an agent whose owning human is an active -/// owner member. This grants an owner's managed agent the same metadata -/// authority only when the agent is itself an active channel member. -async fn actor_is_owned_by_channel_owner( +/// owner/admin member. This grants an owner's or admin's managed agent the same +/// metadata authority only when the agent is itself an active channel member. +async fn actor_is_owned_by_elevated_member( state: &Arc, community_id: buzz_core::CommunityId, members: &[buzz_db::channel::MemberRecord], actor_bytes: &[u8], ) -> anyhow::Result { - for member in members.iter().filter(|member| member.role == "owner") { + for member in members + .iter() + .filter(|member| role_delegates_privileged_metadata(&member.role)) + { if state .db .is_agent_owner(community_id, actor_bytes, &member.pubkey) @@ -695,11 +704,11 @@ pub async fn validate_admin_event( actor_owns_any_owner_agent(state, tenant.community(), &members, &actor_bytes) .await? }; - let actor_is_owned_by_channel_owner = + let actor_is_owned_by_elevated_member = if actor_role.is_none() || actor_owns_owner_agent { false } else { - actor_is_owned_by_channel_owner( + actor_is_owned_by_elevated_member( state, tenant.community(), &members, @@ -710,7 +719,7 @@ pub async fn validate_admin_event( if channel_authz::can_edit_privileged_metadata( actor_role, actor_owns_owner_agent, - actor_is_owned_by_channel_owner, + actor_is_owned_by_elevated_member, ) { Ok(()) } else { @@ -3820,6 +3829,14 @@ pub async fn publish_nipia_unarchived( mod tests { use super::*; + #[test] + fn privileged_metadata_delegation_accepts_owner_and_admin_roles() { + assert!(role_delegates_privileged_metadata("owner")); + assert!(role_delegates_privileged_metadata("admin")); + assert!(!role_delegates_privileged_metadata("member")); + assert!(!role_delegates_privileged_metadata("bot")); + } + #[test] fn workflow_deletion_retry_matches_authorized_dispatch() { let keys = nostr::Keys::generate();