1mod flatten;
4mod pattern;
5
6use std::collections::HashMap;
7
8use tracing::debug;
9use typesec_core::{
10 ResourceId, SubjectId,
11 policy::{PolicyEngine, PolicyResult},
12};
13
14use crate::model::RbacPolicy;
15use flatten::flatten_role;
16use pattern::{GlobPattern, is_glob_pattern};
17
18pub struct RbacEngine {
27 subject_grants: HashMap<String, Vec<CompiledGrant>>,
29 wildcard_subject_grants: Vec<(GlobPattern, Vec<CompiledGrant>)>,
31}
32
33#[derive(Debug, Clone)]
39struct CompiledGrant {
40 permission: String,
41 resource_patterns: Vec<GlobPattern>,
42}
43
44impl RbacEngine {
45 pub fn new(policy: RbacPolicy) -> Result<Self, String> {
49 policy.validate()?;
50
51 let effective_roles: HashMap<String, Vec<flatten::Grant>> = {
53 let mut map = HashMap::new();
54 for role in &policy.roles {
55 let grants = flatten_role(&role.name, &policy);
56 map.insert(role.name.clone(), grants);
57 }
58 map
59 };
60
61 let mut subject_grants: HashMap<String, Vec<CompiledGrant>> = HashMap::new();
64 let mut wildcard_subject_grants: Vec<(GlobPattern, Vec<CompiledGrant>)> = Vec::new();
65 for assignment in &policy.assignments {
66 let mut all_grants: Vec<CompiledGrant> = Vec::new();
67 for role_name in &assignment.roles {
68 if let Some(grants) = effective_roles.get(role_name) {
69 for grant in grants {
70 all_grants.push(CompiledGrant {
71 permission: grant.permission.clone(),
72 resource_patterns: grant
73 .resource_patterns
74 .iter()
75 .map(|p| GlobPattern::compile(p, "resource"))
76 .collect::<Result<_, _>>()?,
77 });
78 }
79 }
80 }
81 if is_glob_pattern(&assignment.subject) {
82 wildcard_subject_grants.push((
83 GlobPattern::compile(&assignment.subject, "subject")?,
84 all_grants,
85 ));
86 } else {
87 subject_grants
88 .entry(assignment.subject.clone())
89 .or_default()
90 .extend(all_grants);
91 }
92 }
93
94 Ok(Self {
95 subject_grants,
96 wildcard_subject_grants,
97 })
98 }
99
100 pub fn from_yaml(yaml: &str) -> Result<Self, String> {
102 let policy = RbacPolicy::from_yaml(yaml).map_err(|e| format!("YAML parse error: {e}"))?;
103 Self::new(policy)
104 }
105}
106
107impl PolicyEngine for RbacEngine {
108 fn check(&self, subject: &SubjectId, action: &str, resource: &ResourceId) -> PolicyResult {
109 let subject = subject.as_str();
110 let resource = resource.as_str();
111 debug!(subject, action, resource, "rbac check");
112
113 let exact_grants = self.subject_grants.get(subject).into_iter().flatten();
114 let wildcard_grants = self
115 .wildcard_subject_grants
116 .iter()
117 .filter(|(pattern, _)| pattern.matches(subject))
118 .flat_map(|(_, grants)| grants);
119
120 let mut matched_subject = false;
121 for grant in exact_grants.chain(wildcard_grants) {
122 matched_subject = true;
123 if grant.permission == action {
124 for pattern in &grant.resource_patterns {
125 if pattern.matches(resource) {
126 return PolicyResult::Allow;
127 }
128 }
129 }
130 }
131
132 if !matched_subject {
133 return PolicyResult::Deny(format!("no role assignments for subject '{subject}'"));
134 }
135
136 PolicyResult::Deny(format!(
137 "no rule grants '{subject}' permission '{action}' on '{resource}'"
138 ))
139 }
140}
141
142#[cfg(test)]
143mod tests;