1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct RbacPolicy {
8 pub roles: Vec<RoleDefinition>,
10 #[serde(default)]
12 pub assignments: Vec<Assignment>,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct RoleDefinition {
18 pub name: String,
20 #[serde(default)]
22 pub permissions: Vec<String>,
23 #[serde(default)]
25 pub resources: Vec<String>,
26 #[serde(default)]
28 pub inherits: Vec<String>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct Assignment {
34 pub subject: String,
36 pub roles: Vec<String>,
38}
39
40impl RbacPolicy {
41 pub fn from_yaml(yaml: &str) -> Result<Self, serde_yaml::Error> {
43 serde_yaml::from_str(yaml)
44 }
45
46 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 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}