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