oxify_authz/
lib.rs

1//! # OxiFY Authorization Engine (ReBAC)
2//!
3//! Google Zanzibar-style Relationship-Based Access Control (ReBAC) implementation.
4//!
5//! ## Architecture
6//!
7//! This crate provides fine-grained authorization based on relationships between
8//! entities rather than traditional role-based access control (RBAC).
9//!
10//! **Key Concepts:**
11//! - **Relation Tuples**: `(namespace, object_id, relation, subject)` representing relationships
12//! - **Check API**: Determines if a subject can perform an action on an object
13//! - **Expand API**: Returns all subjects with a specific relation to an object
14//! - **Reachability Index**: Leopard indexing for O(1) authorization checks
15//!
16//! ## Example
17//!
18//! ```no_run
19//! use oxify_authz::*;
20//!
21//! # async fn example() -> Result<()> {
22//! let engine = AuthzEngine::new("postgres://localhost/db").await?;
23//!
24//! // Define: User alice is an owner of document:123
25//! engine.write_tuple(RelationTuple::new(
26//!     "document",
27//!     "owner",
28//!     "123",
29//!     Subject::User("alice".to_string()),
30//! )).await?;
31//!
32//! // Check: Can alice view document:123?
33//! let allowed = engine.check(CheckRequest {
34//!     namespace: "document".to_string(),
35//!     object_id: "123".to_string(),
36//!     relation: "viewer".to_string(),
37//!     subject: Subject::User("alice".to_string()),
38//!     context: None,
39//! }).await?;
40//! # Ok(())
41//! # }
42//! ```
43
44use serde::{Deserialize, Serialize};
45use std::fmt;
46use thiserror::Error;
47
48// Core modules (SQLite compatible)
49pub mod anomaly;
50pub mod audit;
51pub mod bloom;
52pub mod cache;
53pub mod chaos;
54// pub mod citus;        // Disabled - PostgreSQL Citus specific
55pub 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;
65// pub mod multiregion;  // Disabled - PostgreSQL replicas
66pub mod multitenancy;
67pub mod oauth2;
68// pub mod partitioning; // Disabled - PostgreSQL partitioning
69// pub mod pooling;      // Disabled - PgPoolOptions
70pub 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// pub mod replica;      // Disabled - PostgreSQL read replicas
78#[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::*;
89// pub use citus::*;
90pub 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::*;
99// pub use multiregion::*;
100pub use multitenancy::*;
101pub use oauth2::*;
102// pub use partitioning::*;
103// pub use pooling::*;
104pub use profiling::*;
105pub use quantum::*;
106pub use query_optimizer::*;
107pub use recommendations::*;
108pub use redis_cache::*;
109// pub use replica::*;
110pub 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/// Subject in a relation tuple
135#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
136pub enum Subject {
137    /// A user identified by ID
138    User(String),
139
140    /// A user set (e.g., "team:A#members")
141    /// Format: "namespace:object_id#relation"
142    UserSet {
143        namespace: String,
144        object_id: String,
145        relation: String,
146    },
147}
148
149impl Subject {
150    /// Parse subject from string format
151    /// Examples:
152    /// - "user:alice" → User("alice")
153    /// - "team:A#members" → UserSet { namespace: "team", object_id: "A", relation: "members" }
154    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/// Relation tuple representing a relationship
194/// Example: (document, doc123, owner, user:alice) means "alice owns doc123"
195/// Extended with optional conditions from OxiRS
196#[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    /// Optional condition (e.g., time window, IP address)
204    /// Ported from OxiRS rebac.rs
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub condition: Option<RelationshipCondition>,
207}
208
209impl RelationTuple {
210    /// Create a new relation tuple without conditions
211    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    /// Create a relation tuple with a condition
227    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    /// Check if this tuple's condition is satisfied (without context)
244    pub fn is_condition_satisfied(&self) -> bool {
245        self.condition.as_ref().is_none_or(|c| c.is_satisfied())
246    }
247
248    /// Check if this tuple's condition is satisfied with a request context
249    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/// Request to check if a subject has a relation to an object
257#[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    /// Optional request context for conditional permissions
264    #[serde(skip)]
265    pub context: Option<RequestContext>,
266}
267
268impl CheckRequest {
269    /// Create a new check request
270    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    /// Add request context for conditional permissions
286    pub fn with_context(mut self, context: RequestContext) -> Self {
287        self.context = Some(context);
288        self
289    }
290}
291
292/// Response from a check request
293#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct CheckResponse {
295    pub allowed: bool,
296    pub cached: bool,
297}
298
299/// Request to expand a relation (find all subjects)
300#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct ExpandRequest {
302    pub namespace: String,
303    pub object_id: String,
304    pub relation: String,
305}
306
307/// Response from an expand request
308#[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}