1use std::collections::{HashMap, HashSet};
4
5use glob::Pattern;
6use tracing::debug;
7use typesec_core::policy::{PolicyEngine, PolicyResult};
8
9use crate::model::RbacPolicy;
10
11pub struct RbacEngine {
20 subject_grants: HashMap<String, Vec<Grant>>,
22}
23
24#[derive(Debug, Clone)]
25struct Grant {
26 permission: String,
27 resource_patterns: Vec<String>,
28}
29
30impl RbacEngine {
31 pub fn new(policy: RbacPolicy) -> Result<Self, String> {
35 policy.validate()?;
36
37 let effective_roles: HashMap<String, Vec<Grant>> = {
39 let mut map = HashMap::new();
40 for role in &policy.roles {
41 let grants = flatten_role(&role.name, &policy);
42 map.insert(role.name.clone(), grants);
43 }
44 map
45 };
46
47 let mut subject_grants: HashMap<String, Vec<Grant>> = HashMap::new();
49 for assignment in &policy.assignments {
50 let mut all_grants: Vec<Grant> = Vec::new();
51 for role_name in &assignment.roles {
52 if let Some(grants) = effective_roles.get(role_name) {
53 all_grants.extend(grants.iter().cloned());
54 }
55 }
56 subject_grants
57 .entry(assignment.subject.clone())
58 .or_default()
59 .extend(all_grants);
60 }
61
62 Ok(Self { subject_grants })
63 }
64
65 pub fn from_yaml(yaml: &str) -> Result<Self, String> {
67 let policy = RbacPolicy::from_yaml(yaml).map_err(|e| format!("YAML parse error: {e}"))?;
68 Self::new(policy)
69 }
70}
71
72impl PolicyEngine for RbacEngine {
73 fn check(&self, subject: &str, action: &str, resource: &str) -> PolicyResult {
74 debug!(subject, action, resource, "rbac check");
75
76 let grants = match self.subject_grants.get(subject) {
77 Some(g) => g,
78 None => {
79 return PolicyResult::Deny(format!("no role assignments for subject '{subject}'"));
80 }
81 };
82
83 for grant in grants {
84 if grant.permission == action {
85 for pattern in &grant.resource_patterns {
86 if matches_glob(pattern, resource) {
87 return PolicyResult::Allow;
88 }
89 }
90 }
91 }
92
93 PolicyResult::Deny(format!(
94 "no rule grants '{subject}' permission '{action}' on '{resource}'"
95 ))
96 }
97}
98
99fn flatten_role(role_name: &str, policy: &RbacPolicy) -> Vec<Grant> {
101 let mut seen = HashSet::new();
102 flatten_role_inner(role_name, policy, &mut seen)
103}
104
105fn flatten_role_inner(
106 role_name: &str,
107 policy: &RbacPolicy,
108 seen: &mut HashSet<String>,
109) -> Vec<Grant> {
110 if !seen.insert(role_name.to_owned()) {
111 return vec![]; }
113
114 let role = match policy.roles.iter().find(|r| r.name == role_name) {
115 Some(r) => r,
116 None => return vec![],
117 };
118
119 let mut grants: Vec<Grant> = Vec::new();
120
121 for perm in &role.permissions {
123 grants.push(Grant {
124 permission: perm.clone(),
125 resource_patterns: role.resources.clone(),
126 });
127 }
128
129 for parent_name in &role.inherits {
131 let inherited = flatten_role_inner(parent_name, policy, seen);
132 grants.extend(inherited);
133 }
134
135 grants
136}
137
138fn matches_glob(pattern: &str, resource: &str) -> bool {
141 if pattern == "*" {
142 return true;
143 }
144 Pattern::new(pattern).is_ok_and(|p| p.matches(resource))
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 const YAML: &str = r#"
152roles:
153 - name: analyst
154 permissions: [read, read_sensitive]
155 resources: ["reports/*", "metrics/*"]
156 - name: engineer
157 permissions: [read, write, execute]
158 resources: ["code/*", "infra/*"]
159 - name: admin
160 inherits: [analyst, engineer]
161 permissions: [delete, delegate]
162 resources: ["*"]
163
164assignments:
165 - subject: "agent:data-pipeline"
166 roles: [analyst]
167 - subject: "agent:deploy-bot"
168 roles: [engineer]
169 - subject: "agent:superuser"
170 roles: [admin]
171"#;
172
173 fn engine() -> RbacEngine {
174 RbacEngine::from_yaml(YAML).expect("engine build should succeed")
175 }
176
177 #[test]
178 fn analyst_can_read_reports() {
179 let e = engine();
180 assert_eq!(
181 e.check("agent:data-pipeline", "read", "reports/q1"),
182 PolicyResult::Allow
183 );
184 }
185
186 #[test]
187 fn analyst_cannot_write() {
188 let e = engine();
189 assert!(matches!(
190 e.check("agent:data-pipeline", "write", "reports/q1"),
191 PolicyResult::Deny(_)
192 ));
193 }
194
195 #[test]
196 fn engineer_can_write_code() {
197 let e = engine();
198 assert_eq!(
199 e.check("agent:deploy-bot", "write", "code/main.rs"),
200 PolicyResult::Allow
201 );
202 }
203
204 #[test]
205 fn engineer_cannot_access_reports() {
206 let e = engine();
207 assert!(matches!(
208 e.check("agent:deploy-bot", "read", "reports/q1"),
209 PolicyResult::Deny(_)
210 ));
211 }
212
213 #[test]
214 fn admin_inherits_analyst_and_engineer() {
215 let e = engine();
216 assert_eq!(
218 e.check("agent:superuser", "read_sensitive", "reports/q1"),
219 PolicyResult::Allow
220 );
221 assert_eq!(
223 e.check("agent:superuser", "execute", "code/deploy.sh"),
224 PolicyResult::Allow
225 );
226 assert_eq!(
228 e.check("agent:superuser", "delete", "anything"),
229 PolicyResult::Allow
230 );
231 }
232
233 #[test]
234 fn unknown_subject_is_denied() {
235 let e = engine();
236 assert!(matches!(
237 e.check("agent:ghost", "read", "reports/q1"),
238 PolicyResult::Deny(_)
239 ));
240 }
241}