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