1use serde::{Deserialize, Serialize};
45use std::fmt;
46use thiserror::Error;
47
48pub mod anomaly;
50pub mod audit;
51pub mod bloom;
52pub mod cache;
53pub mod chaos;
54pub mod delegation;
56pub mod edge;
57pub mod engine;
58#[cfg(feature = "grpc")]
59pub mod grpc;
60pub mod hybrid;
61pub mod leopard;
62pub mod memory;
63pub mod metrics;
64pub mod migration;
65pub mod multitenancy;
67pub mod oauth2;
68pub mod profiling;
71#[cfg(any(test, feature = "proptest-support"))]
72pub mod proptest_helpers;
73pub mod quantum;
74pub mod query_optimizer;
75pub mod recommendations;
76pub mod redis_cache;
77#[cfg(test)]
79pub mod test_helpers;
80pub mod types;
81pub mod warming;
82pub mod zkp;
83
84pub use anomaly::*;
85pub use audit::*;
86pub use bloom::*;
87pub use cache::*;
88pub use chaos::*;
89pub use delegation::*;
91pub use edge::*;
92pub use engine::*;
93#[cfg(feature = "grpc")]
94pub use grpc::*;
95pub use hybrid::*;
96pub use leopard::*;
97pub use memory::*;
98pub use metrics::*;
99pub use multitenancy::*;
101pub use oauth2::*;
102pub use profiling::*;
105pub use quantum::*;
106pub use query_optimizer::*;
107pub use recommendations::*;
108pub use redis_cache::*;
109pub use types::*;
111pub use warming::*;
112pub use zkp::*;
113
114pub type Result<T> = std::result::Result<T, AuthzError>;
115
116#[derive(Error, Debug)]
117pub enum AuthzError {
118 #[error("Database error: {0}")]
119 DatabaseError(String),
120
121 #[error("Invalid relation tuple: {0}")]
122 InvalidTuple(String),
123
124 #[error("Permission denied: {0}")]
125 PermissionDenied(String),
126
127 #[error("Cycle detected in relation graph")]
128 CycleDetected,
129
130 #[error("Namespace not found: {0}")]
131 NamespaceNotFound(String),
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
136pub enum Subject {
137 User(String),
139
140 UserSet {
143 namespace: String,
144 object_id: String,
145 relation: String,
146 },
147}
148
149impl Subject {
150 pub fn from_string(s: &str) -> Result<Self> {
155 if let Some(userset) = s.strip_prefix("userset:") {
156 let parts: Vec<&str> = userset.split(&[':', '#'][..]).collect();
157 if parts.len() == 3 {
158 Ok(Subject::UserSet {
159 namespace: parts[0].to_string(),
160 object_id: parts[1].to_string(),
161 relation: parts[2].to_string(),
162 })
163 } else {
164 Err(AuthzError::InvalidTuple(format!(
165 "Invalid userset format: {}",
166 s
167 )))
168 }
169 } else if let Some(user_id) = s.strip_prefix("user:") {
170 Ok(Subject::User(user_id.to_string()))
171 } else {
172 Err(AuthzError::InvalidTuple(format!(
173 "Invalid subject format: {}",
174 s
175 )))
176 }
177 }
178}
179
180impl fmt::Display for Subject {
181 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182 match self {
183 Subject::User(id) => write!(f, "user:{}", id),
184 Subject::UserSet {
185 namespace,
186 object_id,
187 relation,
188 } => write!(f, "userset:{}:{}#{}", namespace, object_id, relation),
189 }
190 }
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
197pub struct RelationTuple {
198 pub namespace: String,
199 pub object_id: String,
200 pub relation: String,
201 pub subject: Subject,
202
203 #[serde(skip_serializing_if = "Option::is_none")]
206 pub condition: Option<RelationshipCondition>,
207}
208
209impl RelationTuple {
210 pub fn new(
212 namespace: impl Into<String>,
213 relation: impl Into<String>,
214 object_id: impl Into<String>,
215 subject: Subject,
216 ) -> Self {
217 Self {
218 namespace: namespace.into(),
219 object_id: object_id.into(),
220 relation: relation.into(),
221 subject,
222 condition: None,
223 }
224 }
225
226 pub fn with_condition(
228 namespace: impl Into<String>,
229 object_id: impl Into<String>,
230 relation: impl Into<String>,
231 subject: Subject,
232 condition: RelationshipCondition,
233 ) -> Self {
234 Self {
235 namespace: namespace.into(),
236 object_id: object_id.into(),
237 relation: relation.into(),
238 subject,
239 condition: Some(condition),
240 }
241 }
242
243 pub fn is_condition_satisfied(&self) -> bool {
245 self.condition.as_ref().is_none_or(|c| c.is_satisfied())
246 }
247
248 pub fn is_condition_satisfied_with_context(&self, context: &RequestContext) -> bool {
250 self.condition
251 .as_ref()
252 .is_none_or(|c| c.is_satisfied_with_context(context))
253 }
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct CheckRequest {
259 pub namespace: String,
260 pub object_id: String,
261 pub relation: String,
262 pub subject: Subject,
263 #[serde(skip)]
265 pub context: Option<RequestContext>,
266}
267
268impl CheckRequest {
269 pub fn new(
271 namespace: impl Into<String>,
272 object_id: impl Into<String>,
273 relation: impl Into<String>,
274 subject: Subject,
275 ) -> Self {
276 Self {
277 namespace: namespace.into(),
278 object_id: object_id.into(),
279 relation: relation.into(),
280 subject,
281 context: None,
282 }
283 }
284
285 pub fn with_context(mut self, context: RequestContext) -> Self {
287 self.context = Some(context);
288 self
289 }
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct CheckResponse {
295 pub allowed: bool,
296 pub cached: bool,
297}
298
299#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct ExpandRequest {
302 pub namespace: String,
303 pub object_id: String,
304 pub relation: String,
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct ExpandResponse {
310 pub subjects: Vec<Subject>,
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316
317 #[test]
318 fn test_subject_parsing() {
319 let user = Subject::from_string("user:alice").unwrap();
320 assert_eq!(user, Subject::User("alice".to_string()));
321
322 let userset = Subject::from_string("userset:team:A#members").unwrap();
323 assert_eq!(
324 userset,
325 Subject::UserSet {
326 namespace: "team".to_string(),
327 object_id: "A".to_string(),
328 relation: "members".to_string(),
329 }
330 );
331 }
332
333 #[test]
334 fn test_subject_serialization() {
335 let user = Subject::User("bob".to_string());
336 assert_eq!(user.to_string(), "user:bob");
337
338 let userset = Subject::UserSet {
339 namespace: "group".to_string(),
340 object_id: "admins".to_string(),
341 relation: "member".to_string(),
342 };
343 assert_eq!(userset.to_string(), "userset:group:admins#member");
344 }
345}