Advanced Provider Features
This guide covers advanced features and patterns for working with storage and resource providers in the SCIM Server library, including conditional operations, versioning, multi-operation patterns, and performance optimization.
Conditional Operations and ETags
The SCIM Server provides built-in support for ETag-based optimistic concurrency control through conditional operations.
ETag Generation
ETags are automatically generated for all resources:
#![allow(unused)] fn main() { use scim_server::{ providers::StandardResourceProvider, storage::InMemoryStorage, resource::{RequestContext, ResourceProvider}, }; let storage = InMemoryStorage::new(); let provider = StandardResourceProvider::new(storage); let context = RequestContext::with_generated_id(); // Create a resource - ETag is automatically generated let user = provider.create_resource( "User", json!({ "userName": "alice@example.com", "displayName": "Alice Smith" }), &context, ).await?; // The resource now has an ETag in its metadata println!("ETag: {}", user.get_version().unwrap()); }
Conditional Updates
Use ETags to prevent lost updates in concurrent scenarios:
#![allow(unused)] fn main() { use scim_server::resource::version::{ConditionalResult, ScimVersion}; // Get the current resource with its ETag let current_user = provider.get_resource("User", "user-123", &context) .await? .ok_or("User not found")?; let current_etag = current_user.get_version().unwrap(); // Attempt conditional update let updated_data = json!({ "id": "user-123", "userName": "alice@example.com", "displayName": "Alice Updated" }); match provider.conditional_update( "User", "user-123", updated_data, &ScimVersion::from_etag(current_etag), &context, ).await? { ConditionalResult::Success(updated_user) => { println!("Update successful: {}", updated_user.get_version().unwrap()); } ConditionalResult::Conflict(conflict) => { println!("Version conflict: expected {}, got {}", conflict.expected_version, conflict.current_version); // Handle conflict - retry, merge, or report error } } }
Conditional Deletes
Safely delete resources with version checking:
#![allow(unused)] fn main() { // Delete only if the version matches match provider.conditional_delete( "User", "user-123", &ScimVersion::from_etag("W/\"abc123\""), &context, ).await? { ConditionalResult::Success(was_deleted) => { if was_deleted { println!("User deleted successfully"); } else { println!("User was already deleted"); } } ConditionalResult::Conflict(conflict) => { println!("Cannot delete: version mismatch"); } } }
If-Match and If-None-Match Headers
Handle HTTP conditional headers:
#![allow(unused)] fn main() { use scim_server::resource::version::VersionCondition; // If-Match: update only if ETag matches let condition = VersionCondition::IfMatch(ScimVersion::from_etag("W/\"abc123\"")); // If-None-Match: update only if ETag doesn't match let condition = VersionCondition::IfNoneMatch(ScimVersion::from_etag("W/\"xyz789\"")); // Apply condition to update let result = provider.conditional_update_with_condition( "User", "user-123", updated_data, condition, &context, ).await?; }
Resource Versioning
Version Management
Resources automatically track version information:
#![allow(unused)] fn main() { use scim_server::resource::version::VersionedResource; // Create versioned resource let versioned_user = provider.create_versioned_resource( "User", user_data, &context, ).await?; println!("Resource version: {}", versioned_user.version); println!("Created at: {}", versioned_user.resource.get_created().unwrap()); println!("Last modified: {}", versioned_user.resource.get_last_modified().unwrap()); // Get resource with version info let versioned = provider.get_versioned_resource("User", "user-123", &context).await?; if let Some(v) = versioned { println!("Current version: {}", v.version); println!("Resource data: {}", serde_json::to_string_pretty(&v.resource)?); } }
Version History
Track changes over time:
#![allow(unused)] fn main() { use scim_server::resource::version::VersionHistory; // Providers can optionally implement version history pub struct VersionedStorageProvider<S> { inner: S, version_store: VersionHistory, } impl<S: StorageProvider> VersionedStorageProvider<S> { async fn get_resource_history( &self, resource_type: &str, id: &str, context: &RequestContext, ) -> Result<Vec<VersionedResource>, Self::Error> { // Return all versions of the resource self.version_store.get_history(resource_type, id, context).await } async fn get_resource_at_version( &self, resource_type: &str, id: &str, version: &ScimVersion, context: &RequestContext, ) -> Result<Option<Resource>, Self::Error> { // Get resource at specific version self.version_store.get_at_version(resource_type, id, version, context).await } } }
Multi-Operation Patterns
⚠️ Note: Bulk operations are not yet implemented. Use these patterns for efficient multi-resource operations.
Batch Processing
Process multiple operations efficiently:
#![allow(unused)] fn main() { use scim_server::{ResourceProvider, RequestContext, ConditionalResult}; use futures::future::try_join_all; use serde_json::json; // Process multiple user creations async fn create_multiple_users( provider: &impl ResourceProvider, tenant_id: &str, users_data: Vec<serde_json::Value> ) -> Result<Vec<ScimUser>, Box<dyn std::error::Error>> { let context = RequestContext::new("batch-create", None); let mut results = Vec::new(); // Sequential processing (safest approach) for user_data in users_data { let user = provider.create_resource("User", user_data, &context).await?; results.push(user); } Ok(results) } // Parallel processing (for independent operations) async fn create_users_parallel( provider: &impl ResourceProvider, tenant_id: &str, users_data: Vec<serde_json::Value> ) -> Result<Vec<ScimUser>, Box<dyn std::error::Error>> { let context = RequestContext::new("parallel-create", None); let futures: Vec<_> = users_data.into_iter() .map(|data| provider.create_resource("User", data, &context)) .collect(); let results = try_join_all(futures).await?; Ok(results) } // Mixed operations with error handling async fn process_mixed_operations( provider: &impl ResourceProvider, tenant_id: &str, ) -> Result<Vec<String>, Box<dyn std::error::Error>> { let context = RequestContext::new("mixed-ops", None); let mut results = Vec::new(); // Create a new user let user_data = json!({ "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "userName": "alice@example.com", "displayName": "Alice Smith" }); match provider.create_resource("User", user_data, &context).await { Ok(user) => { let user_id = user.id().unwrap().to_string(); results.push(format!("Created user: {}", user_id)); // Update the user let update_data = json!({ "displayName": "Alice Johnson" }); match provider.update_resource("User", &user_id, update_data, &context).await { Ok(_) => results.push(format!("Updated user: {}", user_id)), Err(e) => results.push(format!("Failed to update user {}: {}", user_id, e)), } } Err(e) => results.push(format!("Failed to create user: {}", e)), } Ok(results) } }
Transaction-like Operations
While true ACID transactions aren't part of SCIM, you can implement compensating patterns:
#![allow(unused)] fn main() { use scim_server::{ResourceProvider, RequestContext}; struct Operation { operation_type: String, resource_type: String, resource_id: Option<String>, data: serde_json::Value, } struct CompensatingAction { action: String, resource_type: String, resource_id: String, } async fn execute_with_compensation( provider: &impl ResourceProvider, tenant_id: &str, operations: Vec<Operation> ) -> Result<Vec<String>, Box<dyn std::error::Error>> { let context = RequestContext::new("compensating-ops", None); let mut completed = Vec::new(); let mut compensations = Vec::new(); for op in operations { let result = match op.operation_type.as_str() { "CREATE" => { match provider.create_resource(&op.resource_type, op.data, &context).await { Ok(resource) => { let id = resource.id().unwrap().to_string(); compensations.push(CompensatingAction { action: "DELETE".to_string(), resource_type: op.resource_type.clone(), resource_id: id.clone(), }); Ok(format!("Created {}: {}", op.resource_type, id)) } Err(e) => Err(e.into()) } } "UPDATE" => { let id = op.resource_id.unwrap(); match provider.update_resource(&op.resource_type, &id, op.data, &context).await { Ok(_) => Ok(format!("Updated {}: {}", op.resource_type, id)), Err(e) => Err(e.into()) } } _ => Err("Unsupported operation".into()) }; match result { Ok(msg) => completed.push(msg), Err(e) => { // Rollback completed operations for compensation in compensations.iter().rev() { if compensation.action == "DELETE" { let _ = provider.delete_resource( &compensation.resource_type, &compensation.resource_id, &context ).await; } } return Err(e); } } } Ok(completed) } }
Efficient Storage Patterns
Optimize storage operations for multiple resources:
#![allow(unused)] fn main() { use std::collections::HashMap; // Batch retrieval pattern async fn get_multiple_users_by_ids( provider: &impl ResourceProvider, tenant_id: &str, user_ids: Vec<String> ) -> Result<HashMap<String, ScimUser>, Box<dyn std::error::Error>> { let context = RequestContext::new("batch-get", None); let mut users = HashMap::new(); // For now, individual requests (until batch APIs are implemented) for user_id in user_ids { match provider.get_resource("User", &user_id, &context).await { Ok(Some(user)) => { users.insert(user_id, user); } Ok(None) => { // User not found, skip } Err(e) => { eprintln!("Failed to get user {}: {}", user_id, e); // Continue with other users } } } Ok(users) } }
Performance Optimization
Connection Pooling
Optimize database connections:
#![allow(unused)] fn main() { use sqlx::{Pool, Postgres}; use std::time::Duration; pub struct OptimizedPostgresStorage { pool: Pool<Postgres>, } impl OptimizedPostgresStorage { pub async fn new(database_url: &str) -> Result<Self, sqlx::Error> { let pool = sqlx::postgres::PgPoolOptions::new() .max_connections(100) .min_connections(10) .max_lifetime(Duration::from_secs(1800)) // 30 minutes .idle_timeout(Duration::from_secs(600)) // 10 minutes .acquire_timeout(Duration::from_secs(30)) .test_before_acquire(true) .connect(database_url) .await?; Ok(Self { pool }) } } }
Caching Layer
Add caching to storage providers:
#![allow(unused)] fn main() { use tokio::sync::RwLock; use std::collections::HashMap; use std::time::{Duration, Instant}; pub struct CachedStorage<S> { inner: S, cache: Arc<RwLock<HashMap<String, CachedEntry>>>, ttl: Duration, } struct CachedEntry { value: Value, inserted_at: Instant, } impl<S: StorageProvider> CachedStorage<S> { pub fn new(inner: S, ttl: Duration) -> Self { Self { inner, cache: Arc::new(RwLock::new(HashMap::new())), ttl, } } fn cache_key(&self, key: &StorageKey) -> String { format!("{}/{}/{}", key.tenant_id(), key.resource_type(), key.resource_id()) } async fn cleanup_expired(&self) { let mut cache = self.cache.write().await; let now = Instant::now(); cache.retain(|_, entry| now.duration_since(entry.inserted_at) < self.ttl); } } impl<S: StorageProvider> StorageProvider for CachedStorage<S> { type Error = S::Error; async fn get(&self, key: StorageKey) -> Result<Option<Value>, Self::Error> { let cache_key = self.cache_key(&key); // Check cache first { let cache = self.cache.read().await; if let Some(entry) = cache.get(&cache_key) { let age = Instant::now().duration_since(entry.inserted_at); if age < self.ttl { return Ok(Some(entry.value.clone())); } } } // Cache miss - get from storage let result = self.inner.get(key).await?; // Update cache if let Some(ref value) = result { let mut cache = self.cache.write().await; cache.insert(cache_key, CachedEntry { value: value.clone(), inserted_at: Instant::now(), }); } Ok(result) } async fn put(&self, key: StorageKey, data: Value) -> Result<Value, Self::Error> { let result = self.inner.put(key.clone(), data).await?; // Update cache let cache_key = self.cache_key(&key); let mut cache = self.cache.write().await; cache.insert(cache_key, CachedEntry { value: result.clone(), inserted_at: Instant::now(), }); Ok(result) } async fn delete(&self, key: StorageKey) -> Result<bool, Self::Error> { let result = self.inner.delete(key.clone()).await?; // Remove from cache let cache_key = self.cache_key(&key); let mut cache = self.cache.write().await; cache.remove(&cache_key); Ok(result) } // Implement other methods... } }
Indexing for Search
Optimize attribute searches:
#![allow(unused)] fn main() { use std::collections::BTreeMap; pub struct IndexedStorage<S> { inner: S, // Index: (tenant, resource_type, attribute) -> (value -> resource_ids) attribute_index: Arc<RwLock<BTreeMap<String, BTreeMap<String, HashSet<String>>>>>, } impl<S: StorageProvider> IndexedStorage<S> { pub fn new(inner: S) -> Self { Self { inner, attribute_index: Arc::new(RwLock::new(BTreeMap::new())), } } fn index_key(&self, prefix: &StoragePrefix, attribute: &str) -> String { format!("{}/{}#{}", prefix.tenant_id(), prefix.resource_type(), attribute) } async fn update_index(&self, key: &StorageKey, data: &Value) { let mut index = self.attribute_index.write().await; // Index common searchable attributes let searchable_attributes = ["userName", "displayName", "email.value"]; for attr in &searchable_attributes { if let Some(attr_value) = self.extract_attribute_value(data, attr) { let index_key = self.index_key( &StorageKey::prefix(key.tenant_id(), key.resource_type()), attr ); let value_index = index.entry(index_key).or_insert_with(BTreeMap::new); let resource_set = value_index.entry(attr_value).or_insert_with(HashSet::new); resource_set.insert(key.resource_id().to_string()); } } } fn extract_attribute_value(&self, data: &Value, attribute: &str) -> Option<String> { // Simple attribute extraction - can be enhanced for nested attributes data.get(attribute)?.as_str().map(|s| s.to_string()) } } impl<S: StorageProvider> StorageProvider for IndexedStorage<S> { type Error = S::Error; async fn find_by_attribute( &self, prefix: StoragePrefix, attribute: &str, value: &str, ) -> Result<Vec<(StorageKey, Value)>, Self::Error> { let index_key = self.index_key(&prefix, attribute); // Try index first { let index = self.attribute_index.read().await; if let Some(value_index) = index.get(&index_key) { if let Some(resource_ids) = value_index.get(value) { // Found in index - get the resources let mut results = Vec::new(); for resource_id in resource_ids { let key = StorageKey::new( prefix.tenant_id(), prefix.resource_type(), resource_id ); if let Some(data) = self.inner.get(key.clone()).await? { results.push((key, data)); } } return Ok(results); } } } // Fallback to full scan self.inner.find_by_attribute(prefix, attribute, value).await } async fn put(&self, key: StorageKey, data: Value) -> Result<Value, Self::Error> { let result = self.inner.put(key.clone(), data.clone()).await?; // Update indexes self.update_index(&key, &result).await; Ok(result) } // Implement other methods with index maintenance... } }
Custom Validation
Schema Validation
Add custom validation logic:
#![allow(unused)] fn main() { use serde_json::Value; pub trait ResourceValidator: Send + Sync { type Error: std::error::Error + Send + Sync + 'static; async fn validate_resource( &self, resource_type: &str, data: &Value, context: &RequestContext, ) -> Result<(), Self::Error>; async fn validate_update( &self, resource_type: &str, id: &str, current: &Value, updated: &Value, context: &RequestContext, ) -> Result<(), Self::Error>; } pub struct SchemaValidator { schemas: HashMap<String, ResourceSchema>, } impl ResourceValidator for SchemaValidator { type Error = ValidationError; async fn validate_resource( &self, resource_type: &str, data: &Value, context: &RequestContext, ) -> Result<(), Self::Error> { let schema = self.schemas.get(resource_type) .ok_or_else(|| ValidationError::UnknownResourceType(resource_type.to_string()))?; // Validate required fields for required_field in &schema.required_fields { if !data.get(required_field).is_some() { return Err(ValidationError::MissingRequiredField(required_field.clone())); } } // Validate field types and constraints for (field_name, field_schema) in &schema.fields { if let Some(field_value) = data.get(field_name) { self.validate_field(field_value, field_schema)?; } } // Custom business rules self.validate_business_rules(resource_type, data, context).await?; Ok(()) } async fn validate_update( &self, resource_type: &str, id: &str, current: &Value, updated: &Value, context: &RequestContext, ) -> Result<(), Self::Error> { // First validate the updated resource self.validate_resource(resource_type, updated, context).await?; // Check immutable fields let schema = self.schemas.get(resource_type).unwrap(); for immutable_field in &schema.immutable_fields { let current_value = current.get(immutable_field); let updated_value = updated.get(immutable_field); if current_value != updated_value { return Err(ValidationError::ImmutableFieldModified(immutable_field.clone())); } } Ok(()) } } pub struct ValidatingResourceProvider<P, V> { inner: P, validator: V, } impl<P: ResourceProvider, V: ResourceValidator> ResourceProvider for ValidatingResourceProvider<P, V> { type Error = CombinedError<P::Error, V::Error>; async fn create_resource( &self, resource_type: &str, data: Value, context: &RequestContext, ) -> Result<Resource, Self::Error> { // Validate before creation self.validator.validate_resource(resource_type, &data, context) .await .map_err(CombinedError::ValidationError)?; // Delegate to inner provider self.inner.create_resource(resource_type, data, context) .await .map_err(CombinedError::ProviderError) } async fn update_resource( &self, resource_type: &str, id: &str, data: Value, context: &RequestContext, ) -> Result<Resource, Self::Error> { // Get current resource for validation let current = self.inner.get_resource(resource_type, id, context) .await .map_err(CombinedError::ProviderError)? .ok_or_else(|| CombinedError::ProviderError(/* NotFound error */))?; // Validate the update self.validator.validate_update(resource_type, id, current.data(), &data, context) .await .map_err(CombinedError::ValidationError)?; // Perform update self.inner.update_resource(resource_type, id, data, context) .await .map_err(CombinedError::ProviderError) } // Implement other methods... } }
Monitoring and Metrics
Instrumentation
Add comprehensive monitoring:
#![allow(unused)] fn main() { use tracing::{info, warn, error, instrument, Span}; use std::time::Instant; pub struct InstrumentedProvider<P> { inner: P, metrics: Arc<ProviderMetrics>, } pub struct ProviderMetrics { pub operation_counter: metrics::Counter, pub operation_duration: metrics::Histogram, pub error_counter: metrics::Counter, pub active_operations: metrics::Gauge, } impl<P: ResourceProvider> InstrumentedProvider<P> { pub fn new(inner: P, metrics: Arc<ProviderMetrics>) -> Self { Self { inner, metrics } } } impl<P: ResourceProvider> ResourceProvider for InstrumentedProvider<P> { type Error = P::Error; #[instrument(skip(self, data, context), fields( resource_type = resource_type, tenant_id = context.tenant_context.as_ref().map(|t| t.tenant_id.as_str()), operation = "create" ))] async fn create_resource( &self, resource_type: &str, data: Value, context: &RequestContext, ) -> Result<Resource, Self::Error> { let start = Instant::now(); let _guard = self.metrics.active_operations.increment(); info!("Creating resource"); let result = self.inner.create_resource(resource_type, data, context).await; let duration = start.elapsed(); match &result { Ok(resource) => { info!( resource_id = resource.get_id().unwrap_or("unknown"), duration_ms = duration.as_millis(), "Resource created successfully" ); self.metrics.operation_counter .with_labels(&[("operation", "create"), ("status", "success")]) .increment(); } Err(e) => { error!( error = %e, duration_ms = duration.as_millis(), "Failed to create resource" ); self.metrics.operation_counter .with_labels(&[("operation", "create"), ("status", "error")]) .increment(); self.metrics.error_counter .with_labels(&[("operation", "create")]) .increment(); } } self.metrics.operation_duration .with_labels(&[("operation", "create")]) .observe(duration.as_secs_f64()); result } // Similar instrumentation for other methods... } }
Health Checks
Implement comprehensive health monitoring:
#![allow(unused)] fn main() { use serde::{Serialize, Deserialize}; use std::time::{Duration, SystemTime}; #[derive(Debug, Serialize, Deserialize)] pub struct HealthStatus { pub status: HealthState, pub version: String, pub uptime: Duration, pub checks: Vec<HealthCheck>, } #[derive(Debug, Serialize, Deserialize)] pub enum HealthState { Healthy, Degraded, Unhealthy, } #[derive(Debug, Serialize, Deserialize)] pub struct HealthCheck { pub name: String, pub status: HealthState, pub duration: Duration, pub message: Option<String>, } pub trait HealthProvider { async fn health_check(&self) -> Result<HealthStatus, Box<dyn std::error::Error>>; } impl<S: StorageProvider> HealthProvider for StandardResourceProvider<S> { async fn health_check(&self) -> Result<HealthStatus, Box<dyn std::error::Error>> { let start_time = SystemTime::now(); let mut checks = Vec::new(); // Check storage connectivity let storage_check_start = Instant::now(); let storage_status = match self.test_storage_connectivity().await { Ok(_) => HealthState::Healthy, Err(e) => { checks.push(HealthCheck { name: "storage".to_string(), status: HealthState::Unhealthy, duration: storage_check_start.elapsed(), message: Some(e.to_string()), }); HealthState::Unhealthy } }; if matches!(storage_status, HealthState::Healthy) { checks.push(HealthCheck { name: "storage".to_string(), status: HealthState::Healthy, duration: storage_check_start.elapsed(), message: None, }); } // Check resource operations let ops_check_start = Instant::now(); let ops_status = match self.test_basic_operations().await { Ok(_) => HealthState::Healthy, Err(e) => { checks.push(HealthCheck { name: "operations".to_string(), status: HealthState::Unhealthy, duration: ops_check_start.elapsed(), message: Some(e.to_string()), }); HealthState::Unhealthy } }; if matches!(ops_status, HealthState::Healthy) { checks.push(HealthCheck { name: "operations".to_string(), status: HealthState::Healthy, duration: ops_check_start.elapsed(), message: None, }); } // Determine overall status let overall_status = if checks.iter().any(|c| matches!(c.status, HealthState::Unhealthy)) { HealthState::Unhealthy } else if checks.iter().any(|c| matches!(c.status, HealthState::Degraded)) { HealthState::Degraded } else { HealthState::Healthy }; Ok(HealthStatus { status: overall_status, version: env!("CARGO_PKG_VERSION").to_string(), uptime: start_time.elapsed().unwrap_or(Duration::ZERO), checks, }) } } }
Best Practices
Error Handling
Implement comprehensive error handling:
#![allow(unused)] fn main() { #[derive(Debug, thiserror::Error)] pub enum AdvancedProviderError { #[error("Storage error: {0}")] Storage(#[from] StorageError), #[error("Validation error: {0}")] Validation(#[from] ValidationError), #[error("Version conflict: expected {expected}, got {current}")] VersionConflict { expected: String, current: String }, #[error("Rate limit exceeded: {limit} requests per {window:?}")] RateLimitExceeded { limit: u32, window: Duration }, #[error("Quota exceeded: {current}/{limit} resources")] QuotaExceeded { current: usize, limit: usize }, #[error("Circuit breaker open: {service}")] CircuitBreakerOpen { service: String }, } // Implement recovery strategies impl AdvancedProviderError { pub fn is_retryable(&self) -> bool { matches!(self, Self::Storage(StorageError::Internal(_)) | Self::CircuitBreakerOpen { .. } ) } pub fn retry_delay(&self) -> Option<Duration> { match self { Self::RateLimitExceeded { window, .. } => Some(*window), Self::CircuitBreakerOpen { .. } => Some(Duration::from_secs(5)), _ => None, } } } }
Configuration
Use structured configuration:
#![allow(unused)] fn main() { #[derive(Debug, Deserialize)] pub struct AdvancedProviderConfig { pub storage: StorageConfig, pub caching: Option<CacheConfig>, pub validation: ValidationConfig, pub performance: PerformanceConfig, pub monitoring: MonitoringConfig, } #[derive(Debug, Deserialize)] pub struct CacheConfig { pub enabled: bool, pub ttl_seconds: u64, pub max_entries: usize, } #[derive(Debug, Deserialize)] pub struct PerformanceConfig { pub bulk_batch_size: usize, pub connection_pool_size: u32, pub query_timeout_seconds: u64, } #[derive(Debug, Deserialize)] pub struct MonitoringConfig { pub enable_metrics: bool, pub enable_tracing: bool, pub health_check_interval_seconds: u64, } // Factory function pub async fn create_advanced_provider( config: AdvancedProviderConfig, ) -> Result<Box<dyn ResourceProvider<Error = AdvancedProviderError>>, ConfigError> { // Create storage layer let storage = create_storage_provider(&config.storage).await?; // Add caching if configured let storage: Box<dyn StorageProvider<Error = StorageError>> = if let Some(cache_config) = config.caching { if cache_config.enabled { Box::new(CachedStorage::new( storage, Duration::from_secs(cache_config.ttl_seconds), )) } else { storage } } else { storage }; // Create resource provider let provider = StandardResourceProvider::new(storage); // Add validation layer let validator = create_validator(&config.validation)?; let provider = ValidatingResourceProvider::new(provider, validator); }