cts-common 0.34.1-alpha.3

Common types and traits used across the CipherStash ecosystem
Documentation
use super::Scope;
use crate::{
    auth::claims::{WORKSPACE_ADMIN_SCOPE, WORKSPACE_CONTROL_SCOPE, WORKSPACE_MEMBER_SCOPE},
    claims::common::ArrayOrValue,
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use utoipa::ToSchema;

#[derive(Error, Debug, PartialEq)]
#[error("Role error: {0}")]
pub struct RoleError(String);

/// Defines a role in the system.
#[derive(Debug, Copy, Clone, Deserialize, PartialEq, Eq, Hash, Serialize, ToSchema)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum Role {
    // These aliases are used to maintain compatibility with existing tokens.
    /// Defines an admin role with full access to the system.
    #[serde(alias = "Admin", alias = "ADMIN")]
    Admin,

    /// Defines a control role with no encryption/decryption permissions but the ability to manage resources.
    #[serde(alias = "Control", alias = "CONTROL")]
    Control,

    /// Defines a member role with encryption and decryption permissions but no management capabilities.
    #[serde(alias = "Member", alias = "MEMBER")]
    #[default]
    Member,
}

/// Defines a set of [`Role`]s that can be used to represent a user's roles in the system.
/// It can contain multiple roles, a single role, or an empty set.
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize, Default)]
#[serde(rename_all = "lowercase")]
pub struct RoleSet(ArrayOrValue<Role>);

impl Role {
    /// Returns the [`Scope`] associated with the role.
    pub fn scope(&self) -> Scope {
        match self {
            Role::Admin => WORKSPACE_ADMIN_SCOPE,
            Role::Control => WORKSPACE_CONTROL_SCOPE,
            Role::Member => WORKSPACE_MEMBER_SCOPE,
        }
    }

    /// Returns true if this role is a superset of the other role.
    /// For example, `Admin` is a superset of `Member`, but `Member` is not a superset of `Admin`.
    /// This is useful when performing checks to prevent privilege escalation.
    pub fn is_superset_of(&self, other: Role) -> bool {
        self.scope().has_all_permissions(other.scope())
    }
}

impl RoleSet {
    pub fn single(role: Role) -> Self {
        Self(ArrayOrValue::single(role))
    }

    /// Returns the [`Scope`] associated with the roleset.
    pub fn scope(&self) -> Scope {
        self.0
            .iter()
            .map(|role| role.scope())
            .fold(Scope::with_no_permissions(), |acc, scope| acc.merge(scope))
    }

    pub fn has_role(&self, role: Role) -> bool {
        self.0.iter().any(|check| check == role)
    }

    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }

    pub fn len(&self) -> usize {
        self.0.len()
    }

    pub fn add_role(self, role: Role) -> Self {
        Self(self.0.add(role))
    }
}

impl PartialEq<String> for Role {
    fn eq(&self, other: &String) -> bool {
        Into::<&str>::into(*self) == other.as_str()
    }
}

impl PartialEq<&str> for Role {
    fn eq(&self, other: &&str) -> bool {
        Into::<&str>::into(*self) == *other
    }
}

impl TryFrom<String> for Role {
    type Error = RoleError;

    fn try_from(s: String) -> Result<Self, Self::Error> {
        s.as_str().try_into()
    }
}

impl TryFrom<&str> for Role {
    type Error = RoleError;

    fn try_from(s: &str) -> Result<Self, Self::Error> {
        match s.to_lowercase().as_str() {
            "admin" => Ok(Role::Admin),
            "control" => Ok(Role::Control),
            "member" => Ok(Role::Member),
            _ => Err(RoleError(format!("Invalid role: {s}"))),
        }
    }
}

impl From<Role> for &str {
    fn from(role: Role) -> Self {
        match role {
            Role::Admin => "admin",
            Role::Control => "control",
            Role::Member => "member",
        }
    }
}

impl From<Role> for String {
    fn from(role: Role) -> Self {
        Into::<&str>::into(role).to_string()
    }
}

#[cfg(feature = "test_utils")]
impl fake::Dummy<fake::Faker> for Role {
    fn dummy_with_rng<R: rand::Rng + ?Sized>(_: &fake::Faker, _: &mut R) -> Self {
        Role::Admin
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use serde_json::json;

    mod role_set {
        use super::*;

        #[test]
        fn single() {
            let role_set = RoleSet::single(Role::Admin);
            assert_eq!(role_set.len(), 1);
            assert!(role_set.has_role(Role::Admin));

            // Inverse check
            assert!(!role_set.has_role(Role::Member));
        }

        #[test]
        fn empty() {
            let role_set: RoleSet = Default::default();
            assert!(role_set.is_empty());
            assert_eq!(role_set.len(), 0);
        }

        #[test]
        fn multiple_roles() {
            let role_set = RoleSet::default()
                .add_role(Role::Admin)
                .add_role(Role::Member);
            assert_eq!(role_set.len(), 2);
            assert!(role_set.has_role(Role::Admin));
            assert!(role_set.has_role(Role::Member));
        }

        #[test]
        fn scope_single() {
            let role_set = RoleSet::single(Role::Admin);
            assert_eq!(role_set.scope(), WORKSPACE_ADMIN_SCOPE);
        }

        #[test]
        fn scope_multiple() {
            let role_set = RoleSet::default()
                .add_role(Role::Admin)
                .add_role(Role::Member);

            assert_eq!(
                role_set.scope(),
                WORKSPACE_ADMIN_SCOPE.merge(WORKSPACE_MEMBER_SCOPE)
            );
        }
    }

    mod scope {
        use super::*;

        #[test]
        fn admin() {
            assert_eq!(Role::Admin.scope(), WORKSPACE_ADMIN_SCOPE);
        }

        #[test]
        fn control() {
            assert_eq!(Role::Control.scope(), WORKSPACE_CONTROL_SCOPE);
        }

        #[test]
        fn member() {
            assert_eq!(Role::Member.scope(), WORKSPACE_MEMBER_SCOPE);
        }
    }

    mod equality {
        use super::*;

        #[test]
        fn with_string() {
            assert_eq!(Role::Admin, "admin".to_string());
            assert_eq!(Role::Control, "control".to_string());
            assert_eq!(Role::Member, "member".to_string());
        }

        #[test]
        fn with_str() {
            assert_eq!(Role::Admin, "admin");
            assert_eq!(Role::Control, "control");
            assert_eq!(Role::Member, "member");
        }
    }

    mod conversion {
        use super::*;

        #[test]
        fn from_string() -> anyhow::Result<()> {
            assert_eq!(Role::Admin, Role::try_from("admin".to_string())?);
            assert_eq!(Role::Control, Role::try_from("control".to_string())?);
            assert_eq!(Role::Member, Role::try_from("member".to_string())?);
            assert!(Role::try_from("notarole".to_string()).is_err());

            Ok(())
        }

        #[test]
        fn from_str() -> anyhow::Result<()> {
            assert_eq!(Role::Admin, Role::try_from("admin")?);
            assert_eq!(Role::Control, Role::try_from("control")?);
            assert_eq!(Role::Member, Role::try_from("member")?);
            assert!(Role::try_from("notarole").is_err());

            Ok(())
        }
    }

    mod deserialize {
        use super::*;

        #[test]
        fn lowercase() -> anyhow::Result<()> {
            assert_eq!(Role::Admin, serde_json::from_value::<Role>(json!("admin"))?);
            assert_eq!(
                Role::Control,
                serde_json::from_value::<Role>(json!("control"))?
            );
            assert_eq!(
                Role::Member,
                serde_json::from_value::<Role>(json!("member"))?
            );
            assert!(serde_json::from_value::<Role>(json!("notarole")).is_err());

            Ok(())
        }

        #[test]
        fn capitalized() -> anyhow::Result<()> {
            assert_eq!(Role::Admin, serde_json::from_value::<Role>(json!("Admin"))?);
            assert_eq!(
                Role::Control,
                serde_json::from_value::<Role>(json!("Control"))?
            );
            assert_eq!(
                Role::Member,
                serde_json::from_value::<Role>(json!("Member"))?
            );
            assert!(serde_json::from_value::<Role>(json!("Notarole")).is_err());

            Ok(())
        }

        #[test]
        fn allcaps() -> anyhow::Result<()> {
            assert_eq!(Role::Admin, serde_json::from_value::<Role>(json!("ADMIN"))?);
            assert_eq!(
                Role::Control,
                serde_json::from_value::<Role>(json!("CONTROL"))?
            );
            assert_eq!(
                Role::Member,
                serde_json::from_value::<Role>(json!("MEMBER"))?
            );
            assert!(serde_json::from_value::<Role>(json!("NOTAROLE")).is_err());

            Ok(())
        }
    }

    mod serialize {
        use super::*;

        #[test]
        fn capitalisation() {
            assert_eq!(serde_json::to_value(Role::Admin).unwrap(), json!("admin"));
            assert_eq!(
                serde_json::to_value(Role::Control).unwrap(),
                json!("control")
            );
            assert_eq!(serde_json::to_value(Role::Member).unwrap(), json!("member"));
        }
    }

    mod privilege_escalation {
        use super::*;

        #[test]
        fn admin_is_superset() {
            assert!(Role::Admin.is_superset_of(Role::Admin));
            assert!(Role::Admin.is_superset_of(Role::Control));
            assert!(Role::Admin.is_superset_of(Role::Member));
        }

        #[test]
        fn control_is_not_superset_of_admin_or_member() {
            assert!(!Role::Control.is_superset_of(Role::Admin));
            assert!(!Role::Control.is_superset_of(Role::Member));
        }

        #[test]
        fn member_is_not_superset() {
            assert!(!Role::Member.is_superset_of(Role::Admin));
            assert!(!Role::Member.is_superset_of(Role::Control));
            assert!(Role::Member.is_superset_of(Role::Member));
        }
    }
}