sz_orm_core/
tenant_context.rs1use crate::pool::{Pool, PoolConfig};
10use crate::tenant_security::{ColumnMaskingRule, RowLevelSecurityPolicy};
11use crate::PoolError;
12use std::collections::HashMap;
13use std::sync::Arc;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum IsolationStrategy {
23 RowLevel,
25 SchemaIsolation,
27}
28
29#[derive(Debug, Clone, Default)]
31pub struct TenantPermissions {
32 pub row_level_policies: Vec<RowLevelSecurityPolicy>,
34 pub column_masking_rules: Vec<ColumnMaskingRule>,
36 pub roles: Vec<String>,
38}
39
40impl TenantPermissions {
41 pub fn new() -> Self {
43 Self::default()
44 }
45
46 pub fn with_row_level_policy(mut self, policy: RowLevelSecurityPolicy) -> Self {
48 self.row_level_policies.push(policy);
49 self
50 }
51
52 pub fn with_column_masking_rule(mut self, rule: ColumnMaskingRule) -> Self {
54 self.column_masking_rules.push(rule);
55 self
56 }
57
58 pub fn with_roles(mut self, roles: Vec<String>) -> Self {
60 self.roles = roles;
61 self
62 }
63}
64
65std::thread_local! {
67 static TENANT_CONTEXT_THREAD: std::cell::RefCell<Option<TenantContext>> = const { std::cell::RefCell::new(None) };
68}
69
70tokio::task_local! {
72 static TENANT_CONTEXT_TASK: std::cell::RefCell<Option<TenantContext>>;
73}
74
75#[derive(Debug, Clone)]
80pub struct TenantContext {
81 pub tenant_id: i64,
83 pub isolation_strategy: IsolationStrategy,
85 pub permissions: TenantPermissions,
87}
88
89impl TenantContext {
90 pub fn new(tenant_id: i64, isolation_strategy: IsolationStrategy) -> Self {
92 Self {
93 tenant_id,
94 isolation_strategy,
95 permissions: TenantPermissions::new(),
96 }
97 }
98
99 pub fn with_permissions(mut self, permissions: TenantPermissions) -> Self {
101 self.permissions = permissions;
102 self
103 }
104
105 pub fn enter(self) -> TenantContextGuard {
112 TenantContextGuard::enter(self)
113 }
114
115 pub async fn scope<F, R>(self, f: F) -> R
131 where
132 F: std::future::Future<Output = R>,
133 {
134 TENANT_CONTEXT_TASK
135 .scope(std::cell::RefCell::new(Some(self)), f)
136 .await
137 }
138
139 pub fn current() -> Option<TenantContext> {
144 if let Some(ctx) = TENANT_CONTEXT_TASK
146 .try_with(|cell| cell.borrow().clone())
147 .ok()
148 .flatten()
149 {
150 return Some(ctx);
151 }
152 TENANT_CONTEXT_THREAD.with(|cell| cell.borrow().clone())
154 }
155
156 pub fn is_set() -> bool {
158 Self::current().is_some()
159 }
160}
161
162pub struct TenantContextGuard {
167 _prev: Option<TenantContext>,
168}
169
170impl TenantContextGuard {
171 fn enter(context: TenantContext) -> Self {
172 let prev = TENANT_CONTEXT_THREAD.with(|cell| cell.borrow().clone());
173 TENANT_CONTEXT_THREAD.with(|cell| {
174 *cell.borrow_mut() = Some(context);
175 });
176 Self { _prev: prev }
177 }
178}
179
180impl Drop for TenantContextGuard {
181 fn drop(&mut self) {
182 TENANT_CONTEXT_THREAD.with(|cell| {
183 *cell.borrow_mut() = self._prev.take();
184 });
185 }
186}
187
188pub struct SchemaIsolationRouter;
195
196impl SchemaIsolationRouter {
197 pub fn rewrite_table(table: &str, tenant_id: i64) -> String {
206 format!("tenant_{}_{}", tenant_id, table)
207 }
208
209 pub fn rewrite_tables(tables: &[&str], tenant_id: i64) -> Vec<String> {
211 tables
212 .iter()
213 .map(|t| Self::rewrite_table(t, tenant_id))
214 .collect()
215 }
216}
217
218pub struct TenantPoolRegistry {
225 pools: parking_lot::RwLock<HashMap<i64, Arc<Pool>>>,
226 pool_config: PoolConfig,
227}
228
229impl TenantPoolRegistry {
230 pub fn new(pool_config: PoolConfig) -> Self {
232 Self {
233 pools: parking_lot::RwLock::new(HashMap::new()),
234 pool_config,
235 }
236 }
237
238 pub fn get_or_create(
243 &self,
244 tenant_id: i64,
245 factory: &Arc<dyn crate::pool::ConnectionFactory>,
246 ) -> Result<Arc<Pool>, PoolError> {
247 {
249 let pools = self.pools.read();
250 if let Some(pool) = pools.get(&tenant_id) {
251 return Ok(Arc::clone(pool));
252 }
253 }
254
255 let mut pools = self.pools.write();
257 if let Some(pool) = pools.get(&tenant_id) {
259 return Ok(Arc::clone(pool));
260 }
261
262 let pool = Arc::new(Pool::new(self.pool_config.clone(), Arc::clone(factory))?);
263 pools.insert(tenant_id, Arc::clone(&pool));
264 Ok(pool)
265 }
266
267 pub fn switch(
271 &self,
272 tenant_id: i64,
273 factory: &Arc<dyn crate::pool::ConnectionFactory>,
274 ) -> Result<TenantPoolGuard<'_>, PoolError> {
275 let new_pool = self.get_or_create(tenant_id, factory)?;
276 Ok(TenantPoolGuard {
277 registry: self,
278 new_pool,
279 })
280 }
281
282 pub fn tenant_count(&self) -> usize {
284 self.pools.read().len()
285 }
286
287 pub fn config(&self) -> &PoolConfig {
289 &self.pool_config
290 }
291}
292
293pub struct TenantPoolGuard<'a> {
298 registry: &'a TenantPoolRegistry,
299 new_pool: Arc<Pool>,
300}
301
302impl<'a> TenantPoolGuard<'a> {
303 pub fn pool(&self) -> &Arc<Pool> {
305 &self.new_pool
306 }
307
308 pub fn registry(&self) -> &TenantPoolRegistry {
310 self.registry
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 #[tokio::test]
321 async fn test_tenant_context_enter_and_current() {
322 let ctx = TenantContext::new(42, IsolationStrategy::RowLevel);
323 let guard = ctx.enter();
324 let current = TenantContext::current();
325 assert!(current.is_some());
326 assert_eq!(current.unwrap().tenant_id, 42);
327 drop(guard);
328 assert!(TenantContext::current().is_none());
330 }
331
332 #[tokio::test]
333 async fn test_tenant_context_not_set() {
334 let ctx = TenantContext::new(99, IsolationStrategy::RowLevel);
337 ctx.scope(async {
338 assert!(TenantContext::is_set());
339 })
340 .await;
341 }
343
344 #[tokio::test]
345 async fn test_tenant_context_is_set() {
346 let ctx = TenantContext::new(7, IsolationStrategy::SchemaIsolation);
347 ctx.scope(async {
348 assert!(TenantContext::is_set());
349 assert_eq!(TenantContext::current().unwrap().tenant_id, 7);
350 })
351 .await;
352 }
353
354 #[tokio::test]
355 async fn test_tenant_context_nested_scope() {
356 let ctx_a = TenantContext::new(1, IsolationStrategy::RowLevel);
357 ctx_a
358 .scope(async {
359 assert_eq!(TenantContext::current().unwrap().tenant_id, 1);
360
361 let ctx_b = TenantContext::new(2, IsolationStrategy::RowLevel);
362 ctx_b
363 .scope(async {
364 assert_eq!(TenantContext::current().unwrap().tenant_id, 2);
365 })
366 .await;
367
368 assert_eq!(TenantContext::current().unwrap().tenant_id, 1);
370 })
371 .await;
372 }
373
374 #[tokio::test]
375 async fn test_tenant_context_async_isolation() {
376 let ctx_a = TenantContext::new(1, IsolationStrategy::RowLevel);
378 let ctx_b = TenantContext::new(2, IsolationStrategy::RowLevel);
379
380 let handle_a = tokio::spawn(async move {
381 ctx_a
382 .scope(async {
383 tokio::task::yield_now().await;
384 TenantContext::current().unwrap().tenant_id
385 })
386 .await
387 });
388
389 let handle_b = tokio::spawn(async move {
390 ctx_b
391 .scope(async {
392 tokio::task::yield_now().await;
393 TenantContext::current().unwrap().tenant_id
394 })
395 .await
396 });
397
398 let (id_a, id_b) = tokio::join!(handle_a, handle_b);
399 assert_eq!(id_a.unwrap(), 1);
400 assert_eq!(id_b.unwrap(), 2);
401 }
402
403 #[test]
406 fn test_schema_isolation_router_rewrite() {
407 assert_eq!(
408 SchemaIsolationRouter::rewrite_table("users", 42),
409 "tenant_42_users"
410 );
411 assert_eq!(
412 SchemaIsolationRouter::rewrite_table("orders", 1),
413 "tenant_1_orders"
414 );
415 }
416
417 #[test]
418 fn test_schema_isolation_router_different_tenants() {
419 let table_a = SchemaIsolationRouter::rewrite_table("users", 1);
420 let table_b = SchemaIsolationRouter::rewrite_table("users", 2);
421 assert_ne!(table_a, table_b);
422 assert_eq!(table_a, "tenant_1_users");
423 assert_eq!(table_b, "tenant_2_users");
424 }
425
426 #[test]
427 fn test_schema_isolation_router_rewrite_tables() {
428 let rewritten = SchemaIsolationRouter::rewrite_tables(&["users", "orders"], 42);
429 assert_eq!(rewritten, vec!["tenant_42_users", "tenant_42_orders"]);
430 }
431
432 #[test]
435 fn test_tenant_permissions_default() {
436 let perms = TenantPermissions::new();
437 assert!(perms.row_level_policies.is_empty());
438 assert!(perms.column_masking_rules.is_empty());
439 assert!(perms.roles.is_empty());
440 }
441
442 #[test]
443 fn test_isolation_strategy_copy() {
444 let strategy = IsolationStrategy::RowLevel;
445 let copied = strategy;
446 assert_eq!(strategy, copied);
447 }
448}