scim_server/schema/
registry.rs

1//! Schema registry for loading, managing, and accessing SCIM schemas.
2//!
3//! This module provides the SchemaRegistry which handles schema loading from files,
4//! schema management, and provides access to registered schemas for validation.
5
6use super::types::{AttributeDefinition, AttributeType, Schema};
7
8use chrono::{DateTime, FixedOffset};
9use serde_json::Value;
10use std::collections::HashMap;
11use std::fs;
12use std::path::Path;
13
14/// Registry for SCIM schemas with validation capabilities.
15///
16/// The schema registry manages all available schemas and provides validation
17/// services for resources. For the MVP, it contains only the hardcoded core User schema.
18#[derive(Debug, Clone)]
19pub struct SchemaRegistry {
20    core_user_schema: Schema,
21    core_group_schema: Schema,
22    schemas: HashMap<String, Schema>,
23}
24
25impl SchemaRegistry {
26    /// Create a new schema registry with core User schema loaded from file.
27    pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
28        Self::from_schema_dir("schemas")
29    }
30
31    /// Create a schema registry by loading schemas from a directory.
32    pub fn from_schema_dir<P: AsRef<Path>>(
33        schema_dir: P,
34    ) -> Result<Self, Box<dyn std::error::Error>> {
35        let user_schema_path = schema_dir.as_ref().join("User.json");
36        let core_user_schema = Self::load_schema_from_file(&user_schema_path)?;
37
38        let group_schema_path = schema_dir.as_ref().join("Group.json");
39        let core_group_schema = Self::load_schema_from_file(&group_schema_path)?;
40
41        let mut schemas = HashMap::new();
42        schemas.insert(core_user_schema.id.clone(), core_user_schema.clone());
43        schemas.insert(core_group_schema.id.clone(), core_group_schema.clone());
44
45        Ok(Self {
46            core_user_schema,
47            core_group_schema,
48            schemas,
49        })
50    }
51
52    /// Load a schema from a JSON file.
53    fn load_schema_from_file<P: AsRef<Path>>(
54        path: P,
55    ) -> Result<Schema, Box<dyn std::error::Error>> {
56        let content = fs::read_to_string(&path)?;
57        let mut schema: Schema = serde_json::from_str(&content)?;
58
59        // Convert JSON schema format to internal format
60        Self::convert_json_schema(&mut schema);
61
62        Ok(schema)
63    }
64
65    /// Convert JSON schema format to internal AttributeDefinition format.
66    fn convert_json_schema(schema: &mut Schema) {
67        for attr in &mut schema.attributes {
68            Self::convert_attribute_definition(attr);
69        }
70    }
71
72    /// Convert a single attribute definition from JSON format.
73    fn convert_attribute_definition(attr: &mut AttributeDefinition) {
74        // Convert data type from string to enum
75        // This is handled by serde deserialization
76
77        // Process sub-attributes recursively
78        for sub_attr in &mut attr.sub_attributes {
79            Self::convert_attribute_definition(sub_attr);
80        }
81    }
82
83    /// Get all available schemas.
84    pub fn get_schemas(&self) -> Vec<&Schema> {
85        self.schemas.values().collect()
86    }
87
88    /// Get a specific schema by ID.
89    pub fn get_schema(&self, id: &str) -> Option<&Schema> {
90        self.schemas.get(id)
91    }
92
93    /// Get the core User schema.
94    pub fn get_user_schema(&self) -> &Schema {
95        &self.core_user_schema
96    }
97
98    pub fn get_group_schema(&self) -> &Schema {
99        &self.core_group_schema
100    }
101
102    /// Add a schema to the registry.
103    pub fn add_schema(&mut self, schema: Schema) -> Result<(), Box<dyn std::error::Error>> {
104        self.schemas.insert(schema.id.clone(), schema);
105        Ok(())
106    }
107
108    /// Get a schema by ID.
109    pub fn get_schema_by_id(&self, schema_id: &str) -> Option<&Schema> {
110        self.schemas.get(schema_id)
111    }
112
113    /// Validate datetime format using chrono for full RFC3339 compliance
114    ///
115    /// This leverages chrono's well-tested RFC3339 parser, which provides:
116    /// - Full semantic validation (no invalid dates like Feb 30th)
117    /// - Proper timezone handling (+/-HH:MM, Z)
118    /// - Millisecond precision support
119    /// - Leap second awareness
120    ///
121    /// By using chrono, we avoid reimplementing complex datetime validation
122    /// and get specification-compliant parsing for free.
123    pub(super) fn is_valid_datetime_format(&self, value: &str) -> bool {
124        if value.is_empty() {
125            return false;
126        }
127
128        // Delegate to chrono's RFC3339 parser - it's well-tested and handles all edge cases
129        DateTime::<FixedOffset>::parse_from_rfc3339(value).is_ok()
130    }
131
132    /// Validate base64 encoding (basic character set validation)
133    ///
134    /// This performs basic character set validation for base64 data.
135    /// For production use, consider using a dedicated base64 crate like `base64`
136    /// for proper padding validation and decode verification.
137    pub(super) fn is_valid_base64(&self, value: &str) -> bool {
138        if value.is_empty() {
139            return false;
140        }
141
142        // Basic character set validation - sufficient for type checking
143        // Note: Doesn't validate padding rules or decode correctness
144        let base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
145        value.chars().all(|c| base64_chars.contains(c))
146    }
147
148    /// Validate URI format (basic scheme validation)
149    ///
150    /// This performs basic URI scheme validation sufficient for SCIM reference checking.
151    /// For comprehensive URI validation, consider using the `url` crate.
152    pub(super) fn is_valid_uri_format(&self, value: &str) -> bool {
153        if value.is_empty() {
154            return false;
155        }
156
157        // Basic scheme validation - sufficient for SCIM reference URIs
158        // Accepts HTTP(S) URLs and URN schemes commonly used in SCIM
159        value.contains("://") || value.starts_with("urn:")
160    }
161
162    /// Get the type name of a JSON value for error messages.
163    pub(super) fn get_value_type(value: &Value) -> &'static str {
164        match value {
165            Value::Null => "null",
166            Value::Bool(_) => "boolean",
167            Value::Number(n) if n.is_i64() => "integer",
168            Value::Number(_) => "decimal",
169            Value::String(_) => "string",
170            Value::Array(_) => "array",
171            Value::Object(_) => "object",
172        }
173    }
174
175    /// Get attribute definition for a complex attribute
176    pub(super) fn get_complex_attribute_definition(
177        &self,
178        attr_name: &str,
179    ) -> Option<&AttributeDefinition> {
180        // Look in core user schema for the attribute
181        self.core_user_schema
182            .attributes
183            .iter()
184            .find(|attr| attr.name == attr_name && matches!(attr.data_type, AttributeType::Complex))
185    }
186}
187
188impl Default for SchemaRegistry {
189    fn default() -> Self {
190        Self::new().expect("Failed to load default schemas")
191    }
192}