scim_server/providers/
in_memory.rs

1//! Standard in-memory resource provider implementation.
2//!
3//! This module provides a production-ready in-memory implementation of the
4//! ResourceProvider trait that supports both single-tenant and multi-tenant
5//! operations through the unified RequestContext interface.
6//!
7//! # Features
8//!
9//! * Thread-safe concurrent access with RwLock
10//! * Automatic tenant isolation when tenant context is provided
11//! * Fallback to "default" tenant for single-tenant operations
12//! * Comprehensive error handling
13//! * Resource metadata tracking (created/updated timestamps)
14//! * Duplicate detection for userName attributes
15//!
16//! # Example Usage
17//!
18//! ```rust
19//! use scim_server::providers::InMemoryProvider;
20//! use scim_server::resource::{RequestContext, TenantContext, ResourceProvider};
21//! use serde_json::json;
22//!
23//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
24//! let provider = InMemoryProvider::new();
25//!
26//! // Single-tenant operation
27//! let single_context = RequestContext::with_generated_id();
28//! let user_data = json!({
29//!     "userName": "john.doe",
30//!     "displayName": "John Doe"
31//! });
32//! let user = provider.create_resource("User", user_data.clone(), &single_context).await?;
33//!
34//! // Multi-tenant operation
35//! let tenant_context = TenantContext::new("tenant1".to_string(), "client1".to_string());
36//! let multi_context = RequestContext::with_tenant_generated_id(tenant_context);
37//! let tenant_user = provider.create_resource("User", user_data, &multi_context).await?;
38//! # Ok(())
39//! # }
40//! ```
41
42use crate::resource::{
43    ListQuery, RequestContext, Resource, ResourceProvider,
44    conditional_provider::VersionedResource,
45    version::{ConditionalResult, ScimVersion, VersionConflict},
46};
47use log::{debug, info, trace, warn};
48use serde_json::{Value, json};
49use std::collections::HashMap;
50use std::sync::Arc;
51use tokio::sync::RwLock;
52
53/// Thread-safe in-memory resource provider supporting both single and multi-tenant operations.
54///
55/// This provider organizes data as: tenant_id -> resource_type -> resource_id -> resource
56/// For single-tenant operations, it uses "default" as the tenant_id.
57#[derive(Debug, Clone)]
58pub struct InMemoryProvider {
59    // Structure: tenant_id -> resource_type -> resource_id -> resource
60    data: Arc<RwLock<HashMap<String, HashMap<String, HashMap<String, Resource>>>>>,
61    // Track next ID per tenant and resource type for ID generation
62    next_ids: Arc<RwLock<HashMap<String, HashMap<String, u64>>>>,
63}
64
65impl InMemoryProvider {
66    /// Create a new in-memory provider.
67    pub fn new() -> Self {
68        Self {
69            data: Arc::new(RwLock::new(HashMap::new())),
70            next_ids: Arc::new(RwLock::new(HashMap::new())),
71        }
72    }
73
74    /// Get the effective tenant ID for the operation.
75    ///
76    /// Returns the tenant ID from the context, or "default" for single-tenant operations.
77    fn effective_tenant_id(&self, context: &RequestContext) -> String {
78        context.tenant_id().unwrap_or("default").to_string()
79    }
80
81    /// Generate a unique resource ID for the given tenant and resource type.
82    async fn generate_resource_id(&self, tenant_id: &str, resource_type: &str) -> String {
83        let mut next_ids_guard = self.next_ids.write().await;
84        let tenant_ids = next_ids_guard
85            .entry(tenant_id.to_string())
86            .or_insert_with(HashMap::new);
87        let next_id = tenant_ids.entry(resource_type.to_string()).or_insert(1);
88
89        let id = next_id.to_string();
90        *next_id += 1;
91        id
92    }
93
94    /// Check for duplicate userName in User resources within the same tenant.
95    async fn check_username_duplicate(
96        &self,
97        tenant_id: &str,
98        username: &str,
99        exclude_id: Option<&str>,
100    ) -> Result<(), InMemoryError> {
101        let data_guard = self.data.read().await;
102
103        if let Some(tenant_data) = data_guard.get(tenant_id) {
104            if let Some(users) = tenant_data.get("User") {
105                for (existing_id, existing_user) in users {
106                    // Skip the resource we're updating
107                    if Some(existing_id.as_str()) == exclude_id {
108                        continue;
109                    }
110
111                    if let Some(existing_username) = existing_user.get_username() {
112                        if existing_username == username {
113                            return Err(InMemoryError::DuplicateAttribute {
114                                resource_type: "User".to_string(),
115                                attribute: "userName".to_string(),
116                                value: username.to_string(),
117                                tenant_id: tenant_id.to_string(),
118                            });
119                        }
120                    }
121                }
122            }
123        }
124
125        Ok(())
126    }
127
128    /// Add SCIM metadata to a resource.
129    fn add_scim_metadata(&self, mut resource: Resource) -> Resource {
130        let now = chrono::Utc::now().to_rfc3339();
131        resource.add_metadata("/scim/v2", &now, &now);
132        resource
133    }
134
135    /// Clear all data (useful for testing).
136    pub async fn clear(&self) {
137        let mut data_guard = self.data.write().await;
138        let mut ids_guard = self.next_ids.write().await;
139        data_guard.clear();
140        ids_guard.clear();
141    }
142
143    /// Get statistics about stored data.
144    pub async fn get_stats(&self) -> InMemoryStats {
145        let data_guard = self.data.read().await;
146
147        let mut tenant_count = 0;
148        let mut total_resources = 0;
149        let mut resource_types = std::collections::HashSet::new();
150
151        for (_tenant_id, tenant_data) in data_guard.iter() {
152            tenant_count += 1;
153            for (resource_type, resources) in tenant_data.iter() {
154                resource_types.insert(resource_type.clone());
155                total_resources += resources.len();
156            }
157        }
158
159        InMemoryStats {
160            tenant_count,
161            total_resources,
162            resource_type_count: resource_types.len(),
163            resource_types: resource_types.into_iter().collect(),
164        }
165    }
166
167    /// List all resources of a specific type in a tenant.
168    pub async fn list_resources_in_tenant(
169        &self,
170        tenant_id: &str,
171        resource_type: &str,
172    ) -> Vec<Resource> {
173        let data_guard = self.data.read().await;
174
175        data_guard
176            .get(tenant_id)
177            .and_then(|tenant_data| tenant_data.get(resource_type))
178            .map(|resources| resources.values().cloned().collect())
179            .unwrap_or_default()
180    }
181
182    /// Count resources of a specific type for a tenant (used for limit checking).
183    async fn count_resources_for_tenant(&self, tenant_id: &str, resource_type: &str) -> usize {
184        let data_guard = self.data.read().await;
185        data_guard
186            .get(tenant_id)
187            .and_then(|tenant_data| tenant_data.get(resource_type))
188            .map(|resources| resources.len())
189            .unwrap_or(0)
190    }
191}
192
193impl Default for InMemoryProvider {
194    fn default() -> Self {
195        Self::new()
196    }
197}
198
199/// Error types for the in-memory provider.
200#[derive(Debug, thiserror::Error)]
201pub enum InMemoryError {
202    #[error("Resource not found: {resource_type} with id '{id}' in tenant '{tenant_id}'")]
203    ResourceNotFound {
204        resource_type: String,
205        id: String,
206        tenant_id: String,
207    },
208
209    #[error(
210        "Duplicate attribute '{attribute}' with value '{value}' for {resource_type} in tenant '{tenant_id}'"
211    )]
212    DuplicateAttribute {
213        resource_type: String,
214        attribute: String,
215        value: String,
216        tenant_id: String,
217    },
218
219    #[error("Invalid resource data: {message}")]
220    InvalidData { message: String },
221
222    #[error("Query error: {message}")]
223    QueryError { message: String },
224
225    #[error("Internal error: {message}")]
226    Internal { message: String },
227}
228
229/// Statistics about the in-memory provider state.
230#[derive(Debug, Clone)]
231pub struct InMemoryStats {
232    pub tenant_count: usize,
233    pub total_resources: usize,
234    pub resource_type_count: usize,
235    pub resource_types: Vec<String>,
236}
237
238impl ResourceProvider for InMemoryProvider {
239    type Error = InMemoryError;
240
241    async fn create_resource(
242        &self,
243        resource_type: &str,
244        mut data: Value,
245        context: &RequestContext,
246    ) -> Result<Resource, Self::Error> {
247        let tenant_id = self.effective_tenant_id(context);
248
249        info!(
250            "Creating {} resource for tenant '{}' (request: '{}')",
251            resource_type, tenant_id, context.request_id
252        );
253        trace!(
254            "Create data: {}",
255            serde_json::to_string(&data).unwrap_or_else(|_| "invalid json".to_string())
256        );
257
258        // Check permissions first
259        context
260            .validate_operation("create")
261            .map_err(|e| InMemoryError::Internal { message: e })?;
262
263        // Check resource limits if this is a multi-tenant context
264        if let Some(tenant_context) = &context.tenant_context {
265            if resource_type == "User" {
266                if let Some(max_users) = tenant_context.permissions.max_users {
267                    let current_count = self.count_resources_for_tenant(&tenant_id, "User").await;
268                    if current_count >= max_users {
269                        return Err(InMemoryError::Internal {
270                            message: format!(
271                                "User limit exceeded: {}/{}",
272                                current_count, max_users
273                            ),
274                        });
275                    }
276                }
277            } else if resource_type == "Group" {
278                if let Some(max_groups) = tenant_context.permissions.max_groups {
279                    let current_count = self.count_resources_for_tenant(&tenant_id, "Group").await;
280                    if current_count >= max_groups {
281                        return Err(InMemoryError::Internal {
282                            message: format!(
283                                "Group limit exceeded: {}/{}",
284                                current_count, max_groups
285                            ),
286                        });
287                    }
288                }
289            }
290        }
291
292        // Generate ID if not provided
293        if data.get("id").is_none() {
294            let id = self.generate_resource_id(&tenant_id, resource_type).await;
295            if let Some(obj) = data.as_object_mut() {
296                obj.insert("id".to_string(), json!(id));
297            }
298        }
299
300        // Create resource
301        let resource = Resource::from_json(resource_type.to_string(), data).map_err(|e| {
302            InMemoryError::InvalidData {
303                message: format!("Failed to create resource: {}", e),
304            }
305        })?;
306
307        // Check for duplicate userName if this is a User resource
308        if resource_type == "User" {
309            if let Some(username) = resource.get_username() {
310                self.check_username_duplicate(&tenant_id, username, None)
311                    .await?;
312            }
313        }
314
315        // Add metadata
316        let resource_with_meta = self.add_scim_metadata(resource);
317        let resource_id = resource_with_meta.get_id().unwrap_or("unknown").to_string();
318
319        // Store resource
320        let mut data_guard = self.data.write().await;
321        data_guard
322            .entry(tenant_id.clone())
323            .or_insert_with(HashMap::new)
324            .entry(resource_type.to_string())
325            .or_insert_with(HashMap::new)
326            .insert(resource_id.clone(), resource_with_meta.clone());
327
328        Ok(resource_with_meta)
329    }
330
331    async fn get_resource(
332        &self,
333        resource_type: &str,
334        id: &str,
335        context: &RequestContext,
336    ) -> Result<Option<Resource>, Self::Error> {
337        let tenant_id = self.effective_tenant_id(context);
338
339        debug!(
340            "Getting {} resource with ID '{}' for tenant '{}' (request: '{}')",
341            resource_type, id, tenant_id, context.request_id
342        );
343
344        // Check permissions first
345        context
346            .validate_operation("read")
347            .map_err(|e| InMemoryError::Internal { message: e })?;
348
349        let data_guard = self.data.read().await;
350        let resource = data_guard
351            .get(&tenant_id)
352            .and_then(|tenant_data| tenant_data.get(resource_type))
353            .and_then(|type_data| type_data.get(id))
354            .cloned();
355
356        if resource.is_some() {
357            trace!("Resource found and returned");
358        } else {
359            debug!("Resource not found");
360        }
361
362        Ok(resource)
363    }
364
365    async fn update_resource(
366        &self,
367        resource_type: &str,
368        id: &str,
369        mut data: Value,
370        context: &RequestContext,
371    ) -> Result<Resource, Self::Error> {
372        let tenant_id = self.effective_tenant_id(context);
373
374        info!(
375            "Updating {} resource with ID '{}' for tenant '{}' (request: '{}')",
376            resource_type, id, tenant_id, context.request_id
377        );
378        trace!(
379            "Update data: {}",
380            serde_json::to_string(&data).unwrap_or_else(|_| "invalid json".to_string())
381        );
382
383        // Check permissions first
384        context
385            .validate_operation("update")
386            .map_err(|e| InMemoryError::Internal { message: e })?;
387
388        // Ensure ID is set correctly
389        if let Some(obj) = data.as_object_mut() {
390            obj.insert("id".to_string(), json!(id));
391        }
392
393        // Create updated resource
394        let resource = Resource::from_json(resource_type.to_string(), data).map_err(|e| {
395            InMemoryError::InvalidData {
396                message: format!("Failed to update resource: {}", e),
397            }
398        })?;
399
400        // Check for duplicate userName if this is a User resource
401        if resource_type == "User" {
402            if let Some(username) = resource.get_username() {
403                self.check_username_duplicate(&tenant_id, username, Some(id))
404                    .await?;
405            }
406        }
407
408        // Verify resource exists
409        {
410            let data_guard = self.data.read().await;
411            let exists = data_guard
412                .get(&tenant_id)
413                .and_then(|tenant_data| tenant_data.get(resource_type))
414                .and_then(|type_data| type_data.get(id))
415                .is_some();
416
417            if !exists {
418                return Err(InMemoryError::ResourceNotFound {
419                    resource_type: resource_type.to_string(),
420                    id: id.to_string(),
421                    tenant_id,
422                });
423            }
424        }
425
426        // Add metadata (preserve created time, update modified time)
427        let resource_with_meta = self.add_scim_metadata(resource);
428
429        // Store updated resource
430        let mut data_guard = self.data.write().await;
431        data_guard
432            .get_mut(&tenant_id)
433            .and_then(|tenant_data| tenant_data.get_mut(resource_type))
434            .and_then(|type_data| type_data.insert(id.to_string(), resource_with_meta.clone()));
435
436        Ok(resource_with_meta)
437    }
438
439    async fn delete_resource(
440        &self,
441        resource_type: &str,
442        id: &str,
443        context: &RequestContext,
444    ) -> Result<(), Self::Error> {
445        let tenant_id = self.effective_tenant_id(context);
446
447        info!(
448            "Deleting {} resource with ID '{}' for tenant '{}' (request: '{}')",
449            resource_type, id, tenant_id, context.request_id
450        );
451
452        // Check permissions first
453        context
454            .validate_operation("delete")
455            .map_err(|e| InMemoryError::Internal { message: e })?;
456
457        let mut data_guard = self.data.write().await;
458        let removed = data_guard
459            .get_mut(&tenant_id)
460            .and_then(|tenant_data| tenant_data.get_mut(resource_type))
461            .and_then(|type_data| type_data.remove(id))
462            .is_some();
463
464        if !removed {
465            warn!(
466                "Attempted to delete non-existent {} resource with ID '{}' for tenant '{}'",
467                resource_type, id, tenant_id
468            );
469            return Err(InMemoryError::ResourceNotFound {
470                resource_type: resource_type.to_string(),
471                id: id.to_string(),
472                tenant_id,
473            });
474        }
475
476        debug!(
477            "Successfully deleted {} resource with ID '{}' for tenant '{}'",
478            resource_type, id, tenant_id
479        );
480        Ok(())
481    }
482
483    async fn list_resources(
484        &self,
485        resource_type: &str,
486        query: Option<&ListQuery>,
487        context: &RequestContext,
488    ) -> Result<Vec<Resource>, Self::Error> {
489        let tenant_id = self.effective_tenant_id(context);
490
491        debug!(
492            "Listing {} resources for tenant '{}' (request: '{}')",
493            resource_type, tenant_id, context.request_id
494        );
495
496        // Check permissions first
497        context
498            .validate_operation("list")
499            .map_err(|e| InMemoryError::Internal { message: e })?;
500
501        let data_guard = self.data.read().await;
502        let resources: Vec<Resource> = data_guard
503            .get(&tenant_id)
504            .and_then(|tenant_data| tenant_data.get(resource_type))
505            .map(|type_data| type_data.values().cloned().collect())
506            .unwrap_or_default();
507
508        // Apply simple filtering and pagination if query is provided
509        let mut filtered_resources = resources;
510
511        if let Some(q) = query {
512            // Apply start_index and count for pagination
513            if let Some(start_index) = q.start_index {
514                let start = (start_index.saturating_sub(1)) as usize; // SCIM uses 1-based indexing
515                if start < filtered_resources.len() {
516                    filtered_resources = filtered_resources.into_iter().skip(start).collect();
517                } else {
518                    filtered_resources = Vec::new();
519                }
520            }
521
522            if let Some(count) = q.count {
523                filtered_resources.truncate(count as usize);
524            }
525        }
526
527        debug!(
528            "Found {} {} resources for tenant '{}' (after filtering)",
529            filtered_resources.len(),
530            resource_type,
531            tenant_id
532        );
533
534        Ok(filtered_resources)
535    }
536
537    async fn find_resource_by_attribute(
538        &self,
539        resource_type: &str,
540        attribute: &str,
541        value: &Value,
542        context: &RequestContext,
543    ) -> Result<Option<Resource>, Self::Error> {
544        let tenant_id = self.effective_tenant_id(context);
545
546        let data_guard = self.data.read().await;
547        if let Some(tenant_data) = data_guard.get(&tenant_id) {
548            if let Some(type_data) = tenant_data.get(resource_type) {
549                for resource in type_data.values() {
550                    // Handle special structured fields
551                    let found_match = match attribute {
552                        "userName" => {
553                            if let Some(username) = resource.get_username() {
554                                &Value::String(username.to_string()) == value
555                            } else {
556                                false
557                            }
558                        }
559                        "id" => {
560                            if let Some(id) = resource.get_id() {
561                                &Value::String(id.to_string()) == value
562                            } else {
563                                false
564                            }
565                        }
566                        // For other attributes, check the attributes map
567                        _ => {
568                            if let Some(attr_value) = resource.get_attribute(attribute) {
569                                attr_value == value
570                            } else {
571                                false
572                            }
573                        }
574                    };
575
576                    if found_match {
577                        return Ok(Some(resource.clone()));
578                    }
579                }
580            }
581        }
582
583        Ok(None)
584    }
585
586    async fn resource_exists(
587        &self,
588        resource_type: &str,
589        id: &str,
590        context: &RequestContext,
591    ) -> Result<bool, Self::Error> {
592        let tenant_id = self.effective_tenant_id(context);
593
594        let data_guard = self.data.read().await;
595        let exists = data_guard
596            .get(&tenant_id)
597            .and_then(|tenant_data| tenant_data.get(resource_type))
598            .and_then(|type_data| type_data.get(id))
599            .is_some();
600
601        Ok(exists)
602    }
603}
604
605// Essential conditional operations for testing
606impl InMemoryProvider {
607    pub async fn conditional_update(
608        &self,
609        resource_type: &str,
610        id: &str,
611        data: Value,
612        expected_version: &ScimVersion,
613        context: &RequestContext,
614    ) -> Result<ConditionalResult<VersionedResource>, InMemoryError> {
615        let tenant_id = context.tenant_id().unwrap_or("default");
616
617        let mut store = self.data.write().await;
618        let tenant_data = store
619            .entry(tenant_id.to_string())
620            .or_insert_with(HashMap::new);
621        let type_data = tenant_data
622            .entry(resource_type.to_string())
623            .or_insert_with(HashMap::new);
624
625        // Check if resource exists
626        let existing_resource = match type_data.get(id) {
627            Some(resource) => resource,
628            None => return Ok(ConditionalResult::NotFound),
629        };
630
631        // Compute current version
632        let current_version = VersionedResource::new(existing_resource.clone())
633            .version()
634            .clone();
635
636        // Check version match
637        if !current_version.matches(expected_version) {
638            let conflict = VersionConflict::new(
639                expected_version.clone(),
640                current_version,
641                format!(
642                    "Resource {}/{} was modified by another client",
643                    resource_type, id
644                ),
645            );
646            return Ok(ConditionalResult::VersionMismatch(conflict));
647        }
648
649        // Create updated resource
650        let mut updated_resource =
651            Resource::from_json(resource_type.to_string(), data).map_err(|e| {
652                InMemoryError::InvalidData {
653                    message: format!("Failed to update resource: {}", e),
654                }
655            })?;
656
657        // Preserve ID
658        if let Some(original_id) = existing_resource.get_id() {
659            updated_resource
660                .set_id(original_id)
661                .map_err(|e| InMemoryError::InvalidData {
662                    message: format!("Failed to set ID: {}", e),
663                })?;
664        }
665
666        type_data.insert(id.to_string(), updated_resource.clone());
667        Ok(ConditionalResult::Success(VersionedResource::new(
668            updated_resource,
669        )))
670    }
671
672    pub async fn conditional_delete(
673        &self,
674        resource_type: &str,
675        id: &str,
676        expected_version: &ScimVersion,
677        context: &RequestContext,
678    ) -> Result<ConditionalResult<()>, InMemoryError> {
679        let tenant_id = context.tenant_id().unwrap_or("default");
680
681        let mut store = self.data.write().await;
682        let tenant_data = store
683            .entry(tenant_id.to_string())
684            .or_insert_with(HashMap::new);
685        let type_data = tenant_data
686            .entry(resource_type.to_string())
687            .or_insert_with(HashMap::new);
688
689        // Check if resource exists
690        let existing_resource = match type_data.get(id) {
691            Some(resource) => resource,
692            None => return Ok(ConditionalResult::NotFound),
693        };
694
695        // Compute current version
696        let current_version = VersionedResource::new(existing_resource.clone())
697            .version()
698            .clone();
699
700        // Check version match
701        if !current_version.matches(expected_version) {
702            let conflict = VersionConflict::new(
703                expected_version.clone(),
704                current_version,
705                format!(
706                    "Resource {}/{} was modified by another client",
707                    resource_type, id
708                ),
709            );
710            return Ok(ConditionalResult::VersionMismatch(conflict));
711        }
712
713        // Delete resource
714        type_data.remove(id);
715        Ok(ConditionalResult::Success(()))
716    }
717
718    pub async fn get_versioned_resource(
719        &self,
720        resource_type: &str,
721        id: &str,
722        context: &RequestContext,
723    ) -> Result<Option<VersionedResource>, InMemoryError> {
724        match self.get_resource(resource_type, id, context).await? {
725            Some(resource) => Ok(Some(VersionedResource::new(resource))),
726            None => Ok(None),
727        }
728    }
729
730    pub async fn create_versioned_resource(
731        &self,
732        resource_type: &str,
733        data: Value,
734        context: &RequestContext,
735    ) -> Result<VersionedResource, InMemoryError> {
736        let resource = self.create_resource(resource_type, data, context).await?;
737        Ok(VersionedResource::new(resource))
738    }
739}