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