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