Skip to main content

styrene_rbac/
role.rs

1//! Role hierarchy — cumulative privilege tiers.
2
3/// Privilege tiers on the Styrene mesh. Each role inherits all capabilities
4/// from tiers below it. The numeric value determines ordering.
5#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
6#[repr(u8)]
7pub enum Role {
8    /// Explicit deny — all messages dropped, all requests rejected.
9    Blocked = 0,
10    /// No access. Fail-closed default for restrictive deployments.
11    None = 1,
12    /// Mesh peer — chat, basic queries, relay requests.
13    Peer = 10,
14    /// Read-only monitoring — inbox queries, dashboards, datalink.
15    Monitor = 20,
16    /// Fleet operator — config updates, restricted terminal, write ops.
17    Operator = 30,
18    /// Full control — exec, reboot, self-update, full terminal.
19    Admin = 40,
20}
21
22impl Role {
23    /// Parse a role name string (case-insensitive).
24    pub fn from_name(name: &str) -> Option<Self> {
25        match name.to_ascii_lowercase().as_str() {
26            "blocked" => Some(Self::Blocked),
27            "none" => Some(Self::None),
28            "peer" => Some(Self::Peer),
29            "monitor" => Some(Self::Monitor),
30            "operator" => Some(Self::Operator),
31            "admin" => Some(Self::Admin),
32            _ => Option::None,
33        }
34    }
35
36    /// Role name as a lowercase string.
37    pub fn as_str(self) -> &'static str {
38        match self {
39            Self::Blocked => "blocked",
40            Self::None => "none",
41            Self::Peer => "peer",
42            Self::Monitor => "monitor",
43            Self::Operator => "operator",
44            Self::Admin => "admin",
45        }
46    }
47
48    /// Whether this role has any access at all.
49    pub fn has_access(self) -> bool {
50        self >= Self::Peer
51    }
52}
53
54#[cfg(feature = "config")]
55impl<'de> serde::Deserialize<'de> for Role {
56    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
57        let s = String::deserialize(deserializer)?;
58        Self::from_name(&s).ok_or_else(|| serde::de::Error::unknown_variant(&s, ROLE_NAMES))
59    }
60}
61
62#[cfg(feature = "config")]
63impl serde::Serialize for Role {
64    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
65        serializer.serialize_str(self.as_str())
66    }
67}
68
69#[cfg(feature = "config")]
70const ROLE_NAMES: &[&str] = &["blocked", "none", "peer", "monitor", "operator", "admin"];
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn ordering() {
78        assert!(Role::Blocked < Role::None);
79        assert!(Role::None < Role::Peer);
80        assert!(Role::Peer < Role::Monitor);
81        assert!(Role::Monitor < Role::Operator);
82        assert!(Role::Operator < Role::Admin);
83    }
84
85    #[test]
86    fn from_name_case_insensitive() {
87        assert_eq!(Role::from_name("ADMIN"), Some(Role::Admin));
88        assert_eq!(Role::from_name("Peer"), Some(Role::Peer));
89        assert_eq!(Role::from_name("unknown"), Option::None);
90    }
91
92    #[test]
93    fn roundtrip_name() {
94        for role in
95            [Role::Blocked, Role::None, Role::Peer, Role::Monitor, Role::Operator, Role::Admin]
96        {
97            assert_eq!(Role::from_name(role.as_str()), Some(role));
98        }
99    }
100
101    #[test]
102    fn has_access() {
103        assert!(!Role::Blocked.has_access());
104        assert!(!Role::None.has_access());
105        assert!(Role::Peer.has_access());
106        assert!(Role::Monitor.has_access());
107        assert!(Role::Operator.has_access());
108        assert!(Role::Admin.has_access());
109    }
110}