1use crate::{AuthzError, RelationTuple, Result, Subject};
36use serde::{Deserialize, Serialize};
37use std::collections::HashMap;
38use std::sync::Arc;
39use tokio::sync::RwLock;
40
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
43pub struct Resource {
44 pub namespace: String,
45 pub object_id: String,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
50pub struct TenantContext {
51 pub tenant_id: String,
53
54 pub organization: Option<String>,
56
57 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
83pub struct TenantRelationTuple {
84 pub tenant_id: String,
86
87 pub tuple: RelationTuple,
89
90 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#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct TenantQuota {
112 pub tenant_id: String,
114
115 pub max_tuples: usize,
117
118 pub max_checks_per_minute: usize,
120
121 pub max_api_requests_per_minute: usize,
123
124 pub current_tuples: usize,
126
127 pub current_checks: usize,
129
130 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, max_checks_per_minute: 10_000, max_api_requests_per_minute: 1_000, 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 pub fn can_create_tuple(&self) -> bool {
159 self.current_tuples < self.max_tuples
160 }
161
162 pub fn can_check_permission(&self) -> bool {
164 self.current_checks < self.max_checks_per_minute
165 }
166
167 pub fn can_make_api_request(&self) -> bool {
169 self.current_api_requests < self.max_api_requests_per_minute
170 }
171
172 pub fn increment_tuples(&mut self) {
174 self.current_tuples += 1;
175 }
176
177 pub fn decrement_tuples(&mut self) {
179 if self.current_tuples > 0 {
180 self.current_tuples -= 1;
181 }
182 }
183
184 pub fn increment_checks(&mut self) {
186 self.current_checks += 1;
187 }
188
189 pub fn increment_api_requests(&mut self) {
191 self.current_api_requests += 1;
192 }
193
194 pub fn reset_rate_limits(&mut self) {
196 self.current_checks = 0;
197 self.current_api_requests = 0;
198 }
199}
200
201#[allow(dead_code)]
203pub struct MultiTenantEngine {
204 quotas: Arc<RwLock<HashMap<String, TenantQuota>>>,
206
207 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 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 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 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 Ok(true)
240 }
241 }
242
243 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 Ok(true)
259 }
260 }
261
262 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 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 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
311pub struct CrossTenantAccess {
312 pub source_tenant: String,
314
315 pub target_tenant: String,
317
318 pub resource: Resource,
320
321 pub relation: String,
323
324 pub subject: Subject,
326
327 pub granted: bool,
329
330 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#[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 pub fn is_cross_tenant(&self) -> bool {
372 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}