scim_server/multi_tenant/
adapter.rs

1//! Provider adapter utilities for the unified ResourceProvider trait.
2//!
3//! This module provides utilities for working with the unified ResourceProvider trait
4//! that supports both single and multi-tenant operations through the RequestContext.
5//!
6//! Since the ResourceProvider is now unified, these are primarily validation and
7//! convenience utilities rather than true adapters.
8
9use crate::resource::{ListQuery, RequestContext, Resource, ResourceProvider, TenantContext};
10use serde_json::Value;
11use std::future::Future;
12
13/// Error types for adapter operations.
14#[derive(Debug, thiserror::Error)]
15pub enum AdapterError<E> {
16    /// Error from the underlying provider
17    #[error("Provider error: {0}")]
18    Provider(#[source] E),
19
20    /// Tenant validation error
21    #[error("Tenant validation error: {message}")]
22    TenantValidation { message: String },
23
24    /// Context conversion error
25    #[error("Context conversion error: {message}")]
26    ContextConversion { message: String },
27}
28
29/// Validation wrapper that ensures tenant context is properly handled.
30///
31/// This wrapper validates tenant contexts and provides clear error messages
32/// when operations are performed with incorrect tenant contexts.
33pub struct TenantValidatingProvider<P> {
34    inner: P,
35}
36
37impl<P> TenantValidatingProvider<P> {
38    /// Create a new validating provider wrapper.
39    pub fn new(provider: P) -> Self {
40        Self { inner: provider }
41    }
42
43    /// Get reference to the inner provider.
44    pub fn inner(&self) -> &P {
45        &self.inner
46    }
47
48    /// Consume wrapper and return inner provider.
49    pub fn into_inner(self) -> P {
50        self.inner
51    }
52}
53
54impl<P> ResourceProvider for TenantValidatingProvider<P>
55where
56    P: ResourceProvider + Send + Sync,
57    P::Error: Send + Sync + 'static,
58{
59    type Error = AdapterError<P::Error>;
60
61    fn create_resource(
62        &self,
63        resource_type: &str,
64        data: Value,
65        context: &RequestContext,
66    ) -> impl Future<Output = Result<Resource, Self::Error>> + Send {
67        async move {
68            // Validate context consistency
69            self.validate_context_consistency(context)?;
70
71            self.inner
72                .create_resource(resource_type, data, context)
73                .await
74                .map_err(AdapterError::Provider)
75        }
76    }
77
78    fn get_resource(
79        &self,
80        resource_type: &str,
81        id: &str,
82        context: &RequestContext,
83    ) -> impl Future<Output = Result<Option<Resource>, Self::Error>> + Send {
84        async move {
85            self.validate_context_consistency(context)?;
86
87            self.inner
88                .get_resource(resource_type, id, context)
89                .await
90                .map_err(AdapterError::Provider)
91        }
92    }
93
94    fn update_resource(
95        &self,
96        resource_type: &str,
97        id: &str,
98        data: Value,
99        context: &RequestContext,
100    ) -> impl Future<Output = Result<Resource, Self::Error>> + Send {
101        async move {
102            self.validate_context_consistency(context)?;
103
104            self.inner
105                .update_resource(resource_type, id, data, context)
106                .await
107                .map_err(AdapterError::Provider)
108        }
109    }
110
111    fn delete_resource(
112        &self,
113        resource_type: &str,
114        id: &str,
115        context: &RequestContext,
116    ) -> impl Future<Output = Result<(), Self::Error>> + Send {
117        async move {
118            self.validate_context_consistency(context)?;
119
120            self.inner
121                .delete_resource(resource_type, id, context)
122                .await
123                .map_err(AdapterError::Provider)
124        }
125    }
126
127    fn list_resources(
128        &self,
129        resource_type: &str,
130        query: Option<&ListQuery>,
131        context: &RequestContext,
132    ) -> impl Future<Output = Result<Vec<Resource>, Self::Error>> + Send {
133        async move {
134            self.validate_context_consistency(context)?;
135
136            self.inner
137                .list_resources(resource_type, query, context)
138                .await
139                .map_err(AdapterError::Provider)
140        }
141    }
142
143    fn find_resource_by_attribute(
144        &self,
145        resource_type: &str,
146        attribute: &str,
147        value: &Value,
148        context: &RequestContext,
149    ) -> impl Future<Output = Result<Option<Resource>, Self::Error>> + Send {
150        async move {
151            self.validate_context_consistency(context)?;
152
153            self.inner
154                .find_resource_by_attribute(resource_type, attribute, value, context)
155                .await
156                .map_err(AdapterError::Provider)
157        }
158    }
159
160    fn resource_exists(
161        &self,
162        resource_type: &str,
163        id: &str,
164        context: &RequestContext,
165    ) -> impl Future<Output = Result<bool, Self::Error>> + Send {
166        async move {
167            self.validate_context_consistency(context)?;
168
169            self.inner
170                .resource_exists(resource_type, id, context)
171                .await
172                .map_err(AdapterError::Provider)
173        }
174    }
175}
176
177// TenantValidator is implemented via blanket impl
178
179impl<P> TenantValidatingProvider<P>
180where
181    P: ResourceProvider,
182{
183    /// Validate that the request context is internally consistent.
184    fn validate_context_consistency(
185        &self,
186        context: &RequestContext,
187    ) -> Result<(), AdapterError<P::Error>> {
188        // Ensure request ID is not empty
189        if context.request_id.trim().is_empty() {
190            return Err(AdapterError::ContextConversion {
191                message: "Request ID cannot be empty".to_string(),
192            });
193        }
194
195        // Validate tenant context if present
196        if let Some(tenant_context) = &context.tenant_context {
197            if tenant_context.tenant_id.trim().is_empty() {
198                return Err(AdapterError::TenantValidation {
199                    message: "Tenant ID cannot be empty".to_string(),
200                });
201            }
202        }
203
204        Ok(())
205    }
206}
207
208/// Trait for converting providers to single-tenant mode (legacy compatibility).
209///
210/// Since ResourceProvider is now unified, this is mainly for API compatibility.
211pub trait ToSingleTenant<P> {
212    /// Convert to a provider that validates single-tenant contexts.
213    fn to_single_tenant(self) -> TenantValidatingProvider<P>;
214}
215
216impl<P> ToSingleTenant<P> for P
217where
218    P: ResourceProvider,
219{
220    fn to_single_tenant(self) -> TenantValidatingProvider<P> {
221        TenantValidatingProvider::new(self)
222    }
223}
224
225/// Legacy type alias for backward compatibility.
226///
227/// Note: With the unified ResourceProvider, this is now just a validation wrapper.
228pub type SingleTenantAdapter<P> = TenantValidatingProvider<P>;
229
230/// Context conversion utilities.
231pub struct ContextConverter;
232
233impl ContextConverter {
234    /// Create a single-tenant RequestContext.
235    pub fn single_tenant_context(request_id: Option<String>) -> RequestContext {
236        match request_id {
237            Some(id) => RequestContext::new(id),
238            None => RequestContext::with_generated_id(),
239        }
240    }
241
242    /// Create a multi-tenant RequestContext.
243    pub fn multi_tenant_context(
244        tenant_id: String,
245        client_id: Option<String>,
246        request_id: Option<String>,
247    ) -> RequestContext {
248        let tenant_context = TenantContext {
249            tenant_id,
250            client_id: client_id.unwrap_or_else(|| "default-client".to_string()),
251            permissions: Default::default(),
252            isolation_level: Default::default(),
253        };
254
255        match request_id {
256            Some(id) => RequestContext::with_tenant(id, tenant_context),
257            None => RequestContext::with_tenant_generated_id(tenant_context),
258        }
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    #[derive(Debug, thiserror::Error)]
267    #[error("Mock error")]
268    struct MockError;
269
270    struct MockProvider;
271
272    impl ResourceProvider for MockProvider {
273        type Error = MockError;
274
275        async fn create_resource(
276            &self,
277            _resource_type: &str,
278            _data: Value,
279            _context: &RequestContext,
280        ) -> Result<Resource, Self::Error> {
281            Err(MockError)
282        }
283
284        async fn get_resource(
285            &self,
286            _resource_type: &str,
287            _id: &str,
288            _context: &RequestContext,
289        ) -> Result<Option<Resource>, Self::Error> {
290            Ok(None)
291        }
292
293        async fn update_resource(
294            &self,
295            _resource_type: &str,
296            _id: &str,
297            _data: Value,
298            _context: &RequestContext,
299        ) -> Result<Resource, Self::Error> {
300            Err(MockError)
301        }
302
303        async fn delete_resource(
304            &self,
305            _resource_type: &str,
306            _id: &str,
307            _context: &RequestContext,
308        ) -> Result<(), Self::Error> {
309            Ok(())
310        }
311
312        async fn list_resources(
313            &self,
314            _resource_type: &str,
315            _query: Option<&ListQuery>,
316            _context: &RequestContext,
317        ) -> Result<Vec<Resource>, Self::Error> {
318            Ok(vec![])
319        }
320
321        async fn find_resource_by_attribute(
322            &self,
323            _resource_type: &str,
324            _attribute: &str,
325            _value: &Value,
326            _context: &RequestContext,
327        ) -> Result<Option<Resource>, Self::Error> {
328            Ok(None)
329        }
330
331        async fn resource_exists(
332            &self,
333            _resource_type: &str,
334            _id: &str,
335            _context: &RequestContext,
336        ) -> Result<bool, Self::Error> {
337            Ok(false)
338        }
339    }
340
341    #[tokio::test]
342    async fn test_validating_provider() {
343        let provider = MockProvider;
344        let validating_provider = TenantValidatingProvider::new(provider);
345
346        let context = RequestContext::with_generated_id();
347        let result = validating_provider
348            .get_resource("User", "123", &context)
349            .await;
350
351        assert!(result.is_ok());
352    }
353
354    #[tokio::test]
355    async fn test_context_validation() {
356        let provider = MockProvider;
357        let validating_provider = TenantValidatingProvider::new(provider);
358
359        // Empty request ID should fail
360        let context = RequestContext::new("".to_string());
361        let result = validating_provider
362            .get_resource("User", "123", &context)
363            .await;
364
365        assert!(result.is_err());
366        assert!(matches!(
367            result.unwrap_err(),
368            AdapterError::ContextConversion { .. }
369        ));
370    }
371
372    #[test]
373    fn test_context_converter() {
374        // Single-tenant context
375        let context = ContextConverter::single_tenant_context(Some("req-123".to_string()));
376        assert_eq!(context.request_id, "req-123");
377        assert!(context.tenant_context.is_none());
378
379        // Multi-tenant context
380        let context = ContextConverter::multi_tenant_context(
381            "tenant-1".to_string(),
382            Some("client-1".to_string()),
383            Some("req-456".to_string()),
384        );
385        assert_eq!(context.request_id, "req-456");
386        assert!(context.tenant_context.is_some());
387        assert_eq!(context.tenant_id(), Some("tenant-1"));
388    }
389
390    #[test]
391    fn test_to_single_tenant_trait() {
392        let provider = MockProvider;
393        let _validating_provider = provider.to_single_tenant();
394    }
395}