1use chrono::{DateTime, Utc};
7use parking_lot::RwLock;
8use serde::{Deserialize, Serialize};
9use std::collections::{HashMap, HashSet};
10use std::sync::Arc;
11use thiserror::Error;
12use uuid::Uuid;
13
14#[derive(Error, Debug)]
16pub enum MultiTenancyError {
17 #[error("Tenant not found: {0}")]
18 TenantNotFound(String),
19
20 #[error("Tenant already exists: {0}")]
21 TenantAlreadyExists(String),
22
23 #[error("Access denied for tenant {tenant}: {reason}")]
24 AccessDenied { tenant: String, reason: String },
25
26 #[error("Quota exceeded for tenant {tenant}: {quota_type}")]
27 QuotaExceeded { tenant: String, quota_type: String },
28
29 #[error("Invalid tenant configuration: {0}")]
30 InvalidConfig(String),
31
32 #[error("Resource not found: {0}")]
33 ResourceNotFound(String),
34
35 #[error("Unauthorized access: {0}")]
36 Unauthorized(String),
37}
38
39pub type Result<T> = std::result::Result<T, MultiTenancyError>;
40
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
43#[serde(rename_all = "lowercase")]
44pub enum TenantPlan {
45 Free,
46 Starter,
47 Professional,
48 Enterprise,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct ResourceQuota {
54 pub max_scenarios: usize,
56 pub max_concurrent_executions: usize,
58 pub max_orchestrations: usize,
60 pub max_templates: usize,
62 pub max_requests_per_minute: usize,
64 pub max_storage_mb: usize,
66 pub max_users: usize,
68 pub max_experiment_duration_secs: u64,
70}
71
72impl Default for ResourceQuota {
73 fn default() -> Self {
74 Self {
75 max_scenarios: 10,
76 max_concurrent_executions: 3,
77 max_orchestrations: 5,
78 max_templates: 10,
79 max_requests_per_minute: 100,
80 max_storage_mb: 100,
81 max_users: 5,
82 max_experiment_duration_secs: 3600, }
84 }
85}
86
87impl ResourceQuota {
88 pub fn for_plan(plan: &TenantPlan) -> Self {
90 match plan {
91 TenantPlan::Free => Self {
92 max_scenarios: 5,
93 max_concurrent_executions: 1,
94 max_orchestrations: 3,
95 max_templates: 5,
96 max_requests_per_minute: 50,
97 max_storage_mb: 50,
98 max_users: 1,
99 max_experiment_duration_secs: 600, },
101 TenantPlan::Starter => Self {
102 max_scenarios: 20,
103 max_concurrent_executions: 5,
104 max_orchestrations: 10,
105 max_templates: 20,
106 max_requests_per_minute: 200,
107 max_storage_mb: 500,
108 max_users: 5,
109 max_experiment_duration_secs: 3600, },
111 TenantPlan::Professional => Self {
112 max_scenarios: 100,
113 max_concurrent_executions: 20,
114 max_orchestrations: 50,
115 max_templates: 100,
116 max_requests_per_minute: 1000,
117 max_storage_mb: 5000,
118 max_users: 25,
119 max_experiment_duration_secs: 14400, },
121 TenantPlan::Enterprise => Self {
122 max_scenarios: usize::MAX,
123 max_concurrent_executions: 100,
124 max_orchestrations: usize::MAX,
125 max_templates: usize::MAX,
126 max_requests_per_minute: 10000,
127 max_storage_mb: 50000,
128 max_users: usize::MAX,
129 max_experiment_duration_secs: 86400, },
131 }
132 }
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize, Default)]
137pub struct ResourceUsage {
138 pub scenarios: usize,
139 pub concurrent_executions: usize,
140 pub orchestrations: usize,
141 pub templates: usize,
142 pub storage_mb: usize,
143 pub users: usize,
144 pub requests_this_minute: usize,
145 pub last_request_minute: DateTime<Utc>,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct TenantPermissions {
151 pub can_create_scenarios: bool,
153 pub can_execute_scenarios: bool,
155 pub can_view_observability: bool,
157 pub can_manage_resilience: bool,
159 pub can_use_advanced_features: bool,
161 pub can_integrate_external: bool,
163 pub can_use_ml_features: bool,
165 pub can_manage_users: bool,
167 pub custom_permissions: HashSet<String>,
169}
170
171impl TenantPermissions {
172 pub fn for_plan(plan: &TenantPlan) -> Self {
174 match plan {
175 TenantPlan::Free => Self {
176 can_create_scenarios: true,
177 can_execute_scenarios: true,
178 can_view_observability: false,
179 can_manage_resilience: false,
180 can_use_advanced_features: false,
181 can_integrate_external: false,
182 can_use_ml_features: false,
183 can_manage_users: false,
184 custom_permissions: HashSet::new(),
185 },
186 TenantPlan::Starter => Self {
187 can_create_scenarios: true,
188 can_execute_scenarios: true,
189 can_view_observability: true,
190 can_manage_resilience: true,
191 can_use_advanced_features: false,
192 can_integrate_external: false,
193 can_use_ml_features: false,
194 can_manage_users: true,
195 custom_permissions: HashSet::new(),
196 },
197 TenantPlan::Professional => Self {
198 can_create_scenarios: true,
199 can_execute_scenarios: true,
200 can_view_observability: true,
201 can_manage_resilience: true,
202 can_use_advanced_features: true,
203 can_integrate_external: true,
204 can_use_ml_features: true,
205 can_manage_users: true,
206 custom_permissions: HashSet::new(),
207 },
208 TenantPlan::Enterprise => Self {
209 can_create_scenarios: true,
210 can_execute_scenarios: true,
211 can_view_observability: true,
212 can_manage_resilience: true,
213 can_use_advanced_features: true,
214 can_integrate_external: true,
215 can_use_ml_features: true,
216 can_manage_users: true,
217 custom_permissions: HashSet::new(),
218 },
219 }
220 }
221
222 pub fn has_permission(&self, permission: &str) -> bool {
224 match permission {
225 "create_scenarios" => self.can_create_scenarios,
226 "execute_scenarios" => self.can_execute_scenarios,
227 "view_observability" => self.can_view_observability,
228 "manage_resilience" => self.can_manage_resilience,
229 "use_advanced_features" => self.can_use_advanced_features,
230 "integrate_external" => self.can_integrate_external,
231 "use_ml_features" => self.can_use_ml_features,
232 "manage_users" => self.can_manage_users,
233 custom => self.custom_permissions.contains(custom),
234 }
235 }
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize)]
240pub struct Tenant {
241 pub id: String,
242 pub name: String,
243 pub plan: TenantPlan,
244 pub quota: ResourceQuota,
245 pub usage: ResourceUsage,
246 pub permissions: TenantPermissions,
247 pub created_at: DateTime<Utc>,
248 pub updated_at: DateTime<Utc>,
249 pub metadata: HashMap<String, String>,
250 pub enabled: bool,
251}
252
253impl Tenant {
254 pub fn new(name: String, plan: TenantPlan) -> Self {
256 let now = Utc::now();
257 Self {
258 id: Uuid::new_v4().to_string(),
259 name,
260 plan: plan.clone(),
261 quota: ResourceQuota::for_plan(&plan),
262 usage: ResourceUsage::default(),
263 permissions: TenantPermissions::for_plan(&plan),
264 created_at: now,
265 updated_at: now,
266 metadata: HashMap::new(),
267 enabled: true,
268 }
269 }
270
271 pub fn check_quota(&self, resource_type: &str) -> Result<()> {
273 if !self.enabled {
274 return Err(MultiTenancyError::AccessDenied {
275 tenant: self.id.clone(),
276 reason: "Tenant is disabled".to_string(),
277 });
278 }
279
280 match resource_type {
281 "scenario" => {
282 if self.usage.scenarios >= self.quota.max_scenarios {
283 return Err(MultiTenancyError::QuotaExceeded {
284 tenant: self.id.clone(),
285 quota_type: "scenarios".to_string(),
286 });
287 }
288 }
289 "execution" => {
290 if self.usage.concurrent_executions >= self.quota.max_concurrent_executions {
291 return Err(MultiTenancyError::QuotaExceeded {
292 tenant: self.id.clone(),
293 quota_type: "concurrent_executions".to_string(),
294 });
295 }
296 }
297 "orchestration" => {
298 if self.usage.orchestrations >= self.quota.max_orchestrations {
299 return Err(MultiTenancyError::QuotaExceeded {
300 tenant: self.id.clone(),
301 quota_type: "orchestrations".to_string(),
302 });
303 }
304 }
305 "template" => {
306 if self.usage.templates >= self.quota.max_templates {
307 return Err(MultiTenancyError::QuotaExceeded {
308 tenant: self.id.clone(),
309 quota_type: "templates".to_string(),
310 });
311 }
312 }
313 "user" => {
314 if self.usage.users >= self.quota.max_users {
315 return Err(MultiTenancyError::QuotaExceeded {
316 tenant: self.id.clone(),
317 quota_type: "users".to_string(),
318 });
319 }
320 }
321 _ => {}
322 }
323
324 Ok(())
325 }
326
327 pub fn check_rate_limit(&mut self) -> Result<()> {
329 let now = Utc::now();
330 let current_minute = now.format("%Y-%m-%d %H:%M").to_string();
331 let last_minute = self.usage.last_request_minute.format("%Y-%m-%d %H:%M").to_string();
332
333 if current_minute != last_minute {
334 self.usage.requests_this_minute = 0;
336 self.usage.last_request_minute = now;
337 }
338
339 if self.usage.requests_this_minute >= self.quota.max_requests_per_minute {
340 return Err(MultiTenancyError::QuotaExceeded {
341 tenant: self.id.clone(),
342 quota_type: "requests_per_minute".to_string(),
343 });
344 }
345
346 self.usage.requests_this_minute += 1;
347 Ok(())
348 }
349}
350
351pub struct TenantManager {
353 tenants: Arc<RwLock<HashMap<String, Tenant>>>,
354 name_to_id: Arc<RwLock<HashMap<String, String>>>,
355}
356
357impl TenantManager {
358 pub fn new() -> Self {
360 Self {
361 tenants: Arc::new(RwLock::new(HashMap::new())),
362 name_to_id: Arc::new(RwLock::new(HashMap::new())),
363 }
364 }
365
366 pub fn create_tenant(&self, name: String, plan: TenantPlan) -> Result<Tenant> {
368 let mut name_map = self.name_to_id.write();
369
370 if name_map.contains_key(&name) {
371 return Err(MultiTenancyError::TenantAlreadyExists(name));
372 }
373
374 let tenant = Tenant::new(name.clone(), plan);
375 let tenant_id = tenant.id.clone();
376
377 let mut tenants = self.tenants.write();
378 tenants.insert(tenant_id.clone(), tenant.clone());
379 name_map.insert(name, tenant_id);
380
381 Ok(tenant)
382 }
383
384 pub fn get_tenant(&self, tenant_id: &str) -> Result<Tenant> {
386 let tenants = self.tenants.read();
387 tenants
388 .get(tenant_id)
389 .cloned()
390 .ok_or_else(|| MultiTenancyError::TenantNotFound(tenant_id.to_string()))
391 }
392
393 pub fn get_tenant_by_name(&self, name: &str) -> Result<Tenant> {
395 let name_map = self.name_to_id.read();
396 let tenant_id = name_map
397 .get(name)
398 .ok_or_else(|| MultiTenancyError::TenantNotFound(name.to_string()))?;
399
400 self.get_tenant(tenant_id)
401 }
402
403 pub fn update_tenant(&self, tenant: Tenant) -> Result<()> {
405 let mut tenants = self.tenants.write();
406
407 if !tenants.contains_key(&tenant.id) {
408 return Err(MultiTenancyError::TenantNotFound(tenant.id.clone()));
409 }
410
411 tenants.insert(tenant.id.clone(), tenant);
412 Ok(())
413 }
414
415 pub fn delete_tenant(&self, tenant_id: &str) -> Result<()> {
417 let mut tenants = self.tenants.write();
418 let tenant = tenants
419 .remove(tenant_id)
420 .ok_or_else(|| MultiTenancyError::TenantNotFound(tenant_id.to_string()))?;
421
422 let mut name_map = self.name_to_id.write();
423 name_map.remove(&tenant.name);
424
425 Ok(())
426 }
427
428 pub fn list_tenants(&self) -> Vec<Tenant> {
430 let tenants = self.tenants.read();
431 tenants.values().cloned().collect()
432 }
433
434 pub fn increment_usage(&self, tenant_id: &str, resource_type: &str) -> Result<()> {
436 let mut tenants = self.tenants.write();
437 let tenant = tenants
438 .get_mut(tenant_id)
439 .ok_or_else(|| MultiTenancyError::TenantNotFound(tenant_id.to_string()))?;
440
441 match resource_type {
442 "scenario" => tenant.usage.scenarios += 1,
443 "execution" => tenant.usage.concurrent_executions += 1,
444 "orchestration" => tenant.usage.orchestrations += 1,
445 "template" => tenant.usage.templates += 1,
446 "user" => tenant.usage.users += 1,
447 _ => {}
448 }
449
450 Ok(())
451 }
452
453 pub fn decrement_usage(&self, tenant_id: &str, resource_type: &str) -> Result<()> {
455 let mut tenants = self.tenants.write();
456 let tenant = tenants
457 .get_mut(tenant_id)
458 .ok_or_else(|| MultiTenancyError::TenantNotFound(tenant_id.to_string()))?;
459
460 match resource_type {
461 "scenario" => {
462 if tenant.usage.scenarios > 0 {
463 tenant.usage.scenarios -= 1;
464 }
465 }
466 "execution" => {
467 if tenant.usage.concurrent_executions > 0 {
468 tenant.usage.concurrent_executions -= 1;
469 }
470 }
471 "orchestration" => {
472 if tenant.usage.orchestrations > 0 {
473 tenant.usage.orchestrations -= 1;
474 }
475 }
476 "template" => {
477 if tenant.usage.templates > 0 {
478 tenant.usage.templates -= 1;
479 }
480 }
481 "user" => {
482 if tenant.usage.users > 0 {
483 tenant.usage.users -= 1;
484 }
485 }
486 _ => {}
487 }
488
489 Ok(())
490 }
491
492 pub fn check_permission(&self, tenant_id: &str, permission: &str) -> Result<()> {
494 let tenant = self.get_tenant(tenant_id)?;
495
496 if !tenant.enabled {
497 return Err(MultiTenancyError::AccessDenied {
498 tenant: tenant_id.to_string(),
499 reason: "Tenant is disabled".to_string(),
500 });
501 }
502
503 if !tenant.permissions.has_permission(permission) {
504 return Err(MultiTenancyError::AccessDenied {
505 tenant: tenant_id.to_string(),
506 reason: format!("Missing permission: {}", permission),
507 });
508 }
509
510 Ok(())
511 }
512
513 pub fn check_and_increment(&self, tenant_id: &str, resource_type: &str) -> Result<()> {
515 let tenant = self.get_tenant(tenant_id)?;
516 tenant.check_quota(resource_type)?;
517 self.increment_usage(tenant_id, resource_type)?;
518 Ok(())
519 }
520
521 pub fn upgrade_plan(&self, tenant_id: &str, new_plan: TenantPlan) -> Result<()> {
523 let mut tenant = self.get_tenant(tenant_id)?;
524
525 if new_plan <= tenant.plan {
526 return Err(MultiTenancyError::InvalidConfig(
527 "New plan must be higher than current plan".to_string(),
528 ));
529 }
530
531 tenant.plan = new_plan.clone();
532 tenant.quota = ResourceQuota::for_plan(&new_plan);
533 tenant.permissions = TenantPermissions::for_plan(&new_plan);
534 tenant.updated_at = Utc::now();
535
536 self.update_tenant(tenant)?;
537 Ok(())
538 }
539
540 pub fn disable_tenant(&self, tenant_id: &str) -> Result<()> {
542 let mut tenant = self.get_tenant(tenant_id)?;
543 tenant.enabled = false;
544 tenant.updated_at = Utc::now();
545 self.update_tenant(tenant)?;
546 Ok(())
547 }
548
549 pub fn enable_tenant(&self, tenant_id: &str) -> Result<()> {
551 let mut tenant = self.get_tenant(tenant_id)?;
552 tenant.enabled = true;
553 tenant.updated_at = Utc::now();
554 self.update_tenant(tenant)?;
555 Ok(())
556 }
557}
558
559impl Default for TenantManager {
560 fn default() -> Self {
561 Self::new()
562 }
563}
564
565#[cfg(test)]
566mod tests {
567 use super::*;
568
569 #[test]
570 fn test_tenant_creation() {
571 let manager = TenantManager::new();
572 let tenant = manager.create_tenant("test-tenant".to_string(), TenantPlan::Starter).unwrap();
573
574 assert_eq!(tenant.name, "test-tenant");
575 assert_eq!(tenant.plan, TenantPlan::Starter);
576 assert!(tenant.enabled);
577 }
578
579 #[test]
580 fn test_duplicate_tenant() {
581 let manager = TenantManager::new();
582 manager.create_tenant("test-tenant".to_string(), TenantPlan::Free).unwrap();
583
584 let result = manager.create_tenant("test-tenant".to_string(), TenantPlan::Free);
585 assert!(result.is_err());
586 }
587
588 #[test]
589 fn test_quota_checking() {
590 let tenant = Tenant::new("test".to_string(), TenantPlan::Free);
591
592 assert!(tenant.check_quota("scenario").is_ok());
594
595 let mut tenant_with_usage = tenant.clone();
597 tenant_with_usage.usage.scenarios = tenant_with_usage.quota.max_scenarios;
598
599 assert!(tenant_with_usage.check_quota("scenario").is_err());
600 }
601
602 #[test]
603 fn test_permission_checking() {
604 let free_tenant = Tenant::new("free".to_string(), TenantPlan::Free);
605 let pro_tenant = Tenant::new("pro".to_string(), TenantPlan::Professional);
606
607 assert!(!free_tenant.permissions.has_permission("use_ml_features"));
608 assert!(pro_tenant.permissions.has_permission("use_ml_features"));
609 }
610
611 #[test]
612 fn test_plan_upgrade() {
613 let manager = TenantManager::new();
614 let tenant = manager.create_tenant("test".to_string(), TenantPlan::Free).unwrap();
615
616 manager.upgrade_plan(&tenant.id, TenantPlan::Professional).unwrap();
617
618 let updated = manager.get_tenant(&tenant.id).unwrap();
619 assert_eq!(updated.plan, TenantPlan::Professional);
620 assert!(updated.permissions.has_permission("use_ml_features"));
621 }
622
623 #[test]
624 fn test_usage_tracking() {
625 let manager = TenantManager::new();
626 let tenant = manager.create_tenant("test".to_string(), TenantPlan::Starter).unwrap();
627
628 manager.increment_usage(&tenant.id, "scenario").unwrap();
629 manager.increment_usage(&tenant.id, "scenario").unwrap();
630
631 let updated = manager.get_tenant(&tenant.id).unwrap();
632 assert_eq!(updated.usage.scenarios, 2);
633
634 manager.decrement_usage(&tenant.id, "scenario").unwrap();
635 let updated = manager.get_tenant(&tenant.id).unwrap();
636 assert_eq!(updated.usage.scenarios, 1);
637 }
638}