scim_server/providers/helpers/tenant.rs
1//! Multi-tenant context management helper trait.
2//!
3//! This module provides reusable functionality for managing multi-tenant context
4//! in SCIM ResourceProvider implementations. It handles tenant ID resolution,
5//! context validation, and tenant isolation patterns.
6//!
7//! # Multi-Tenant Patterns
8//!
9//! This implementation supports common multi-tenant patterns:
10//! - Single-tenant mode (no tenant context required)
11//! - Multi-tenant mode with explicit tenant identification
12//! - Tenant isolation with fallback to "default" tenant
13//! - Context validation and extraction
14//!
15//! # Usage
16//!
17//! ```rust,no_run
18//! // MultiTenantProvider provides helper methods for tenant isolation:
19//! // - effective_tenant_id(): Extract tenant ID from context
20//! // - tenant_scoped_key(): Generate tenant-specific storage keys
21//! // - tenant_scoped_prefix(): Generate tenant-specific prefixes
22//! // - generate_tenant_resource_id(): Generate tenant-scoped resource IDs
23//! //
24//! // When implemented by a ResourceProvider, enables automatic tenant isolation
25//! // across all operations without additional code
26//! ```
27
28use crate::providers::ResourceProvider;
29use crate::resource::{RequestContext, TenantContext};
30use uuid::Uuid;
31
32/// Trait providing multi-tenant context management functionality.
33///
34/// This trait extends ResourceProvider with multi-tenant capabilities including
35/// tenant ID resolution, context validation, and key generation for tenant isolation.
36/// Most implementers can use the default implementations which provide standard
37/// multi-tenant patterns.
38pub trait MultiTenantProvider: ResourceProvider {
39 /// Get the effective tenant ID for an operation.
40 ///
41 /// Resolves the tenant ID from the request context using standard patterns:
42 /// - If context has a tenant ID, use it
43 /// - If no tenant ID, fall back to "default" for single-tenant operations
44 /// - Ensures consistent tenant identification across operations
45 ///
46 /// # Arguments
47 /// * `context` - The request context containing tenant information
48 ///
49 /// # Returns
50 /// The effective tenant ID to use for the operation
51 ///
52 /// # Example
53 /// ```rust,no_run
54 /// use scim_server::resource::{RequestContext, TenantContext};
55 ///
56 /// let context = RequestContext::with_generated_id();
57 /// // MultiTenantProvider.effective_tenant_id(&context) returns: "default"
58 ///
59 /// let tenant_context = TenantContext::new("acme-corp".to_string(), "client-123".to_string());
60 /// let multi_context = RequestContext::with_tenant_generated_id(tenant_context);
61 /// // MultiTenantProvider.effective_tenant_id(&multi_context) returns: "acme-corp"
62 /// ```
63 fn effective_tenant_id(&self, context: &RequestContext) -> String {
64 context.tenant_id().unwrap_or("default").to_string()
65 }
66
67 /// Create a tenant-scoped storage key.
68 ///
69 /// Generates a storage key that includes tenant information for proper isolation.
70 /// This ensures resources from different tenants don't interfere with each other.
71 ///
72 /// # Arguments
73 /// * `tenant_id` - The tenant identifier
74 /// * `resource_type` - The type of resource (e.g., "Users", "Groups")
75 /// * `resource_id` - The unique identifier of the resource
76 ///
77 /// # Returns
78 /// A tenant-scoped key for storage operations
79 ///
80 /// # Example
81 /// ```rust,no_run
82 /// // MultiTenantProvider.tenant_scoped_key("acme-corp", "Users", "123")
83 /// // Returns: "tenant:acme-corp:Users:123"
84 /// //
85 /// // Used for generating tenant-specific storage keys that prevent
86 /// // cross-tenant data access
87 /// ```
88 fn tenant_scoped_key(&self, tenant_id: &str, resource_type: &str, resource_id: &str) -> String {
89 format!("tenant:{}:{}:{}", tenant_id, resource_type, resource_id)
90 }
91
92 /// Create a tenant-scoped prefix for listing operations.
93 ///
94 /// Generates a key prefix that can be used to list all resources of a given type
95 /// within a specific tenant, enabling efficient tenant-isolated queries.
96 ///
97 /// # Arguments
98 /// * `tenant_id` - The tenant identifier
99 /// * `resource_type` - The type of resource (e.g., "Users", "Groups")
100 ///
101 /// # Returns
102 /// A tenant-scoped prefix for listing operations
103 ///
104 /// # Example
105 /// ```rust,no_run
106 /// // MultiTenantProvider.tenant_scoped_prefix("acme-corp", "Users")
107 /// // Returns: "tenant:acme-corp:Users:"
108 /// //
109 /// // Used for generating tenant-specific prefixes for resource queries
110 /// // and bulk operations
111 /// ```
112 fn tenant_scoped_prefix(&self, tenant_id: &str, resource_type: &str) -> String {
113 format!("tenant:{}:{}:", tenant_id, resource_type)
114 }
115
116 /// Validate that a request context is appropriate for multi-tenant operations.
117 ///
118 /// Checks that the request context contains valid tenant information when
119 /// operating in multi-tenant mode. Can be used to enforce tenant requirements.
120 ///
121 /// # Arguments
122 /// * `context` - The request context to validate
123 /// * `require_tenant` - Whether a tenant ID is required (true for strict multi-tenant)
124 ///
125 /// # Returns
126 /// `true` if the context is valid for the tenant requirements
127 fn is_valid_tenant_context(&self, context: &RequestContext, require_tenant: bool) -> bool {
128 if require_tenant {
129 context.tenant_id().is_some()
130 } else {
131 true // Always valid in mixed-mode operations
132 }
133 }
134
135 /// Extract tenant context information from a request context.
136 ///
137 /// Retrieves the complete tenant context including tenant ID and client ID
138 /// if available, useful for detailed tenant tracking and auditing.
139 ///
140 /// # Arguments
141 /// * `context` - The request context to extract from
142 ///
143 /// # Returns
144 /// The tenant context if present, None for single-tenant operations
145 fn extract_tenant_context<'a>(&self, context: &'a RequestContext) -> Option<&'a TenantContext> {
146 context.tenant_context.as_ref()
147 }
148
149 /// Generate a unique resource ID within a tenant scope.
150 ///
151 /// Creates a unique identifier for a new resource within a specific tenant.
152 /// The default implementation uses UUIDs for uniqueness across tenants.
153 ///
154 /// # Arguments
155 /// * `tenant_id` - The tenant identifier (for context, not included in ID)
156 /// * `resource_type` - The type of resource (for context, not included in ID)
157 ///
158 /// # Returns
159 /// A unique resource identifier
160 ///
161 /// # Example
162 /// ```rust,no_run
163 /// // MultiTenantProvider.generate_tenant_resource_id("acme-corp", "Users")
164 /// // Returns: "123e4567-e89b-12d3-a456-426614174000" (UUID format)
165 /// //
166 /// // Generates globally unique IDs that are deterministically associated
167 /// // with the tenant and resource type for consistent resource identification
168 /// ```
169 fn generate_tenant_resource_id(&self, _tenant_id: &str, _resource_type: &str) -> String {
170 Uuid::new_v4().to_string()
171 }
172
173 /// Check if two request contexts belong to the same tenant.
174 ///
175 /// Compares tenant information between two request contexts to determine
176 /// if they represent operations within the same tenant scope.
177 ///
178 /// # Arguments
179 /// * `context1` - First request context
180 /// * `context2` - Second request context
181 ///
182 /// # Returns
183 /// `true` if both contexts belong to the same effective tenant
184 fn same_tenant(&self, context1: &RequestContext, context2: &RequestContext) -> bool {
185 self.effective_tenant_id(context1) == self.effective_tenant_id(context2)
186 }
187
188 /// Create a tenant-specific error message.
189 ///
190 /// Generates error messages that include tenant context for better debugging
191 /// and audit trails in multi-tenant environments.
192 ///
193 /// # Arguments
194 /// * `context` - The request context for tenant information
195 /// * `base_message` - The base error message
196 ///
197 /// # Returns
198 /// An error message with tenant context
199 fn tenant_error_message(&self, context: &RequestContext, base_message: &str) -> String {
200 let tenant_id = self.effective_tenant_id(context);
201 if tenant_id == "default" {
202 base_message.to_string()
203 } else {
204 format!("[Tenant: {}] {}", tenant_id, base_message)
205 }
206 }
207
208 /// Check if the provider is operating in single-tenant mode for a context.
209 ///
210 /// Determines whether a specific request context represents single-tenant
211 /// operation (no explicit tenant specified).
212 ///
213 /// # Arguments
214 /// * `context` - The request context to check
215 ///
216 /// # Returns
217 /// `true` if operating in single-tenant mode for this context
218 fn is_single_tenant_context(&self, context: &RequestContext) -> bool {
219 context.tenant_id().is_none()
220 }
221
222 /// Check if the provider is operating in multi-tenant mode for a context.
223 ///
224 /// Determines whether a specific request context represents multi-tenant
225 /// operation (explicit tenant specified).
226 ///
227 /// # Arguments
228 /// * `context` - The request context to check
229 ///
230 /// # Returns
231 /// `true` if operating in multi-tenant mode for this context
232 fn is_multi_tenant_context(&self, context: &RequestContext) -> bool {
233 context.tenant_id().is_some()
234 }
235
236 /// Get the client ID associated with a tenant context.
237 ///
238 /// Extracts the client identifier from a multi-tenant request context,
239 /// useful for client-specific operations or auditing.
240 ///
241 /// # Arguments
242 /// * `context` - The request context to extract from
243 ///
244 /// # Returns
245 /// The client ID if present in a multi-tenant context
246 fn get_client_id<'a>(&self, context: &'a RequestContext) -> Option<&'a str> {
247 context
248 .tenant_context
249 .as_ref()
250 .map(|tc| tc.client_id.as_str())
251 }
252
253 /// Normalize a tenant ID for consistent storage and comparison.
254 ///
255 /// Applies consistent formatting rules to tenant IDs to ensure
256 /// reliable storage key generation and tenant comparison.
257 ///
258 /// # Arguments
259 /// * `tenant_id` - The raw tenant ID to normalize
260 ///
261 /// # Returns
262 /// The normalized tenant ID
263 ///
264 /// # Default Behavior
265 /// - Converts to lowercase
266 /// - Trims whitespace
267 /// - Replaces spaces with hyphens
268 /// - Validates basic format
269 fn normalize_tenant_id(&self, tenant_id: &str) -> String {
270 tenant_id
271 .trim()
272 .to_lowercase()
273 .replace(' ', "-")
274 .chars()
275 .filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
276 .collect()
277 }
278
279 /// Validate that a tenant ID meets format requirements.
280 ///
281 /// Checks that a tenant identifier follows acceptable patterns for
282 /// use in storage keys and URL paths.
283 ///
284 /// # Arguments
285 /// * `tenant_id` - The tenant ID to validate
286 ///
287 /// # Returns
288 /// `true` if the tenant ID is valid for use
289 fn is_valid_tenant_id(&self, tenant_id: &str) -> bool {
290 !tenant_id.trim().is_empty()
291 && tenant_id.len() <= 64
292 && tenant_id
293 .chars()
294 .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
295 && !tenant_id.starts_with('-')
296 && !tenant_id.ends_with('-')
297 }
298}
299
300/// Default implementation for any ResourceProvider
301impl<T: ResourceProvider> MultiTenantProvider for T {}