Skip to main content

heliosdb_proxy/multi_tenancy/
config.rs

1//! Multi-Tenancy Configuration Types
2//!
3//! This module provides configuration structures for multi-tenant proxy operation.
4//!
5//! # Example
6//!
7//! ```rust
8//! use heliosdb::proxy::multi_tenancy::{
9//!     TenantConfig, IsolationStrategy, TenantPoolConfig, TenantRateLimits,
10//! };
11//!
12//! let config = TenantConfig::builder()
13//!     .id("tenant_a")
14//!     .name("Acme Corp")
15//!     .isolation(IsolationStrategy::Schema {
16//!         database_name: "shared_db".to_string(),
17//!         schema_name: "tenant_a".to_string(),
18//!     })
19//!     .max_connections(50)
20//!     .qps_limit(1000)
21//!     .build();
22//! ```
23
24use std::collections::HashMap;
25use std::time::Duration;
26
27/// Unique tenant identifier
28#[derive(Debug, Clone, PartialEq, Eq, Hash)]
29pub struct TenantId(pub String);
30
31impl TenantId {
32    /// Create a new tenant ID
33    pub fn new(id: impl Into<String>) -> Self {
34        Self(id.into())
35    }
36
37    /// Get the tenant ID as a string slice
38    pub fn as_str(&self) -> &str {
39        &self.0
40    }
41}
42
43impl std::fmt::Display for TenantId {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        write!(f, "{}", self.0)
46    }
47}
48
49impl From<&str> for TenantId {
50    fn from(s: &str) -> Self {
51        Self(s.to_string())
52    }
53}
54
55impl From<String> for TenantId {
56    fn from(s: String) -> Self {
57        Self(s)
58    }
59}
60
61/// Tenant isolation strategy
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum IsolationStrategy {
64    /// Separate database per tenant
65    Database {
66        /// Database name for this tenant
67        database_name: String,
68    },
69
70    /// Separate schema per tenant (same database)
71    Schema {
72        /// Database name containing the schema
73        database_name: String,
74        /// Schema name for this tenant
75        schema_name: String,
76    },
77
78    /// Row-level security (same tables)
79    Row {
80        /// Database name
81        database_name: String,
82        /// Column name containing tenant ID
83        tenant_column: String,
84    },
85
86    /// Branch per tenant (HeliosDB-Lite specific)
87    Branch {
88        /// Branch name for this tenant
89        branch_name: String,
90    },
91}
92
93impl IsolationStrategy {
94    /// Create database isolation strategy
95    pub fn database(name: impl Into<String>) -> Self {
96        Self::Database {
97            database_name: name.into(),
98        }
99    }
100
101    /// Create schema isolation strategy
102    pub fn schema(database: impl Into<String>, schema: impl Into<String>) -> Self {
103        Self::Schema {
104            database_name: database.into(),
105            schema_name: schema.into(),
106        }
107    }
108
109    /// Create row-level isolation strategy
110    pub fn row(database: impl Into<String>, column: impl Into<String>) -> Self {
111        Self::Row {
112            database_name: database.into(),
113            tenant_column: column.into(),
114        }
115    }
116
117    /// Create branch isolation strategy
118    pub fn branch(name: impl Into<String>) -> Self {
119        Self::Branch {
120            branch_name: name.into(),
121        }
122    }
123
124    /// Get the database name for this strategy
125    pub fn database_name(&self) -> Option<&str> {
126        match self {
127            Self::Database { database_name } => Some(database_name),
128            Self::Schema { database_name, .. } => Some(database_name),
129            Self::Row { database_name, .. } => Some(database_name),
130            Self::Branch { .. } => None,
131        }
132    }
133
134    /// Get the schema name if using schema isolation
135    pub fn schema_name(&self) -> Option<&str> {
136        match self {
137            Self::Schema { schema_name, .. } => Some(schema_name),
138            _ => None,
139        }
140    }
141
142    /// Get the tenant column if using row-level isolation
143    pub fn tenant_column(&self) -> Option<&str> {
144        match self {
145            Self::Row { tenant_column, .. } => Some(tenant_column),
146            _ => None,
147        }
148    }
149
150    /// Get the branch name if using branch isolation
151    pub fn branch_name(&self) -> Option<&str> {
152        match self {
153            Self::Branch { branch_name } => Some(branch_name),
154            _ => None,
155        }
156    }
157
158    /// Check if this strategy requires query transformation
159    pub fn requires_query_transform(&self) -> bool {
160        matches!(self, Self::Row { .. })
161    }
162
163    /// Check if this strategy requires connection routing
164    pub fn requires_connection_routing(&self) -> bool {
165        matches!(self, Self::Database { .. } | Self::Branch { .. })
166    }
167
168    /// Get display name for this strategy
169    pub fn strategy_name(&self) -> &'static str {
170        match self {
171            Self::Database { .. } => "database",
172            Self::Schema { .. } => "schema",
173            Self::Row { .. } => "row",
174            Self::Branch { .. } => "branch",
175        }
176    }
177}
178
179/// Tenant-specific rate limits
180#[derive(Debug, Clone)]
181pub struct TenantRateLimits {
182    /// Maximum queries per second
183    pub qps_limit: u32,
184
185    /// Maximum concurrent connections
186    pub max_connections: u32,
187
188    /// Maximum query duration before kill
189    pub max_query_duration: Duration,
190
191    /// Maximum result size (bytes)
192    pub max_result_size: u64,
193
194    /// Maximum rows per query
195    pub max_rows_per_query: u64,
196
197    /// Burst allowance (multiplier over qps_limit for short bursts)
198    pub burst_multiplier: f32,
199}
200
201impl Default for TenantRateLimits {
202    fn default() -> Self {
203        Self {
204            qps_limit: 100,
205            max_connections: 10,
206            max_query_duration: Duration::from_secs(60),
207            max_result_size: 100 * 1024 * 1024, // 100MB
208            max_rows_per_query: 100_000,
209            burst_multiplier: 2.0,
210        }
211    }
212}
213
214impl TenantRateLimits {
215    /// Create new rate limits with QPS limit
216    pub fn with_qps(qps: u32) -> Self {
217        Self {
218            qps_limit: qps,
219            ..Default::default()
220        }
221    }
222
223    /// Set the QPS limit
224    pub fn qps_limit(mut self, limit: u32) -> Self {
225        self.qps_limit = limit;
226        self
227    }
228
229    /// Set the max connections
230    pub fn max_connections(mut self, limit: u32) -> Self {
231        self.max_connections = limit;
232        self
233    }
234
235    /// Set the max query duration
236    pub fn max_query_duration(mut self, duration: Duration) -> Self {
237        self.max_query_duration = duration;
238        self
239    }
240
241    /// Set the burst multiplier
242    pub fn burst_multiplier(mut self, multiplier: f32) -> Self {
243        self.burst_multiplier = multiplier;
244        self
245    }
246}
247
248/// Tenant-specific connection pool configuration
249#[derive(Debug, Clone)]
250pub struct TenantPoolConfig {
251    /// Maximum connections in pool
252    pub max_connections: u32,
253
254    /// Minimum idle connections
255    pub min_idle: u32,
256
257    /// Idle connection timeout
258    pub idle_timeout: Duration,
259
260    /// Maximum connection lifetime
261    pub max_lifetime: Duration,
262
263    /// Connection acquire timeout
264    pub acquire_timeout: Duration,
265
266    /// Whether this tenant uses dedicated pool
267    pub dedicated_pool: bool,
268}
269
270impl Default for TenantPoolConfig {
271    fn default() -> Self {
272        Self {
273            max_connections: 10,
274            min_idle: 1,
275            idle_timeout: Duration::from_secs(600),
276            max_lifetime: Duration::from_secs(3600),
277            acquire_timeout: Duration::from_secs(5),
278            dedicated_pool: false,
279        }
280    }
281}
282
283impl TenantPoolConfig {
284    /// Create pool config with max connections
285    pub fn with_max_connections(max: u32) -> Self {
286        Self {
287            max_connections: max,
288            ..Default::default()
289        }
290    }
291
292    /// Set dedicated pool flag
293    pub fn dedicated(mut self) -> Self {
294        self.dedicated_pool = true;
295        self
296    }
297
298    /// Set min idle connections
299    pub fn min_idle(mut self, min: u32) -> Self {
300        self.min_idle = min;
301        self
302    }
303
304    /// Set idle timeout
305    pub fn idle_timeout(mut self, timeout: Duration) -> Self {
306        self.idle_timeout = timeout;
307        self
308    }
309}
310
311/// Tenant permissions and restrictions
312#[derive(Debug, Clone)]
313pub struct TenantPermissions {
314    /// Allowed SQL operations (SELECT, INSERT, UPDATE, DELETE, DDL)
315    pub allowed_operations: Vec<String>,
316
317    /// Blocked tables (cannot access)
318    pub blocked_tables: Vec<String>,
319
320    /// Read-only mode
321    pub read_only: bool,
322
323    /// Can execute DDL statements
324    pub allow_ddl: bool,
325
326    /// Can execute EXPLAIN/ANALYZE
327    pub allow_explain: bool,
328
329    /// Can access system tables
330    pub allow_system_access: bool,
331
332    /// Maximum tables per query (for complexity limiting)
333    pub max_tables_per_query: u32,
334}
335
336impl Default for TenantPermissions {
337    fn default() -> Self {
338        Self {
339            allowed_operations: vec![
340                "SELECT".to_string(),
341                "INSERT".to_string(),
342                "UPDATE".to_string(),
343                "DELETE".to_string(),
344            ],
345            blocked_tables: vec![],
346            read_only: false,
347            allow_ddl: false,
348            allow_explain: true,
349            allow_system_access: false,
350            max_tables_per_query: 10,
351        }
352    }
353}
354
355impl TenantPermissions {
356    /// Create read-only permissions
357    pub fn read_only() -> Self {
358        Self {
359            allowed_operations: vec!["SELECT".to_string()],
360            read_only: true,
361            ..Default::default()
362        }
363    }
364
365    /// Create full access permissions
366    pub fn full_access() -> Self {
367        Self {
368            allowed_operations: vec![
369                "SELECT".to_string(),
370                "INSERT".to_string(),
371                "UPDATE".to_string(),
372                "DELETE".to_string(),
373                "CREATE".to_string(),
374                "ALTER".to_string(),
375                "DROP".to_string(),
376            ],
377            allow_ddl: true,
378            allow_system_access: true,
379            ..Default::default()
380        }
381    }
382
383    /// Check if operation is allowed
384    pub fn is_operation_allowed(&self, operation: &str) -> bool {
385        self.allowed_operations
386            .iter()
387            .any(|op| op.eq_ignore_ascii_case(operation))
388    }
389
390    /// Check if table access is allowed
391    pub fn is_table_allowed(&self, table: &str) -> bool {
392        !self.blocked_tables.iter().any(|t| t.eq_ignore_ascii_case(table))
393    }
394}
395
396/// AI workload configuration per tenant
397#[derive(Debug, Clone)]
398pub struct TenantAiConfig {
399    /// Knowledge base identifier
400    pub knowledge_base: Option<String>,
401
402    /// Embedding model to use
403    pub embedding_model: String,
404
405    /// Maximum retrieval results
406    pub retrieval_limit: u32,
407
408    /// Token budget per day
409    pub daily_token_budget: Option<u64>,
410
411    /// Enable agent workspace
412    pub agent_workspace_enabled: bool,
413
414    /// Maximum concurrent agents
415    pub max_concurrent_agents: u32,
416}
417
418impl Default for TenantAiConfig {
419    fn default() -> Self {
420        Self {
421            knowledge_base: None,
422            embedding_model: "default".to_string(),
423            retrieval_limit: 10,
424            daily_token_budget: None,
425            agent_workspace_enabled: true,
426            max_concurrent_agents: 5,
427        }
428    }
429}
430
431/// Full tenant configuration
432#[derive(Debug, Clone)]
433pub struct TenantConfig {
434    /// Tenant identifier
435    pub id: TenantId,
436
437    /// Display name
438    pub name: String,
439
440    /// Isolation strategy
441    pub isolation: IsolationStrategy,
442
443    /// Rate limits
444    pub rate_limits: TenantRateLimits,
445
446    /// Connection pool settings
447    pub pool: TenantPoolConfig,
448
449    /// Permissions and restrictions
450    pub permissions: TenantPermissions,
451
452    /// AI workload configuration
453    pub ai_config: TenantAiConfig,
454
455    /// Custom metadata
456    pub metadata: HashMap<String, String>,
457
458    /// Whether tenant is enabled
459    pub enabled: bool,
460
461    /// Tenant creation timestamp
462    pub created_at: std::time::SystemTime,
463}
464
465impl TenantConfig {
466    /// Create a new tenant config builder
467    pub fn builder() -> TenantConfigBuilder {
468        TenantConfigBuilder::new()
469    }
470
471    /// Create with minimal configuration
472    pub fn new(id: impl Into<TenantId>, isolation: IsolationStrategy) -> Self {
473        Self {
474            id: id.into(),
475            name: String::new(),
476            isolation,
477            rate_limits: TenantRateLimits::default(),
478            pool: TenantPoolConfig::default(),
479            permissions: TenantPermissions::default(),
480            ai_config: TenantAiConfig::default(),
481            metadata: HashMap::new(),
482            enabled: true,
483            created_at: std::time::SystemTime::now(),
484        }
485    }
486
487    /// Check if tenant is in a healthy state
488    pub fn is_healthy(&self) -> bool {
489        self.enabled
490    }
491
492    /// Get effective max connections considering pool config
493    pub fn effective_max_connections(&self) -> u32 {
494        self.pool.max_connections.min(self.rate_limits.max_connections)
495    }
496}
497
498/// Builder for TenantConfig
499#[derive(Debug, Default)]
500pub struct TenantConfigBuilder {
501    id: Option<TenantId>,
502    name: Option<String>,
503    isolation: Option<IsolationStrategy>,
504    rate_limits: Option<TenantRateLimits>,
505    pool: Option<TenantPoolConfig>,
506    permissions: Option<TenantPermissions>,
507    ai_config: Option<TenantAiConfig>,
508    metadata: HashMap<String, String>,
509    enabled: bool,
510}
511
512impl TenantConfigBuilder {
513    /// Create a new builder
514    pub fn new() -> Self {
515        Self {
516            enabled: true,
517            ..Default::default()
518        }
519    }
520
521    /// Set tenant ID
522    pub fn id(mut self, id: impl Into<TenantId>) -> Self {
523        self.id = Some(id.into());
524        self
525    }
526
527    /// Set tenant name
528    pub fn name(mut self, name: impl Into<String>) -> Self {
529        self.name = Some(name.into());
530        self
531    }
532
533    /// Set isolation strategy
534    pub fn isolation(mut self, strategy: IsolationStrategy) -> Self {
535        self.isolation = Some(strategy);
536        self
537    }
538
539    /// Set database isolation
540    pub fn database_isolation(self, database: impl Into<String>) -> Self {
541        self.isolation(IsolationStrategy::database(database))
542    }
543
544    /// Set schema isolation
545    pub fn schema_isolation(self, database: impl Into<String>, schema: impl Into<String>) -> Self {
546        self.isolation(IsolationStrategy::schema(database, schema))
547    }
548
549    /// Set row-level isolation
550    pub fn row_isolation(self, database: impl Into<String>, column: impl Into<String>) -> Self {
551        self.isolation(IsolationStrategy::row(database, column))
552    }
553
554    /// Set branch isolation
555    pub fn branch_isolation(self, branch: impl Into<String>) -> Self {
556        self.isolation(IsolationStrategy::branch(branch))
557    }
558
559    /// Set rate limits
560    pub fn rate_limits(mut self, limits: TenantRateLimits) -> Self {
561        self.rate_limits = Some(limits);
562        self
563    }
564
565    /// Set QPS limit
566    pub fn qps_limit(mut self, limit: u32) -> Self {
567        let mut limits = self.rate_limits.take().unwrap_or_default();
568        limits.qps_limit = limit;
569        self.rate_limits = Some(limits);
570        self
571    }
572
573    /// Set max connections
574    pub fn max_connections(mut self, max: u32) -> Self {
575        let mut pool = self.pool.take().unwrap_or_default();
576        pool.max_connections = max;
577        self.pool = Some(pool);
578
579        let mut limits = self.rate_limits.take().unwrap_or_default();
580        limits.max_connections = max;
581        self.rate_limits = Some(limits);
582        self
583    }
584
585    /// Set pool configuration
586    pub fn pool(mut self, config: TenantPoolConfig) -> Self {
587        self.pool = Some(config);
588        self
589    }
590
591    /// Set permissions
592    pub fn permissions(mut self, perms: TenantPermissions) -> Self {
593        self.permissions = Some(perms);
594        self
595    }
596
597    /// Set read-only mode
598    pub fn read_only(self) -> Self {
599        self.permissions(TenantPermissions::read_only())
600    }
601
602    /// Set AI configuration
603    pub fn ai_config(mut self, config: TenantAiConfig) -> Self {
604        self.ai_config = Some(config);
605        self
606    }
607
608    /// Add metadata
609    pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
610        self.metadata.insert(key.into(), value.into());
611        self
612    }
613
614    /// Set enabled state
615    pub fn enabled(mut self, enabled: bool) -> Self {
616        self.enabled = enabled;
617        self
618    }
619
620    /// Build the TenantConfig
621    pub fn build(self) -> TenantConfig {
622        TenantConfig {
623            id: self.id.expect("tenant id is required"),
624            name: self.name.unwrap_or_default(),
625            isolation: self.isolation.expect("isolation strategy is required"),
626            rate_limits: self.rate_limits.unwrap_or_default(),
627            pool: self.pool.unwrap_or_default(),
628            permissions: self.permissions.unwrap_or_default(),
629            ai_config: self.ai_config.unwrap_or_default(),
630            metadata: self.metadata,
631            enabled: self.enabled,
632            created_at: std::time::SystemTime::now(),
633        }
634    }
635}
636
637/// How tenants are identified from requests
638#[derive(Debug, Clone)]
639pub enum IdentificationMethod {
640    /// Extract from HTTP header
641    Header {
642        /// Header name (e.g., "X-Tenant-Id")
643        header_name: String,
644    },
645
646    /// Extract from username prefix
647    UsernamePrefix {
648        /// Separator character (e.g., '.')
649        separator: char,
650    },
651
652    /// Extract from JWT claim
653    JwtClaim {
654        /// Claim name (e.g., "tenant_id")
655        claim_name: String,
656        /// JWT issuer for validation
657        issuer: Option<String>,
658    },
659
660    /// Extract from database name
661    DatabaseName,
662
663    /// SQL context variable
664    SqlContext {
665        /// Variable name (e.g., "helios.tenant_id")
666        variable_name: String,
667    },
668}
669
670impl Default for IdentificationMethod {
671    fn default() -> Self {
672        Self::Header {
673            header_name: "X-Tenant-Id".to_string(),
674        }
675    }
676}
677
678impl IdentificationMethod {
679    /// Create header identification
680    pub fn header(name: impl Into<String>) -> Self {
681        Self::Header {
682            header_name: name.into(),
683        }
684    }
685
686    /// Create username prefix identification
687    pub fn username_prefix(separator: char) -> Self {
688        Self::UsernamePrefix { separator }
689    }
690
691    /// Create JWT claim identification
692    pub fn jwt_claim(claim: impl Into<String>) -> Self {
693        Self::JwtClaim {
694            claim_name: claim.into(),
695            issuer: None,
696        }
697    }
698
699    /// Create database name identification
700    pub fn database_name() -> Self {
701        Self::DatabaseName
702    }
703
704    /// Create SQL context identification
705    pub fn sql_context(variable: impl Into<String>) -> Self {
706        Self::SqlContext {
707            variable_name: variable.into(),
708        }
709    }
710}
711
712/// Global multi-tenancy configuration
713#[derive(Debug, Clone)]
714pub struct MultiTenancyConfig {
715    /// Whether multi-tenancy is enabled
716    pub enabled: bool,
717
718    /// How to identify tenants
719    pub identification: IdentificationMethod,
720
721    /// Default tenant configuration
722    pub default_config: TenantConfig,
723
724    /// Whether to allow unknown tenants
725    pub allow_unknown_tenants: bool,
726
727    /// Whether to create tenants on-demand
728    pub auto_create_tenants: bool,
729
730    /// Maximum tenants allowed
731    pub max_tenants: u32,
732
733    /// Enable cross-tenant analytics for admins
734    pub cross_tenant_analytics: bool,
735
736    /// Admin user pattern (for cross-tenant access)
737    pub admin_user_pattern: Option<String>,
738}
739
740impl Default for MultiTenancyConfig {
741    fn default() -> Self {
742        Self {
743            enabled: false,
744            identification: IdentificationMethod::default(),
745            default_config: TenantConfig::new(
746                TenantId::new("default"),
747                IsolationStrategy::schema("public", "public"),
748            ),
749            allow_unknown_tenants: false,
750            auto_create_tenants: false,
751            max_tenants: 1000,
752            cross_tenant_analytics: false,
753            admin_user_pattern: None,
754        }
755    }
756}
757
758impl MultiTenancyConfig {
759    /// Create enabled multi-tenancy config
760    pub fn enabled() -> Self {
761        Self {
762            enabled: true,
763            ..Default::default()
764        }
765    }
766
767    /// Set identification method
768    pub fn with_identification(mut self, method: IdentificationMethod) -> Self {
769        self.identification = method;
770        self
771    }
772
773    /// Set default tenant config
774    pub fn with_default_config(mut self, config: TenantConfig) -> Self {
775        self.default_config = config;
776        self
777    }
778
779    /// Allow unknown tenants
780    pub fn allow_unknown(mut self) -> Self {
781        self.allow_unknown_tenants = true;
782        self
783    }
784
785    /// Enable auto-creation of tenants
786    pub fn auto_create(mut self) -> Self {
787        self.auto_create_tenants = true;
788        self
789    }
790}
791
792#[cfg(test)]
793mod tests {
794    use super::*;
795
796    #[test]
797    fn test_tenant_id() {
798        let id = TenantId::new("test_tenant");
799        assert_eq!(id.as_str(), "test_tenant");
800        assert_eq!(id.to_string(), "test_tenant");
801
802        let id2: TenantId = "another".into();
803        assert_eq!(id2.as_str(), "another");
804    }
805
806    #[test]
807    fn test_isolation_strategy() {
808        let db = IsolationStrategy::database("mydb");
809        assert_eq!(db.database_name(), Some("mydb"));
810        assert_eq!(db.strategy_name(), "database");
811        assert!(db.requires_connection_routing());
812        assert!(!db.requires_query_transform());
813
814        let schema = IsolationStrategy::schema("mydb", "myschema");
815        assert_eq!(schema.database_name(), Some("mydb"));
816        assert_eq!(schema.schema_name(), Some("myschema"));
817        assert_eq!(schema.strategy_name(), "schema");
818
819        let row = IsolationStrategy::row("mydb", "tenant_id");
820        assert_eq!(row.tenant_column(), Some("tenant_id"));
821        assert!(row.requires_query_transform());
822
823        let branch = IsolationStrategy::branch("tenant_branch");
824        assert_eq!(branch.branch_name(), Some("tenant_branch"));
825        assert!(branch.requires_connection_routing());
826    }
827
828    #[test]
829    fn test_tenant_config_builder() {
830        let config = TenantConfig::builder()
831            .id("tenant_a")
832            .name("Acme Corp")
833            .schema_isolation("shared_db", "tenant_a")
834            .max_connections(50)
835            .qps_limit(1000)
836            .metadata("tier", "enterprise")
837            .build();
838
839        assert_eq!(config.id.as_str(), "tenant_a");
840        assert_eq!(config.name, "Acme Corp");
841        assert_eq!(config.pool.max_connections, 50);
842        assert_eq!(config.rate_limits.qps_limit, 1000);
843        assert_eq!(config.metadata.get("tier"), Some(&"enterprise".to_string()));
844    }
845
846    #[test]
847    fn test_tenant_permissions() {
848        let default = TenantPermissions::default();
849        assert!(default.is_operation_allowed("SELECT"));
850        assert!(default.is_operation_allowed("select"));
851        assert!(!default.is_operation_allowed("CREATE"));
852        assert!(!default.allow_ddl);
853
854        let read_only = TenantPermissions::read_only();
855        assert!(read_only.is_operation_allowed("SELECT"));
856        assert!(!read_only.is_operation_allowed("INSERT"));
857        assert!(read_only.read_only);
858
859        let full = TenantPermissions::full_access();
860        assert!(full.is_operation_allowed("CREATE"));
861        assert!(full.allow_ddl);
862    }
863
864    #[test]
865    fn test_identification_methods() {
866        let header = IdentificationMethod::header("X-Tenant-Id");
867        assert!(matches!(header, IdentificationMethod::Header { header_name } if header_name == "X-Tenant-Id"));
868
869        let prefix = IdentificationMethod::username_prefix('.');
870        assert!(matches!(prefix, IdentificationMethod::UsernamePrefix { separator: '.' }));
871
872        let jwt = IdentificationMethod::jwt_claim("tenant_id");
873        assert!(matches!(jwt, IdentificationMethod::JwtClaim { claim_name, .. } if claim_name == "tenant_id"));
874    }
875
876    #[test]
877    fn test_multi_tenancy_config() {
878        let config = MultiTenancyConfig::enabled()
879            .with_identification(IdentificationMethod::header("X-Org-Id"))
880            .allow_unknown()
881            .auto_create();
882
883        assert!(config.enabled);
884        assert!(config.allow_unknown_tenants);
885        assert!(config.auto_create_tenants);
886    }
887}