oxify_authz/
types.rs

1//! Core types for the authorization system
2//!
3//! Ported from OxiRS (<https://github.com/cool-japan/oxirs>)
4//! Original implementation: Copyright (c) OxiRS Contributors
5//! Adapted for OxiFY
6//! License: MIT OR Apache-2.0 (compatible with OxiRS)
7
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::net::IpAddr;
11
12/// Request context for conditional permission evaluation
13#[derive(Debug, Clone, Default)]
14pub struct RequestContext {
15    /// Client IP address
16    pub client_ip: Option<IpAddr>,
17
18    /// Custom attributes (e.g., user role, device type, location)
19    pub attributes: HashMap<String, String>,
20
21    /// Timestamp of the request (defaults to now)
22    pub timestamp: chrono::DateTime<chrono::Utc>,
23}
24
25impl RequestContext {
26    /// Create a new request context with current timestamp
27    pub fn new() -> Self {
28        Self {
29            client_ip: None,
30            attributes: HashMap::new(),
31            timestamp: chrono::Utc::now(),
32        }
33    }
34
35    /// Set the client IP address
36    pub fn with_client_ip(mut self, ip: IpAddr) -> Self {
37        self.client_ip = Some(ip);
38        self
39    }
40
41    /// Add a custom attribute
42    pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
43        self.attributes.insert(key.into(), value.into());
44        self
45    }
46
47    /// Get an attribute value
48    pub fn get_attribute(&self, key: &str) -> Option<&String> {
49        self.attributes.get(key)
50    }
51}
52
53/// Conditions that can be attached to relationships
54/// Ported from OxiRS rebac.rs
55#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
56#[serde(tag = "type", rename_all = "snake_case")]
57pub enum RelationshipCondition {
58    /// Time-based condition
59    TimeWindow {
60        not_before: Option<chrono::DateTime<chrono::Utc>>,
61        not_after: Option<chrono::DateTime<chrono::Utc>>,
62    },
63
64    /// IP address condition (CIDR notation supported)
65    IpAddress { allowed_ips: Vec<String> },
66
67    /// Custom attribute-based condition
68    Attribute { key: String, value: String },
69
70    /// Combined condition (ALL must be satisfied)
71    All {
72        conditions: Vec<RelationshipCondition>,
73    },
74
75    /// Combined condition (ANY can be satisfied)
76    Any {
77        conditions: Vec<RelationshipCondition>,
78    },
79}
80
81impl RelationshipCondition {
82    /// Check if this condition is satisfied with a request context
83    pub fn is_satisfied_with_context(&self, context: &RequestContext) -> bool {
84        match self {
85            RelationshipCondition::TimeWindow {
86                not_before,
87                not_after,
88            } => {
89                let now = context.timestamp;
90                let after_start = not_before.is_none_or(|start| now >= start);
91                let before_end = not_after.is_none_or(|end| now <= end);
92                after_start && before_end
93            }
94            RelationshipCondition::IpAddress { allowed_ips } => {
95                if let Some(client_ip) = context.client_ip {
96                    // Check if client IP matches any allowed IP
97                    // For simplicity, we do exact string matching
98                    // In production, you'd want CIDR matching
99                    let client_ip_str = client_ip.to_string();
100                    allowed_ips.iter().any(|ip| {
101                        // Support exact match or CIDR (basic)
102                        if ip.contains('/') {
103                            // CIDR notation - for now, just check prefix
104                            // Full CIDR matching would require ipnetwork crate
105                            client_ip_str.starts_with(ip.split('/').next().unwrap_or(""))
106                        } else {
107                            // Exact match
108                            &client_ip_str == ip
109                        }
110                    })
111                } else {
112                    // No client IP provided - deny by default for security
113                    false
114                }
115            }
116            RelationshipCondition::Attribute { key, value } => {
117                // Check if the request context has the required attribute with the expected value
118                context.get_attribute(key) == Some(value)
119            }
120            RelationshipCondition::All { conditions } => {
121                // All conditions must be satisfied
122                conditions
123                    .iter()
124                    .all(|c| c.is_satisfied_with_context(context))
125            }
126            RelationshipCondition::Any { conditions } => {
127                // At least one condition must be satisfied
128                conditions
129                    .iter()
130                    .any(|c| c.is_satisfied_with_context(context))
131            }
132        }
133    }
134
135    /// Check if this condition is satisfied (without context)
136    /// Deprecated: Use is_satisfied_with_context instead
137    pub fn is_satisfied(&self) -> bool {
138        // Create a default context and check
139        let context = RequestContext::new();
140        self.is_satisfied_with_context(&context)
141    }
142}
143
144/// Namespace configuration defining relations and their permissions
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct NamespaceConfig {
147    pub name: String,
148    pub relations: Vec<RelationConfig>,
149}
150
151/// Relation configuration with computed/inherited permissions
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct RelationConfig {
154    pub name: String,
155
156    /// Relations that inherit this relation
157    /// Example: "viewer" inherits from "owner" (owners can also view)
158    pub inherits_from: Vec<String>,
159
160    /// Union of relations (subject has ANY of these relations)
161    pub union: Vec<String>,
162
163    /// Intersection of relations (subject has ALL of these relations)
164    pub intersection: Vec<String>,
165}
166
167impl NamespaceConfig {
168    /// Create a document namespace with standard permissions
169    pub fn document_namespace() -> Self {
170        NamespaceConfig {
171            name: "document".to_string(),
172            relations: vec![
173                RelationConfig {
174                    name: "owner".to_string(),
175                    inherits_from: vec![],
176                    union: vec![],
177                    intersection: vec![],
178                },
179                RelationConfig {
180                    name: "editor".to_string(),
181                    inherits_from: vec!["owner".to_string()],
182                    union: vec![],
183                    intersection: vec![],
184                },
185                RelationConfig {
186                    name: "viewer".to_string(),
187                    inherits_from: vec!["owner".to_string(), "editor".to_string()],
188                    union: vec![],
189                    intersection: vec![],
190                },
191            ],
192        }
193    }
194
195    /// Create a folder namespace with hierarchical permissions
196    pub fn folder_namespace() -> Self {
197        NamespaceConfig {
198            name: "folder".to_string(),
199            relations: vec![
200                RelationConfig {
201                    name: "parent".to_string(),
202                    inherits_from: vec![],
203                    union: vec![],
204                    intersection: vec![],
205                },
206                RelationConfig {
207                    name: "owner".to_string(),
208                    inherits_from: vec![],
209                    union: vec![],
210                    intersection: vec![],
211                },
212                RelationConfig {
213                    name: "viewer".to_string(),
214                    inherits_from: vec!["owner".to_string()],
215                    union: vec![],
216                    intersection: vec![],
217                },
218            ],
219        }
220    }
221}
222
223/// Authorization decision with audit trail
224#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct AuthzDecision {
226    pub allowed: bool,
227    pub reason: String,
228    pub depth: usize, // How many hops in the relation graph
229    pub cached: bool,
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn test_namespace_configs() {
238        let doc_ns = NamespaceConfig::document_namespace();
239        assert_eq!(doc_ns.name, "document");
240        assert_eq!(doc_ns.relations.len(), 3);
241
242        let viewer_relation = doc_ns
243            .relations
244            .iter()
245            .find(|r| r.name == "viewer")
246            .unwrap();
247        assert_eq!(viewer_relation.inherits_from.len(), 2);
248    }
249}