prax_query/tenant/
config.rs

1//! Tenant configuration.
2
3use super::resolver::TenantResolver;
4use super::strategy::{DatabaseConfig, IsolationStrategy, RowLevelConfig, SchemaConfig};
5use std::sync::Arc;
6
7/// Configuration for multi-tenant support.
8#[derive(Clone)]
9pub struct TenantConfig {
10    /// The isolation strategy.
11    pub strategy: IsolationStrategy,
12    /// Whether tenant context is required for all queries.
13    pub require_tenant: bool,
14    /// Default tenant ID for queries without context.
15    pub default_tenant: Option<String>,
16    /// Allow superuser to bypass tenant filtering.
17    pub allow_bypass: bool,
18    /// Tenant resolver for dynamic tenant lookup.
19    pub resolver: Option<Arc<dyn TenantResolver>>,
20    /// Whether to enforce tenant on write operations.
21    pub enforce_on_writes: bool,
22    /// Whether to log tenant context with queries.
23    pub log_tenant_context: bool,
24}
25
26impl std::fmt::Debug for TenantConfig {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        f.debug_struct("TenantConfig")
29            .field("strategy", &self.strategy)
30            .field("require_tenant", &self.require_tenant)
31            .field("default_tenant", &self.default_tenant)
32            .field("allow_bypass", &self.allow_bypass)
33            .field("enforce_on_writes", &self.enforce_on_writes)
34            .field("log_tenant_context", &self.log_tenant_context)
35            .finish()
36    }
37}
38
39impl TenantConfig {
40    /// Create a row-level isolation config.
41    pub fn row_level(column: impl Into<String>) -> Self {
42        Self {
43            strategy: IsolationStrategy::row_level(column),
44            require_tenant: true,
45            default_tenant: None,
46            allow_bypass: true,
47            resolver: None,
48            enforce_on_writes: true,
49            log_tenant_context: false,
50        }
51    }
52
53    /// Create a schema-based isolation config.
54    pub fn schema_based() -> Self {
55        Self {
56            strategy: IsolationStrategy::schema_based(),
57            require_tenant: true,
58            default_tenant: None,
59            allow_bypass: true,
60            resolver: None,
61            enforce_on_writes: true,
62            log_tenant_context: false,
63        }
64    }
65
66    /// Create a database-based isolation config.
67    pub fn database_based() -> Self {
68        Self {
69            strategy: IsolationStrategy::database_based(),
70            require_tenant: true,
71            default_tenant: None,
72            allow_bypass: true,
73            resolver: None,
74            enforce_on_writes: true,
75            log_tenant_context: false,
76        }
77    }
78
79    /// Create a builder for advanced configuration.
80    pub fn builder() -> TenantConfigBuilder {
81        TenantConfigBuilder::default()
82    }
83
84    /// Set the default tenant.
85    pub fn with_default_tenant(mut self, tenant: impl Into<String>) -> Self {
86        self.default_tenant = Some(tenant.into());
87        self
88    }
89
90    /// Don't require tenant context (use default if missing).
91    pub fn optional(mut self) -> Self {
92        self.require_tenant = false;
93        self
94    }
95
96    /// Disable superuser bypass.
97    pub fn without_bypass(mut self) -> Self {
98        self.allow_bypass = false;
99        self
100    }
101
102    /// Set the tenant resolver.
103    pub fn with_resolver<R: TenantResolver + 'static>(mut self, resolver: R) -> Self {
104        self.resolver = Some(Arc::new(resolver));
105        self
106    }
107
108    /// Enable tenant context logging.
109    pub fn with_logging(mut self) -> Self {
110        self.log_tenant_context = true;
111        self
112    }
113
114    /// Get the row-level config.
115    pub fn row_level_config(&self) -> Option<&RowLevelConfig> {
116        self.strategy.row_level_config()
117    }
118
119    /// Get the schema config.
120    pub fn schema_config(&self) -> Option<&SchemaConfig> {
121        self.strategy.schema_config()
122    }
123
124    /// Get the database config.
125    pub fn database_config(&self) -> Option<&DatabaseConfig> {
126        self.strategy.database_config()
127    }
128}
129
130/// Builder for advanced tenant configuration.
131#[derive(Default)]
132pub struct TenantConfigBuilder {
133    strategy: Option<IsolationStrategy>,
134    require_tenant: bool,
135    default_tenant: Option<String>,
136    allow_bypass: bool,
137    resolver: Option<Arc<dyn TenantResolver>>,
138    enforce_on_writes: bool,
139    log_tenant_context: bool,
140}
141
142impl TenantConfigBuilder {
143    /// Create a new builder.
144    pub fn new() -> Self {
145        Self {
146            require_tenant: true,
147            allow_bypass: true,
148            enforce_on_writes: true,
149            ..Default::default()
150        }
151    }
152
153    /// Set the isolation strategy.
154    pub fn strategy(mut self, strategy: IsolationStrategy) -> Self {
155        self.strategy = Some(strategy);
156        self
157    }
158
159    /// Use row-level isolation.
160    pub fn row_level(mut self, config: RowLevelConfig) -> Self {
161        self.strategy = Some(IsolationStrategy::RowLevel(config));
162        self
163    }
164
165    /// Use schema-based isolation.
166    pub fn schema(mut self, config: SchemaConfig) -> Self {
167        self.strategy = Some(IsolationStrategy::Schema(config));
168        self
169    }
170
171    /// Use database-based isolation.
172    pub fn database(mut self, config: DatabaseConfig) -> Self {
173        self.strategy = Some(IsolationStrategy::Database(config));
174        self
175    }
176
177    /// Require tenant context.
178    pub fn require_tenant(mut self, require: bool) -> Self {
179        self.require_tenant = require;
180        self
181    }
182
183    /// Set the default tenant.
184    pub fn default_tenant(mut self, tenant: impl Into<String>) -> Self {
185        self.default_tenant = Some(tenant.into());
186        self
187    }
188
189    /// Allow bypass for superusers.
190    pub fn allow_bypass(mut self, allow: bool) -> Self {
191        self.allow_bypass = allow;
192        self
193    }
194
195    /// Set the tenant resolver.
196    pub fn resolver<R: TenantResolver + 'static>(mut self, resolver: R) -> Self {
197        self.resolver = Some(Arc::new(resolver));
198        self
199    }
200
201    /// Enforce tenant on writes.
202    pub fn enforce_on_writes(mut self, enforce: bool) -> Self {
203        self.enforce_on_writes = enforce;
204        self
205    }
206
207    /// Enable tenant context logging.
208    pub fn log_context(mut self, log: bool) -> Self {
209        self.log_tenant_context = log;
210        self
211    }
212
213    /// Build the config.
214    pub fn build(self) -> TenantConfig {
215        TenantConfig {
216            strategy: self
217                .strategy
218                .unwrap_or_else(|| IsolationStrategy::row_level("tenant_id")),
219            require_tenant: self.require_tenant,
220            default_tenant: self.default_tenant,
221            allow_bypass: self.allow_bypass,
222            resolver: self.resolver,
223            enforce_on_writes: self.enforce_on_writes,
224            log_tenant_context: self.log_tenant_context,
225        }
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    #[test]
234    fn test_row_level_config() {
235        let config = TenantConfig::row_level("org_id")
236            .with_default_tenant("default")
237            .with_logging();
238
239        assert!(config.strategy.is_row_level());
240        assert_eq!(config.default_tenant, Some("default".to_string()));
241        assert!(config.log_tenant_context);
242    }
243
244    #[test]
245    fn test_schema_config() {
246        let config = TenantConfig::schema_based().optional().without_bypass();
247
248        assert!(config.strategy.is_schema_based());
249        assert!(!config.require_tenant);
250        assert!(!config.allow_bypass);
251    }
252
253    #[test]
254    fn test_builder() {
255        let config = TenantConfig::builder()
256            .row_level(RowLevelConfig::new("tenant_id").with_database_rls())
257            .default_tenant("system")
258            .log_context(true)
259            .build();
260
261        assert!(config.strategy.is_row_level());
262        assert!(config.row_level_config().unwrap().use_database_rls);
263    }
264}