oxigdal_security/access_control/
abac.rs1use crate::access_control::{AccessControlEvaluator, AccessDecision, AccessRequest, Action};
4use crate::error::Result;
5use dashmap::DashMap;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::sync::Arc;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct AbacPolicy {
13 pub id: String,
15 pub name: String,
17 pub description: Option<String>,
19 pub subject_conditions: Vec<Condition>,
21 pub resource_conditions: Vec<Condition>,
23 pub context_conditions: Vec<Condition>,
25 pub actions: Vec<Action>,
27 pub effect: PolicyEffect,
29 pub priority: i32,
31}
32
33impl AbacPolicy {
34 pub fn new(id: String, name: String, actions: Vec<Action>, effect: PolicyEffect) -> Self {
36 Self {
37 id,
38 name,
39 description: None,
40 subject_conditions: Vec::new(),
41 resource_conditions: Vec::new(),
42 context_conditions: Vec::new(),
43 actions,
44 effect,
45 priority: 0,
46 }
47 }
48
49 pub fn with_description(mut self, description: String) -> Self {
51 self.description = Some(description);
52 self
53 }
54
55 pub fn with_subject_condition(mut self, condition: Condition) -> Self {
57 self.subject_conditions.push(condition);
58 self
59 }
60
61 pub fn with_resource_condition(mut self, condition: Condition) -> Self {
63 self.resource_conditions.push(condition);
64 self
65 }
66
67 pub fn with_context_condition(mut self, condition: Condition) -> Self {
69 self.context_conditions.push(condition);
70 self
71 }
72
73 pub fn with_priority(mut self, priority: i32) -> Self {
75 self.priority = priority;
76 self
77 }
78
79 pub fn evaluate(&self, request: &AccessRequest) -> Option<PolicyEffect> {
81 if !self.actions.contains(&request.action) {
83 return None;
84 }
85
86 if !self.evaluate_conditions(&self.subject_conditions, &request.subject.attributes) {
88 return None;
89 }
90
91 if !self.evaluate_conditions(&self.resource_conditions, &request.resource.attributes) {
93 return None;
94 }
95
96 if !self.evaluate_conditions(&self.context_conditions, &request.context.attributes) {
98 return None;
99 }
100
101 Some(self.effect)
102 }
103
104 fn evaluate_conditions(
105 &self,
106 conditions: &[Condition],
107 attributes: &HashMap<String, String>,
108 ) -> bool {
109 conditions.iter().all(|cond| cond.evaluate(attributes))
110 }
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
115pub enum PolicyEffect {
116 Allow,
118 Deny,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct Condition {
125 pub key: String,
127 pub operator: ConditionOperator,
129 pub value: String,
131}
132
133impl Condition {
134 pub fn new(key: String, operator: ConditionOperator, value: String) -> Self {
136 Self {
137 key,
138 operator,
139 value,
140 }
141 }
142
143 pub fn evaluate(&self, attributes: &HashMap<String, String>) -> bool {
145 let attr_value = match attributes.get(&self.key) {
146 Some(v) => v,
147 None => return false,
148 };
149
150 match self.operator {
151 ConditionOperator::Equals => attr_value == &self.value,
152 ConditionOperator::NotEquals => attr_value != &self.value,
153 ConditionOperator::Contains => attr_value.contains(&self.value),
154 ConditionOperator::StartsWith => attr_value.starts_with(&self.value),
155 ConditionOperator::EndsWith => attr_value.ends_with(&self.value),
156 ConditionOperator::GreaterThan => {
157 if let (Ok(a), Ok(b)) = (attr_value.parse::<f64>(), self.value.parse::<f64>()) {
158 a > b
159 } else {
160 false
161 }
162 }
163 ConditionOperator::LessThan => {
164 if let (Ok(a), Ok(b)) = (attr_value.parse::<f64>(), self.value.parse::<f64>()) {
165 a < b
166 } else {
167 false
168 }
169 }
170 ConditionOperator::In => {
171 let values: Vec<&str> = self.value.split(',').map(|s| s.trim()).collect();
172 values.contains(&attr_value.as_str())
173 }
174 }
175 }
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
180pub enum ConditionOperator {
181 Equals,
183 NotEquals,
185 Contains,
187 StartsWith,
189 EndsWith,
191 GreaterThan,
193 LessThan,
195 In,
197}
198
199pub struct AbacEngine {
201 policies: Arc<DashMap<String, AbacPolicy>>,
202 cache: Arc<DashMap<u64, AccessDecision>>,
204}
205
206impl AbacEngine {
207 pub fn new() -> Self {
209 Self {
210 policies: Arc::new(DashMap::new()),
211 cache: Arc::new(DashMap::new()),
212 }
213 }
214
215 pub fn add_policy(&self, policy: AbacPolicy) -> Result<()> {
217 self.policies.insert(policy.id.clone(), policy);
218 self.cache.clear(); Ok(())
220 }
221
222 pub fn remove_policy(&self, policy_id: &str) -> Result<()> {
224 self.policies.remove(policy_id);
225 self.cache.clear();
226 Ok(())
227 }
228
229 pub fn get_policy(&self, policy_id: &str) -> Option<AbacPolicy> {
231 self.policies.get(policy_id).map(|p| p.clone())
232 }
233
234 pub fn list_policies(&self) -> Vec<AbacPolicy> {
236 let mut policies: Vec<_> = self.policies.iter().map(|p| p.clone()).collect();
237 policies.sort_by_key(|x| std::cmp::Reverse(x.priority));
239 policies
240 }
241
242 pub fn clear_policies(&self) {
244 self.policies.clear();
245 self.cache.clear();
246 }
247
248 pub fn clear_cache(&self) {
250 self.cache.clear();
251 }
252
253 pub fn cache_stats(&self) -> (usize, usize) {
255 (self.cache.len(), self.policies.len())
256 }
257
258 fn compute_request_hash(request: &AccessRequest) -> u64 {
259 use std::collections::hash_map::DefaultHasher;
260 use std::hash::{Hash, Hasher};
261
262 let mut hasher = DefaultHasher::new();
263 request.subject.id.hash(&mut hasher);
264 request.resource.id.hash(&mut hasher);
265 format!("{:?}", request.action).hash(&mut hasher);
266 hasher.finish()
267 }
268}
269
270impl Default for AbacEngine {
271 fn default() -> Self {
272 Self::new()
273 }
274}
275
276impl AccessControlEvaluator for AbacEngine {
277 fn evaluate(&self, request: &AccessRequest) -> Result<AccessDecision> {
278 let cache_key = Self::compute_request_hash(request);
280 if let Some(decision) = self.cache.get(&cache_key) {
281 return Ok(*decision);
282 }
283
284 let policies = self.list_policies();
286 let mut explicit_allow = false;
287 let mut explicit_deny = false;
288
289 for policy in policies {
290 if let Some(effect) = policy.evaluate(request) {
291 match effect {
292 PolicyEffect::Allow => explicit_allow = true,
293 PolicyEffect::Deny => {
294 explicit_deny = true;
295 break; }
297 }
298 }
299 }
300
301 let decision = if explicit_deny {
302 AccessDecision::Deny
303 } else if explicit_allow {
304 AccessDecision::Allow
305 } else {
306 AccessDecision::Deny };
308
309 self.cache.insert(cache_key, decision);
311
312 Ok(decision)
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319 use crate::access_control::{AccessContext, Resource, ResourceType, Subject, SubjectType};
320
321 #[test]
322 fn test_condition_evaluation() {
323 let condition = Condition::new(
324 "department".to_string(),
325 ConditionOperator::Equals,
326 "engineering".to_string(),
327 );
328
329 let mut attributes = HashMap::new();
330 attributes.insert("department".to_string(), "engineering".to_string());
331
332 assert!(condition.evaluate(&attributes));
333
334 attributes.insert("department".to_string(), "sales".to_string());
335 assert!(!condition.evaluate(&attributes));
336 }
337
338 #[test]
339 fn test_condition_operators() {
340 let mut attributes = HashMap::new();
341 attributes.insert("name".to_string(), "test_user".to_string());
342 attributes.insert("age".to_string(), "25".to_string());
343
344 let cond = Condition::new(
345 "name".to_string(),
346 ConditionOperator::StartsWith,
347 "test".to_string(),
348 );
349 assert!(cond.evaluate(&attributes));
350
351 let cond = Condition::new(
352 "age".to_string(),
353 ConditionOperator::GreaterThan,
354 "20".to_string(),
355 );
356 assert!(cond.evaluate(&attributes));
357
358 let cond = Condition::new(
359 "name".to_string(),
360 ConditionOperator::In,
361 "test_user, admin, guest".to_string(),
362 );
363 assert!(cond.evaluate(&attributes));
364 }
365
366 #[test]
367 fn test_abac_policy() {
368 let policy = AbacPolicy::new(
369 "policy-1".to_string(),
370 "Engineering Read Access".to_string(),
371 vec![Action::Read],
372 PolicyEffect::Allow,
373 )
374 .with_subject_condition(Condition::new(
375 "department".to_string(),
376 ConditionOperator::Equals,
377 "engineering".to_string(),
378 ));
379
380 let subject = Subject::new("user-123".to_string(), SubjectType::User)
381 .with_attribute("department".to_string(), "engineering".to_string());
382
383 let resource = Resource::new("dataset-456".to_string(), ResourceType::Dataset);
384 let context = AccessContext::new();
385
386 let request = AccessRequest::new(subject, resource, Action::Read, context);
387
388 assert_eq!(policy.evaluate(&request), Some(PolicyEffect::Allow));
389 }
390
391 #[test]
392 fn test_abac_engine() {
393 let engine = AbacEngine::new();
394
395 let policy = AbacPolicy::new(
396 "policy-1".to_string(),
397 "Allow Read".to_string(),
398 vec![Action::Read],
399 PolicyEffect::Allow,
400 );
401
402 engine.add_policy(policy).expect("Failed to add policy");
403
404 let subject = Subject::new("user-123".to_string(), SubjectType::User);
405 let resource = Resource::new("dataset-456".to_string(), ResourceType::Dataset);
406 let context = AccessContext::new();
407 let request = AccessRequest::new(subject, resource, Action::Read, context);
408
409 let decision = engine.evaluate(&request).expect("Evaluation failed");
410 assert_eq!(decision, AccessDecision::Allow);
411 }
412}