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