Skip to main content

typesec_rbac/
model.rs

1//! Serde data model for RBAC YAML policies.
2
3use serde::{Deserialize, Serialize};
4
5/// The root of an RBAC policy file.
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct RbacPolicy {
8    /// Role definitions.
9    pub roles: Vec<RoleDefinition>,
10    /// Subject → role assignments.
11    #[serde(default)]
12    pub assignments: Vec<Assignment>,
13}
14
15/// A role with its permission set and resource patterns.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct RoleDefinition {
18    /// The role's canonical name.
19    pub name: String,
20    /// Permissions granted by this role (must match `Permission::name()` values).
21    #[serde(default)]
22    pub permissions: Vec<String>,
23    /// Glob patterns for resources this role applies to.
24    #[serde(default)]
25    pub resources: Vec<String>,
26    /// Roles whose permissions this role inherits.
27    #[serde(default)]
28    pub inherits: Vec<String>,
29}
30
31/// Maps a subject to one or more roles.
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct Assignment {
34    /// The subject identifier (e.g., `"agent:data-pipeline"`).
35    pub subject: String,
36    /// The roles assigned to this subject.
37    pub roles: Vec<String>,
38}
39
40impl RbacPolicy {
41    /// Parse an RBAC policy from a YAML string.
42    pub fn from_yaml(yaml: &str) -> Result<Self, serde_yaml::Error> {
43        serde_yaml::from_str(yaml)
44    }
45
46    /// Validate the policy: check that `inherits` references valid role names,
47    /// and that there are no circular inheritance chains.
48    pub fn validate(&self) -> Result<(), String> {
49        let names: std::collections::HashSet<&str> =
50            self.roles.iter().map(|r| r.name.as_str()).collect();
51
52        for role in &self.roles {
53            for parent in &role.inherits {
54                if !names.contains(parent.as_str()) {
55                    return Err(format!(
56                        "role '{}' inherits from unknown role '{}'",
57                        role.name, parent
58                    ));
59                }
60            }
61        }
62
63        // Detect cycles via DFS.
64        let mut visiting = std::collections::HashSet::new();
65        for role in &self.roles {
66            self.check_cycle(&role.name, &mut visiting, &std::collections::HashSet::new())?;
67        }
68
69        Ok(())
70    }
71
72    fn check_cycle(
73        &self,
74        role_name: &str,
75        visited: &mut std::collections::HashSet<String>,
76        ancestors: &std::collections::HashSet<String>,
77    ) -> Result<(), String> {
78        if ancestors.contains(role_name) {
79            return Err(format!(
80                "circular inheritance detected for role '{role_name}'"
81            ));
82        }
83        if visited.contains(role_name) {
84            return Ok(());
85        }
86
87        let mut ancestors = ancestors.clone();
88        ancestors.insert(role_name.to_owned());
89
90        if let Some(role) = self.roles.iter().find(|r| r.name == role_name) {
91            for parent in &role.inherits {
92                self.check_cycle(parent, visited, &ancestors)?;
93            }
94        }
95
96        visited.insert(role_name.to_owned());
97        Ok(())
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    const VALID_YAML: &str = r#"
106roles:
107  - name: analyst
108    permissions: [read, read_sensitive]
109    resources: ["reports/*"]
110  - name: admin
111    inherits: [analyst]
112    permissions: [write, delete]
113    resources: ["*"]
114
115assignments:
116  - subject: "agent:pipeline"
117    roles: [analyst]
118"#;
119
120    #[test]
121    fn parses_valid_yaml() {
122        let policy = RbacPolicy::from_yaml(VALID_YAML).expect("parse should succeed");
123        assert_eq!(policy.roles.len(), 2);
124        assert_eq!(policy.assignments.len(), 1);
125        assert!(policy.validate().is_ok());
126    }
127
128    #[test]
129    fn detects_unknown_parent() {
130        let yaml = r#"
131roles:
132  - name: engineer
133    inherits: [nonexistent]
134assignments: []
135"#;
136        let policy = RbacPolicy::from_yaml(yaml).expect("parse ok");
137        assert!(policy.validate().is_err());
138    }
139
140    #[test]
141    fn detects_cycle() {
142        let yaml = r#"
143roles:
144  - name: a
145    inherits: [b]
146  - name: b
147    inherits: [a]
148assignments: []
149"#;
150        let policy = RbacPolicy::from_yaml(yaml).expect("parse ok");
151        assert!(policy.validate().is_err());
152    }
153}