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 for role in &self.roles {
67 walk_inheritance(&role.name, self, &mut |_| {})?;
68 }
69
70 Ok(())
71 }
72}
73
74pub(crate) fn walk_inheritance(
87 start: &str,
88 policy: &RbacPolicy,
89 visit: &mut impl FnMut(&RoleDefinition),
90) -> Result<(), String> {
91 let mut visited = std::collections::HashSet::new();
92 let mut path = std::collections::HashSet::new();
93 walk_inheritance_inner(start, policy, &mut visited, &mut path, visit)
94}
95
96fn walk_inheritance_inner(
97 role_name: &str,
98 policy: &RbacPolicy,
99 visited: &mut std::collections::HashSet<String>,
100 path: &mut std::collections::HashSet<String>,
101 visit: &mut impl FnMut(&RoleDefinition),
102) -> Result<(), String> {
103 if path.contains(role_name) {
104 return Err(format!(
105 "circular inheritance detected for role '{role_name}'"
106 ));
107 }
108 if !visited.insert(role_name.to_owned()) {
109 return Ok(());
110 }
111
112 let Some(role) = policy.roles.iter().find(|r| r.name == role_name) else {
113 return Ok(());
114 };
115
116 path.insert(role_name.to_owned());
117 visit(role);
118 for parent in &role.inherits {
119 walk_inheritance_inner(parent, policy, visited, path, visit)?;
120 }
121 path.remove(role_name);
122 Ok(())
123}
124
125#[cfg(test)]
126mod tests;