Skip to main content

oxigdal_security/access_control/
policies.rs

1//! Policy engine for combining RBAC and ABAC.
2
3use crate::access_control::{
4    AccessControlEvaluator, AccessDecision, AccessRequest, abac::AbacEngine, rbac::RbacEngine,
5};
6use crate::error::Result;
7use serde::{Deserialize, Serialize};
8use std::sync::Arc;
9
10/// Policy enforcement mode.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12pub enum EnforcementMode {
13    /// Only use RBAC.
14    RbacOnly,
15    /// Only use ABAC.
16    AbacOnly,
17    /// Both must allow (AND).
18    Both,
19    /// Either can allow (OR).
20    Either,
21    /// ABAC first, fallback to RBAC.
22    AbacThenRbac,
23    /// RBAC first, fallback to ABAC.
24    RbacThenAbac,
25}
26
27/// Combined policy engine with RBAC and ABAC.
28pub struct PolicyEngine {
29    rbac: Arc<RbacEngine>,
30    abac: Arc<AbacEngine>,
31    enforcement_mode: parking_lot::RwLock<EnforcementMode>,
32}
33
34impl PolicyEngine {
35    /// Create a new policy engine.
36    pub fn new(rbac: Arc<RbacEngine>, abac: Arc<AbacEngine>) -> Self {
37        Self {
38            rbac,
39            abac,
40            enforcement_mode: parking_lot::RwLock::new(EnforcementMode::Either),
41        }
42    }
43
44    /// Set enforcement mode.
45    pub fn set_enforcement_mode(&self, mode: EnforcementMode) {
46        *self.enforcement_mode.write() = mode;
47    }
48
49    /// Get enforcement mode.
50    pub fn get_enforcement_mode(&self) -> EnforcementMode {
51        *self.enforcement_mode.read()
52    }
53
54    /// Get RBAC engine.
55    pub fn rbac(&self) -> &Arc<RbacEngine> {
56        &self.rbac
57    }
58
59    /// Get ABAC engine.
60    pub fn abac(&self) -> &Arc<AbacEngine> {
61        &self.abac
62    }
63}
64
65impl AccessControlEvaluator for PolicyEngine {
66    fn evaluate(&self, request: &AccessRequest) -> Result<AccessDecision> {
67        let mode = self.get_enforcement_mode();
68
69        match mode {
70            EnforcementMode::RbacOnly => self.rbac.evaluate(request),
71            EnforcementMode::AbacOnly => self.abac.evaluate(request),
72            EnforcementMode::Both => {
73                let rbac_decision = self.rbac.evaluate(request)?;
74                let abac_decision = self.abac.evaluate(request)?;
75
76                if rbac_decision == AccessDecision::Allow && abac_decision == AccessDecision::Allow
77                {
78                    Ok(AccessDecision::Allow)
79                } else {
80                    Ok(AccessDecision::Deny)
81                }
82            }
83            EnforcementMode::Either => {
84                let rbac_decision = self.rbac.evaluate(request)?;
85                let abac_decision = self.abac.evaluate(request)?;
86
87                if rbac_decision == AccessDecision::Allow || abac_decision == AccessDecision::Allow
88                {
89                    Ok(AccessDecision::Allow)
90                } else {
91                    Ok(AccessDecision::Deny)
92                }
93            }
94            EnforcementMode::AbacThenRbac => {
95                let abac_decision = self.abac.evaluate(request)?;
96                if abac_decision == AccessDecision::Allow {
97                    Ok(AccessDecision::Allow)
98                } else {
99                    self.rbac.evaluate(request)
100                }
101            }
102            EnforcementMode::RbacThenAbac => {
103                let rbac_decision = self.rbac.evaluate(request)?;
104                if rbac_decision == AccessDecision::Allow {
105                    Ok(AccessDecision::Allow)
106                } else {
107                    self.abac.evaluate(request)
108                }
109            }
110        }
111    }
112}
113
114/// Spatial access control for region-based restrictions.
115pub struct SpatialAccessControl {
116    /// Region boundaries (region_id -> (min_lon, min_lat, max_lon, max_lat)).
117    regions: dashmap::DashMap<String, (f64, f64, f64, f64)>,
118    /// Subject to allowed regions.
119    subject_regions: dashmap::DashMap<String, Vec<String>>,
120}
121
122impl SpatialAccessControl {
123    /// Create a new spatial access control.
124    pub fn new() -> Self {
125        Self {
126            regions: dashmap::DashMap::new(),
127            subject_regions: dashmap::DashMap::new(),
128        }
129    }
130
131    /// Define a region boundary.
132    pub fn define_region(
133        &self,
134        region_id: String,
135        min_lon: f64,
136        min_lat: f64,
137        max_lon: f64,
138        max_lat: f64,
139    ) -> Result<()> {
140        self.regions
141            .insert(region_id, (min_lon, min_lat, max_lon, max_lat));
142        Ok(())
143    }
144
145    /// Grant subject access to a region.
146    pub fn grant_region_access(&self, subject_id: &str, region_id: String) -> Result<()> {
147        self.subject_regions
148            .entry(subject_id.to_string())
149            .or_default()
150            .push(region_id);
151        Ok(())
152    }
153
154    /// Check if subject can access a point.
155    pub fn can_access_point(&self, subject_id: &str, lon: f64, lat: f64) -> bool {
156        if let Some(regions) = self.subject_regions.get(subject_id) {
157            for region_id in regions.iter() {
158                if let Some(bounds) = self.regions.get(region_id) {
159                    let (min_lon, min_lat, max_lon, max_lat) = *bounds;
160                    if lon >= min_lon && lon <= max_lon && lat >= min_lat && lat <= max_lat {
161                        return true;
162                    }
163                }
164            }
165        }
166        false
167    }
168
169    /// Check if subject can access a bounding box.
170    pub fn can_access_bbox(
171        &self,
172        subject_id: &str,
173        min_lon: f64,
174        min_lat: f64,
175        max_lon: f64,
176        max_lat: f64,
177    ) -> bool {
178        if let Some(regions) = self.subject_regions.get(subject_id) {
179            for region_id in regions.iter() {
180                if let Some(bounds) = self.regions.get(region_id) {
181                    let (r_min_lon, r_min_lat, r_max_lon, r_max_lat) = *bounds;
182                    // Check if bbox is entirely within region
183                    if min_lon >= r_min_lon
184                        && max_lon <= r_max_lon
185                        && min_lat >= r_min_lat
186                        && max_lat <= r_max_lat
187                    {
188                        return true;
189                    }
190                }
191            }
192        }
193        false
194    }
195}
196
197impl Default for SpatialAccessControl {
198    fn default() -> Self {
199        Self::new()
200    }
201}
202
203/// Temporal access control for time-based restrictions.
204pub struct TemporalAccessControl {
205    /// Subject to time windows (start, end).
206    time_windows: dashmap::DashMap<String, Vec<(chrono::NaiveTime, chrono::NaiveTime)>>,
207    /// Subject to date ranges.
208    date_ranges: dashmap::DashMap<String, Vec<(chrono::NaiveDate, Option<chrono::NaiveDate>)>>,
209}
210
211impl TemporalAccessControl {
212    /// Create a new temporal access control.
213    pub fn new() -> Self {
214        Self {
215            time_windows: dashmap::DashMap::new(),
216            date_ranges: dashmap::DashMap::new(),
217        }
218    }
219
220    /// Set allowed time window for subject (e.g., 9:00-17:00).
221    pub fn set_time_window(
222        &self,
223        subject_id: String,
224        start: chrono::NaiveTime,
225        end: chrono::NaiveTime,
226    ) {
227        self.time_windows
228            .entry(subject_id)
229            .or_default()
230            .push((start, end));
231    }
232
233    /// Set allowed date range for subject.
234    pub fn set_date_range(
235        &self,
236        subject_id: String,
237        start: chrono::NaiveDate,
238        end: Option<chrono::NaiveDate>,
239    ) {
240        self.date_ranges
241            .entry(subject_id)
242            .or_default()
243            .push((start, end));
244    }
245
246    /// Check if subject can access at current time.
247    pub fn can_access_now(&self, subject_id: &str) -> bool {
248        let now = chrono::Utc::now();
249        let current_time = now.time();
250        let current_date = now.date_naive();
251
252        // Check time windows
253        if let Some(windows) = self.time_windows.get(subject_id) {
254            let mut time_allowed = false;
255            for (start, end) in windows.iter() {
256                if current_time >= *start && current_time <= *end {
257                    time_allowed = true;
258                    break;
259                }
260            }
261            if !time_allowed && !windows.is_empty() {
262                return false;
263            }
264        }
265
266        // Check date ranges
267        if let Some(ranges) = self.date_ranges.get(subject_id) {
268            let mut date_allowed = false;
269            for (start, end) in ranges.iter() {
270                if current_date >= *start {
271                    if let Some(end_date) = end {
272                        if current_date <= *end_date {
273                            date_allowed = true;
274                            break;
275                        }
276                    } else {
277                        date_allowed = true;
278                        break;
279                    }
280                }
281            }
282            if !date_allowed && !ranges.is_empty() {
283                return false;
284            }
285        }
286
287        true
288    }
289}
290
291impl Default for TemporalAccessControl {
292    fn default() -> Self {
293        Self::new()
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300
301    #[test]
302    fn test_policy_engine_either_mode() {
303        let rbac = Arc::new(RbacEngine::new());
304        let abac = Arc::new(AbacEngine::new());
305        let engine = PolicyEngine::new(rbac, abac);
306
307        engine.set_enforcement_mode(EnforcementMode::Either);
308        assert_eq!(engine.get_enforcement_mode(), EnforcementMode::Either);
309    }
310
311    #[test]
312    fn test_spatial_access_control() {
313        let sac = SpatialAccessControl::new();
314
315        // Define US region
316        sac.define_region(
317            "us".to_string(),
318            -125.0, // min_lon
319            24.0,   // min_lat
320            -66.0,  // max_lon
321            49.0,   // max_lat
322        )
323        .expect("Failed to define region");
324
325        sac.grant_region_access("user-123", "us".to_string())
326            .expect("Failed to grant access");
327
328        // Point in US
329        assert!(sac.can_access_point("user-123", -100.0, 40.0));
330
331        // Point outside US
332        assert!(!sac.can_access_point("user-123", 0.0, 51.0));
333    }
334
335    #[test]
336    fn test_spatial_bbox_access() {
337        let sac = SpatialAccessControl::new();
338
339        sac.define_region("region-1".to_string(), 0.0, 0.0, 10.0, 10.0)
340            .expect("Failed to define region");
341
342        sac.grant_region_access("user-123", "region-1".to_string())
343            .expect("Failed to grant access");
344
345        // Bbox within region
346        assert!(sac.can_access_bbox("user-123", 1.0, 1.0, 9.0, 9.0));
347
348        // Bbox partially outside region
349        assert!(!sac.can_access_bbox("user-123", 5.0, 5.0, 15.0, 15.0));
350    }
351
352    #[test]
353    fn test_temporal_access_control() {
354        let tac = TemporalAccessControl::new();
355
356        // Set time window: 9:00 - 17:00
357        let start = chrono::NaiveTime::from_hms_opt(9, 0, 0).expect("Invalid time");
358        let end = chrono::NaiveTime::from_hms_opt(17, 0, 0).expect("Invalid time");
359        tac.set_time_window("user-123".to_string(), start, end);
360
361        // This test depends on current time, so we just check it doesn't panic
362        let _ = tac.can_access_now("user-123");
363    }
364}