oxify_authz/
multitenancy.rs

1//! Multi-Tenancy Support for Authorization
2//!
3//! Provides tenant isolation, cross-tenant sharing, and per-tenant quotas.
4//!
5//! ## Features
6//!
7//! - **Tenant Isolation**: Logical partitioning by tenant_id
8//! - **Cross-Tenant Sharing**: Explicit permission grants across tenants
9//! - **Per-Tenant Quotas**: Resource limits and rate limiting
10//! - **Audit Logging**: Track all cross-tenant access
11//!
12//! ## Example
13//!
14//! ```rust
15//! use oxify_authz::multitenancy::{TenantContext, MultiTenantEngine, TenantQuota};
16//!
17//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
18//! let tenant_ctx = TenantContext::new("tenant-123");
19//! let engine = MultiTenantEngine::new();
20//!
21//! // Set tenant quota
22//! let quota = TenantQuota::new("tenant-123").with_max_tuples(1000);
23//! engine.set_quota(quota).await?;
24//!
25//! // Check quota before operations
26//! let can_create = engine.check_tuple_quota("tenant-123").await?;
27//! assert!(can_create);
28//!
29//! // Increment usage
30//! engine.increment_tuple_count("tenant-123").await?;
31//! # Ok(())
32//! # }
33//! ```
34
35use crate::{AuthzError, RelationTuple, Result, Subject};
36use serde::{Deserialize, Serialize};
37use std::collections::HashMap;
38use std::sync::Arc;
39use tokio::sync::RwLock;
40
41/// Resource identifier
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
43pub struct Resource {
44    pub namespace: String,
45    pub object_id: String,
46}
47
48/// Tenant context for multi-tenancy
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
50pub struct TenantContext {
51    /// Unique tenant identifier
52    pub tenant_id: String,
53
54    /// Optional organization name for display
55    pub organization: Option<String>,
56
57    /// Tenant-specific metadata
58    pub metadata: HashMap<String, String>,
59}
60
61impl TenantContext {
62    pub fn new(tenant_id: impl Into<String>) -> Self {
63        Self {
64            tenant_id: tenant_id.into(),
65            organization: None,
66            metadata: HashMap::new(),
67        }
68    }
69
70    pub fn with_organization(mut self, org: impl Into<String>) -> Self {
71        self.organization = Some(org.into());
72        self
73    }
74
75    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
76        self.metadata.insert(key.into(), value.into());
77        self
78    }
79}
80
81/// Tenant-specific relation tuple
82#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
83pub struct TenantRelationTuple {
84    /// Tenant identifier
85    pub tenant_id: String,
86
87    /// Standard relation tuple
88    pub tuple: RelationTuple,
89
90    /// Cross-tenant flag (if true, accessible across tenants)
91    pub cross_tenant: bool,
92}
93
94impl TenantRelationTuple {
95    pub fn new(tenant_id: String, tuple: RelationTuple) -> Self {
96        Self {
97            tenant_id,
98            tuple,
99            cross_tenant: false,
100        }
101    }
102
103    pub fn with_cross_tenant(mut self, cross_tenant: bool) -> Self {
104        self.cross_tenant = cross_tenant;
105        self
106    }
107}
108
109/// Tenant quotas and limits
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct TenantQuota {
112    /// Tenant identifier
113    pub tenant_id: String,
114
115    /// Maximum number of relation tuples
116    pub max_tuples: usize,
117
118    /// Maximum permission checks per minute
119    pub max_checks_per_minute: usize,
120
121    /// Maximum API requests per minute
122    pub max_api_requests_per_minute: usize,
123
124    /// Current usage
125    pub current_tuples: usize,
126
127    /// Current checks in current window
128    pub current_checks: usize,
129
130    /// Current API requests in current window
131    pub current_api_requests: usize,
132}
133
134impl TenantQuota {
135    pub fn new(tenant_id: impl Into<String>) -> Self {
136        Self {
137            tenant_id: tenant_id.into(),
138            max_tuples: 100_000,                // Default: 100k tuples
139            max_checks_per_minute: 10_000,      // Default: 10k checks/min
140            max_api_requests_per_minute: 1_000, // Default: 1k API req/min
141            current_tuples: 0,
142            current_checks: 0,
143            current_api_requests: 0,
144        }
145    }
146
147    pub fn with_max_tuples(mut self, max: usize) -> Self {
148        self.max_tuples = max;
149        self
150    }
151
152    pub fn with_max_checks_per_minute(mut self, max: usize) -> Self {
153        self.max_checks_per_minute = max;
154        self
155    }
156
157    /// Check if tuple creation is allowed
158    pub fn can_create_tuple(&self) -> bool {
159        self.current_tuples < self.max_tuples
160    }
161
162    /// Check if permission check is allowed
163    pub fn can_check_permission(&self) -> bool {
164        self.current_checks < self.max_checks_per_minute
165    }
166
167    /// Check if API request is allowed
168    pub fn can_make_api_request(&self) -> bool {
169        self.current_api_requests < self.max_api_requests_per_minute
170    }
171
172    /// Increment tuple count
173    pub fn increment_tuples(&mut self) {
174        self.current_tuples += 1;
175    }
176
177    /// Decrement tuple count
178    pub fn decrement_tuples(&mut self) {
179        if self.current_tuples > 0 {
180            self.current_tuples -= 1;
181        }
182    }
183
184    /// Increment check count
185    pub fn increment_checks(&mut self) {
186        self.current_checks += 1;
187    }
188
189    /// Increment API request count
190    pub fn increment_api_requests(&mut self) {
191        self.current_api_requests += 1;
192    }
193
194    /// Reset rate limit counters (called every minute)
195    pub fn reset_rate_limits(&mut self) {
196        self.current_checks = 0;
197        self.current_api_requests = 0;
198    }
199}
200
201/// Multi-tenant authorization engine
202#[allow(dead_code)]
203pub struct MultiTenantEngine {
204    /// Tenant quotas (tenant_id -> quota)
205    quotas: Arc<RwLock<HashMap<String, TenantQuota>>>,
206
207    /// Cross-tenant permissions (for audit)
208    cross_tenant_log: Arc<RwLock<Vec<CrossTenantAccess>>>,
209}
210
211impl MultiTenantEngine {
212    pub fn new() -> Self {
213        Self {
214            quotas: Arc::new(RwLock::new(HashMap::new())),
215            cross_tenant_log: Arc::new(RwLock::new(Vec::new())),
216        }
217    }
218
219    /// Create or update tenant quota
220    pub async fn set_quota(&self, quota: TenantQuota) -> Result<()> {
221        let mut quotas = self.quotas.write().await;
222        quotas.insert(quota.tenant_id.clone(), quota);
223        Ok(())
224    }
225
226    /// Get tenant quota
227    pub async fn get_quota(&self, tenant_id: &str) -> Result<Option<TenantQuota>> {
228        let quotas = self.quotas.read().await;
229        Ok(quotas.get(tenant_id).cloned())
230    }
231
232    /// Check if tenant can create a tuple
233    pub async fn check_tuple_quota(&self, tenant_id: &str) -> Result<bool> {
234        let quotas = self.quotas.read().await;
235        if let Some(quota) = quotas.get(tenant_id) {
236            Ok(quota.can_create_tuple())
237        } else {
238            // No quota set = allow
239            Ok(true)
240        }
241    }
242
243    /// Check if tenant can perform permission check
244    pub async fn check_permission_quota(&self, tenant_id: &str) -> Result<bool> {
245        let mut quotas = self.quotas.write().await;
246        if let Some(quota) = quotas.get_mut(tenant_id) {
247            if quota.can_check_permission() {
248                quota.increment_checks();
249                Ok(true)
250            } else {
251                Err(AuthzError::PermissionDenied(format!(
252                    "Tenant {} exceeded permission check quota",
253                    tenant_id
254                )))
255            }
256        } else {
257            // No quota set = allow
258            Ok(true)
259        }
260    }
261
262    /// Increment tuple count for tenant
263    pub async fn increment_tuple_count(&self, tenant_id: &str) -> Result<()> {
264        let mut quotas = self.quotas.write().await;
265        if let Some(quota) = quotas.get_mut(tenant_id) {
266            quota.increment_tuples();
267        }
268        Ok(())
269    }
270
271    /// Decrement tuple count for tenant
272    pub async fn decrement_tuple_count(&self, tenant_id: &str) -> Result<()> {
273        let mut quotas = self.quotas.write().await;
274        if let Some(quota) = quotas.get_mut(tenant_id) {
275            quota.decrement_tuples();
276        }
277        Ok(())
278    }
279
280    /// Log cross-tenant access
281    pub async fn log_cross_tenant_access(&self, access: CrossTenantAccess) -> Result<()> {
282        let mut log = self.cross_tenant_log.write().await;
283        log.push(access);
284        Ok(())
285    }
286
287    /// Get cross-tenant access log
288    pub async fn get_cross_tenant_log(&self) -> Result<Vec<CrossTenantAccess>> {
289        let log = self.cross_tenant_log.read().await;
290        Ok(log.clone())
291    }
292
293    /// Reset rate limits for all tenants (called every minute)
294    pub async fn reset_all_rate_limits(&self) -> Result<()> {
295        let mut quotas = self.quotas.write().await;
296        for quota in quotas.values_mut() {
297            quota.reset_rate_limits();
298        }
299        Ok(())
300    }
301}
302
303impl Default for MultiTenantEngine {
304    fn default() -> Self {
305        Self::new()
306    }
307}
308
309/// Cross-tenant access record for auditing
310#[derive(Debug, Clone, Serialize, Deserialize)]
311pub struct CrossTenantAccess {
312    /// Source tenant ID
313    pub source_tenant: String,
314
315    /// Target tenant ID
316    pub target_tenant: String,
317
318    /// Resource accessed
319    pub resource: Resource,
320
321    /// Relation checked
322    pub relation: String,
323
324    /// Subject performing access
325    pub subject: Subject,
326
327    /// Access granted or denied
328    pub granted: bool,
329
330    /// Timestamp (Unix timestamp)
331    pub timestamp: i64,
332}
333
334impl CrossTenantAccess {
335    pub fn new(
336        source_tenant: String,
337        target_tenant: String,
338        resource: Resource,
339        relation: String,
340        subject: Subject,
341        granted: bool,
342    ) -> Self {
343        Self {
344            source_tenant,
345            target_tenant,
346            resource,
347            relation,
348            subject,
349            granted,
350            timestamp: chrono::Utc::now().timestamp(),
351        }
352    }
353}
354
355/// Tenant-aware resource
356#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
357pub struct TenantResource {
358    pub tenant_id: String,
359    pub resource: Resource,
360}
361
362impl TenantResource {
363    pub fn new(tenant_id: impl Into<String>, resource: Resource) -> Self {
364        Self {
365            tenant_id: tenant_id.into(),
366            resource,
367        }
368    }
369
370    /// Check if this resource is accessible across tenants
371    pub fn is_cross_tenant(&self) -> bool {
372        // In a real implementation, check database or cache
373        false
374    }
375}
376
377#[cfg(test)]
378mod tests {
379    use super::*;
380
381    #[test]
382    fn test_tenant_context_creation() {
383        let ctx = TenantContext::new("tenant-123");
384        assert_eq!(ctx.tenant_id, "tenant-123");
385        assert!(ctx.organization.is_none());
386
387        let ctx = ctx.with_organization("ACME Corp");
388        assert_eq!(ctx.organization.unwrap(), "ACME Corp");
389    }
390
391    #[test]
392    fn test_tenant_quota() {
393        let mut quota = TenantQuota::new("tenant-123")
394            .with_max_tuples(1000)
395            .with_max_checks_per_minute(100);
396
397        assert_eq!(quota.max_tuples, 1000);
398        assert_eq!(quota.max_checks_per_minute, 100);
399
400        assert!(quota.can_create_tuple());
401        quota.increment_tuples();
402        assert_eq!(quota.current_tuples, 1);
403
404        assert!(quota.can_check_permission());
405        quota.increment_checks();
406        assert_eq!(quota.current_checks, 1);
407
408        quota.reset_rate_limits();
409        assert_eq!(quota.current_checks, 0);
410    }
411
412    #[tokio::test]
413    async fn test_multi_tenant_engine() {
414        let engine = MultiTenantEngine::new();
415
416        let quota = TenantQuota::new("tenant-123").with_max_tuples(100);
417
418        assert!(engine.set_quota(quota).await.is_ok());
419
420        let retrieved = engine.get_quota("tenant-123").await;
421        assert!(retrieved.is_ok());
422        assert!(retrieved.unwrap().is_some());
423
424        assert!(engine.check_tuple_quota("tenant-123").await.is_ok());
425        assert!(engine.increment_tuple_count("tenant-123").await.is_ok());
426    }
427
428    #[test]
429    fn test_cross_tenant_access() {
430        let resource = Resource {
431            namespace: "document".to_string(),
432            object_id: "123".to_string(),
433        };
434
435        let subject = Subject::User("alice".to_string());
436
437        let access = CrossTenantAccess::new(
438            "tenant-A".to_string(),
439            "tenant-B".to_string(),
440            resource,
441            "viewer".to_string(),
442            subject,
443            true,
444        );
445
446        assert_eq!(access.source_tenant, "tenant-A");
447        assert_eq!(access.target_tenant, "tenant-B");
448        assert!(access.granted);
449    }
450
451    #[test]
452    fn test_tenant_resource() {
453        let resource = Resource {
454            namespace: "document".to_string(),
455            object_id: "123".to_string(),
456        };
457
458        let tenant_resource = TenantResource::new("tenant-123", resource.clone());
459
460        assert_eq!(tenant_resource.tenant_id, "tenant-123");
461        assert_eq!(tenant_resource.resource, resource);
462    }
463}