roles.rs (8923B)
1 use std::collections::{BTreeMap, BTreeSet}; 2 3 use crate::errors::{GroupError, GroupErrorKind}; 4 5 pub const MAX_ROLE_NAME_BYTES: usize = 64; 6 pub const PERMANENT_RELAY_OVERRIDE_ROLE: &str = "relay_owner"; 7 8 #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 9 pub struct RoleName(String); 10 11 impl RoleName { 12 pub fn new(value: &str) -> Result<Self, GroupError> { 13 if value.is_empty() { 14 return Err(invalid_role("role name must not be empty")); 15 } 16 if value.len() > MAX_ROLE_NAME_BYTES { 17 return Err(invalid_role(format!( 18 "role name must be at most {MAX_ROLE_NAME_BYTES} bytes" 19 ))); 20 } 21 if value.trim() != value { 22 return Err(invalid_role( 23 "role name must not contain leading or trailing whitespace", 24 )); 25 } 26 if value.chars().any(char::is_control) { 27 return Err(invalid_role( 28 "role name must not contain control characters", 29 )); 30 } 31 if value.chars().any(char::is_whitespace) { 32 return Err(invalid_role("role name must not contain whitespace")); 33 } 34 Ok(Self(value.to_owned())) 35 } 36 37 pub fn permanent_relay_override() -> Self { 38 Self(PERMANENT_RELAY_OVERRIDE_ROLE.to_owned()) 39 } 40 41 pub fn as_str(&self) -> &str { 42 &self.0 43 } 44 45 pub fn is_permanent_relay_override(&self) -> bool { 46 self.as_str() == PERMANENT_RELAY_OVERRIDE_ROLE 47 } 48 } 49 50 impl core::fmt::Display for RoleName { 51 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 52 formatter.write_str(self.as_str()) 53 } 54 } 55 56 impl core::fmt::Debug for RoleName { 57 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 58 formatter.debug_tuple("RoleName").field(&self.0).finish() 59 } 60 } 61 62 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 63 pub enum Capability { 64 ManageMembers, 65 ManageRoles, 66 ManageMetadata, 67 DeleteEvents, 68 DeleteGroup, 69 CreateInvites, 70 ManageInvites, 71 RelayOverride, 72 } 73 74 impl Capability { 75 pub fn all() -> [Self; 8] { 76 [ 77 Self::ManageMembers, 78 Self::ManageRoles, 79 Self::ManageMetadata, 80 Self::DeleteEvents, 81 Self::DeleteGroup, 82 Self::CreateInvites, 83 Self::ManageInvites, 84 Self::RelayOverride, 85 ] 86 } 87 88 pub fn as_str(self) -> &'static str { 89 match self { 90 Self::ManageMembers => "manage_members", 91 Self::ManageRoles => "manage_roles", 92 Self::ManageMetadata => "manage_metadata", 93 Self::DeleteEvents => "delete_events", 94 Self::DeleteGroup => "delete_group", 95 Self::CreateInvites => "create_invites", 96 Self::ManageInvites => "manage_invites", 97 Self::RelayOverride => "relay_override", 98 } 99 } 100 101 pub fn from_label(value: &str) -> Result<Self, GroupError> { 102 match value { 103 "manage_members" => Ok(Self::ManageMembers), 104 "manage_roles" => Ok(Self::ManageRoles), 105 "manage_metadata" => Ok(Self::ManageMetadata), 106 "delete_events" => Ok(Self::DeleteEvents), 107 "delete_group" => Ok(Self::DeleteGroup), 108 "create_invites" => Ok(Self::CreateInvites), 109 "manage_invites" => Ok(Self::ManageInvites), 110 "relay_override" => Ok(Self::RelayOverride), 111 _ => Err(GroupError::invalid( 112 GroupErrorKind::MissingCapability, 113 format!("unknown group capability {value}"), 114 )), 115 } 116 } 117 } 118 119 #[derive(Debug, Clone, PartialEq, Eq, Default)] 120 pub struct CapabilitySet { 121 capabilities: BTreeSet<Capability>, 122 } 123 124 impl CapabilitySet { 125 pub fn new(capabilities: impl IntoIterator<Item = Capability>) -> Self { 126 Self { 127 capabilities: capabilities.into_iter().collect(), 128 } 129 } 130 131 pub fn empty() -> Self { 132 Self::default() 133 } 134 135 pub fn permanent_relay_override() -> Self { 136 Self::new(Capability::all()) 137 } 138 139 pub fn insert(&mut self, capability: Capability) { 140 self.capabilities.insert(capability); 141 } 142 143 pub fn contains(&self, capability: Capability) -> bool { 144 self.capabilities.contains(&capability) 145 } 146 147 pub fn is_empty(&self) -> bool { 148 self.capabilities.is_empty() 149 } 150 151 pub fn iter(&self) -> impl Iterator<Item = Capability> + '_ { 152 self.capabilities.iter().copied() 153 } 154 155 pub fn labels(&self) -> Vec<&'static str> { 156 self.iter().map(Capability::as_str).collect() 157 } 158 159 pub fn from_labels(labels: &[String]) -> Result<Self, GroupError> { 160 labels 161 .iter() 162 .map(|label| Capability::from_label(label)) 163 .collect::<Result<Vec<_>, _>>() 164 .map(Self::new) 165 } 166 167 fn extend_from(&mut self, other: &CapabilitySet) { 168 self.capabilities.extend(other.iter()); 169 } 170 } 171 172 #[derive(Debug, Clone, PartialEq, Eq)] 173 pub struct RoleDefinition { 174 name: RoleName, 175 capabilities: CapabilitySet, 176 description: Option<String>, 177 } 178 179 impl RoleDefinition { 180 pub fn new(name: RoleName, capabilities: CapabilitySet, description: Option<String>) -> Self { 181 Self { 182 name, 183 capabilities, 184 description, 185 } 186 } 187 188 pub fn name(&self) -> &RoleName { 189 &self.name 190 } 191 192 pub fn capabilities(&self) -> &CapabilitySet { 193 &self.capabilities 194 } 195 196 pub fn description(&self) -> Option<&str> { 197 self.description.as_deref() 198 } 199 } 200 201 pub fn resolve_capabilities<'a>( 202 definitions: impl IntoIterator<Item = &'a RoleDefinition>, 203 roles: impl IntoIterator<Item = &'a RoleName>, 204 ) -> Result<CapabilitySet, GroupError> { 205 let definitions = definitions 206 .into_iter() 207 .map(|definition| (definition.name().clone(), definition)) 208 .collect::<BTreeMap<_, _>>(); 209 let mut resolved = CapabilitySet::empty(); 210 for role in roles { 211 if role.is_permanent_relay_override() { 212 resolved.extend_from(&CapabilitySet::permanent_relay_override()); 213 continue; 214 } 215 let Some(definition) = definitions.get(role) else { 216 return Err(GroupError::restricted( 217 GroupErrorKind::MissingCapability, 218 format!("unknown group role {}", role.as_str()), 219 )); 220 }; 221 resolved.extend_from(definition.capabilities()); 222 } 223 Ok(resolved) 224 } 225 226 fn invalid_role(message: impl Into<String>) -> GroupError { 227 GroupError::invalid(GroupErrorKind::InvalidRole, message) 228 } 229 230 #[cfg(test)] 231 mod tests { 232 use super::{Capability, CapabilitySet, RoleDefinition, RoleName, resolve_capabilities}; 233 use crate::GroupErrorKind; 234 235 #[test] 236 fn role_name_validation_is_strict() { 237 assert_eq!( 238 RoleName::new("").expect_err("empty").message(), 239 "role name must not be empty" 240 ); 241 assert_eq!( 242 RoleName::new("a role").expect_err("space").message(), 243 "role name must not contain whitespace" 244 ); 245 assert_eq!( 246 RoleName::new(" role").expect_err("trim").message(), 247 "role name must not contain leading or trailing whitespace" 248 ); 249 assert_eq!( 250 RoleName::new("role\nname").expect_err("control").message(), 251 "role name must not contain control characters" 252 ); 253 } 254 255 #[test] 256 fn resolves_role_capabilities_and_rejects_unknown_roles() { 257 let moderator = RoleName::new("moderator").expect("role"); 258 let definition = RoleDefinition::new( 259 moderator.clone(), 260 CapabilitySet::new([Capability::ManageMembers, Capability::DeleteEvents]), 261 Some("Moderates group members".to_owned()), 262 ); 263 let resolved = resolve_capabilities([&definition], [&moderator]).expect("capabilities"); 264 265 assert!(resolved.contains(Capability::ManageMembers)); 266 assert!(resolved.contains(Capability::DeleteEvents)); 267 assert!(!resolved.contains(Capability::DeleteGroup)); 268 assert_eq!(definition.description(), Some("Moderates group members")); 269 270 let unknown = RoleName::new("unknown").expect("unknown"); 271 let error = resolve_capabilities([&definition], [&unknown]).expect_err("unknown"); 272 assert_eq!(error.kind(), GroupErrorKind::MissingCapability); 273 assert_eq!(error.message(), "unknown group role unknown"); 274 } 275 276 #[test] 277 fn permanent_relay_override_grants_every_capability() { 278 let role = RoleName::permanent_relay_override(); 279 let resolved = resolve_capabilities([], [&role]).expect("capabilities"); 280 281 assert!(role.is_permanent_relay_override()); 282 for capability in Capability::all() { 283 assert!(resolved.contains(capability), "{}", capability.as_str()); 284 } 285 } 286 }