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
393 .blocked_tables
394 .iter()
395 .any(|t| t.eq_ignore_ascii_case(table))
396 }
397}
398
399#[derive(Debug, Clone)]
401pub struct TenantAiConfig {
402 pub knowledge_base: Option<String>,
404
405 pub embedding_model: String,
407
408 pub retrieval_limit: u32,
410
411 pub daily_token_budget: Option<u64>,
413
414 pub agent_workspace_enabled: bool,
416
417 pub max_concurrent_agents: u32,
419}
420
421impl Default for TenantAiConfig {
422 fn default() -> Self {
423 Self {
424 knowledge_base: None,
425 embedding_model: "default".to_string(),
426 retrieval_limit: 10,
427 daily_token_budget: None,
428 agent_workspace_enabled: true,
429 max_concurrent_agents: 5,
430 }
431 }
432}
433
434#[derive(Debug, Clone)]
436pub struct TenantConfig {
437 pub id: TenantId,
439
440 pub name: String,
442
443 pub isolation: IsolationStrategy,
445
446 pub rate_limits: TenantRateLimits,
448
449 pub pool: TenantPoolConfig,
451
452 pub permissions: TenantPermissions,
454
455 pub ai_config: TenantAiConfig,
457
458 pub metadata: HashMap<String, String>,
460
461 pub enabled: bool,
463
464 pub created_at: std::time::SystemTime,
466}
467
468impl TenantConfig {
469 pub fn builder() -> TenantConfigBuilder {
471 TenantConfigBuilder::new()
472 }
473
474 pub fn new(id: impl Into<TenantId>, isolation: IsolationStrategy) -> Self {
476 Self {
477 id: id.into(),
478 name: String::new(),
479 isolation,
480 rate_limits: TenantRateLimits::default(),
481 pool: TenantPoolConfig::default(),
482 permissions: TenantPermissions::default(),
483 ai_config: TenantAiConfig::default(),
484 metadata: HashMap::new(),
485 enabled: true,
486 created_at: std::time::SystemTime::now(),
487 }
488 }
489
490 pub fn is_healthy(&self) -> bool {
492 self.enabled
493 }
494
495 pub fn effective_max_connections(&self) -> u32 {
497 self.pool
498 .max_connections
499 .min(self.rate_limits.max_connections)
500 }
501}
502
503#[derive(Debug, Default)]
505pub struct TenantConfigBuilder {
506 id: Option<TenantId>,
507 name: Option<String>,
508 isolation: Option<IsolationStrategy>,
509 rate_limits: Option<TenantRateLimits>,
510 pool: Option<TenantPoolConfig>,
511 permissions: Option<TenantPermissions>,
512 ai_config: Option<TenantAiConfig>,
513 metadata: HashMap<String, String>,
514 enabled: bool,
515}
516
517impl TenantConfigBuilder {
518 pub fn new() -> Self {
520 Self {
521 enabled: true,
522 ..Default::default()
523 }
524 }
525
526 pub fn id(mut self, id: impl Into<TenantId>) -> Self {
528 self.id = Some(id.into());
529 self
530 }
531
532 pub fn name(mut self, name: impl Into<String>) -> Self {
534 self.name = Some(name.into());
535 self
536 }
537
538 pub fn isolation(mut self, strategy: IsolationStrategy) -> Self {
540 self.isolation = Some(strategy);
541 self
542 }
543
544 pub fn database_isolation(self, database: impl Into<String>) -> Self {
546 self.isolation(IsolationStrategy::database(database))
547 }
548
549 pub fn schema_isolation(self, database: impl Into<String>, schema: impl Into<String>) -> Self {
551 self.isolation(IsolationStrategy::schema(database, schema))
552 }
553
554 pub fn row_isolation(self, database: impl Into<String>, column: impl Into<String>) -> Self {
556 self.isolation(IsolationStrategy::row(database, column))
557 }
558
559 pub fn branch_isolation(self, branch: impl Into<String>) -> Self {
561 self.isolation(IsolationStrategy::branch(branch))
562 }
563
564 pub fn rate_limits(mut self, limits: TenantRateLimits) -> Self {
566 self.rate_limits = Some(limits);
567 self
568 }
569
570 pub fn qps_limit(mut self, limit: u32) -> Self {
572 let mut limits = self.rate_limits.take().unwrap_or_default();
573 limits.qps_limit = limit;
574 self.rate_limits = Some(limits);
575 self
576 }
577
578 pub fn max_connections(mut self, max: u32) -> Self {
580 let mut pool = self.pool.take().unwrap_or_default();
581 pool.max_connections = max;
582 self.pool = Some(pool);
583
584 let mut limits = self.rate_limits.take().unwrap_or_default();
585 limits.max_connections = max;
586 self.rate_limits = Some(limits);
587 self
588 }
589
590 pub fn pool(mut self, config: TenantPoolConfig) -> Self {
592 self.pool = Some(config);
593 self
594 }
595
596 pub fn permissions(mut self, perms: TenantPermissions) -> Self {
598 self.permissions = Some(perms);
599 self
600 }
601
602 pub fn read_only(self) -> Self {
604 self.permissions(TenantPermissions::read_only())
605 }
606
607 pub fn ai_config(mut self, config: TenantAiConfig) -> Self {
609 self.ai_config = Some(config);
610 self
611 }
612
613 pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
615 self.metadata.insert(key.into(), value.into());
616 self
617 }
618
619 pub fn enabled(mut self, enabled: bool) -> Self {
621 self.enabled = enabled;
622 self
623 }
624
625 pub fn build(self) -> TenantConfig {
627 TenantConfig {
628 id: self.id.expect("tenant id is required"),
629 name: self.name.unwrap_or_default(),
630 isolation: self.isolation.expect("isolation strategy is required"),
631 rate_limits: self.rate_limits.unwrap_or_default(),
632 pool: self.pool.unwrap_or_default(),
633 permissions: self.permissions.unwrap_or_default(),
634 ai_config: self.ai_config.unwrap_or_default(),
635 metadata: self.metadata,
636 enabled: self.enabled,
637 created_at: std::time::SystemTime::now(),
638 }
639 }
640}
641
642#[derive(Debug, Clone)]
644pub enum IdentificationMethod {
645 Header {
647 header_name: String,
649 },
650
651 UsernamePrefix {
653 separator: char,
655 },
656
657 JwtClaim {
659 claim_name: String,
661 issuer: Option<String>,
663 },
664
665 DatabaseName,
667
668 SqlContext {
670 variable_name: String,
672 },
673}
674
675impl Default for IdentificationMethod {
676 fn default() -> Self {
677 Self::Header {
678 header_name: "X-Tenant-Id".to_string(),
679 }
680 }
681}
682
683impl IdentificationMethod {
684 pub fn header(name: impl Into<String>) -> Self {
686 Self::Header {
687 header_name: name.into(),
688 }
689 }
690
691 pub fn username_prefix(separator: char) -> Self {
693 Self::UsernamePrefix { separator }
694 }
695
696 pub fn jwt_claim(claim: impl Into<String>) -> Self {
698 Self::JwtClaim {
699 claim_name: claim.into(),
700 issuer: None,
701 }
702 }
703
704 pub fn database_name() -> Self {
706 Self::DatabaseName
707 }
708
709 pub fn sql_context(variable: impl Into<String>) -> Self {
711 Self::SqlContext {
712 variable_name: variable.into(),
713 }
714 }
715}
716
717#[derive(Debug, Clone)]
719pub struct MultiTenancyConfig {
720 pub enabled: bool,
722
723 pub identification: IdentificationMethod,
725
726 pub default_config: TenantConfig,
728
729 pub allow_unknown_tenants: bool,
731
732 pub auto_create_tenants: bool,
734
735 pub max_tenants: u32,
737
738 pub cross_tenant_analytics: bool,
740
741 pub admin_user_pattern: Option<String>,
743}
744
745impl Default for MultiTenancyConfig {
746 fn default() -> Self {
747 Self {
748 enabled: false,
749 identification: IdentificationMethod::default(),
750 default_config: TenantConfig::new(
751 TenantId::new("default"),
752 IsolationStrategy::schema("public", "public"),
753 ),
754 allow_unknown_tenants: false,
755 auto_create_tenants: false,
756 max_tenants: 1000,
757 cross_tenant_analytics: false,
758 admin_user_pattern: None,
759 }
760 }
761}
762
763impl MultiTenancyConfig {
764 pub fn enabled() -> Self {
766 Self {
767 enabled: true,
768 ..Default::default()
769 }
770 }
771
772 pub fn with_identification(mut self, method: IdentificationMethod) -> Self {
774 self.identification = method;
775 self
776 }
777
778 pub fn with_default_config(mut self, config: TenantConfig) -> Self {
780 self.default_config = config;
781 self
782 }
783
784 pub fn allow_unknown(mut self) -> Self {
786 self.allow_unknown_tenants = true;
787 self
788 }
789
790 pub fn auto_create(mut self) -> Self {
792 self.auto_create_tenants = true;
793 self
794 }
795}
796
797#[cfg(test)]
798mod tests {
799 use super::*;
800
801 #[test]
802 fn test_tenant_id() {
803 let id = TenantId::new("test_tenant");
804 assert_eq!(id.as_str(), "test_tenant");
805 assert_eq!(id.to_string(), "test_tenant");
806
807 let id2: TenantId = "another".into();
808 assert_eq!(id2.as_str(), "another");
809 }
810
811 #[test]
812 fn test_isolation_strategy() {
813 let db = IsolationStrategy::database("mydb");
814 assert_eq!(db.database_name(), Some("mydb"));
815 assert_eq!(db.strategy_name(), "database");
816 assert!(db.requires_connection_routing());
817 assert!(!db.requires_query_transform());
818
819 let schema = IsolationStrategy::schema("mydb", "myschema");
820 assert_eq!(schema.database_name(), Some("mydb"));
821 assert_eq!(schema.schema_name(), Some("myschema"));
822 assert_eq!(schema.strategy_name(), "schema");
823
824 let row = IsolationStrategy::row("mydb", "tenant_id");
825 assert_eq!(row.tenant_column(), Some("tenant_id"));
826 assert!(row.requires_query_transform());
827
828 let branch = IsolationStrategy::branch("tenant_branch");
829 assert_eq!(branch.branch_name(), Some("tenant_branch"));
830 assert!(branch.requires_connection_routing());
831 }
832
833 #[test]
834 fn test_tenant_config_builder() {
835 let config = TenantConfig::builder()
836 .id("tenant_a")
837 .name("Acme Corp")
838 .schema_isolation("shared_db", "tenant_a")
839 .max_connections(50)
840 .qps_limit(1000)
841 .metadata("tier", "enterprise")
842 .build();
843
844 assert_eq!(config.id.as_str(), "tenant_a");
845 assert_eq!(config.name, "Acme Corp");
846 assert_eq!(config.pool.max_connections, 50);
847 assert_eq!(config.rate_limits.qps_limit, 1000);
848 assert_eq!(config.metadata.get("tier"), Some(&"enterprise".to_string()));
849 }
850
851 #[test]
852 fn test_tenant_permissions() {
853 let default = TenantPermissions::default();
854 assert!(default.is_operation_allowed("SELECT"));
855 assert!(default.is_operation_allowed("select"));
856 assert!(!default.is_operation_allowed("CREATE"));
857 assert!(!default.allow_ddl);
858
859 let read_only = TenantPermissions::read_only();
860 assert!(read_only.is_operation_allowed("SELECT"));
861 assert!(!read_only.is_operation_allowed("INSERT"));
862 assert!(read_only.read_only);
863
864 let full = TenantPermissions::full_access();
865 assert!(full.is_operation_allowed("CREATE"));
866 assert!(full.allow_ddl);
867 }
868
869 #[test]
870 fn test_identification_methods() {
871 let header = IdentificationMethod::header("X-Tenant-Id");
872 assert!(
873 matches!(header, IdentificationMethod::Header { header_name } if header_name == "X-Tenant-Id")
874 );
875
876 let prefix = IdentificationMethod::username_prefix('.');
877 assert!(matches!(
878 prefix,
879 IdentificationMethod::UsernamePrefix { separator: '.' }
880 ));
881
882 let jwt = IdentificationMethod::jwt_claim("tenant_id");
883 assert!(
884 matches!(jwt, IdentificationMethod::JwtClaim { claim_name, .. } if claim_name == "tenant_id")
885 );
886 }
887
888 #[test]
889 fn test_multi_tenancy_config() {
890 let config = MultiTenancyConfig::enabled()
891 .with_identification(IdentificationMethod::header("X-Org-Id"))
892 .allow_unknown()
893 .auto_create();
894
895 assert!(config.enabled);
896 assert!(config.allow_unknown_tenants);
897 assert!(config.auto_create_tenants);
898 }
899}