tangle


git clone https://radroots.dev/git/tangle.git
Log | Files | Refs | README | LICENSE

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 }