1use std::collections::HashMap;
25use std::time::Duration;
26
27#[derive(Debug, Clone, PartialEq, Eq, Hash)]
29pub struct TenantId(pub String);
30
31impl TenantId {
32 pub fn new(id: impl Into<String>) -> Self {
34 Self(id.into())
35 }
36
37 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#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum IsolationStrategy {
64 Database {
66 database_name: String,
68 },
69
70 Schema {
72 database_name: String,
74 schema_name: String,
76 },
77
78 Row {
80 database_name: String,
82 tenant_column: String,
84 },
85
86 Branch {
88 branch_name: String,
90 },
91}
92
93impl IsolationStrategy {
94 pub fn database(name: impl Into<String>) -> Self {
96 Self::Database {
97 database_name: name.into(),
98 }
99 }
100
101 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 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 pub fn branch(name: impl Into<String>) -> Self {
119 Self::Branch {
120 branch_name: name.into(),
121 }
122 }
123
124 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 pub fn schema_name(&self) -> Option<&str> {
136 match self {
137 Self::Schema { schema_name, .. } => Some(schema_name),
138 _ => None,
139 }
140 }
141
142 pub fn tenant_column(&self) -> Option<&str> {
144 match self {
145 Self::Row { tenant_column, .. } => Some(tenant_column),
146 _ => None,
147 }
148 }
149
150 pub fn branch_name(&self) -> Option<&str> {
152 match self {
153 Self::Branch { branch_name } => Some(branch_name),
154 _ => None,
155 }
156 }
157
158 pub fn requires_query_transform(&self) -> bool {
160 matches!(self, Self::Row { .. })
161 }
162
163 pub fn requires_connection_routing(&self) -> bool {
165 matches!(self, Self::Database { .. } | Self::Branch { .. })
166 }
167
168 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#[derive(Debug, Clone)]
181pub struct TenantRateLimits {
182 pub qps_limit: u32,
184
185 pub max_connections: u32,
187
188 pub max_query_duration: Duration,
190
191 pub max_result_size: u64,
193
194 pub max_rows_per_query: u64,
196
197 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, max_rows_per_query: 100_000,
209 burst_multiplier: 2.0,
210 }
211 }
212}
213
214impl TenantRateLimits {
215 pub fn with_qps(qps: u32) -> Self {
217 Self {
218 qps_limit: qps,
219 ..Default::default()
220 }
221 }
222
223 pub fn qps_limit(mut self, limit: u32) -> Self {
225 self.qps_limit = limit;
226 self
227 }
228
229 pub fn max_connections(mut self, limit: u32) -> Self {
231 self.max_connections = limit;
232 self
233 }
234
235 pub fn max_query_duration(mut self, duration: Duration) -> Self {
237 self.max_query_duration = duration;
238 self
239 }
240
241 pub fn burst_multiplier(mut self, multiplier: f32) -> Self {
243 self.burst_multiplier = multiplier;
244 self
245 }
246}
247
248#[derive(Debug, Clone)]
250pub struct TenantPoolConfig {
251 pub max_connections: u32,
253
254 pub min_idle: u32,
256
257 pub idle_timeout: Duration,
259
260 pub max_lifetime: Duration,
262
263 pub acquire_timeout: Duration,
265
266 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 pub fn with_max_connections(max: u32) -> Self {
286 Self {
287 max_connections: max,
288 ..Default::default()
289 }
290 }
291
292 pub fn dedicated(mut self) -> Self {
294 self.dedicated_pool = true;
295 self
296 }
297
298 pub fn min_idle(mut self, min: u32) -> Self {
300 self.min_idle = min;
301 self
302 }
303
304 pub fn idle_timeout(mut self, timeout: Duration) -> Self {
306 self.idle_timeout = timeout;
307 self
308 }
309}
310
311#[derive(Debug, Clone)]
313pub struct TenantPermissions {
314 pub allowed_operations: Vec<String>,
316
317 pub blocked_tables: Vec<String>,
319
320 pub read_only: bool,
322
323 pub allow_ddl: bool,
325
326 pub allow_explain: bool,
328
329 pub allow_system_access: bool,
331
332 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 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 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 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 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#[derive(Debug, Clone)]
398pub struct TenantAiConfig {
399 pub knowledge_base: Option<String>,
401
402 pub embedding_model: String,
404
405 pub retrieval_limit: u32,
407
408 pub daily_token_budget: Option<u64>,
410
411 pub agent_workspace_enabled: bool,
413
414 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#[derive(Debug, Clone)]
433pub struct TenantConfig {
434 pub id: TenantId,
436
437 pub name: String,
439
440 pub isolation: IsolationStrategy,
442
443 pub rate_limits: TenantRateLimits,
445
446 pub pool: TenantPoolConfig,
448
449 pub permissions: TenantPermissions,
451
452 pub ai_config: TenantAiConfig,
454
455 pub metadata: HashMap<String, String>,
457
458 pub enabled: bool,
460
461 pub created_at: std::time::SystemTime,
463}
464
465impl TenantConfig {
466 pub fn builder() -> TenantConfigBuilder {
468 TenantConfigBuilder::new()
469 }
470
471 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 pub fn is_healthy(&self) -> bool {
489 self.enabled
490 }
491
492 pub fn effective_max_connections(&self) -> u32 {
494 self.pool.max_connections.min(self.rate_limits.max_connections)
495 }
496}
497
498#[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 pub fn new() -> Self {
515 Self {
516 enabled: true,
517 ..Default::default()
518 }
519 }
520
521 pub fn id(mut self, id: impl Into<TenantId>) -> Self {
523 self.id = Some(id.into());
524 self
525 }
526
527 pub fn name(mut self, name: impl Into<String>) -> Self {
529 self.name = Some(name.into());
530 self
531 }
532
533 pub fn isolation(mut self, strategy: IsolationStrategy) -> Self {
535 self.isolation = Some(strategy);
536 self
537 }
538
539 pub fn database_isolation(self, database: impl Into<String>) -> Self {
541 self.isolation(IsolationStrategy::database(database))
542 }
543
544 pub fn schema_isolation(self, database: impl Into<String>, schema: impl Into<String>) -> Self {
546 self.isolation(IsolationStrategy::schema(database, schema))
547 }
548
549 pub fn row_isolation(self, database: impl Into<String>, column: impl Into<String>) -> Self {
551 self.isolation(IsolationStrategy::row(database, column))
552 }
553
554 pub fn branch_isolation(self, branch: impl Into<String>) -> Self {
556 self.isolation(IsolationStrategy::branch(branch))
557 }
558
559 pub fn rate_limits(mut self, limits: TenantRateLimits) -> Self {
561 self.rate_limits = Some(limits);
562 self
563 }
564
565 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 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 pub fn pool(mut self, config: TenantPoolConfig) -> Self {
587 self.pool = Some(config);
588 self
589 }
590
591 pub fn permissions(mut self, perms: TenantPermissions) -> Self {
593 self.permissions = Some(perms);
594 self
595 }
596
597 pub fn read_only(self) -> Self {
599 self.permissions(TenantPermissions::read_only())
600 }
601
602 pub fn ai_config(mut self, config: TenantAiConfig) -> Self {
604 self.ai_config = Some(config);
605 self
606 }
607
608 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 pub fn enabled(mut self, enabled: bool) -> Self {
616 self.enabled = enabled;
617 self
618 }
619
620 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#[derive(Debug, Clone)]
639pub enum IdentificationMethod {
640 Header {
642 header_name: String,
644 },
645
646 UsernamePrefix {
648 separator: char,
650 },
651
652 JwtClaim {
654 claim_name: String,
656 issuer: Option<String>,
658 },
659
660 DatabaseName,
662
663 SqlContext {
665 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 pub fn header(name: impl Into<String>) -> Self {
681 Self::Header {
682 header_name: name.into(),
683 }
684 }
685
686 pub fn username_prefix(separator: char) -> Self {
688 Self::UsernamePrefix { separator }
689 }
690
691 pub fn jwt_claim(claim: impl Into<String>) -> Self {
693 Self::JwtClaim {
694 claim_name: claim.into(),
695 issuer: None,
696 }
697 }
698
699 pub fn database_name() -> Self {
701 Self::DatabaseName
702 }
703
704 pub fn sql_context(variable: impl Into<String>) -> Self {
706 Self::SqlContext {
707 variable_name: variable.into(),
708 }
709 }
710}
711
712#[derive(Debug, Clone)]
714pub struct MultiTenancyConfig {
715 pub enabled: bool,
717
718 pub identification: IdentificationMethod,
720
721 pub default_config: TenantConfig,
723
724 pub allow_unknown_tenants: bool,
726
727 pub auto_create_tenants: bool,
729
730 pub max_tenants: u32,
732
733 pub cross_tenant_analytics: bool,
735
736 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 pub fn enabled() -> Self {
761 Self {
762 enabled: true,
763 ..Default::default()
764 }
765 }
766
767 pub fn with_identification(mut self, method: IdentificationMethod) -> Self {
769 self.identification = method;
770 self
771 }
772
773 pub fn with_default_config(mut self, config: TenantConfig) -> Self {
775 self.default_config = config;
776 self
777 }
778
779 pub fn allow_unknown(mut self) -> Self {
781 self.allow_unknown_tenants = true;
782 self
783 }
784
785 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}