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