rskit_authz/engine/
evaluate.rs1use std::collections::{HashMap, HashSet};
4
5use rskit_errors::{AppError, AppResult};
6use serde_json::Value;
7
8use super::model::{
9 AttributeSource, Condition, Decision, Effect, Operator, Permission, Policy, Request, Role,
10};
11use crate::checker::Checker;
12use crate::matcher::match_any;
13
14#[derive(Debug, Clone)]
16pub struct Engine {
17 roles: HashMap<String, Role>,
18 policies: Vec<Policy>,
19}
20
21impl Engine {
22 pub fn new(roles: Vec<Role>, policies: Vec<Policy>) -> AppResult<Self> {
24 let mut indexed_roles = HashMap::with_capacity(roles.len());
25 for role in roles {
26 if role.name.is_empty() {
27 return Err(AppError::invalid_input(
28 "role.name",
29 "role name is required",
30 ));
31 }
32 if indexed_roles.insert(role.name.clone(), role).is_some() {
33 return Err(AppError::invalid_input("role.name", "duplicate role"));
34 }
35 }
36 Ok(Self {
37 roles: indexed_roles,
38 policies,
39 })
40 }
41
42 #[must_use]
48 pub fn authorize(&self, request: &Request) -> Decision {
49 for policy in &self.policies {
51 if policy.effect == Effect::Deny && policy.matches(request) {
52 return Decision {
53 allowed: false,
54 reason: String::from("denied by policy"),
55 };
56 }
57 }
58
59 if self.role_allows(request, &request.subject.roles, &mut HashSet::new()) {
61 return Decision {
62 allowed: true,
63 reason: String::from("allowed by role grant"),
64 };
65 }
66
67 for policy in &self.policies {
69 if policy.effect == Effect::Allow && policy.matches(request) {
70 return Decision {
71 allowed: true,
72 reason: String::from("allowed by policy"),
73 };
74 }
75 }
76
77 Decision {
78 allowed: false,
79 reason: String::from("default deny"),
80 }
81 }
82
83 fn role_allows(
84 &self,
85 request: &Request,
86 role_names: &[String],
87 visited: &mut HashSet<String>,
88 ) -> bool {
89 for role_name in role_names {
90 if !visited.insert(role_name.clone()) {
91 continue;
92 }
93 let Some(role) = self.roles.get(role_name) else {
94 continue;
95 };
96 if role
97 .permissions
98 .iter()
99 .any(|permission| permission.matches_request(request))
100 {
101 return true;
102 }
103 if self.role_allows(request, &role.inherits, visited) {
104 return true;
105 }
106 }
107 false
108 }
109}
110
111impl Checker for Engine {
112 fn authorize(&self, request: &Request) -> Decision {
113 Self::authorize(self, request)
114 }
115}
116
117impl Permission {
118 fn matches_request(&self, request: &Request) -> bool {
119 self.matches(&request.resource.resource_type, &request.action)
120 && self
121 .conditions
122 .iter()
123 .all(|condition| condition.matches(request))
124 }
125}
126
127impl Policy {
128 fn matches(&self, request: &Request) -> bool {
129 if !match_dimension(&self.resources, &request.resource.resource_type)
130 || !match_dimension(&self.actions, &request.action)
131 {
132 return false;
133 }
134 self.conditions
135 .iter()
136 .all(|condition| condition.matches(request))
137 }
138}
139
140impl Condition {
141 fn matches(&self, request: &Request) -> bool {
142 let Some(actual) = attribute_value(request, self.source, &self.key) else {
143 return false;
144 };
145
146 if let (Some(compare_source), Some(compare_key)) =
147 (self.compare_source, self.compare_key.as_deref())
148 {
149 let Some(other) = attribute_value(request, compare_source, compare_key) else {
150 return false;
151 };
152 match self.operator {
154 Operator::Equals | Operator::OneOf => actual == other,
155 Operator::NotEquals => actual != other,
156 }
157 } else {
158 match self.operator {
160 Operator::Equals => self.values.len() == 1 && actual == self.values[0],
161 Operator::NotEquals => self.values.len() == 1 && actual != self.values[0],
162 Operator::OneOf => self.values.iter().any(|v| v == &actual),
163 }
164 }
165 }
166}
167
168fn match_dimension(patterns: &[String], value: &str) -> bool {
169 !patterns.is_empty() && match_any(patterns, value)
170}
171
172fn attribute_value(request: &Request, source: AttributeSource, key: &str) -> Option<Value> {
173 match source {
174 AttributeSource::Subject => {
175 if key == "id" {
176 return (!request.subject.id.is_empty())
177 .then(|| Value::String(request.subject.id.clone()));
178 }
179 request.subject.attributes.get(key).cloned()
180 }
181 AttributeSource::Resource => match key {
182 "id" => (!request.resource.id.is_empty())
183 .then(|| Value::String(request.resource.id.clone())),
184 "type" => (!request.resource.resource_type.is_empty())
185 .then(|| Value::String(request.resource.resource_type.clone())),
186 _ => request.resource.attributes.get(key).cloned(),
187 },
188 AttributeSource::Context => request.context.get(key).cloned(),
189 }
190}