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 {}