cts_common/auth/claims/
role.rs

1use super::Scope;
2use crate::{
3    auth::claims::{WORKSPACE_ADMIN_SCOPE, WORKSPACE_CONTROL_SCOPE, WORKSPACE_MEMBER_SCOPE},
4    claims::common::ArrayOrValue,
5};
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8
9#[derive(Error, Debug, PartialEq)]
10#[error("Role error: {0}")]
11pub struct RoleError(String);
12
13/// Defines a role in the system.
14#[derive(Debug, Copy, Clone, Deserialize, PartialEq, Eq, Hash, Serialize)]
15#[serde(rename_all = "lowercase")]
16#[derive(Default)]
17pub enum Role {
18    // These aliases are used to maintain compatibility with existing tokens.
19    /// Defines an admin role with full access to the system.
20    #[serde(alias = "Admin", alias = "ADMIN")]
21    Admin,
22
23    /// Defines a control role with no encryption/decryption permissions but the ability to manage resources.
24    #[serde(alias = "Control", alias = "CONTROL")]
25    Control,
26
27    /// Defines a member role with encryption and decryption permissions but no management capabilities.
28    #[serde(alias = "Member", alias = "MEMBER")]
29    #[default]
30    Member,
31}
32
33/// Defines a set of [`Role`]s that can be used to represent a user's roles in the system.
34/// It can contain multiple roles, a single role, or an empty set.
35#[derive(Debug, Clone, Deserialize, PartialEq, Serialize, Default)]
36#[serde(rename_all = "lowercase")]
37pub struct RoleSet(ArrayOrValue<Role>);
38
39impl Role {
40    /// Returns the [`Scope`] associated with the role.
41    pub fn scope(&self) -> Scope {
42        match self {
43            Role::Admin => WORKSPACE_ADMIN_SCOPE,
44            Role::Control => WORKSPACE_CONTROL_SCOPE,
45            Role::Member => WORKSPACE_MEMBER_SCOPE,
46        }
47    }
48
49    /// Returns true if this role is a superset of the other role.
50    /// For example, `Admin` is a superset of `Member`, but `Member` is not a superset of `Admin`.
51    /// This is useful when performing checks to prevent privilege escalation.
52    pub fn is_superset_of(&self, other: Role) -> bool {
53        self.scope().has_all_permissions(other.scope())
54    }
55}
56
57impl RoleSet {
58    pub fn single(role: Role) -> Self {
59        Self(ArrayOrValue::single(role))
60    }
61
62    /// Returns the [`Scope`] associated with the roleset.
63    pub fn scope(&self) -> Scope {
64        self.0
65            .iter()
66            .map(|role| role.scope())
67            .fold(Scope::with_no_permissions(), |acc, scope| acc.merge(scope))
68    }
69
70    pub fn has_role(&self, role: Role) -> bool {
71        self.0.iter().any(|check| check == role)
72    }
73
74    pub fn is_empty(&self) -> bool {
75        self.len() == 0
76    }
77
78    pub fn len(&self) -> usize {
79        self.0.len()
80    }
81
82    pub fn add_role(self, role: Role) -> Self {
83        Self(self.0.add(role))
84    }
85}
86
87impl PartialEq<String> for Role {
88    fn eq(&self, other: &String) -> bool {
89        Into::<&str>::into(*self) == other.as_str()
90    }
91}
92
93impl PartialEq<&str> for Role {
94    fn eq(&self, other: &&str) -> bool {
95        Into::<&str>::into(*self) == *other
96    }
97}
98
99impl TryFrom<String> for Role {
100    type Error = RoleError;
101
102    fn try_from(s: String) -> Result<Self, Self::Error> {
103        s.as_str().try_into()
104    }
105}
106
107impl TryFrom<&str> for Role {
108    type Error = RoleError;
109
110    fn try_from(s: &str) -> Result<Self, Self::Error> {
111        match s.to_lowercase().as_str() {
112            "admin" => Ok(Role::Admin),
113            "control" => Ok(Role::Control),
114            "member" => Ok(Role::Member),
115            _ => Err(RoleError(format!("Invalid role: {s}"))),
116        }
117    }
118}
119
120impl From<Role> for &str {
121    fn from(role: Role) -> Self {
122        match role {
123            Role::Admin => "admin",
124            Role::Control => "control",
125            Role::Member => "member",
126        }
127    }
128}
129
130impl From<Role> for String {
131    fn from(role: Role) -> Self {
132        Into::<&str>::into(role).to_string()
133    }
134}
135
136#[cfg(feature = "test_utils")]
137impl fake::Dummy<fake::Faker> for Role {
138    fn dummy_with_rng<R: rand::Rng + ?Sized>(_: &fake::Faker, _: &mut R) -> Self {
139        Role::Admin
140    }
141}
142
143#[cfg(test)]
144mod test {
145    use super::*;
146    use serde_json::json;
147
148    mod role_set {
149        use super::*;
150
151        #[test]
152        fn single() {
153            let role_set = RoleSet::single(Role::Admin);
154            assert_eq!(role_set.len(), 1);
155            assert!(role_set.has_role(Role::Admin));
156
157            // Inverse check
158            assert!(!role_set.has_role(Role::Member));
159        }
160
161        #[test]
162        fn empty() {
163            let role_set: RoleSet = Default::default();
164            assert!(role_set.is_empty());
165            assert_eq!(role_set.len(), 0);
166        }
167
168        #[test]
169        fn multiple_roles() {
170            let role_set = RoleSet::default()
171                .add_role(Role::Admin)
172                .add_role(Role::Member);
173            assert_eq!(role_set.len(), 2);
174            assert!(role_set.has_role(Role::Admin));
175            assert!(role_set.has_role(Role::Member));
176        }
177
178        #[test]
179        fn scope_single() {
180            let role_set = RoleSet::single(Role::Admin);
181            assert_eq!(role_set.scope(), WORKSPACE_ADMIN_SCOPE);
182        }
183
184        #[test]
185        fn scope_multiple() {
186            let role_set = RoleSet::default()
187                .add_role(Role::Admin)
188                .add_role(Role::Member);
189
190            assert_eq!(
191                role_set.scope(),
192                WORKSPACE_ADMIN_SCOPE.merge(WORKSPACE_MEMBER_SCOPE)
193            );
194        }
195    }
196
197    mod scope {
198        use super::*;
199
200        #[test]
201        fn admin() {
202            assert_eq!(Role::Admin.scope(), WORKSPACE_ADMIN_SCOPE);
203        }
204
205        #[test]
206        fn control() {
207            assert_eq!(Role::Control.scope(), WORKSPACE_CONTROL_SCOPE);
208        }
209
210        #[test]
211        fn member() {
212            assert_eq!(Role::Member.scope(), WORKSPACE_MEMBER_SCOPE);
213        }
214    }
215
216    mod equality {
217        use super::*;
218
219        #[test]
220        fn with_string() {
221            assert_eq!(Role::Admin, "admin".to_string());
222            assert_eq!(Role::Control, "control".to_string());
223            assert_eq!(Role::Member, "member".to_string());
224        }
225
226        #[test]
227        fn with_str() {
228            assert_eq!(Role::Admin, "admin");
229            assert_eq!(Role::Control, "control");
230            assert_eq!(Role::Member, "member");
231        }
232    }
233
234    mod conversion {
235        use super::*;
236
237        #[test]
238        fn from_string() -> anyhow::Result<()> {
239            assert_eq!(Role::Admin, Role::try_from("admin".to_string())?);
240            assert_eq!(Role::Control, Role::try_from("control".to_string())?);
241            assert_eq!(Role::Member, Role::try_from("member".to_string())?);
242            assert!(Role::try_from("notarole".to_string()).is_err());
243
244            Ok(())
245        }
246
247        #[test]
248        fn from_str() -> anyhow::Result<()> {
249            assert_eq!(Role::Admin, Role::try_from("admin")?);
250            assert_eq!(Role::Control, Role::try_from("control")?);
251            assert_eq!(Role::Member, Role::try_from("member")?);
252            assert!(Role::try_from("notarole").is_err());
253
254            Ok(())
255        }
256    }
257
258    mod deserialize {
259        use super::*;
260
261        #[test]
262        fn lowercase() -> anyhow::Result<()> {
263            assert_eq!(Role::Admin, serde_json::from_value::<Role>(json!("admin"))?);
264            assert_eq!(
265                Role::Control,
266                serde_json::from_value::<Role>(json!("control"))?
267            );
268            assert_eq!(
269                Role::Member,
270                serde_json::from_value::<Role>(json!("member"))?
271            );
272            assert!(serde_json::from_value::<Role>(json!("notarole")).is_err());
273
274            Ok(())
275        }
276
277        #[test]
278        fn capitalized() -> anyhow::Result<()> {
279            assert_eq!(Role::Admin, serde_json::from_value::<Role>(json!("Admin"))?);
280            assert_eq!(
281                Role::Control,
282                serde_json::from_value::<Role>(json!("Control"))?
283            );
284            assert_eq!(
285                Role::Member,
286                serde_json::from_value::<Role>(json!("Member"))?
287            );
288            assert!(serde_json::from_value::<Role>(json!("Notarole")).is_err());
289
290            Ok(())
291        }
292
293        #[test]
294        fn allcaps() -> anyhow::Result<()> {
295            assert_eq!(Role::Admin, serde_json::from_value::<Role>(json!("ADMIN"))?);
296            assert_eq!(
297                Role::Control,
298                serde_json::from_value::<Role>(json!("CONTROL"))?
299            );
300            assert_eq!(
301                Role::Member,
302                serde_json::from_value::<Role>(json!("MEMBER"))?
303            );
304            assert!(serde_json::from_value::<Role>(json!("NOTAROLE")).is_err());
305
306            Ok(())
307        }
308    }
309
310    mod serialize {
311        use super::*;
312
313        #[test]
314        fn capitalisation() {
315            assert_eq!(serde_json::to_value(Role::Admin).unwrap(), json!("admin"));
316            assert_eq!(
317                serde_json::to_value(Role::Control).unwrap(),
318                json!("control")
319            );
320            assert_eq!(serde_json::to_value(Role::Member).unwrap(), json!("member"));
321        }
322    }
323
324    mod privilege_escalation {
325        use super::*;
326
327        #[test]
328        fn admin_is_superset() {
329            assert!(Role::Admin.is_superset_of(Role::Admin));
330            assert!(Role::Admin.is_superset_of(Role::Control));
331            assert!(Role::Admin.is_superset_of(Role::Member));
332        }
333
334        #[test]
335        fn control_is_not_superset_of_admin_or_member() {
336            assert!(!Role::Control.is_superset_of(Role::Admin));
337            assert!(!Role::Control.is_superset_of(Role::Member));
338        }
339
340        #[test]
341        fn member_is_not_superset() {
342            assert!(!Role::Member.is_superset_of(Role::Admin));
343            assert!(!Role::Member.is_superset_of(Role::Control));
344            assert!(Role::Member.is_superset_of(Role::Member));
345        }
346    }
347}