scim_server/schema/
validation.rs

1//! Schema validation logic for SCIM resources.
2//!
3//! This module provides comprehensive validation for SCIM resources using a hybrid approach:
4//! - Type-safe validation for core primitives via value objects
5//! - Schema-driven validation for complex attributes and business rules
6//! - JSON flexibility for extensible attributes
7
8use super::registry::SchemaRegistry;
9use super::types::{AttributeDefinition, AttributeType, Uniqueness};
10use crate::error::{ValidationError, ValidationResult};
11use crate::resource::core::{RequestContext, Resource};
12use crate::resource::provider::ResourceProvider;
13use crate::resource::value_objects::SchemaUri;
14use serde_json::{Map, Value};
15
16/// Operation context for SCIM resource validation.
17///
18/// Different SCIM operations have different validation requirements:
19/// - CREATE: Server generates ID, readonly attributes forbidden
20/// - UPDATE: ID required, readonly attributes ignored/forbidden
21/// - PATCH: ID required, partial updates allowed
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum OperationContext {
24    /// Resource creation operation - server generates ID and metadata
25    Create,
26    /// Resource replacement operation - full resource update
27    Update,
28    /// Resource modification operation - partial resource update
29    Patch,
30}
31
32impl SchemaRegistry {
33    /// Validate a SCIM resource with async provider integration for uniqueness checks.
34    ///
35    /// This method performs both synchronous schema validation and async provider-based
36    /// uniqueness validation when required by the schema.
37    ///
38    /// # Arguments
39    /// * `resource_type` - The type of resource to validate (e.g., "User", "Group")
40    /// * `resource_json` - The JSON resource data to validate
41    /// * `context` - The operation context (Create, Update, or Patch)
42    /// * `provider` - The resource provider for uniqueness validation
43    /// * `request_context` - The request context for tenant/scope information
44    ///
45    /// # Returns
46    /// * `Ok(())` if validation passes
47    /// * `Err(ValidationError)` if validation fails
48    pub async fn validate_json_resource_with_provider<P>(
49        &self,
50        resource_type: &str,
51        resource_json: &Value,
52        context: OperationContext,
53        provider: &P,
54        request_context: &RequestContext,
55    ) -> ValidationResult<()>
56    where
57        P: ResourceProvider,
58    {
59        // 1. First perform all synchronous validation
60        self.validate_json_resource_with_context(resource_type, resource_json, context)?;
61
62        // 2. Perform async uniqueness validation if needed
63        self.validate_uniqueness_constraints(
64            resource_type,
65            resource_json,
66            context,
67            provider,
68            request_context,
69        )
70        .await?;
71
72        Ok(())
73    }
74
75    /// Validate uniqueness constraints by checking with the provider.
76    async fn validate_uniqueness_constraints<P>(
77        &self,
78        resource_type: &str,
79        resource_json: &Value,
80        context: OperationContext,
81        provider: &P,
82        request_context: &RequestContext,
83    ) -> ValidationResult<()>
84    where
85        P: ResourceProvider,
86    {
87        // Get the schema for this resource type
88        let schema = match resource_type {
89            "User" => self.get_user_schema(),
90            "Group" => self.get_group_schema(),
91            _ => return Ok(()), // Unknown resource type, no uniqueness constraints
92        };
93
94        // Check each attribute marked as server unique
95        for attr in &schema.attributes {
96            if attr.uniqueness == Uniqueness::Server {
97                if let Some(value) = resource_json.get(&attr.name) {
98                    // For updates, exclude the current resource from uniqueness check
99                    let exclude_id = match context {
100                        OperationContext::Update | OperationContext::Patch => {
101                            resource_json.get("id").and_then(|v| v.as_str())
102                        }
103                        OperationContext::Create => None,
104                    };
105
106                    // Check if this value already exists
107                    let existing = provider
108                        .find_resource_by_attribute(
109                            resource_type,
110                            &attr.name,
111                            value,
112                            request_context,
113                        )
114                        .await
115                        .map_err(|e| ValidationError::Custom {
116                            message: format!("Failed to check uniqueness: {}", e),
117                        })?;
118
119                    if let Some(existing_resource) = existing {
120                        // If we found a resource, check if it's the same one (for updates)
121                        let is_same_resource = exclude_id
122                            .map(|current_id| {
123                                existing_resource.id.as_ref().map(|id| id.as_str())
124                                    == Some(current_id)
125                            })
126                            .unwrap_or(false);
127
128                        if !is_same_resource {
129                            return Err(ValidationError::ServerUniquenessViolation {
130                                attribute: attr.name.clone(),
131                                value: value.to_string(),
132                            });
133                        }
134                    }
135                }
136            }
137        }
138
139        Ok(())
140    }
141
142    /// Validate a SCIM resource using the hybrid value object approach.
143    ///
144    /// This method validates both the type-safe core attributes and the
145    /// schema-driven complex attributes, providing comprehensive validation
146    /// while maintaining performance and flexibility.
147    pub fn validate_resource_hybrid(&self, resource: &Resource) -> ValidationResult<()> {
148        // 1. Core primitive validation is already done during Resource construction
149        // via value objects, so we focus on schema-driven validation
150
151        // 2. Validate against each registered schema
152        for schema_uri in &resource.schemas {
153            if let Some(schema) = self.get_schema_by_id(schema_uri.as_str()) {
154                self.validate_against_schema(resource, schema)?;
155            } else {
156                return Err(ValidationError::UnknownSchemaUri {
157                    uri: schema_uri.as_str().to_string(),
158                });
159            }
160        }
161
162        // 3. Validate schema combinations
163        self.validate_schema_combinations(&resource.schemas)?;
164
165        // 4. Validate multi-valued attributes in extended attributes
166        self.validate_multi_valued_attributes(&resource.attributes)?;
167
168        // 5. Validate complex attributes in extended attributes
169        self.validate_complex_attributes(&resource.attributes)?;
170
171        // 6. Validate attribute characteristics for extended attributes
172        self.validate_attribute_characteristics(&resource.attributes)?;
173
174        Ok(())
175    }
176
177    /// Validate resource against a specific schema.
178    fn validate_against_schema(
179        &self,
180        resource: &Resource,
181        schema: &super::types::Schema,
182    ) -> ValidationResult<()> {
183        // Convert resource to JSON for schema validation
184        let resource_json = resource.to_json()?;
185
186        // Use existing resource validation logic
187        self.validate_resource(schema, &resource_json)
188    }
189
190    /// Validate a raw JSON resource (legacy support).
191    ///
192    /// This method first constructs a Resource from JSON (which validates
193    /// core primitives) and then performs schema validation.
194    /// Validate a SCIM resource with operation context awareness.
195    ///
196    /// This method performs context-aware validation that varies based on the operation:
197    /// - CREATE: Rejects client-provided IDs and readonly attributes
198    /// - UPDATE/PATCH: Requires IDs and handles readonly attributes appropriately
199    ///
200    /// # Arguments
201    /// * `resource_type` - The type of resource to validate (e.g., "User", "Group")
202    /// * `resource_json` - The JSON resource data to validate
203    /// * `context` - The operation context (Create, Update, or Patch)
204    ///
205    /// # Returns
206    /// * `Ok(())` if validation passes
207    /// * `Err(ValidationError)` if validation fails with specific error details
208    pub fn validate_json_resource_with_context(
209        &self,
210        resource_type: &str,
211        resource_json: &Value,
212        context: OperationContext,
213    ) -> ValidationResult<()> {
214        // First validate schemas are present and not empty
215        if let Some(schemas_value) = resource_json.get("schemas") {
216            if let Some(schemas_array) = schemas_value.as_array() {
217                if schemas_array.is_empty() {
218                    return Err(ValidationError::EmptySchemas);
219                }
220
221                // Check for duplicate schema URIs
222                let mut seen_schemas = std::collections::HashSet::new();
223                for schema_value in schemas_array {
224                    if let Some(schema_uri) = schema_value.as_str() {
225                        if !seen_schemas.insert(schema_uri) {
226                            return Err(ValidationError::DuplicateSchemaUri {
227                                uri: schema_uri.to_string(),
228                            });
229                        }
230                    }
231                }
232            } else {
233                return Err(ValidationError::MissingSchemas);
234            }
235        } else {
236            return Err(ValidationError::MissingSchemas);
237        }
238
239        // Validate meta.resourceType requirement - only if meta object exists
240        if let Some(meta_value) = resource_json.get("meta") {
241            if let Some(meta_obj) = meta_value.as_object() {
242                if !meta_obj.contains_key("resourceType") {
243                    return Err(ValidationError::MissingResourceType);
244                }
245            }
246        }
247
248        // Context-aware ID validation
249        match context {
250            OperationContext::Create => {
251                // CREATE: Client should NOT provide ID
252                if resource_json
253                    .as_object()
254                    .map(|obj| obj.contains_key("id"))
255                    .unwrap_or(false)
256                {
257                    return Err(ValidationError::ClientProvidedId);
258                }
259            }
260            OperationContext::Update | OperationContext::Patch => {
261                // UPDATE/PATCH: ID is required
262                if !resource_json
263                    .as_object()
264                    .map(|obj| obj.contains_key("id"))
265                    .unwrap_or(false)
266                {
267                    return Err(ValidationError::MissingId);
268                }
269            }
270        }
271
272        // Context-aware readonly attribute validation
273        if let Some(meta_value) = resource_json.get("meta") {
274            if let Some(meta_obj) = meta_value.as_object() {
275                let readonly_fields = ["created", "lastModified", "location", "version"];
276                let has_readonly = readonly_fields
277                    .iter()
278                    .any(|field| meta_obj.contains_key(*field));
279
280                if has_readonly {
281                    match context {
282                        OperationContext::Create => {
283                            // CREATE: Readonly attributes should not be provided by client
284                            return Err(ValidationError::ClientProvidedMeta);
285                        }
286                        OperationContext::Update | OperationContext::Patch => {
287                            // UPDATE/PATCH: Readonly attributes are allowed (server will ignore them)
288                            // This is compliant with SCIM specification
289                        }
290                    }
291                }
292            }
293        }
294
295        // Preliminary validation for specific SCIM errors before resource construction
296        self.validate_multi_valued_attributes_preliminary(resource_type, resource_json)?;
297
298        // Then convert to Resource (validates core primitives)
299        let resource = Resource::from_json(resource_type.to_string(), resource_json.clone())?;
300
301        // Finally validate using hybrid approach
302        self.validate_resource_hybrid(&resource)
303    }
304
305    /// Map resource type to schema URI.
306    fn resource_type_to_schema_uri(resource_type: &str) -> Option<&'static str> {
307        match resource_type {
308            "User" => Some("urn:ietf:params:scim:schemas:core:2.0:User"),
309            "Group" => Some("urn:ietf:params:scim:schemas:core:2.0:Group"),
310            _ => None,
311        }
312    }
313
314    /// Preliminary validation for multi-valued attributes to catch specific SCIM errors
315    /// before resource construction.
316    fn validate_multi_valued_attributes_preliminary(
317        &self,
318        resource_type: &str,
319        resource_json: &Value,
320    ) -> ValidationResult<()> {
321        let obj = resource_json
322            .as_object()
323            .ok_or_else(|| ValidationError::custom("Resource must be a JSON object"))?;
324
325        // Get the schema URI for this resource type
326        let schema_uri = Self::resource_type_to_schema_uri(resource_type)
327            .ok_or_else(|| ValidationError::custom("Unknown resource type"))?;
328
329        // Get the schema for this resource type
330        let schema = self
331            .get_schema(schema_uri)
332            .ok_or_else(|| ValidationError::custom("Schema not found"))?;
333
334        // Check emails attribute for required sub-attributes
335        if let Some(emails_value) = obj.get("emails") {
336            if let Some(emails_array) = emails_value.as_array() {
337                // Find the emails attribute definition
338                if let Some(emails_attr) =
339                    schema.attributes.iter().find(|attr| attr.name == "emails")
340                {
341                    self.validate_required_sub_attributes(emails_attr, emails_array)?;
342                }
343            }
344        }
345
346        // Check other multi-valued attributes if needed
347        // Add more preliminary validations here as required
348
349        Ok(())
350    }
351
352    /// Validate schema URI combinations.
353    fn validate_schema_combinations(&self, schemas: &[SchemaUri]) -> ValidationResult<()> {
354        if schemas.is_empty() {
355            return Err(ValidationError::MissingSchemas);
356        }
357
358        // Check for conflicting schemas (basic validation)
359        let schema_strings: Vec<String> = schemas.iter().map(|s| s.as_str().to_string()).collect();
360
361        // Ensure we have at least one core schema
362        let has_user_schema = schema_strings.iter().any(|s| s.contains("User"));
363        let has_group_schema = schema_strings.iter().any(|s| s.contains("Group"));
364
365        if !has_user_schema && !has_group_schema {
366            return Err(ValidationError::custom(
367                "Resource must have at least one core schema",
368            ));
369        }
370
371        // Don't allow both User and Group schemas
372        if has_user_schema && has_group_schema {
373            return Err(ValidationError::custom(
374                "Resource cannot have both User and Group schemas",
375            ));
376        }
377
378        Ok(())
379    }
380
381    /// Validate a resource attribute against its schema definition.
382    fn validate_attribute(
383        &self,
384        attr_def: &AttributeDefinition,
385        value: &Value,
386    ) -> ValidationResult<()> {
387        // Check if attribute is required but missing
388        if attr_def.required && value.is_null() {
389            return Err(ValidationError::MissingRequiredAttribute {
390                attribute: attr_def.name.clone(),
391            });
392        }
393
394        // Skip validation for null optional attributes
395        if value.is_null() {
396            return Ok(());
397        }
398
399        // Validate data type
400        self.validate_attribute_value(attr_def, value)?;
401
402        // Validate mutability if this is an update operation
403        // (This would need request context to determine operation type)
404
405        // Validate uniqueness constraints
406        // (This would need external data to check uniqueness)
407
408        Ok(())
409    }
410
411    /// Validate an attribute value against its expected data type.
412    fn validate_attribute_value(
413        &self,
414        attr_def: &AttributeDefinition,
415        value: &Value,
416    ) -> ValidationResult<()> {
417        self.validate_attribute_value_with_context(attr_def, value, None)
418    }
419
420    fn validate_attribute_value_with_context(
421        &self,
422        attr_def: &AttributeDefinition,
423        value: &Value,
424        parent_attr: Option<&str>,
425    ) -> ValidationResult<()> {
426        // Skip validation for null optional attributes
427        if value.is_null() && !attr_def.required {
428            return Ok(());
429        }
430
431        // Check if required attribute is null
432        if value.is_null() && attr_def.required {
433            return Err(ValidationError::MissingRequiredAttribute {
434                attribute: attr_def.name.clone(),
435            });
436        }
437
438        match attr_def.data_type {
439            AttributeType::String => {
440                if !value.is_string() {
441                    return Err(ValidationError::InvalidAttributeType {
442                        attribute: attr_def.name.clone(),
443                        expected: "string".to_string(),
444                        actual: Self::get_value_type(value).to_string(),
445                    });
446                }
447
448                // Validate case sensitivity for string attributes
449                let str_value = value.as_str().unwrap();
450                if attr_def.case_exact {
451                    // For case-exact attributes, check for mixed case patterns
452                    self.validate_case_exact_string(&attr_def.name, str_value)?;
453                }
454
455                // Validate canonical values if defined
456                if !attr_def.canonical_values.is_empty() {
457                    self.validate_canonical_value_with_context(attr_def, str_value, parent_attr)?;
458                }
459            }
460            AttributeType::Boolean => {
461                if !value.is_boolean() {
462                    return Err(ValidationError::InvalidAttributeType {
463                        attribute: attr_def.name.clone(),
464                        expected: "boolean".to_string(),
465                        actual: Self::get_value_type(value).to_string(),
466                    });
467                }
468            }
469            AttributeType::Decimal => {
470                if !value.is_number() {
471                    return Err(ValidationError::InvalidAttributeType {
472                        attribute: attr_def.name.clone(),
473                        expected: "decimal".to_string(),
474                        actual: Self::get_value_type(value).to_string(),
475                    });
476                }
477            }
478            AttributeType::Integer => {
479                if !value.is_i64() {
480                    return Err(ValidationError::InvalidAttributeType {
481                        attribute: attr_def.name.clone(),
482                        expected: "integer".to_string(),
483                        actual: Self::get_value_type(value).to_string(),
484                    });
485                }
486            }
487            AttributeType::DateTime => {
488                if let Some(date_str) = value.as_str() {
489                    if !self.is_valid_datetime_format(date_str) {
490                        return Err(ValidationError::InvalidDateTimeFormat {
491                            attribute: attr_def.name.clone(),
492                            value: date_str.to_string(),
493                        });
494                    }
495                } else {
496                    return Err(ValidationError::InvalidAttributeType {
497                        attribute: attr_def.name.clone(),
498                        expected: "string (datetime)".to_string(),
499                        actual: Self::get_value_type(value).to_string(),
500                    });
501                }
502            }
503            AttributeType::Binary => {
504                if let Some(binary_str) = value.as_str() {
505                    if !self.is_valid_base64(binary_str) {
506                        return Err(ValidationError::InvalidBinaryData {
507                            attribute: attr_def.name.clone(),
508                            details: "Invalid base64 encoding".to_string(),
509                        });
510                    }
511                } else {
512                    return Err(ValidationError::InvalidAttributeType {
513                        attribute: attr_def.name.clone(),
514                        expected: "string (base64)".to_string(),
515                        actual: Self::get_value_type(value).to_string(),
516                    });
517                }
518            }
519            AttributeType::Reference => {
520                if let Some(ref_str) = value.as_str() {
521                    if !self.is_valid_uri_format(ref_str) {
522                        return Err(ValidationError::InvalidReferenceUri {
523                            attribute: attr_def.name.clone(),
524                            uri: ref_str.to_string(),
525                        });
526                    }
527                } else {
528                    return Err(ValidationError::InvalidAttributeType {
529                        attribute: attr_def.name.clone(),
530                        expected: "string (URI)".to_string(),
531                        actual: Self::get_value_type(value).to_string(),
532                    });
533                }
534            }
535            AttributeType::Complex => {
536                if value.is_array() {
537                    // Multi-valued complex attribute
538                    self.validate_multi_valued_array(attr_def, value)?;
539                } else if value.is_object() {
540                    // Single complex attribute
541                    self.validate_complex_attribute_structure(attr_def, value)?;
542                } else {
543                    return Err(ValidationError::InvalidAttributeType {
544                        attribute: attr_def.name.clone(),
545                        expected: "object or array".to_string(),
546                        actual: Self::get_value_type(value).to_string(),
547                    });
548                }
549            }
550        }
551
552        Ok(())
553    }
554
555    /// Validate multi-valued attributes in the resource.
556    fn validate_multi_valued_attributes(
557        &self,
558        attributes: &Map<String, Value>,
559    ) -> ValidationResult<()> {
560        for (attr_name, attr_value) in attributes {
561            if let Some(_attr_value_array) = attr_value.as_array() {
562                // Find attribute definition
563                if let Some(attr_def) = self.get_complex_attribute_definition(attr_name) {
564                    if attr_def.multi_valued {
565                        self.validate_multi_valued_array(attr_def, attr_value)?;
566                    }
567                }
568            }
569        }
570        Ok(())
571    }
572
573    /// Validate a multi-valued attribute array.
574    fn validate_multi_valued_array(
575        &self,
576        attr_def: &AttributeDefinition,
577        value: &Value,
578    ) -> ValidationResult<()> {
579        let array = value
580            .as_array()
581            .ok_or_else(|| ValidationError::InvalidAttributeType {
582                attribute: attr_def.name.clone(),
583                expected: "array".to_string(),
584                actual: Self::get_value_type(value).to_string(),
585            })?;
586
587        for item in array {
588            match attr_def.data_type {
589                AttributeType::Complex => {
590                    self.validate_complex_attribute_structure(attr_def, item)?;
591                }
592                _ => {
593                    self.validate_attribute_value(attr_def, item)?;
594                }
595            }
596        }
597
598        // Validate required sub-attributes for complex multi-valued attributes
599        if matches!(attr_def.data_type, AttributeType::Complex) {
600            self.validate_required_sub_attributes(attr_def, array)?;
601
602            // Check for multiple primary values
603            let primary_count = array
604                .iter()
605                .filter(|item| {
606                    item.get("primary")
607                        .and_then(|p| p.as_bool())
608                        .unwrap_or(false)
609                })
610                .count();
611
612            if primary_count > 1 {
613                return Err(ValidationError::MultiplePrimaryValues {
614                    attribute: attr_def.name.clone(),
615                });
616            }
617        }
618
619        Ok(())
620    }
621
622    /// Validate required sub-attributes in multi-valued complex attributes.
623    fn validate_required_sub_attributes(
624        &self,
625        attr_def: &AttributeDefinition,
626        array: &[Value],
627    ) -> ValidationResult<()> {
628        for item in array {
629            if let Some(obj) = item.as_object() {
630                for sub_attr in &attr_def.sub_attributes {
631                    if sub_attr.required && !obj.contains_key(&sub_attr.name) {
632                        return Err(ValidationError::MissingRequiredSubAttribute {
633                            attribute: attr_def.name.clone(),
634                            sub_attribute: sub_attr.name.clone(),
635                        });
636                    }
637                }
638            }
639        }
640        Ok(())
641    }
642
643    /// Validate complex attributes in the resource.
644    fn validate_complex_attributes(&self, attributes: &Map<String, Value>) -> ValidationResult<()> {
645        for (attr_name, attr_value) in attributes {
646            if let Some(attr_def) = self.get_complex_attribute_definition(attr_name) {
647                if matches!(attr_def.data_type, AttributeType::Complex) {
648                    if attr_def.multi_valued {
649                        // Multi-valued complex attribute (handled elsewhere)
650                        continue;
651                    } else {
652                        // Single complex attribute
653                        self.validate_complex_attribute_structure(attr_def, attr_value)?;
654                    }
655                }
656            }
657        }
658        Ok(())
659    }
660
661    /// Validate the structure of a complex attribute.
662    fn validate_complex_attribute_structure(
663        &self,
664        attr_def: &AttributeDefinition,
665        value: &Value,
666    ) -> ValidationResult<()> {
667        let obj = value
668            .as_object()
669            .ok_or_else(|| ValidationError::InvalidAttributeType {
670                attribute: attr_def.name.clone(),
671                expected: "object".to_string(),
672                actual: Self::get_value_type(value).to_string(),
673            })?;
674
675        // Validate known sub-attributes
676        self.validate_known_sub_attributes(attr_def, obj)?;
677
678        // Validate sub-attribute types
679        self.validate_sub_attribute_types(attr_def, obj)?;
680
681        // Validate no nested complex attributes
682        self.validate_no_nested_complex(attr_def, obj)?;
683
684        // Validate required sub-attributes
685        self.validate_required_sub_attributes_complex(attr_def, obj)?;
686
687        Ok(())
688    }
689
690    /// Validate that only known sub-attributes are present.
691    fn validate_known_sub_attributes(
692        &self,
693        attr_def: &AttributeDefinition,
694        obj: &Map<String, Value>,
695    ) -> ValidationResult<()> {
696        let known_sub_attrs: Vec<&str> = attr_def
697            .sub_attributes
698            .iter()
699            .map(|sa| sa.name.as_str())
700            .collect();
701
702        for key in obj.keys() {
703            if !known_sub_attrs.contains(&key.as_str()) {
704                return Err(ValidationError::UnknownSubAttribute {
705                    attribute: attr_def.name.clone(),
706                    sub_attribute: key.clone(),
707                });
708            }
709        }
710
711        Ok(())
712    }
713
714    /// Validate sub-attribute data types.
715    fn validate_sub_attribute_types(
716        &self,
717        attr_def: &AttributeDefinition,
718        obj: &Map<String, Value>,
719    ) -> ValidationResult<()> {
720        for (key, value) in obj {
721            if let Some(sub_attr_def) = attr_def.sub_attributes.iter().find(|sa| sa.name == *key) {
722                self.validate_attribute_value_with_context(
723                    sub_attr_def,
724                    value,
725                    Some(&attr_def.name),
726                )?;
727            }
728        }
729        Ok(())
730    }
731
732    /// Validate that complex attributes don't contain nested complex attributes.
733    fn validate_no_nested_complex(
734        &self,
735        attr_def: &AttributeDefinition,
736        _obj: &Map<String, Value>,
737    ) -> ValidationResult<()> {
738        for sub_attr in &attr_def.sub_attributes {
739            if matches!(sub_attr.data_type, AttributeType::Complex) {
740                return Err(ValidationError::NestedComplexAttributes {
741                    attribute: format!("{}.{}", attr_def.name, sub_attr.name),
742                });
743            }
744        }
745        Ok(())
746    }
747
748    /// Validate required sub-attributes in complex attributes.
749    fn validate_required_sub_attributes_complex(
750        &self,
751        attr_def: &AttributeDefinition,
752        obj: &Map<String, Value>,
753    ) -> ValidationResult<()> {
754        for sub_attr in &attr_def.sub_attributes {
755            if sub_attr.required && !obj.contains_key(&sub_attr.name) {
756                return Err(ValidationError::MissingRequiredSubAttribute {
757                    attribute: attr_def.name.clone(),
758                    sub_attribute: sub_attr.name.clone(),
759                });
760            }
761        }
762        Ok(())
763    }
764
765    /// Validate attribute characteristics (mutability, case sensitivity, etc.).
766    fn validate_attribute_characteristics(
767        &self,
768        attributes: &Map<String, Value>,
769    ) -> ValidationResult<()> {
770        // This would typically require request context to determine operation type
771        // For now, we'll implement basic characteristic validation
772
773        for (attr_name, attr_value) in attributes {
774            // Validate case sensitivity for attributes that require it
775            self.validate_case_sensitivity(attr_name, attr_value)?;
776
777            // Additional characteristic validation can be added here
778        }
779
780        Ok(())
781    }
782
783    /// Validate case sensitivity requirements for attributes.
784    fn validate_case_sensitivity(
785        &self,
786        attr_name: &str,
787        attr_value: &Value,
788    ) -> ValidationResult<()> {
789        // Special validation for resourceType data type
790        if attr_name == "resourceType" && !attr_value.is_string() {
791            return Err(ValidationError::InvalidMetaStructure);
792        }
793
794        // Find attribute definition to check case sensitivity
795        if let Some(attr_def) = self
796            .get_user_schema()
797            .attributes
798            .iter()
799            .find(|attr| attr.name == attr_name)
800        {
801            if attr_def.case_exact && attr_value.is_string() {
802                let str_value = attr_value.as_str().unwrap();
803                self.validate_case_exact_string(attr_name, str_value)?;
804            }
805        }
806
807        // Validate case sensitivity for complex attributes
808        if attr_value.is_array() {
809            if let Some(array) = attr_value.as_array() {
810                for item in array {
811                    self.validate_complex_case_sensitivity(attr_name, item)?;
812                }
813            }
814        }
815
816        Ok(())
817    }
818
819    /// Validate that a case-exact string follows proper casing rules.
820    fn validate_case_exact_string(&self, attr_name: &str, value: &str) -> ValidationResult<()> {
821        // Special handling for resourceType - validate against allowed values first
822        if attr_name == "resourceType" {
823            let allowed_types = ["User", "Group"];
824            if !allowed_types.contains(&value) {
825                return Err(ValidationError::InvalidResourceType {
826                    resource_type: value.to_string(),
827                });
828            }
829            return Ok(());
830        }
831
832        // For SCIM, case-exact typically means consistent casing
833        // Check for problematic mixed case patterns that suggest inconsistency
834        if self.has_inconsistent_casing(value) {
835            return Err(ValidationError::CaseSensitivityViolation {
836                attribute: attr_name.to_string(),
837                details: format!(
838                    "Attribute '{}' requires consistent casing but found mixed case in '{}'",
839                    attr_name, value
840                ),
841            });
842        }
843        Ok(())
844    }
845
846    /// Check if a string has inconsistent casing patterns.
847    fn has_inconsistent_casing(&self, value: &str) -> bool {
848        // For ID attributes, mixed case like "MixedCase123" is problematic
849        // This is a heuristic - in practice this would be configurable
850        if value.len() > 1 {
851            let has_upper = value.chars().any(|c| c.is_uppercase());
852            let has_lower = value.chars().any(|c| c.is_lowercase());
853
854            // If it has both upper and lower case letters, it's mixed case
855            if has_upper && has_lower {
856                // Allow common patterns like camelCase or PascalCase
857                // But flag obvious mixed patterns
858                let first_char = value.chars().next().unwrap();
859                let rest = &value[1..];
860
861                // Simple heuristic: if first char is uppercase and rest has mixed case
862                // or if it looks like random mixed case, flag it
863                if first_char.is_uppercase()
864                    && rest.chars().any(|c| c.is_uppercase())
865                    && rest.chars().any(|c| c.is_lowercase())
866                {
867                    return true;
868                }
869            }
870        }
871        false
872    }
873
874    /// Validate canonical values considering case sensitivity.
875
876    fn validate_canonical_value_with_context(
877        &self,
878        attr_def: &AttributeDefinition,
879        value: &str,
880        parent_attr: Option<&str>,
881    ) -> ValidationResult<()> {
882        // For SCIM 2.0, canonical values must match exactly as defined in the schema
883        // regardless of the caseExact setting. The caseExact setting affects how
884        // the server handles submitted values for storage/comparison, but canonical
885        // values are predefined constants that must be matched exactly.
886        if !attr_def.canonical_values.contains(&value.to_string()) {
887            let attribute_name = if let Some(parent) = parent_attr {
888                format!("{}.{}", parent, attr_def.name)
889            } else {
890                attr_def.name.clone()
891            };
892
893            return Err(ValidationError::InvalidCanonicalValue {
894                attribute: attribute_name,
895                value: value.to_string(),
896                allowed: attr_def.canonical_values.clone(),
897            });
898        }
899        Ok(())
900    }
901
902    /// Validate case sensitivity for complex multi-valued attributes.
903    fn validate_complex_case_sensitivity(
904        &self,
905        attr_name: &str,
906        value: &Value,
907    ) -> ValidationResult<()> {
908        if let Some(obj) = value.as_object() {
909            if let Some(attr_def) = self.get_complex_attribute_definition(attr_name) {
910                for (sub_attr_name, sub_attr_value) in obj {
911                    if let Some(sub_attr_def) = attr_def
912                        .sub_attributes
913                        .iter()
914                        .find(|sa| sa.name == *sub_attr_name)
915                    {
916                        if !sub_attr_def.case_exact && sub_attr_value.is_string() {
917                            // Case-insensitive validation/normalization
918                            // Implementation would depend on specific requirements
919                        }
920                    }
921                }
922            }
923        }
924        Ok(())
925    }
926
927    /// Validate a resource against a specific schema (legacy method).
928    ///
929    /// This method validates a JSON resource against a schema definition,
930    /// checking attributes, types, and constraints.
931    pub fn validate_resource(
932        &self,
933        schema: &super::types::Schema,
934        resource: &Value,
935    ) -> ValidationResult<()> {
936        let obj = resource
937            .as_object()
938            .ok_or_else(|| ValidationError::custom("Resource must be a JSON object"))?;
939
940        // Validate each defined attribute in the schema
941        for attr_def in &schema.attributes {
942            if let Some(value) = obj.get(&attr_def.name) {
943                self.validate_attribute(attr_def, value)?;
944            } else if attr_def.required {
945                return Err(ValidationError::MissingRequiredAttribute {
946                    attribute: attr_def.name.clone(),
947                });
948            }
949        }
950
951        // Check for unknown attributes (strict validation)
952        for (field_name, _) in obj {
953            if !schema
954                .attributes
955                .iter()
956                .any(|attr| attr.name == *field_name)
957            {
958                // Allow standard SCIM attributes
959                if !["schemas", "id", "externalId", "meta"].contains(&field_name.as_str()) {
960                    return Err(ValidationError::UnknownAttributeForSchema {
961                        attribute: field_name.clone(),
962                        schema: schema.id.clone(),
963                    });
964                }
965            }
966        }
967
968        Ok(())
969    }
970}