scim_server/resource/
provider.rs

1//! Resource provider trait for implementing SCIM data access.
2//!
3//! This module defines the core trait that users must implement to provide
4//! data storage and retrieval for SCIM resources. The design is async-first
5//! and provides comprehensive error handling with built-in ETag concurrency control.
6//!
7//! ## ETag Concurrency Control
8//!
9//! All ResourceProvider implementations automatically support conditional operations
10//! for optimistic concurrency control. The trait provides default implementations
11//! that work with any storage backend.
12//!
13//! ## Multi-Tenant Support
14//!
15//! The unified ResourceProvider supports both single-tenant and multi-tenant
16//! operations through the RequestContext. Single-tenant operations use
17//! context.tenant_context = None, while multi-tenant operations provide
18//! tenant information in context.tenant_context = Some(tenant_context).
19//!
20//! ## Example Implementation
21//!
22//! ```rust,no_run
23//! use scim_server::resource::{
24//!     provider::ResourceProvider,
25//!     core::{RequestContext, Resource, ListQuery},
26//!     version::{ScimVersion, ConditionalResult},
27//!     conditional_provider::VersionedResource,
28//! };
29//! use serde_json::Value;
30//! use std::collections::HashMap;
31//! use std::sync::Arc;
32//! use tokio::sync::RwLock;
33//!
34//! #[derive(Clone)]
35//! struct MyProvider {
36//!     data: Arc<RwLock<HashMap<String, Resource>>>,
37//! }
38//!
39//! #[derive(Debug, thiserror::Error)]
40//! #[error("Provider error: {0}")]
41//! struct MyError(String);
42//!
43//! impl ResourceProvider for MyProvider {
44//!     type Error = MyError;
45//!
46//!     async fn create_resource(&self, resource_type: &str, data: Value, context: &RequestContext) -> Result<Resource, Self::Error> {
47//!         // Your implementation here
48//!         let resource = Resource::from_json(resource_type.to_string(), data)
49//!             .map_err(|e| MyError(e.to_string()))?;
50//!         let mut store = self.data.write().await;
51//!         let id = resource.get_id().unwrap_or("generated-id").to_string();
52//!         store.insert(id, resource.clone());
53//!         Ok(resource)
54//!     }
55//!
56//!     // ... implement other required methods ...
57//!     # async fn get_resource(&self, _resource_type: &str, _id: &str, _context: &RequestContext) -> Result<Option<Resource>, Self::Error> {
58//!     #     Ok(None)
59//!     # }
60//!     # async fn update_resource(&self, _resource_type: &str, _id: &str, _data: Value, _context: &RequestContext) -> Result<Resource, Self::Error> {
61//!     #     Err(MyError("Not implemented".to_string()))
62//!     # }
63//!     # async fn delete_resource(&self, _resource_type: &str, _id: &str, _context: &RequestContext) -> Result<(), Self::Error> {
64//!     #     Ok(())
65//!     # }
66//!     # async fn list_resources(&self, _resource_type: &str, _query: Option<&ListQuery>, _context: &RequestContext) -> Result<Vec<Resource>, Self::Error> {
67//!     #     Ok(vec![])
68//!     # }
69//!     # async fn find_resource_by_attribute(&self, _resource_type: &str, _attribute: &str, _value: &Value, _context: &RequestContext) -> Result<Option<Resource>, Self::Error> {
70//!     #     Ok(None)
71//!     # }
72//!     # async fn resource_exists(&self, _resource_type: &str, _id: &str, _context: &RequestContext) -> Result<bool, Self::Error> {
73//!     #     Ok(false)
74//!     # }
75//!
76//!     // Conditional operations are provided by default with automatic version checking
77//!     // Override for more efficient provider-specific implementations:
78//!     //
79//!     // async fn conditional_update(&self, resource_type: &str, id: &str, data: Value,
80//!     //                           expected_version: &ScimVersion, context: &RequestContext)
81//!     //                           -> Result<ConditionalResult<VersionedResource>, Self::Error> {
82//!     //     // Your optimized conditional update logic
83//!     // }
84//! }
85//! ```
86
87use super::conditional_provider::VersionedResource;
88use super::core::{ListQuery, RequestContext, Resource};
89use super::version::{ConditionalResult, ScimVersion};
90use serde_json::Value;
91use std::future::Future;
92
93/// Unified resource provider trait supporting both single and multi-tenant operations.
94///
95/// This trait provides a unified interface for SCIM resource operations that works
96/// for both single-tenant and multi-tenant scenarios:
97///
98/// - **Single-tenant**: Operations use RequestContext with tenant_context = None
99/// - **Multi-tenant**: Operations use RequestContext with tenant_context = Some(...)
100///
101/// The provider implementation can check `context.tenant_id()` to determine
102/// the effective tenant for the operation.
103pub trait ResourceProvider {
104    type Error: std::error::Error + Send + Sync + 'static;
105
106    /// Create a resource for the tenant specified in the request context.
107    ///
108    /// # Arguments
109    /// * `resource_type` - The type of resource to create (e.g., "User", "Group")
110    /// * `data` - The resource data as JSON
111    /// * `context` - Request context containing tenant information (if multi-tenant)
112    ///
113    /// # Returns
114    /// The created resource with any server-generated fields (id, metadata, etc.)
115    ///
116    /// # Tenant Handling
117    /// - Single-tenant: `context.tenant_id()` returns `None`
118    /// - Multi-tenant: `context.tenant_id()` returns `Some(tenant_id)`
119    fn create_resource(
120        &self,
121        resource_type: &str,
122        data: Value,
123        context: &RequestContext,
124    ) -> impl Future<Output = Result<Resource, Self::Error>> + Send;
125
126    /// Get a resource by ID from the tenant specified in the request context.
127    ///
128    /// # Arguments
129    /// * `resource_type` - The type of resource to retrieve
130    /// * `id` - The unique identifier of the resource
131    /// * `context` - Request context containing tenant information (if multi-tenant)
132    ///
133    /// # Returns
134    /// The resource if found, None if not found within the tenant scope
135    fn get_resource(
136        &self,
137        resource_type: &str,
138        id: &str,
139        context: &RequestContext,
140    ) -> impl Future<Output = Result<Option<Resource>, Self::Error>> + Send;
141
142    /// Update a resource in the tenant specified in the request context.
143    ///
144    /// # Arguments
145    /// * `resource_type` - The type of resource to update
146    /// * `id` - The unique identifier of the resource
147    /// * `data` - The updated resource data as JSON
148    /// * `context` - Request context containing tenant information (if multi-tenant)
149    ///
150    /// # Returns
151    /// The updated resource
152    fn update_resource(
153        &self,
154        resource_type: &str,
155        id: &str,
156        data: Value,
157        context: &RequestContext,
158    ) -> impl Future<Output = Result<Resource, Self::Error>> + Send;
159
160    /// Delete a resource from the tenant specified in the request context.
161    ///
162    /// # Arguments
163    /// * `resource_type` - The type of resource to delete
164    /// * `id` - The unique identifier of the resource
165    /// * `context` - Request context containing tenant information (if multi-tenant)
166    fn delete_resource(
167        &self,
168        resource_type: &str,
169        id: &str,
170        context: &RequestContext,
171    ) -> impl Future<Output = Result<(), Self::Error>> + Send;
172
173    /// List resources from the tenant specified in the request context.
174    ///
175    /// # Arguments
176    /// * `resource_type` - The type of resources to list
177    /// * `query` - Optional query parameters for filtering, sorting, pagination
178    /// * `context` - Request context containing tenant information (if multi-tenant)
179    ///
180    /// # Returns
181    /// A vector of resources from the specified tenant
182    fn list_resources(
183        &self,
184        resource_type: &str,
185        _query: Option<&ListQuery>,
186        context: &RequestContext,
187    ) -> impl Future<Output = Result<Vec<Resource>, Self::Error>> + Send;
188
189    /// Find a resource by attribute value within the tenant specified in the request context.
190    ///
191    /// # Arguments
192    /// * `resource_type` - The type of resource to search
193    /// * `attribute` - The attribute name to search by
194    /// * `value` - The attribute value to search for
195    /// * `context` - Request context containing tenant information (if multi-tenant)
196    ///
197    /// # Returns
198    /// The first matching resource, if found within the tenant scope
199    fn find_resource_by_attribute(
200        &self,
201        resource_type: &str,
202        attribute: &str,
203        value: &Value,
204        context: &RequestContext,
205    ) -> impl Future<Output = Result<Option<Resource>, Self::Error>> + Send;
206
207    /// Check if a resource exists within the tenant specified in the request context.
208    ///
209    /// # Arguments
210    /// * `resource_type` - The type of resource to check
211    /// * `id` - The unique identifier of the resource
212    /// * `context` - Request context containing tenant information (if multi-tenant)
213    ///
214    /// # Returns
215    /// True if the resource exists within the tenant scope, false otherwise
216    fn resource_exists(
217        &self,
218        resource_type: &str,
219        id: &str,
220        context: &RequestContext,
221    ) -> impl Future<Output = Result<bool, Self::Error>> + Send;
222
223    /// Conditionally update a resource if the version matches.
224    ///
225    /// This operation will only succeed if the current resource version matches
226    /// the expected version, preventing accidental overwriting of modified resources.
227    /// This provides optimistic concurrency control for SCIM operations.
228    ///
229    /// # ETag Concurrency Control
230    ///
231    /// This method implements the core of ETag-based conditional operations:
232    /// - Fetches the current resource and its version
233    /// - Compares the current version with the expected version
234    /// - Only proceeds with the update if versions match
235    /// - Returns version conflict information if they don't match
236    ///
237    /// # Arguments
238    /// * `resource_type` - The type of resource to update
239    /// * `id` - The unique identifier of the resource
240    /// * `data` - The updated resource data as JSON
241    /// * `expected_version` - The version the client expects the resource to have
242    /// * `context` - Request context containing tenant information
243    ///
244    /// # Returns
245    /// * `Success(VersionedResource)` - Update succeeded with new version
246    /// * `VersionMismatch(VersionConflict)` - Resource was modified by another client
247    /// * `NotFound` - Resource does not exist
248    ///
249    /// # Default Implementation
250    /// The default implementation provides automatic conditional update support
251    /// by checking the current resource version before performing the update.
252    /// Providers can override this for more efficient implementations that
253    /// perform version checking at the storage layer.
254    ///
255    /// # Examples
256    /// ```rust,no_run
257    /// use scim_server::resource::{
258    ///     provider::ResourceProvider,
259    ///     version::{ScimVersion, ConditionalResult},
260    ///     conditional_provider::VersionedResource,
261    ///     RequestContext,
262    /// };
263    /// use serde_json::json;
264    ///
265    /// # async fn example<P: ResourceProvider + Sync>(provider: &P) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
266    /// let context = RequestContext::with_generated_id();
267    /// let expected_version = ScimVersion::from_hash("abc123");
268    /// let update_data = json!({"userName": "new.name", "active": false});
269    ///
270    /// match provider.conditional_update("User", "123", update_data, &expected_version, &context).await? {
271    ///     ConditionalResult::Success(versioned_resource) => {
272    ///         println!("Update successful, new version: {}",
273    ///                 versioned_resource.version().to_http_header());
274    ///     },
275    ///     ConditionalResult::VersionMismatch(conflict) => {
276    ///         println!("Version conflict: expected {}, current {}",
277    ///                 conflict.expected, conflict.current);
278    ///     },
279    ///     ConditionalResult::NotFound => {
280    ///         println!("Resource not found");
281    ///     }
282    /// }
283    /// # Ok(())
284    /// # }
285    /// ```
286    fn conditional_update(
287        &self,
288        resource_type: &str,
289        id: &str,
290        data: Value,
291        expected_version: &ScimVersion,
292        context: &RequestContext,
293    ) -> impl Future<Output = Result<ConditionalResult<VersionedResource>, Self::Error>> + Send
294    where
295        Self: Sync,
296    {
297        async move {
298            // Default implementation: get current resource, check version, then update
299            match self.get_resource(resource_type, id, context).await? {
300                Some(current_resource) => {
301                    let current_versioned = VersionedResource::new(current_resource);
302                    if current_versioned.version().matches(expected_version) {
303                        let updated = self
304                            .update_resource(resource_type, id, data, context)
305                            .await?;
306                        Ok(ConditionalResult::Success(VersionedResource::new(updated)))
307                    } else {
308                        Ok(ConditionalResult::VersionMismatch(
309                            super::version::VersionConflict::standard_message(
310                                expected_version.clone(),
311                                current_versioned.version().clone(),
312                            ),
313                        ))
314                    }
315                }
316                None => Ok(ConditionalResult::NotFound),
317            }
318        }
319    }
320
321    /// Conditionally delete a resource if the version matches.
322    ///
323    /// This operation will only succeed if the current resource version matches
324    /// the expected version, preventing accidental deletion of modified resources.
325    /// This is critical for maintaining data integrity in concurrent environments.
326    ///
327    /// # ETag Concurrency Control
328    ///
329    /// This method prevents accidental deletion of resources that have been
330    /// modified by other clients:
331    /// - Fetches the current resource and its version
332    /// - Compares the current version with the expected version
333    /// - Only proceeds with the deletion if versions match
334    /// - Ensures the client is deleting the resource they intended to delete
335    ///
336    /// # Arguments
337    /// * `resource_type` - The type of resource to delete
338    /// * `id` - The unique identifier of the resource
339    /// * `expected_version` - The version the client expects the resource to have
340    /// * `context` - Request context containing tenant information
341    ///
342    /// # Returns
343    /// * `Success(())` - Delete succeeded
344    /// * `VersionMismatch(VersionConflict)` - Resource was modified by another client
345    /// * `NotFound` - Resource does not exist
346    ///
347    /// # Default Implementation
348    /// The default implementation provides automatic conditional delete support
349    /// by checking the current resource version before performing the delete.
350    /// Providers can override this for more efficient implementations that
351    /// perform version checking at the storage layer.
352    ///
353    /// # Examples
354    /// ```rust,no_run
355    /// use scim_server::resource::{
356    ///     provider::ResourceProvider,
357    ///     version::{ScimVersion, ConditionalResult},
358    ///     RequestContext,
359    /// };
360    ///
361    /// # async fn example<P: ResourceProvider + Sync>(provider: &P) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
362    /// let context = RequestContext::with_generated_id();
363    /// let expected_version = ScimVersion::from_hash("def456");
364    ///
365    /// match provider.conditional_delete("User", "123", &expected_version, &context).await? {
366    ///     ConditionalResult::Success(()) => {
367    ///         println!("User deleted successfully");
368    ///     },
369    ///     ConditionalResult::VersionMismatch(conflict) => {
370    ///         println!("Cannot delete: resource was modified. Expected {}, current {}",
371    ///                 conflict.expected, conflict.current);
372    ///     },
373    ///     ConditionalResult::NotFound => {
374    ///         println!("User not found");
375    ///     }
376    /// }
377    /// # Ok(())
378    /// # }
379    /// ```
380    fn conditional_delete(
381        &self,
382        resource_type: &str,
383        id: &str,
384        expected_version: &ScimVersion,
385        context: &RequestContext,
386    ) -> impl Future<Output = Result<ConditionalResult<()>, Self::Error>> + Send
387    where
388        Self: Sync,
389    {
390        async move {
391            // Default implementation: get current resource, check version, then delete
392            match self.get_resource(resource_type, id, context).await? {
393                Some(current_resource) => {
394                    let current_versioned = VersionedResource::new(current_resource);
395                    if current_versioned.version().matches(expected_version) {
396                        self.delete_resource(resource_type, id, context).await?;
397                        Ok(ConditionalResult::Success(()))
398                    } else {
399                        Ok(ConditionalResult::VersionMismatch(
400                            super::version::VersionConflict::standard_message(
401                                expected_version.clone(),
402                                current_versioned.version().clone(),
403                            ),
404                        ))
405                    }
406                }
407                None => Ok(ConditionalResult::NotFound),
408            }
409        }
410    }
411
412    /// Get a resource with its version information.
413    ///
414    /// This is a convenience method that returns both the resource and its version
415    /// information wrapped in a [`VersionedResource`]. This is useful when you need
416    /// both the resource data and its version for subsequent conditional operations.
417    ///
418    /// The default implementation calls the existing `get_resource` method and
419    /// automatically wraps the result in a `VersionedResource` with a computed version.
420    ///
421    /// # Arguments
422    /// * `resource_type` - The type of resource to retrieve
423    /// * `id` - The unique identifier of the resource
424    /// * `context` - Request context containing tenant information
425    ///
426    /// # Returns
427    /// The versioned resource if found, `None` if not found
428    ///
429    /// # Examples
430    /// ```rust,no_run
431    /// use scim_server::resource::{
432    ///     provider::ResourceProvider,
433    ///     RequestContext,
434    /// };
435    ///
436    /// # async fn example<P: ResourceProvider + Sync>(provider: &P) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
437    /// let context = RequestContext::with_generated_id();
438    ///
439    /// if let Some(versioned_resource) = provider.get_versioned_resource("User", "123", &context).await? {
440    ///     println!("Resource ID: {}", versioned_resource.resource().get_id().unwrap_or("unknown"));
441    ///     println!("Resource version: {}", versioned_resource.version().to_http_header());
442    ///
443    ///     // Can use the version for subsequent conditional operations
444    ///     let current_version = versioned_resource.version().clone();
445    ///     // ... use current_version for conditional_update or conditional_delete
446    /// }
447    /// # Ok(())
448    /// # }
449    /// ```
450    fn get_versioned_resource(
451        &self,
452        resource_type: &str,
453        id: &str,
454        context: &RequestContext,
455    ) -> impl Future<Output = Result<Option<VersionedResource>, Self::Error>> + Send
456    where
457        Self: Sync,
458    {
459        async move {
460            match self.get_resource(resource_type, id, context).await? {
461                Some(resource) => Ok(Some(VersionedResource::new(resource))),
462                None => Ok(None),
463            }
464        }
465    }
466}
467
468/// Extension trait providing convenience methods for common provider operations.
469///
470/// This trait automatically implements ergonomic helper methods for both single-tenant
471/// and multi-tenant scenarios on any type that implements ResourceProvider.
472pub trait ResourceProviderExt: ResourceProvider {
473    /// Convenience method for single-tenant resource creation.
474    ///
475    /// Creates a RequestContext with no tenant information and calls create_resource.
476    fn create_single_tenant(
477        &self,
478        resource_type: &str,
479        data: Value,
480        request_id: Option<String>,
481    ) -> impl Future<Output = Result<Resource, Self::Error>> + Send
482    where
483        Self: Sync,
484    {
485        async move {
486            let context = match request_id {
487                Some(id) => RequestContext::new(id),
488                None => RequestContext::with_generated_id(),
489            };
490            self.create_resource(resource_type, data, &context).await
491        }
492    }
493
494    /// Convenience method for multi-tenant resource creation.
495    ///
496    /// Creates a RequestContext with the specified tenant and calls create_resource.
497    fn create_multi_tenant(
498        &self,
499        tenant_id: &str,
500        resource_type: &str,
501        data: Value,
502        request_id: Option<String>,
503    ) -> impl Future<Output = Result<Resource, Self::Error>> + Send
504    where
505        Self: Sync,
506    {
507        async move {
508            use super::core::TenantContext;
509
510            let tenant_context = TenantContext {
511                tenant_id: tenant_id.to_string(),
512                client_id: "default-client".to_string(),
513                permissions: Default::default(),
514                isolation_level: Default::default(),
515            };
516
517            let context = match request_id {
518                Some(id) => RequestContext::with_tenant(id, tenant_context),
519                None => RequestContext::with_tenant_generated_id(tenant_context),
520            };
521
522            self.create_resource(resource_type, data, &context).await
523        }
524    }
525
526    /// Convenience method for single-tenant resource retrieval.
527    fn get_single_tenant(
528        &self,
529        resource_type: &str,
530        id: &str,
531        request_id: Option<String>,
532    ) -> impl Future<Output = Result<Option<Resource>, Self::Error>> + Send
533    where
534        Self: Sync,
535    {
536        async move {
537            let context = match request_id {
538                Some(req_id) => RequestContext::new(req_id),
539                None => RequestContext::with_generated_id(),
540            };
541            self.get_resource(resource_type, id, &context).await
542        }
543    }
544
545    /// Convenience method for multi-tenant resource retrieval.
546    fn get_multi_tenant(
547        &self,
548        tenant_id: &str,
549        resource_type: &str,
550        id: &str,
551        request_id: Option<String>,
552    ) -> impl Future<Output = Result<Option<Resource>, Self::Error>> + Send
553    where
554        Self: Sync,
555    {
556        async move {
557            use super::core::TenantContext;
558
559            let tenant_context = TenantContext {
560                tenant_id: tenant_id.to_string(),
561                client_id: "default-client".to_string(),
562                permissions: Default::default(),
563                isolation_level: Default::default(),
564            };
565
566            let context = match request_id {
567                Some(req_id) => RequestContext::with_tenant(req_id, tenant_context),
568                None => RequestContext::with_tenant_generated_id(tenant_context),
569            };
570
571            self.get_resource(resource_type, id, &context).await
572        }
573    }
574}
575
576/// Blanket implementation of ResourceProviderExt for all types implementing ResourceProvider.
577impl<T: ResourceProvider> ResourceProviderExt for T {}