1use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::net::IpAddr;
11
12#[derive(Debug, Clone, Default)]
14pub struct RequestContext {
15 pub client_ip: Option<IpAddr>,
17
18 pub attributes: HashMap<String, String>,
20
21 pub timestamp: chrono::DateTime<chrono::Utc>,
23}
24
25impl RequestContext {
26 pub fn new() -> Self {
28 Self {
29 client_ip: None,
30 attributes: HashMap::new(),
31 timestamp: chrono::Utc::now(),
32 }
33 }
34
35 pub fn with_client_ip(mut self, ip: IpAddr) -> Self {
37 self.client_ip = Some(ip);
38 self
39 }
40
41 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 pub fn get_attribute(&self, key: &str) -> Option<&String> {
49 self.attributes.get(key)
50 }
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
56#[serde(tag = "type", rename_all = "snake_case")]
57pub enum RelationshipCondition {
58 TimeWindow {
60 not_before: Option<chrono::DateTime<chrono::Utc>>,
61 not_after: Option<chrono::DateTime<chrono::Utc>>,
62 },
63
64 IpAddress { allowed_ips: Vec<String> },
66
67 Attribute { key: String, value: String },
69
70 All {
72 conditions: Vec<RelationshipCondition>,
73 },
74
75 Any {
77 conditions: Vec<RelationshipCondition>,
78 },
79}
80
81impl RelationshipCondition {
82 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 let client_ip_str = client_ip.to_string();
100 allowed_ips.iter().any(|ip| {
101 if ip.contains('/') {
103 client_ip_str.starts_with(ip.split('/').next().unwrap_or(""))
106 } else {
107 &client_ip_str == ip
109 }
110 })
111 } else {
112 false
114 }
115 }
116 RelationshipCondition::Attribute { key, value } => {
117 context.get_attribute(key) == Some(value)
119 }
120 RelationshipCondition::All { conditions } => {
121 conditions
123 .iter()
124 .all(|c| c.is_satisfied_with_context(context))
125 }
126 RelationshipCondition::Any { conditions } => {
127 conditions
129 .iter()
130 .any(|c| c.is_satisfied_with_context(context))
131 }
132 }
133 }
134
135 pub fn is_satisfied(&self) -> bool {
138 let context = RequestContext::new();
140 self.is_satisfied_with_context(&context)
141 }
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct NamespaceConfig {
147 pub name: String,
148 pub relations: Vec<RelationConfig>,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct RelationConfig {
154 pub name: String,
155
156 pub inherits_from: Vec<String>,
159
160 pub union: Vec<String>,
162
163 pub intersection: Vec<String>,
165}
166
167impl NamespaceConfig {
168 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct AuthzDecision {
226 pub allowed: bool,
227 pub reason: String,
228 pub depth: usize, 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}