mockforge_chaos/
multi_tenancy.rs

1//! Multi-Tenancy Support
2//!
3//! Provides isolation and resource management for multiple tenants in MockForge.
4//! Supports tenant-specific configurations, quotas, and access controls.
5
6use 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/// Multi-tenancy errors
15#[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/// Tenant plan/tier
42#[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/// Resource quotas for a tenant
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct ResourceQuota {
54    /// Maximum number of active scenarios
55    pub max_scenarios: usize,
56    /// Maximum number of concurrent executions
57    pub max_concurrent_executions: usize,
58    /// Maximum number of orchestrations
59    pub max_orchestrations: usize,
60    /// Maximum number of templates
61    pub max_templates: usize,
62    /// Maximum number of API requests per minute
63    pub max_requests_per_minute: usize,
64    /// Maximum storage in MB
65    pub max_storage_mb: usize,
66    /// Maximum number of users per tenant
67    pub max_users: usize,
68    /// Maximum duration for chaos experiments in seconds
69    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, // 1 hour
83        }
84    }
85}
86
87impl ResourceQuota {
88    /// Get quotas for a specific plan
89    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, // 10 minutes
100            },
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, // 1 hour
110            },
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, // 4 hours
120            },
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, // 24 hours
130            },
131        }
132    }
133}
134
135/// Current resource usage for a tenant
136#[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/// Tenant permissions
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct TenantPermissions {
151    /// Can create chaos scenarios
152    pub can_create_scenarios: bool,
153    /// Can execute scenarios
154    pub can_execute_scenarios: bool,
155    /// Can access observability data
156    pub can_view_observability: bool,
157    /// Can manage resilience patterns
158    pub can_manage_resilience: bool,
159    /// Can access advanced features
160    pub can_use_advanced_features: bool,
161    /// Can integrate with external systems
162    pub can_integrate_external: bool,
163    /// Can use ML features
164    pub can_use_ml_features: bool,
165    /// Can manage users
166    pub can_manage_users: bool,
167    /// Custom permissions
168    pub custom_permissions: HashSet<String>,
169}
170
171impl TenantPermissions {
172    /// Get permissions for a specific plan
173    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    /// Check if tenant has a specific permission
223    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/// Tenant information
239#[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    /// Create a new tenant
255    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    /// Check if tenant can perform action within quota
272    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    /// Check rate limiting
328    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            // Reset counter for new minute
335            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
351/// Multi-tenancy manager
352pub struct TenantManager {
353    tenants: Arc<RwLock<HashMap<String, Tenant>>>,
354    name_to_id: Arc<RwLock<HashMap<String, String>>>,
355}
356
357impl TenantManager {
358    /// Create a new tenant manager
359    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    /// Create a new tenant
367    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    /// Get tenant by ID
385    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    /// Get tenant by name
394    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    /// Update tenant
404    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    /// Delete tenant
416    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    /// List all tenants
429    pub fn list_tenants(&self) -> Vec<Tenant> {
430        let tenants = self.tenants.read();
431        tenants.values().cloned().collect()
432    }
433
434    /// Increment usage counter
435    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    /// Decrement usage counter
454    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    /// Check permission for tenant
493    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    /// Check quota and increment if allowed
514    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    /// Upgrade tenant plan
522    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    /// Disable tenant
541    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    /// Enable tenant
550    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        // Should be OK initially
593        assert!(tenant.check_quota("scenario").is_ok());
594
595        // Simulate exceeding quota
596        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}