scim_server/providers/
standard.rs

1//! Standard resource provider implementation with pluggable storage.
2//!
3//! This module provides a production-ready implementation of the ResourceProvider
4//! trait that separates SCIM protocol logic from storage concerns through the
5//! StorageProvider interface.
6//!
7//! # Features
8//!
9//! * Pluggable storage backends through the StorageProvider trait
10//! * Complete SCIM protocol logic preservation
11//! * Automatic tenant isolation when tenant context is provided
12//! * Fallback to "default" tenant for single-tenant operations
13//! * Comprehensive error handling
14//! * Resource metadata tracking (created/updated timestamps)
15//! * Duplicate detection for userName attributes
16//!
17//! # Example Usage
18//!
19//! ```rust
20//! use scim_server::providers::StandardResourceProvider;
21//! use scim_server::storage::InMemoryStorage;
22//! use scim_server::resource::{RequestContext, TenantContext, ResourceProvider};
23//! use serde_json::json;
24//!
25//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
26//! let storage = InMemoryStorage::new();
27//! let provider = StandardResourceProvider::new(storage);
28//!
29//! // Single-tenant operation
30//! let single_context = RequestContext::with_generated_id();
31//! let user_data = json!({
32//!     "userName": "john.doe",
33//!     "displayName": "John Doe"
34//! });
35//! let user = provider.create_resource("User", user_data.clone(), &single_context).await?;
36//!
37//! // Multi-tenant operation
38//! let tenant_context = TenantContext::new("tenant1".to_string(), "client1".to_string());
39//! let multi_context = RequestContext::with_tenant_generated_id(tenant_context);
40//! let tenant_user = provider.create_resource("User", user_data, &multi_context).await?;
41//! # Ok(())
42//! # }
43//! ```
44
45use crate::providers::in_memory::{InMemoryError, InMemoryStats};
46use crate::resource::{
47    ListQuery, RequestContext, Resource, ResourceProvider,
48    conditional_provider::VersionedResource,
49    version::{ConditionalResult, ScimVersion},
50};
51use crate::storage::{StorageKey, StorageProvider};
52use log::{debug, info, trace, warn};
53use serde_json::{Value, json};
54
55
56/// Standard resource provider with pluggable storage backend.
57///
58/// This provider separates SCIM protocol logic from storage concerns by delegating
59/// data persistence to a StorageProvider implementation while handling all SCIM-specific
60/// business logic, validation, and metadata management.
61#[derive(Debug, Clone)]
62pub struct StandardResourceProvider<S: StorageProvider> {
63    // Pluggable storage backend
64    storage: S,
65}
66
67impl<S: StorageProvider> StandardResourceProvider<S> {
68    /// Create a new standard provider with the given storage backend.
69    pub fn new(storage: S) -> Self {
70        Self { storage }
71    }
72
73    /// Get the effective tenant ID for the operation.
74    ///
75    /// Returns the tenant ID from the context, or "default" for single-tenant operations.
76    fn effective_tenant_id(&self, context: &RequestContext) -> String {
77        context.tenant_id().unwrap_or("default").to_string()
78    }
79
80    /// Generate a unique resource ID for the given tenant and resource type.
81    async fn generate_resource_id(&self, _tenant_id: &str, _resource_type: &str) -> String {
82        // Use UUID for simple, unique ID generation
83        uuid::Uuid::new_v4().to_string()
84    }
85
86    /// Check for duplicate userName in User resources within the same tenant.
87    async fn check_username_duplicate(
88        &self,
89        tenant_id: &str,
90        username: &str,
91        exclude_id: Option<&str>,
92    ) -> Result<(), InMemoryError> {
93        let prefix = StorageKey::prefix(tenant_id, "User");
94        let matches = self
95            .storage
96            .find_by_attribute(prefix, "userName", username)
97            .await
98            .map_err(|e| InMemoryError::Internal {
99                message: format!("Storage error during username check: {}", e),
100            })?;
101
102        for (key, _data) in matches {
103            // Skip the resource we're updating
104            if Some(key.resource_id()) != exclude_id {
105                return Err(InMemoryError::DuplicateAttribute {
106                    resource_type: "User".to_string(),
107                    attribute: "userName".to_string(),
108                    value: username.to_string(),
109                    tenant_id: tenant_id.to_string(),
110                });
111            }
112        }
113
114        Ok(())
115    }
116
117    /// Add SCIM metadata to a resource.
118    fn add_scim_metadata(&self, mut resource: Resource) -> Resource {
119        // Use the non-deprecated create_meta method with proper base URL
120        if let Err(_e) = resource.create_meta("https://example.com/scim/v2") {
121            return resource;
122        }
123
124        // Add version to the meta using content-based versioning
125        if let Some(meta) = resource.get_meta().cloned() {
126            // Generate version from resource content
127            let resource_json = resource.to_json().unwrap_or_default();
128            let content_bytes = resource_json.to_string().as_bytes().to_vec();
129            let scim_version = ScimVersion::from_content(&content_bytes);
130            let version = scim_version.to_http_header();
131
132            if let Ok(meta_with_version) = meta.with_version(version) {
133                resource.set_meta(meta_with_version);
134            }
135        }
136
137        resource
138    }
139
140    /// Clear all data from storage.
141    ///
142    /// Removes all resources from all tenants by delegating to the storage backend's
143    /// clear operation. This method provides a consistent interface for clearing data
144    /// regardless of the underlying storage implementation.
145    ///
146    /// # Behavior
147    ///
148    /// - Delegates to [`StorageProvider::clear`] for actual data removal
149    /// - Logs warnings if the clear operation fails
150    /// - Primarily intended for testing scenarios
151    /// - After successful clearing, [`get_stats`] should report zero resources
152    ///
153    /// # Examples
154    ///
155    /// ```rust
156    /// use scim_server::providers::StandardResourceProvider;
157    /// use scim_server::storage::InMemoryStorage;
158    ///
159    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
160    /// let storage = InMemoryStorage::new();
161    /// let provider = StandardResourceProvider::new(storage);
162    ///
163    /// // ... create some resources ...
164    /// provider.clear().await;
165    ///
166    /// let stats = provider.get_stats().await;
167    /// assert_eq!(stats.total_resources, 0);
168    /// # Ok(())
169    /// # }
170    /// ```
171    ///
172    /// [`StorageProvider::clear`]: crate::storage::StorageProvider::clear
173    /// [`get_stats`]: Self::get_stats
174    pub async fn clear(&self) {
175        // Delegate to storage backend for proper clearing
176        if let Err(e) = self.storage.clear().await {
177            warn!("Failed to clear storage: {:?}", e);
178        }
179    }
180
181    /// Get comprehensive statistics about stored data across all tenants.
182    ///
183    /// Dynamically discovers all tenants and resource types from storage to provide
184    /// accurate statistics without relying on hardcoded patterns. This method uses
185    /// the storage provider's discovery capabilities to enumerate actual data.
186    ///
187    /// # Returns
188    ///
189    /// [`InMemoryStats`] containing:
190    /// - `tenant_count`: Number of tenants with at least one resource
191    /// - `total_resources`: Sum of all resources across all tenants and types
192    /// - `resource_type_count`: Number of distinct resource types found
193    /// - `resource_types`: List of all resource type names
194    ///
195    /// # Errors
196    ///
197    /// This method handles storage errors gracefully by using default values
198    /// (empty collections) when discovery operations fail.
199    ///
200    /// # Examples
201    ///
202    /// ```rust
203    /// use scim_server::providers::StandardResourceProvider;
204    /// use scim_server::storage::InMemoryStorage;
205    /// use scim_server::resource::{RequestContext, TenantContext};
206    ///
207    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
208    /// let storage = InMemoryStorage::new();
209    /// let provider = StandardResourceProvider::new(storage);
210    ///
211    /// // ... create resources in multiple tenants ...
212    ///
213    /// let stats = provider.get_stats().await;
214    /// println!("Total resources: {}", stats.total_resources);
215    /// println!("Active tenants: {}", stats.tenant_count);
216    /// println!("Resource types: {:?}", stats.resource_types);
217    /// # Ok(())
218    /// # }
219    /// ```
220    pub async fn get_stats(&self) -> InMemoryStats {
221        // Dynamically discover all tenants and resource types from storage
222        let tenants = self.storage.list_tenants().await.unwrap_or_default();
223        let resource_types = self.storage.list_all_resource_types().await.unwrap_or_default();
224
225        let mut total_resources = 0;
226
227        // Count total resources across all tenants and resource types
228        for tenant_id in &tenants {
229            for resource_type in &resource_types {
230                let prefix = StorageKey::prefix(tenant_id, resource_type);
231                if let Ok(count) = self.storage.count(prefix).await {
232                    total_resources += count;
233                }
234            }
235        }
236
237        InMemoryStats {
238            tenant_count: tenants.len(),
239            total_resources,
240            resource_type_count: resource_types.len(),
241            resource_types,
242        }
243    }
244
245    /// List all resources of a specific type in a tenant.
246    pub async fn list_resources_in_tenant(
247        &self,
248        tenant_id: &str,
249        resource_type: &str,
250    ) -> Vec<Resource> {
251        let prefix = StorageKey::prefix(tenant_id, resource_type);
252        match self.storage.list(prefix, 0, usize::MAX).await {
253            Ok(storage_results) => {
254                let mut resources = Vec::new();
255                for (_key, data) in storage_results {
256                    match Resource::from_json(resource_type.to_string(), data) {
257                        Ok(resource) => resources.push(resource),
258                        Err(e) => {
259                            warn!(
260                                "Failed to deserialize resource in list_resources_in_tenant: {}",
261                                e
262                            );
263                        }
264                    }
265                }
266                resources
267            }
268            Err(e) => {
269                warn!("Storage error in list_resources_in_tenant: {}", e);
270                Vec::new()
271            }
272        }
273    }
274
275    /// Count resources of a specific type for a tenant (used for limit checking).
276    async fn count_resources_for_tenant(&self, tenant_id: &str, resource_type: &str) -> usize {
277        let prefix = StorageKey::prefix(tenant_id, resource_type);
278        match self.storage.count(prefix).await {
279            Ok(count) => count,
280            Err(e) => {
281                warn!("Storage error in count_resources_for_tenant: {}", e);
282                0
283            }
284        }
285    }
286}
287
288// Note: No Default implementation for StandardResourceProvider as it requires storage parameter
289
290// Reuse error and stats types from the in_memory module for compatibility
291
292impl<S: StorageProvider> ResourceProvider for StandardResourceProvider<S> {
293    type Error = InMemoryError;
294
295    async fn create_resource(
296        &self,
297        resource_type: &str,
298        mut data: Value,
299        context: &RequestContext,
300    ) -> Result<Resource, Self::Error> {
301        let tenant_id = self.effective_tenant_id(context);
302
303        info!(
304            "Creating {} resource for tenant '{}' (request: '{}')",
305            resource_type, tenant_id, context.request_id
306        );
307        trace!(
308            "Create data: {}",
309            serde_json::to_string(&data).unwrap_or_else(|_| "invalid json".to_string())
310        );
311
312        // Check permissions first
313        context
314            .validate_operation("create")
315            .map_err(|e| InMemoryError::Internal { message: e })?;
316
317        // Check resource limits if this is a multi-tenant context
318        if let Some(tenant_context) = &context.tenant_context {
319            if resource_type == "User" {
320                if let Some(max_users) = tenant_context.permissions.max_users {
321                    let current_count = self.count_resources_for_tenant(&tenant_id, "User").await;
322                    if current_count >= max_users {
323                        return Err(InMemoryError::Internal {
324                            message: format!(
325                                "User limit exceeded: {}/{}",
326                                current_count, max_users
327                            ),
328                        });
329                    }
330                }
331            } else if resource_type == "Group" {
332                if let Some(max_groups) = tenant_context.permissions.max_groups {
333                    let current_count = self.count_resources_for_tenant(&tenant_id, "Group").await;
334                    if current_count >= max_groups {
335                        return Err(InMemoryError::Internal {
336                            message: format!(
337                                "Group limit exceeded: {}/{}",
338                                current_count, max_groups
339                            ),
340                        });
341                    }
342                }
343            }
344        }
345
346        // Generate ID if not provided
347        if data.get("id").is_none() {
348            let id = self.generate_resource_id(&tenant_id, resource_type).await;
349            if let Some(obj) = data.as_object_mut() {
350                obj.insert("id".to_string(), json!(id));
351            }
352        }
353
354        // Create resource
355        let resource = Resource::from_json(resource_type.to_string(), data).map_err(|e| {
356            InMemoryError::InvalidData {
357                message: format!("Failed to create resource: {}", e),
358            }
359        })?;
360
361        // Check for duplicate userName if this is a User resource
362        if resource_type == "User" {
363            if let Some(username) = resource.get_username() {
364                self.check_username_duplicate(&tenant_id, username, None)
365                    .await?;
366            }
367        }
368
369        // Add metadata
370        let resource_with_meta = self.add_scim_metadata(resource);
371        let resource_id = resource_with_meta.get_id().unwrap_or("unknown").to_string();
372
373        // Store resource using storage provider
374        let key = StorageKey::new(&tenant_id, resource_type, &resource_id);
375        let stored_data = self
376            .storage
377            .put(
378                key,
379                resource_with_meta
380                    .to_json()
381                    .map_err(|e| InMemoryError::Internal {
382                        message: format!("Failed to serialize resource: {}", e),
383                    })?,
384            )
385            .await
386            .map_err(|e| InMemoryError::Internal {
387                message: format!("Storage error during create: {}", e),
388            })?;
389
390        // Return the resource as stored
391        Resource::from_json(resource_type.to_string(), stored_data).map_err(|e| {
392            InMemoryError::InvalidData {
393                message: format!("Failed to deserialize stored resource: {}", e),
394            }
395        })
396    }
397
398    async fn get_resource(
399        &self,
400        resource_type: &str,
401        id: &str,
402        context: &RequestContext,
403    ) -> Result<Option<Resource>, Self::Error> {
404        let tenant_id = self.effective_tenant_id(context);
405
406        debug!(
407            "Getting {} resource with ID '{}' for tenant '{}' (request: '{}')",
408            resource_type, id, tenant_id, context.request_id
409        );
410
411        // Check permissions first
412        context
413            .validate_operation("read")
414            .map_err(|e| InMemoryError::Internal { message: e })?;
415
416        let key = StorageKey::new(&tenant_id, resource_type, id);
417        let resource_data = self
418            .storage
419            .get(key)
420            .await
421            .map_err(|e| InMemoryError::Internal {
422                message: format!("Storage error during get: {}", e),
423            })?;
424
425        let resource = match resource_data {
426            Some(data) => {
427                let resource =
428                    Resource::from_json(resource_type.to_string(), data).map_err(|e| {
429                        InMemoryError::InvalidData {
430                            message: format!("Failed to deserialize resource: {}", e),
431                        }
432                    })?;
433                trace!("Resource found and returned");
434                Some(resource)
435            }
436            None => {
437                debug!("Resource not found");
438                None
439            }
440        };
441
442        Ok(resource)
443    }
444
445    async fn update_resource(
446        &self,
447        resource_type: &str,
448        id: &str,
449        mut data: Value,
450        context: &RequestContext,
451    ) -> Result<Resource, Self::Error> {
452        let tenant_id = self.effective_tenant_id(context);
453
454        info!(
455            "Updating {} resource with ID '{}' for tenant '{}' (request: '{}')",
456            resource_type, id, tenant_id, context.request_id
457        );
458        trace!(
459            "Update data: {}",
460            serde_json::to_string(&data).unwrap_or_else(|_| "invalid json".to_string())
461        );
462
463        // Check permissions first
464        context
465            .validate_operation("update")
466            .map_err(|e| InMemoryError::Internal { message: e })?;
467
468        // Ensure ID is set correctly
469        if let Some(obj) = data.as_object_mut() {
470            obj.insert("id".to_string(), json!(id));
471        }
472
473        // Create updated resource
474        let resource = Resource::from_json(resource_type.to_string(), data).map_err(|e| {
475            InMemoryError::InvalidData {
476                message: format!("Failed to update resource: {}", e),
477            }
478        })?;
479
480        // Check for duplicate userName if this is a User resource
481        if resource_type == "User" {
482            if let Some(username) = resource.get_username() {
483                self.check_username_duplicate(&tenant_id, username, Some(id))
484                    .await?;
485            }
486        }
487
488        // Verify resource exists using storage provider
489        let key = StorageKey::new(&tenant_id, resource_type, id);
490        let exists =
491            self.storage
492                .exists(key.clone())
493                .await
494                .map_err(|e| InMemoryError::Internal {
495                    message: format!("Storage error during existence check: {}", e),
496                })?;
497
498        if !exists {
499            return Err(InMemoryError::ResourceNotFound {
500                resource_type: resource_type.to_string(),
501                id: id.to_string(),
502                tenant_id,
503            });
504        }
505
506        // Add metadata (preserve created time, update modified time)
507        let resource_with_meta = self.add_scim_metadata(resource);
508
509        // Store updated resource using storage provider
510        let stored_data = self
511            .storage
512            .put(
513                key,
514                resource_with_meta
515                    .to_json()
516                    .map_err(|e| InMemoryError::Internal {
517                        message: format!("Failed to serialize resource: {}", e),
518                    })?,
519            )
520            .await
521            .map_err(|e| InMemoryError::Internal {
522                message: format!("Storage error during update: {}", e),
523            })?;
524
525        // Return the updated resource as stored
526        Resource::from_json(resource_type.to_string(), stored_data).map_err(|e| {
527            InMemoryError::InvalidData {
528                message: format!("Failed to deserialize updated resource: {}", e),
529            }
530        })
531    }
532
533    async fn delete_resource(
534        &self,
535        resource_type: &str,
536        id: &str,
537        context: &RequestContext,
538    ) -> Result<(), Self::Error> {
539        let tenant_id = self.effective_tenant_id(context);
540
541        info!(
542            "Deleting {} resource with ID '{}' for tenant '{}' (request: '{}')",
543            resource_type, id, tenant_id, context.request_id
544        );
545
546        // Check permissions first
547        context
548            .validate_operation("delete")
549            .map_err(|e| InMemoryError::Internal { message: e })?;
550
551        // Delete resource using storage provider
552        let key = StorageKey::new(&tenant_id, resource_type, id);
553        let removed = self
554            .storage
555            .delete(key)
556            .await
557            .map_err(|e| InMemoryError::Internal {
558                message: format!("Storage error during delete: {}", e),
559            })?;
560
561        if !removed {
562            warn!(
563                "Attempted to delete non-existent {} resource with ID '{}' for tenant '{}'",
564                resource_type, id, tenant_id
565            );
566            return Err(InMemoryError::ResourceNotFound {
567                resource_type: resource_type.to_string(),
568                id: id.to_string(),
569                tenant_id,
570            });
571        }
572
573        debug!(
574            "Successfully deleted {} resource with ID '{}' for tenant '{}'",
575            resource_type, id, tenant_id
576        );
577        Ok(())
578    }
579
580    async fn list_resources(
581        &self,
582        resource_type: &str,
583        query: Option<&ListQuery>,
584        context: &RequestContext,
585    ) -> Result<Vec<Resource>, Self::Error> {
586        let tenant_id = self.effective_tenant_id(context);
587
588        debug!(
589            "Listing {} resources for tenant '{}' (request: '{}')",
590            resource_type, tenant_id, context.request_id
591        );
592
593        // Check permissions first
594        context
595            .validate_operation("list")
596            .map_err(|e| InMemoryError::Internal { message: e })?;
597
598        // List resources using storage provider
599        let prefix = StorageKey::prefix(&tenant_id, resource_type);
600        let storage_results = self
601            .storage
602            .list(prefix, 0, usize::MAX) // Get all resources for now, apply pagination later
603            .await
604            .map_err(|e| InMemoryError::Internal {
605                message: format!("Storage error during list: {}", e),
606            })?;
607
608        // Convert storage results to Resource objects
609        let mut resources = Vec::new();
610        for (_key, data) in storage_results {
611            match Resource::from_json(resource_type.to_string(), data) {
612                Ok(resource) => resources.push(resource),
613                Err(e) => {
614                    warn!("Failed to deserialize resource during list: {}", e);
615                    // Continue with other resources instead of failing entirely
616                }
617            }
618        }
619
620        // Apply simple filtering and pagination if query is provided
621        let mut filtered_resources = resources;
622
623        if let Some(q) = query {
624            // Apply start_index and count for pagination
625            if let Some(start_index) = q.start_index {
626                let start = (start_index.saturating_sub(1)) as usize; // SCIM uses 1-based indexing
627                if start < filtered_resources.len() {
628                    filtered_resources = filtered_resources.into_iter().skip(start).collect();
629                } else {
630                    filtered_resources = Vec::new();
631                }
632            }
633
634            if let Some(count) = q.count {
635                filtered_resources.truncate(count as usize);
636            }
637        }
638
639        debug!(
640            "Found {} {} resources for tenant '{}' (after filtering)",
641            filtered_resources.len(),
642            resource_type,
643            tenant_id
644        );
645
646        Ok(filtered_resources)
647    }
648
649    async fn find_resource_by_attribute(
650        &self,
651        resource_type: &str,
652        attribute: &str,
653        value: &Value,
654        context: &RequestContext,
655    ) -> Result<Option<Resource>, Self::Error> {
656        let tenant_id = self.effective_tenant_id(context);
657
658        // Find resource by attribute using storage provider
659        let prefix = StorageKey::prefix(&tenant_id, resource_type);
660        let value_str = match value {
661            Value::String(s) => s.clone(),
662            _ => value.to_string().trim_matches('"').to_string(),
663        };
664
665        let matches = self
666            .storage
667            .find_by_attribute(prefix, attribute, &value_str)
668            .await
669            .map_err(|e| InMemoryError::Internal {
670                message: format!("Storage error during find by attribute: {}", e),
671            })?;
672
673        // Return the first match
674        for (_key, data) in matches {
675            match Resource::from_json(resource_type.to_string(), data) {
676                Ok(resource) => return Ok(Some(resource)),
677                Err(e) => {
678                    warn!("Failed to deserialize resource during find: {}", e);
679                    continue;
680                }
681            }
682        }
683
684        Ok(None)
685    }
686
687    async fn resource_exists(
688        &self,
689        resource_type: &str,
690        id: &str,
691        context: &RequestContext,
692    ) -> Result<bool, Self::Error> {
693        let tenant_id = self.effective_tenant_id(context);
694
695        let key = StorageKey::new(&tenant_id, resource_type, id);
696        self.storage
697            .exists(key)
698            .await
699            .map_err(|e| InMemoryError::Internal {
700                message: format!("Storage error during exists check: {}", e),
701            })
702    }
703
704    async fn patch_resource(
705        &self,
706        resource_type: &str,
707        id: &str,
708        patch_request: &Value,
709        context: &RequestContext,
710    ) -> Result<Resource, Self::Error> {
711        let _tenant_id = self.effective_tenant_id(context);
712
713        // Check for ETag validation if provided in patch request
714        if let Some(etag_value) = patch_request.get("etag") {
715            if let Some(etag_str) = etag_value.as_str() {
716                // Get current resource to check version
717                let tenant_id = self.effective_tenant_id(context);
718                let key = StorageKey::new(&tenant_id, resource_type, id);
719
720                match self.storage.get(key).await {
721                    Ok(Some(current_data)) => {
722                        // Parse current resource to get version
723                        let current_resource = Resource::from_json(resource_type.to_string(), current_data)
724                            .map_err(|e| InMemoryError::InvalidData {
725                                message: format!("Failed to deserialize current resource: {}", e),
726                            })?;
727
728                        // Get current resource version for comparison
729                        if let Some(current_version) = current_resource.get_meta().and_then(|m| m.version.as_ref()) {
730                            let current_etag = current_version.as_str();
731                            // Compare the provided ETag with current version
732                            // Remove W/ prefix if present for comparison
733                            let normalized_current = current_etag.trim_start_matches("W/").trim_matches('"');
734                            let normalized_provided = etag_str.trim_start_matches("W/").trim_matches('"');
735
736                            if normalized_current != normalized_provided {
737                                return Err(InMemoryError::PreconditionFailed {
738                                    message: format!("ETag mismatch. Expected '{}', got '{}'", normalized_current, normalized_provided),
739                                });
740                            }
741                        }
742                    }
743                    Ok(None) => {
744                        return Err(InMemoryError::NotFound {
745                            resource_type: resource_type.to_string(),
746                            id: id.to_string(),
747                        });
748                    }
749                    Err(_) => {
750                        return Err(InMemoryError::Internal {
751                            message: "Failed to retrieve resource for ETag validation".to_string(),
752                        });
753                    }
754                }
755            }
756        }
757
758        // Extract operations from patch request
759        let operations = patch_request
760            .get("Operations")
761            .and_then(|ops| ops.as_array())
762            .ok_or(InMemoryError::InvalidInput {
763                message: "PATCH request must contain Operations array".to_string(),
764            })?;
765
766        // Validate that operations array is not empty
767        if operations.is_empty() {
768            return Err(InMemoryError::InvalidInput {
769                message: "Operations array cannot be empty".to_string(),
770            });
771        }
772
773        // Get current resource and apply patch
774        let tenant_id = self.effective_tenant_id(context);
775        let key = StorageKey::new(&tenant_id, resource_type, id);
776
777        match self.storage.get(key.clone()).await {
778            Ok(Some(mut current_data)) => {
779                // Apply each patch operation
780                for operation in operations {
781                    self.apply_patch_operation(&mut current_data, operation)?;
782                }
783
784                // Update version
785                let new_version = ScimVersion::from_content(
786                    serde_json::to_string(&current_data).unwrap().as_bytes(),
787                );
788                if let Some(obj) = current_data.as_object_mut() {
789                    obj.insert("version".to_string(), json!(new_version.to_string()));
790                }
791
792                // Store updated resource
793                self.storage
794                    .put(key, current_data.clone())
795                    .await
796                    .map_err(|_| InMemoryError::Internal {
797                        message: "Failed to store patched resource".to_string(),
798                    })?;
799
800                // Parse and return updated resource
801                let updated_resource = Resource::from_json(resource_type.to_string(), current_data)
802                    .map_err(|e| InMemoryError::InvalidInput {
803                        message: format!("Failed to deserialize patched resource: {}", e),
804                    })?;
805
806                Ok(updated_resource)
807            }
808            Ok(None) => Err(InMemoryError::NotFound {
809                resource_type: resource_type.to_string(),
810                id: id.to_string(),
811            }),
812            Err(_) => Err(InMemoryError::Internal {
813                message: "Failed to retrieve resource for patch".to_string(),
814            }),
815        }
816    }
817
818    /// Override the default patch operation implementation
819    fn apply_patch_operation(
820        &self,
821        resource_data: &mut Value,
822        operation: &Value,
823    ) -> Result<(), Self::Error> {
824        let op =
825            operation
826                .get("op")
827                .and_then(|v| v.as_str())
828                .ok_or(InMemoryError::InvalidInput {
829                    message: "PATCH operation must have 'op' field".to_string(),
830                })?;
831
832        let path = operation.get("path").and_then(|v| v.as_str());
833        let value = operation.get("value");
834
835        // Check if the operation targets a readonly attribute
836        if let Some(path_str) = path {
837            if self.is_readonly_attribute(path_str) {
838                return Err(InMemoryError::InvalidInput {
839                    message: format!("Cannot modify readonly attribute: {}", path_str),
840                });
841            }
842        }
843
844        match op.to_lowercase().as_str() {
845            "add" => self.apply_add_operation(resource_data, path, value),
846            "remove" => self.apply_remove_operation(resource_data, path),
847            "replace" => self.apply_replace_operation(resource_data, path, value),
848            _ => Err(InMemoryError::InvalidInput {
849                message: format!("Unsupported PATCH operation: {}", op),
850            }),
851        }
852    }
853
854}
855
856impl<S: StorageProvider> StandardResourceProvider<S> {
857    /// Check if an attribute path refers to a readonly attribute
858    fn is_readonly_attribute(&self, path: &str) -> bool {
859        // SCIM readonly attributes according to RFC 7643
860        match path.to_lowercase().as_str() {
861            // Meta attributes that are readonly
862            "meta.created" => true,
863            "meta.resourcetype" => true,
864            "meta.location" => true,
865            "id" => true,
866            // Complex attribute readonly subattributes
867            path if path.starts_with("meta.") && (path.ends_with(".created") || path.ends_with(".resourcetype") || path.ends_with(".location")) => true,
868            _ => false,
869        }
870    }
871
872    /// Apply ADD operation
873    fn apply_add_operation(
874        &self,
875        resource_data: &mut Value,
876        path: Option<&str>,
877        value: Option<&Value>,
878    ) -> Result<(), InMemoryError> {
879        let value = value.ok_or(InMemoryError::InvalidInput {
880            message: "ADD operation requires a value".to_string(),
881        })?;
882
883        match path {
884            Some(path_str) => {
885                self.set_value_at_path(resource_data, path_str, value.clone())?;
886            }
887            None => {
888                // No path means add to root - merge objects
889                if let (Some(current_obj), Some(value_obj)) =
890                    (resource_data.as_object_mut(), value.as_object())
891                {
892                    for (key, val) in value_obj {
893                        current_obj.insert(key.clone(), val.clone());
894                    }
895                }
896            }
897        }
898        Ok(())
899    }
900
901    /// Apply REMOVE operation
902    fn apply_remove_operation(
903        &self,
904        resource_data: &mut Value,
905        path: Option<&str>,
906    ) -> Result<(), InMemoryError> {
907        if let Some(path_str) = path {
908            self.remove_value_at_path(resource_data, path_str)?;
909        }
910        Ok(())
911    }
912
913    /// Apply REPLACE operation
914    fn apply_replace_operation(
915        &self,
916        resource_data: &mut Value,
917        path: Option<&str>,
918        value: Option<&Value>,
919    ) -> Result<(), InMemoryError> {
920        let value = value.ok_or(InMemoryError::InvalidInput {
921            message: "REPLACE operation requires a value".to_string(),
922        })?;
923
924        match path {
925            Some(path_str) => {
926                self.set_value_at_path(resource_data, path_str, value.clone())?;
927            }
928            None => {
929                // No path means replace entire resource
930                *resource_data = value.clone();
931            }
932        }
933        Ok(())
934    }
935
936    /// Set a value at a complex path (e.g., "name.givenName")
937    fn set_value_at_path(
938        &self,
939        data: &mut Value,
940        path: &str,
941        value: Value,
942    ) -> Result<(), InMemoryError> {
943        // Validate the path first
944        if !self.is_valid_scim_path(path) {
945            return Err(InMemoryError::InvalidInput {
946                message: format!("Invalid SCIM path: '{}'", path),
947            });
948        }
949
950        let parts: Vec<&str> = path.split('.').collect();
951
952        if parts.len() == 1 {
953            // Simple path - handle multivalued attributes specially
954            if let Some(obj) = data.as_object_mut() {
955                let attribute_name = parts[0];
956
957                // Check if this is a multivalued attribute that should be appended to
958                if Self::is_multivalued_attribute(attribute_name) {
959                    if let Some(existing) = obj.get_mut(attribute_name) {
960                        if let Some(existing_array) = existing.as_array_mut() {
961                            // If the value being added is an array, replace the entire array
962                            if value.is_array() {
963                                obj.insert(attribute_name.to_string(), value);
964                            } else {
965                                // If the value is a single object, append to existing array
966                                existing_array.push(value);
967                            }
968                            return Ok(());
969                        }
970                    }
971                    // If no existing array, create new one
972                    let new_array = if value.is_array() {
973                        value
974                    } else {
975                        json!([value])
976                    };
977                    obj.insert(attribute_name.to_string(), new_array);
978                } else {
979                    // Single-valued attribute - replace
980                    obj.insert(attribute_name.to_string(), value);
981                }
982            }
983            return Ok(());
984        }
985
986        // Complex path - navigate to the parent and create intermediate objects if needed
987        let mut current = data;
988
989        for part in &parts[..parts.len() - 1] {
990            if let Some(obj) = current.as_object_mut() {
991                let entry = obj
992                    .entry(part.to_string())
993                    .or_insert_with(|| Value::Object(serde_json::Map::new()));
994                current = entry;
995            } else {
996                return Err(InMemoryError::InvalidInput {
997                    message: format!(
998                        "Cannot navigate path '{}' - intermediate value is not an object",
999                        path
1000                    ),
1001                });
1002            }
1003        }
1004
1005        // Set the final value
1006        if let Some(obj) = current.as_object_mut() {
1007            obj.insert(parts.last().unwrap().to_string(), value);
1008        } else {
1009            return Err(InMemoryError::InvalidInput {
1010                message: format!(
1011                    "Cannot set value at path '{}' - target is not an object",
1012                    path
1013                ),
1014            });
1015        }
1016
1017        Ok(())
1018    }
1019
1020    /// Remove a value at a complex path (e.g., "name.givenName")
1021    fn remove_value_at_path(&self, data: &mut Value, path: &str) -> Result<(), InMemoryError> {
1022        // Validate the path first
1023        if !self.is_valid_scim_path(path) {
1024            return Err(InMemoryError::InvalidInput {
1025                message: format!("Invalid SCIM path: '{}'", path),
1026            });
1027        }
1028
1029        let parts: Vec<&str> = path.split('.').collect();
1030
1031        if parts.len() == 1 {
1032            // Simple path
1033            if let Some(obj) = data.as_object_mut() {
1034                obj.remove(parts[0]);
1035            }
1036            return Ok(());
1037        }
1038
1039        // Complex path - navigate to the parent
1040        let mut current = data;
1041
1042        for part in &parts[..parts.len() - 1] {
1043            if let Some(obj) = current.as_object_mut() {
1044                // If the path component doesn't exist, treat as success (idempotent remove)
1045                match obj.get_mut(*part) {
1046                    Some(value) => current = value,
1047                    None => return Ok(()), // Path doesn't exist, nothing to remove
1048                }
1049            } else {
1050                return Err(InMemoryError::InvalidInput {
1051                    message: format!(
1052                        "Cannot navigate path '{}' - intermediate value is not an object",
1053                        path
1054                    ),
1055                });
1056            }
1057        }
1058
1059        // Remove the final value
1060        if let Some(obj) = current.as_object_mut() {
1061            obj.remove(*parts.last().unwrap());
1062        }
1063
1064        Ok(())
1065    }
1066
1067    /// Check if an attribute is multivalued
1068    fn is_multivalued_attribute(attribute_name: &str) -> bool {
1069        matches!(
1070            attribute_name,
1071            "emails" | "phoneNumbers" | "addresses" | "groups" | "members"
1072        )
1073    }
1074
1075    /// Validate if a path represents a valid SCIM attribute
1076    fn is_valid_scim_path(&self, path: &str) -> bool {
1077        // Handle schema URN prefixed paths (e.g., "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User.department")
1078        let actual_path = if path.contains(':') && path.contains("urn:ietf:params:scim:schemas:") {
1079            // Extract the attribute part after the schema URN
1080            if let Some(colon_pos) = path.rfind(':') {
1081                let after_colon = &path[colon_pos + 1..];
1082                // If there's a dot after the resource type, get the attribute part
1083                if let Some(dot_pos) = after_colon.find('.') {
1084                    &after_colon[dot_pos + 1..]
1085                } else {
1086                    after_colon
1087                }
1088            } else {
1089                path
1090            }
1091        } else {
1092            path
1093        };
1094
1095        // Handle filter expressions first
1096        if actual_path.contains('[') {
1097            // Basic filter syntax validation
1098            if let Some(bracket_start) = actual_path.find('[') {
1099                let before_bracket = &actual_path[..bracket_start];
1100                if !self.is_valid_simple_path(before_bracket) {
1101                    return false;
1102                }
1103                // Check for malformed filter syntax - must have closing bracket
1104                let remaining = &actual_path[bracket_start..];
1105                if !remaining.ends_with(']') || remaining.matches('[').count() != remaining.matches(']').count() {
1106                    return false;
1107                }
1108                return true;
1109            }
1110        }
1111
1112        // Handle complex paths (e.g., "name.givenName")
1113        let parts: Vec<&str> = actual_path.split('.').collect();
1114
1115        // First part must be a valid root attribute
1116        if !self.is_valid_simple_path(parts[0]) {
1117            return false;
1118        }
1119
1120        // If there are sub-attributes, validate them
1121        if parts.len() > 1 {
1122            // For now, allow common sub-attributes for known complex types
1123            if parts[0] == "name" {
1124                return matches!(parts[1], "formatted" | "familyName" | "givenName" | "middleName" | "honorificPrefix" | "honorificSuffix");
1125            }
1126            if parts[0] == "meta" {
1127                return matches!(parts[1], "resourceType" | "created" | "lastModified" | "location" | "version");
1128            }
1129            // For other complex attributes, we'll be permissive for now
1130            // In a full implementation, this would check against schema definitions
1131        }
1132
1133        true
1134    }
1135
1136    /// Check if a simple path (single attribute name) is valid
1137    fn is_valid_simple_path(&self, attribute: &str) -> bool {
1138        // Standard SCIM User attributes
1139        let user_attributes = [
1140            "id", "externalId", "userName", "name", "displayName", "nickName", "profileUrl",
1141            "title", "userType", "preferredLanguage", "locale", "timezone", "active",
1142            "password", "emails", "phoneNumbers", "addresses", "groups", "entitlements",
1143            "roles", "x509Certificates", "meta"
1144        ];
1145
1146        // Standard SCIM Group attributes
1147        let group_attributes = [
1148            "id", "externalId", "displayName", "members", "meta"
1149        ];
1150
1151        // Enterprise extension attributes
1152        let enterprise_attributes = [
1153            "employeeNumber", "costCenter", "organization", "division", "department", "manager"
1154        ];
1155
1156        // Check against known attributes
1157        user_attributes.contains(&attribute) ||
1158        group_attributes.contains(&attribute) ||
1159        enterprise_attributes.contains(&attribute)
1160    }
1161}
1162
1163// Essential conditional operations for testing
1164impl<S: StorageProvider> StandardResourceProvider<S> {
1165    pub async fn conditional_update(
1166        &self,
1167        resource_type: &str,
1168        id: &str,
1169        data: Value,
1170        expected_version: &ScimVersion,
1171        context: &RequestContext,
1172    ) -> Result<ConditionalResult<VersionedResource>, InMemoryError> {
1173        let tenant_id = self.effective_tenant_id(context);
1174        let key = StorageKey::new(&tenant_id, resource_type, id);
1175
1176        // Get current resource to check version
1177        match self.storage.get(key.clone()).await {
1178            Ok(Some(current_data)) => {
1179                // Parse current resource to extract version
1180                let current_resource =
1181                    Resource::from_json(resource_type.to_string(), current_data.clone()).map_err(
1182                        |e| InMemoryError::InvalidInput {
1183                            message: format!("Failed to deserialize stored resource: {}", e),
1184                        },
1185                    )?;
1186
1187                // Check if version matches
1188                let current_version = VersionedResource::new(current_resource.clone())
1189                    .version()
1190                    .clone();
1191                if &current_version != expected_version {
1192                    use crate::resource::version::VersionConflict;
1193                    return Ok(ConditionalResult::VersionMismatch(VersionConflict::new(
1194                        expected_version.clone(),
1195                        current_version,
1196                        "Resource was modified by another client",
1197                    )));
1198                }
1199
1200                // Version matches, proceed with update
1201                let mut updated_resource = Resource::from_json(resource_type.to_string(), data)
1202                    .map_err(|e| InMemoryError::InvalidInput {
1203                        message: format!("Failed to create updated resource: {}", e),
1204                    })?;
1205
1206                // Preserve the ID
1207                if let Some(original_id) = current_resource.get_id() {
1208                    updated_resource.set_id(original_id).map_err(|e| {
1209                        InMemoryError::InvalidInput {
1210                            message: format!("Failed to set ID: {}", e),
1211                        }
1212                    })?;
1213                }
1214
1215                // Store updated resource - convert back to JSON for storage
1216                let updated_data =
1217                    updated_resource
1218                        .to_json()
1219                        .map_err(|e| InMemoryError::InvalidInput {
1220                            message: format!("Failed to serialize updated resource: {}", e),
1221                        })?;
1222
1223                self.storage
1224                    .put(key, updated_data)
1225                    .await
1226                    .map_err(|_| InMemoryError::Internal {
1227                        message: "Failed to store updated resource".to_string(),
1228                    })?;
1229
1230                Ok(ConditionalResult::Success(VersionedResource::new(
1231                    updated_resource,
1232                )))
1233            }
1234            Ok(None) => Err(InMemoryError::NotFound {
1235                resource_type: resource_type.to_string(),
1236                id: id.to_string(),
1237            }),
1238            Err(_) => Err(InMemoryError::Internal {
1239                message: "Failed to retrieve resource for conditional update".to_string(),
1240            }),
1241        }
1242    }
1243
1244    pub async fn conditional_delete(
1245        &self,
1246        resource_type: &str,
1247        id: &str,
1248        expected_version: &ScimVersion,
1249        context: &RequestContext,
1250    ) -> Result<ConditionalResult<()>, InMemoryError> {
1251        let tenant_id = self.effective_tenant_id(context);
1252        let key = StorageKey::new(&tenant_id, resource_type, id);
1253
1254        // Get current resource to check version
1255        match self.storage.get(key.clone()).await {
1256            Ok(Some(current_data)) => {
1257                // Parse current resource to extract version
1258                let current_resource = Resource::from_json(resource_type.to_string(), current_data)
1259                    .map_err(|e| InMemoryError::InvalidInput {
1260                        message: format!("Failed to deserialize stored resource: {}", e),
1261                    })?;
1262
1263                // Check if version matches
1264                let current_version = VersionedResource::new(current_resource.clone())
1265                    .version()
1266                    .clone();
1267                if &current_version != expected_version {
1268                    use crate::resource::version::VersionConflict;
1269                    return Ok(ConditionalResult::VersionMismatch(VersionConflict::new(
1270                        expected_version.clone(),
1271                        current_version,
1272                        "Resource was modified by another client",
1273                    )));
1274                }
1275
1276                // Version matches, proceed with delete
1277                self.storage
1278                    .delete(key)
1279                    .await
1280                    .map_err(|_| InMemoryError::Internal {
1281                        message: "Failed to delete resource".to_string(),
1282                    })?;
1283
1284                Ok(ConditionalResult::Success(()))
1285            }
1286            Ok(None) => Err(InMemoryError::NotFound {
1287                resource_type: resource_type.to_string(),
1288                id: id.to_string(),
1289            }),
1290            Err(_) => Err(InMemoryError::Internal {
1291                message: "Failed to retrieve resource for conditional delete".to_string(),
1292            }),
1293        }
1294    }
1295
1296    pub async fn conditional_patch_resource(
1297        &self,
1298        resource_type: &str,
1299        id: &str,
1300        patch_request: &Value,
1301        expected_version: &ScimVersion,
1302        context: &RequestContext,
1303    ) -> Result<ConditionalResult<VersionedResource>, InMemoryError> {
1304        let tenant_id = self.effective_tenant_id(context);
1305        let key = StorageKey::new(&tenant_id, resource_type, id);
1306
1307        // Get current resource to check version
1308        match self.storage.get(key.clone()).await {
1309            Ok(Some(current_data)) => {
1310                // Parse current resource to extract version
1311                let current_resource =
1312                    Resource::from_json(resource_type.to_string(), current_data.clone()).map_err(
1313                        |e| InMemoryError::InvalidInput {
1314                            message: format!("Failed to deserialize stored resource: {}", e),
1315                        },
1316                    )?;
1317
1318                // Check if version matches
1319                let current_version = VersionedResource::new(current_resource.clone())
1320                    .version()
1321                    .clone();
1322                if &current_version != expected_version {
1323                    use crate::resource::version::VersionConflict;
1324                    return Ok(ConditionalResult::VersionMismatch(VersionConflict::new(
1325                        expected_version.clone(),
1326                        current_version,
1327                        "Resource was modified by another client",
1328                    )));
1329                }
1330
1331                // Version matches, proceed with patch
1332                let mut patched_data = current_data;
1333
1334                // Apply patch operations
1335                if let Some(operations) = patch_request.get("Operations") {
1336                    if let Some(ops_array) = operations.as_array() {
1337                        for operation in ops_array {
1338                            self.apply_patch_operation(&mut patched_data, operation)?;
1339                        }
1340                    }
1341                }
1342
1343                // Parse patched resource with proper resource type
1344                let patched_resource = Resource::from_json(resource_type.to_string(), patched_data)
1345                    .map_err(|e| InMemoryError::InvalidInput {
1346                        message: format!("Failed to deserialize patched resource: {}", e),
1347                    })?;
1348
1349                // Store patched resource - convert back to JSON for storage
1350                let patched_json =
1351                    patched_resource
1352                        .to_json()
1353                        .map_err(|e| InMemoryError::InvalidInput {
1354                            message: format!("Failed to serialize patched resource: {}", e),
1355                        })?;
1356
1357                self.storage
1358                    .put(key, patched_json)
1359                    .await
1360                    .map_err(|_| InMemoryError::Internal {
1361                        message: "Failed to store patched resource".to_string(),
1362                    })?;
1363
1364                Ok(ConditionalResult::Success(VersionedResource::new(
1365                    patched_resource,
1366                )))
1367            }
1368            Ok(None) => Err(InMemoryError::NotFound {
1369                resource_type: resource_type.to_string(),
1370                id: id.to_string(),
1371            }),
1372            Err(_) => Err(InMemoryError::Internal {
1373                message: "Failed to retrieve resource for conditional patch".to_string(),
1374            }),
1375        }
1376    }
1377
1378    pub async fn get_versioned_resource(
1379        &self,
1380        resource_type: &str,
1381        id: &str,
1382        context: &RequestContext,
1383    ) -> Result<Option<VersionedResource>, InMemoryError> {
1384        match self.get_resource(resource_type, id, context).await? {
1385            Some(resource) => Ok(Some(VersionedResource::new(resource))),
1386            None => Ok(None),
1387        }
1388    }
1389
1390    pub async fn create_versioned_resource(
1391        &self,
1392        resource_type: &str,
1393        data: Value,
1394        context: &RequestContext,
1395    ) -> Result<VersionedResource, InMemoryError> {
1396        let resource = self.create_resource(resource_type, data, context).await?;
1397        Ok(VersionedResource::new(resource))
1398    }
1399}