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    /// Apply PATCH operations to a resource within the tenant specified in the request context.
468    ///
469    /// # Arguments
470    /// * `resource_type` - The type of resource to patch
471    /// * `id` - The unique identifier of the resource
472    /// * `patch_request` - The PATCH operation request as JSON (RFC 7644 Section 3.5.2)
473    /// * `context` - Request context containing tenant information (if multi-tenant)
474    ///
475    /// # Returns
476    /// The updated resource after applying the patch operations
477    ///
478    /// # PATCH Operations
479    /// Supports the three SCIM PATCH operations:
480    /// - `add` - Add new attribute values
481    /// - `remove` - Remove attribute values
482    /// - `replace` - Replace existing attribute values
483    ///
484    /// # Default Implementation
485    /// The default implementation provides basic PATCH operation support by:
486    /// 1. Fetching the current resource
487    /// 2. Applying each operation in sequence
488    /// 3. Updating the resource with the modified data
489    fn patch_resource(
490        &self,
491        resource_type: &str,
492        id: &str,
493        patch_request: &Value,
494        context: &RequestContext,
495    ) -> impl Future<Output = Result<Resource, Self::Error>> + Send
496    where
497        Self: Sync,
498    {
499        async move {
500            // Get the current resource
501            let current = self
502                .get_resource(resource_type, id, context)
503                .await?
504                .ok_or_else(|| {
505                    // This will need to be converted to the provider's error type
506                    // For now, we'll use a placeholder that will be handled by implementers
507                    // In practice, providers should define their own NotFound error variant
508                    unreachable!("Resource not found - providers must handle this case")
509                })?;
510
511            // Extract operations from patch request
512            let operations = patch_request
513                .get("Operations")
514                .and_then(|ops| ops.as_array())
515                .ok_or_else(|| {
516                    unreachable!("Invalid patch request - providers must handle this case")
517                })?;
518
519            // Apply operations to create modified resource data
520            let mut modified_data = current.to_json().map_err(|_| {
521                unreachable!("Failed to serialize resource - providers must handle this case")
522            })?;
523
524            for operation in operations {
525                self.apply_patch_operation(&mut modified_data, operation)?;
526            }
527
528            // Update the resource with modified data
529            self.update_resource(resource_type, id, modified_data, context)
530                .await
531        }
532    }
533
534    /// Apply a single PATCH operation to resource data.
535    ///
536    /// This is a helper method used by the default patch_resource implementation.
537    /// Providers can override this method to customize patch operation behavior.
538    ///
539    /// # Arguments
540    /// * `resource_data` - Mutable reference to the resource JSON data
541    /// * `operation` - The patch operation to apply
542    ///
543    /// # Returns
544    /// Result indicating success or failure of the operation
545    fn apply_patch_operation(
546        &self,
547        _resource_data: &mut Value,
548        _operation: &Value,
549    ) -> Result<(), Self::Error> {
550        // This is a simplified implementation that providers should override
551        // with proper SCIM PATCH semantics
552        // Default implementation is intentionally minimal
553        Ok(())
554    }
555}
556
557/// Extension trait providing convenience methods for common provider operations.
558///
559/// This trait automatically implements ergonomic helper methods for both single-tenant
560/// and multi-tenant scenarios on any type that implements ResourceProvider.
561pub trait ResourceProviderExt: ResourceProvider {
562    /// Convenience method for single-tenant resource creation.
563    ///
564    /// Creates a RequestContext with no tenant information and calls create_resource.
565    fn create_single_tenant(
566        &self,
567        resource_type: &str,
568        data: Value,
569        request_id: Option<String>,
570    ) -> impl Future<Output = Result<Resource, Self::Error>> + Send
571    where
572        Self: Sync,
573    {
574        async move {
575            let context = match request_id {
576                Some(id) => RequestContext::new(id),
577                None => RequestContext::with_generated_id(),
578            };
579            self.create_resource(resource_type, data, &context).await
580        }
581    }
582
583    /// Convenience method for multi-tenant resource creation.
584    ///
585    /// Creates a RequestContext with the specified tenant and calls create_resource.
586    fn create_multi_tenant(
587        &self,
588        tenant_id: &str,
589        resource_type: &str,
590        data: Value,
591        request_id: Option<String>,
592    ) -> impl Future<Output = Result<Resource, Self::Error>> + Send
593    where
594        Self: Sync,
595    {
596        async move {
597            use super::core::TenantContext;
598
599            let tenant_context = TenantContext {
600                tenant_id: tenant_id.to_string(),
601                client_id: "default-client".to_string(),
602                permissions: Default::default(),
603                isolation_level: Default::default(),
604            };
605
606            let context = match request_id {
607                Some(id) => RequestContext::with_tenant(id, tenant_context),
608                None => RequestContext::with_tenant_generated_id(tenant_context),
609            };
610
611            self.create_resource(resource_type, data, &context).await
612        }
613    }
614
615    /// Convenience method for single-tenant resource retrieval.
616    fn get_single_tenant(
617        &self,
618        resource_type: &str,
619        id: &str,
620        request_id: Option<String>,
621    ) -> impl Future<Output = Result<Option<Resource>, Self::Error>> + Send
622    where
623        Self: Sync,
624    {
625        async move {
626            let context = match request_id {
627                Some(req_id) => RequestContext::new(req_id),
628                None => RequestContext::with_generated_id(),
629            };
630            self.get_resource(resource_type, id, &context).await
631        }
632    }
633
634    /// Convenience method for multi-tenant resource retrieval.
635    fn get_multi_tenant(
636        &self,
637        tenant_id: &str,
638        resource_type: &str,
639        id: &str,
640        request_id: Option<String>,
641    ) -> impl Future<Output = Result<Option<Resource>, Self::Error>> + Send
642    where
643        Self: Sync,
644    {
645        async move {
646            use super::core::TenantContext;
647
648            let tenant_context = TenantContext {
649                tenant_id: tenant_id.to_string(),
650                client_id: "default-client".to_string(),
651                permissions: Default::default(),
652                isolation_level: Default::default(),
653            };
654
655            let context = match request_id {
656                Some(req_id) => RequestContext::with_tenant(req_id, tenant_context),
657                None => RequestContext::with_tenant_generated_id(tenant_context),
658            };
659
660            self.get_resource(resource_type, id, &context).await
661        }
662    }
663}
664
665/// Blanket implementation of ResourceProviderExt for all types implementing ResourceProvider.
666impl<T: ResourceProvider> ResourceProviderExt for T {}