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