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 {}