Skip to main content

oxigdal_security/access_control/
abac.rs

1//! Attribute-Based Access Control (ABAC).
2
3use 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/// ABAC policy.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct AbacPolicy {
13    /// Policy ID.
14    pub id: String,
15    /// Policy name.
16    pub name: String,
17    /// Policy description.
18    pub description: Option<String>,
19    /// Subject conditions.
20    pub subject_conditions: Vec<Condition>,
21    /// Resource conditions.
22    pub resource_conditions: Vec<Condition>,
23    /// Context conditions.
24    pub context_conditions: Vec<Condition>,
25    /// Actions allowed.
26    pub actions: Vec<Action>,
27    /// Effect (Allow or Deny).
28    pub effect: PolicyEffect,
29    /// Priority (higher priority policies evaluated first).
30    pub priority: i32,
31}
32
33impl AbacPolicy {
34    /// Create a new ABAC policy.
35    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    /// Set description.
50    pub fn with_description(mut self, description: String) -> Self {
51        self.description = Some(description);
52        self
53    }
54
55    /// Add subject condition.
56    pub fn with_subject_condition(mut self, condition: Condition) -> Self {
57        self.subject_conditions.push(condition);
58        self
59    }
60
61    /// Add resource condition.
62    pub fn with_resource_condition(mut self, condition: Condition) -> Self {
63        self.resource_conditions.push(condition);
64        self
65    }
66
67    /// Add context condition.
68    pub fn with_context_condition(mut self, condition: Condition) -> Self {
69        self.context_conditions.push(condition);
70        self
71    }
72
73    /// Set priority.
74    pub fn with_priority(mut self, priority: i32) -> Self {
75        self.priority = priority;
76        self
77    }
78
79    /// Evaluate policy against a request.
80    pub fn evaluate(&self, request: &AccessRequest) -> Option<PolicyEffect> {
81        // Check if action matches
82        if !self.actions.contains(&request.action) {
83            return None;
84        }
85
86        // Evaluate subject conditions
87        if !self.evaluate_conditions(&self.subject_conditions, &request.subject.attributes) {
88            return None;
89        }
90
91        // Evaluate resource conditions
92        if !self.evaluate_conditions(&self.resource_conditions, &request.resource.attributes) {
93            return None;
94        }
95
96        // Evaluate context conditions
97        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/// Policy effect.
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
115pub enum PolicyEffect {
116    /// Allow access.
117    Allow,
118    /// Deny access.
119    Deny,
120}
121
122/// Attribute condition.
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct Condition {
125    /// Attribute key.
126    pub key: String,
127    /// Operator.
128    pub operator: ConditionOperator,
129    /// Expected value.
130    pub value: String,
131}
132
133impl Condition {
134    /// Create a new condition.
135    pub fn new(key: String, operator: ConditionOperator, value: String) -> Self {
136        Self {
137            key,
138            operator,
139            value,
140        }
141    }
142
143    /// Evaluate condition against attributes.
144    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/// Condition operator.
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
180pub enum ConditionOperator {
181    /// Equals.
182    Equals,
183    /// Not equals.
184    NotEquals,
185    /// Contains substring.
186    Contains,
187    /// Starts with.
188    StartsWith,
189    /// Ends with.
190    EndsWith,
191    /// Greater than (numeric).
192    GreaterThan,
193    /// Less than (numeric).
194    LessThan,
195    /// In list.
196    In,
197}
198
199/// ABAC policy engine.
200pub struct AbacEngine {
201    policies: Arc<DashMap<String, AbacPolicy>>,
202    /// Policy evaluation cache (request_hash -> decision).
203    cache: Arc<DashMap<u64, AccessDecision>>,
204}
205
206impl AbacEngine {
207    /// Create a new ABAC engine.
208    pub fn new() -> Self {
209        Self {
210            policies: Arc::new(DashMap::new()),
211            cache: Arc::new(DashMap::new()),
212        }
213    }
214
215    /// Add a policy.
216    pub fn add_policy(&self, policy: AbacPolicy) -> Result<()> {
217        self.policies.insert(policy.id.clone(), policy);
218        self.cache.clear(); // Clear cache when policies change
219        Ok(())
220    }
221
222    /// Remove a policy.
223    pub fn remove_policy(&self, policy_id: &str) -> Result<()> {
224        self.policies.remove(policy_id);
225        self.cache.clear();
226        Ok(())
227    }
228
229    /// Get a policy by ID.
230    pub fn get_policy(&self, policy_id: &str) -> Option<AbacPolicy> {
231        self.policies.get(policy_id).map(|p| p.clone())
232    }
233
234    /// List all policies.
235    pub fn list_policies(&self) -> Vec<AbacPolicy> {
236        let mut policies: Vec<_> = self.policies.iter().map(|p| p.clone()).collect();
237        // Sort by priority (descending)
238        policies.sort_by_key(|x| std::cmp::Reverse(x.priority));
239        policies
240    }
241
242    /// Clear all policies.
243    pub fn clear_policies(&self) {
244        self.policies.clear();
245        self.cache.clear();
246    }
247
248    /// Clear evaluation cache.
249    pub fn clear_cache(&self) {
250        self.cache.clear();
251    }
252
253    /// Get cache statistics.
254    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        // Check cache first
279        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        // Evaluate policies in priority order
285        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; // Deny takes precedence
296                    }
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 // Default deny
307        };
308
309        // Cache the decision
310        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}