Skip to main content

gsp/
roles.rs

1//! Role registry and permission checks (GSP §5 — `ROLE_CHANGE`).
2//!
3//! GSP defines `ROLE_CHANGE` but leaves the role hierarchy and the
4//! permission semantics up to the application. [`RoleRegistry`] captures
5//! both pieces in a small, allocation-light data structure:
6//!
7//! * a numeric **role id** is bound to a stable role name (for logs);
8//! * each role carries a [`Permissions`] bitset that the application can
9//!   query before applying side-effecting signals.
10
11use gbp_core::MemberId;
12use std::collections::HashMap;
13
14/// Application-defined permission bits. The first eight slots are
15/// pre-named to cover the common audio/text/role permissions; bits 8..32
16/// are free for application use.
17#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
18pub struct Permissions(pub u32);
19
20impl Permissions {
21    /// Bit 0: may publish to text streams (GTP).
22    pub const SEND_TEXT: u32 = 1 << 0;
23    /// Bit 1: may publish to audio streams (GAP).
24    pub const SEND_AUDIO: u32 = 1 << 1;
25    /// Bit 2: may emit signals (GSP).
26    pub const SEND_SIGNAL: u32 = 1 << 2;
27    /// Bit 3: may mute / unmute other members.
28    pub const MUTE_OTHERS: u32 = 1 << 3;
29    /// Bit 4: may approve `ROLE_CHANGE` requests.
30    pub const ASSIGN_ROLES: u32 = 1 << 4;
31    /// Bit 5: may invite new members (drives MLS adds).
32    pub const INVITE: u32 = 1 << 5;
33    /// Bit 6: may remove members from the group.
34    pub const REMOVE_MEMBERS: u32 = 1 << 6;
35    /// Bit 7: may close / archive the group.
36    pub const CLOSE_GROUP: u32 = 1 << 7;
37
38    /// `true` if every bit in `mask` is set.
39    pub const fn has(self, mask: u32) -> bool {
40        self.0 & mask == mask
41    }
42
43    /// Bitwise OR.
44    pub const fn with(self, mask: u32) -> Self {
45        Self(self.0 | mask)
46    }
47
48    /// Bitwise AND-NOT.
49    pub const fn without(self, mask: u32) -> Self {
50        Self(self.0 & !mask)
51    }
52}
53
54impl core::ops::BitOr<u32> for Permissions {
55    type Output = Self;
56    fn bitor(self, rhs: u32) -> Self::Output {
57        Self(self.0 | rhs)
58    }
59}
60
61/// Role definition in [`RoleRegistry`].
62#[derive(Clone, Debug)]
63pub struct RoleSpec {
64    /// Numeric role id (matches the `role_claim` field on the wire).
65    pub id: u32,
66    /// Stable human-readable name for logs.
67    pub name: String,
68    /// Permissions granted by the role.
69    pub permissions: Permissions,
70}
71
72/// Errors returned by [`RoleRegistry`].
73#[derive(Debug, thiserror::Error)]
74pub enum RoleError {
75    /// The role id is not registered.
76    #[error("unknown role: {0}")]
77    UnknownRole(u32),
78    /// The acting member is not authorised for this operation.
79    #[error("member {member} lacks permission 0x{permission:08X}")]
80    Unauthorised {
81        /// Acting member.
82        member: MemberId,
83        /// Required permission mask.
84        permission: u32,
85    },
86}
87
88/// Bidirectional mapping of role ids to [`RoleSpec`]s plus an assignment
89/// table tracking each member's current role.
90#[derive(Default)]
91pub struct RoleRegistry {
92    roles: HashMap<u32, RoleSpec>,
93    assignments: HashMap<MemberId, u32>,
94}
95
96impl RoleRegistry {
97    /// Empty registry.
98    pub fn new() -> Self {
99        Self::default()
100    }
101
102    /// Registers a role. Replaces any existing role with the same id.
103    pub fn define(&mut self, spec: RoleSpec) {
104        self.roles.insert(spec.id, spec);
105    }
106
107    /// Convenience: defines a role from primitive components.
108    pub fn define_role(&mut self, id: u32, name: impl Into<String>, permissions: Permissions) {
109        self.define(RoleSpec {
110            id,
111            name: name.into(),
112            permissions,
113        });
114    }
115
116    /// Looks up a role by id.
117    pub fn role(&self, id: u32) -> Option<&RoleSpec> {
118        self.roles.get(&id)
119    }
120
121    /// Iterates every defined role.
122    pub fn roles(&self) -> impl Iterator<Item = &RoleSpec> {
123        self.roles.values()
124    }
125
126    /// Assigns a role to a member.
127    pub fn assign(&mut self, member: MemberId, role_id: u32) -> Result<(), RoleError> {
128        if !self.roles.contains_key(&role_id) {
129            return Err(RoleError::UnknownRole(role_id));
130        }
131        self.assignments.insert(member, role_id);
132        Ok(())
133    }
134
135    /// Returns the role currently assigned to `member`, if any.
136    pub fn role_of(&self, member: MemberId) -> Option<&RoleSpec> {
137        let id = self.assignments.get(&member)?;
138        self.roles.get(id)
139    }
140
141    /// Returns the effective permissions of `member` (zero if no role is
142    /// assigned).
143    pub fn permissions_of(&self, member: MemberId) -> Permissions {
144        self.role_of(member)
145            .map(|r| r.permissions)
146            .unwrap_or_default()
147    }
148
149    /// `Ok(())` if `member` carries every bit in `mask`; otherwise
150    /// [`RoleError::Unauthorised`].
151    pub fn require(&self, member: MemberId, mask: u32) -> Result<(), RoleError> {
152        if self.permissions_of(member).has(mask) {
153            Ok(())
154        } else {
155            Err(RoleError::Unauthorised {
156                member,
157                permission: mask,
158            })
159        }
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn permissions_and_require() {
169        let mut r = RoleRegistry::new();
170        r.define_role(1, "viewer", Permissions::default());
171        r.define_role(
172            10,
173            "speaker",
174            Permissions::default() | Permissions::SEND_TEXT | Permissions::SEND_AUDIO,
175        );
176        r.define_role(
177            100,
178            "admin",
179            Permissions::default()
180                | Permissions::SEND_TEXT
181                | Permissions::SEND_AUDIO
182                | Permissions::MUTE_OTHERS
183                | Permissions::ASSIGN_ROLES,
184        );
185
186        r.assign(2, 10).unwrap();
187        r.assign(3, 100).unwrap();
188
189        assert!(r.require(2, Permissions::SEND_TEXT).is_ok());
190        assert!(r.require(2, Permissions::MUTE_OTHERS).is_err());
191        assert!(r.require(3, Permissions::MUTE_OTHERS).is_ok());
192    }
193
194    #[test]
195    fn unknown_role_rejected() {
196        let mut r = RoleRegistry::new();
197        let err = r.assign(1, 42).unwrap_err();
198        assert!(matches!(err, RoleError::UnknownRole(42)));
199    }
200}