Schema Overview
This guide provides an overview of SCIM schemas and how they work within the SCIM Server library. Understanding schemas is essential for extending the server with custom attributes and resource types.
What are SCIM Schemas?
SCIM (System for Cross-domain Identity Management) schemas define the structure, attributes, and validation rules for resources like Users and Groups. They provide a standardized way to describe what data can be stored and how it should be formatted.
Core Concepts
- Schema URI: Unique identifier for each schema (e.g.,
urn:ietf:params:scim:schemas:core:2.0:User) - Resource Types: The types of objects that can be managed (User, Group, custom types)
- Attributes: The fields that make up a resource (name, email, etc.)
- Extensions: Additional schemas that add custom attributes to existing resource types
Standard SCIM Schemas
Core User Schema
urn:ietf:params:scim:schemas:core:2.0:User
Defines essential user attributes:
id- Unique identifieruserName- Primary identifier for authenticationname- User's full name (complex attribute)displayName- Name for display purposesemails- Email addresses (multi-valued)phoneNumbers- Phone numbers (multi-valued)active- Whether the user account is active
Core Group Schema
urn:ietf:params:scim:schemas:core:2.0:Group
Defines group attributes:
id- Unique identifierdisplayName- Group's display namemembers- Group members (multi-valued complex)
Enterprise User Extension
urn:ietf:params:scim:schemas:extension:enterprise:2.0:User
Adds enterprise-specific attributes:
employeeNumber- Employee identifierdepartment- Department namemanager- Reference to managerorganization- Organization name
Schema Architecture
Schema Definition Structure
#![allow(unused)] fn main() { use scim_server::schema::{SchemaDefinition, AttributeDefinition}; use serde::{Serialize, Deserialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SchemaDefinition { pub id: String, // Schema URI pub name: String, // Human-readable name pub description: String, // Schema description pub attributes: Vec<AttributeDefinition>, // Attribute definitions pub meta: SchemaMeta, // Metadata } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SchemaMeta { pub resource_type: String, pub location: String, pub created: DateTime<Utc>, pub last_modified: DateTime<Utc>, pub version: String, } }
Attribute Definition
#![allow(unused)] fn main() { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AttributeDefinition { pub name: String, // Attribute name pub attribute_type: AttributeType, // Data type pub multi_valued: bool, // Can have multiple values pub description: String, // Human-readable description pub required: bool, // Must be present pub case_exact: bool, // Case-sensitive comparison pub mutability: Mutability, // When attribute can be modified pub returned: Returned, // When attribute is returned pub uniqueness: Uniqueness, // Uniqueness constraint pub reference_types: Option<Vec<String>>, // For reference attributes pub canonical_values: Option<Vec<String>>, // Predefined valid values pub sub_attributes: Option<Vec<AttributeDefinition>>, // For complex types } }
Attribute Types
#![allow(unused)] fn main() { #[derive(Debug, Clone, Serialize, Deserialize)] pub enum AttributeType { String, // Text data Boolean, // True/false values Decimal, // Floating-point numbers Integer, // Whole numbers DateTime, // ISO 8601 date-time Binary, // Base64-encoded binary data Reference, // Reference to another resource Complex, // Nested object with sub-attributes } }
Mutability Levels
#![allow(unused)] fn main() { #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Mutability { ReadOnly, // Cannot be modified by client ReadWrite, // Can be read and written Immutable, // Can only be set during creation WriteOnly, // Can be written but not read (e.g., passwords) } }
Return Behavior
#![allow(unused)] fn main() { #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Returned { Always, // Always returned Never, // Never returned (e.g., passwords) Default, // Returned by default Request, // Only returned when explicitly requested } }
Uniqueness Constraints
#![allow(unused)] fn main() { #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Uniqueness { None, // No uniqueness constraint Server, // Unique within the server Global, // Globally unique } }
Schema Registry
The schema registry manages all available schemas and provides validation services:
#![allow(unused)] fn main() { use scim_server::schema::SchemaRegistry; use std::collections::HashMap; pub struct SchemaRegistry { schemas: HashMap<String, SchemaDefinition>, resource_schemas: HashMap<String, Vec<String>>, // resource_type -> schema_uris } impl SchemaRegistry { pub fn new() -> Self { let mut registry = Self { schemas: HashMap::new(), resource_schemas: HashMap::new(), }; // Register core schemas registry.register_core_schemas(); registry } pub fn register_schema(&mut self, schema: SchemaDefinition) -> Result<(), SchemaError> { // Validate schema definition self.validate_schema(&schema)?; // Store schema self.schemas.insert(schema.id.clone(), schema); Ok(()) } pub fn get_schema(&self, schema_uri: &str) -> Option<&SchemaDefinition> { self.schemas.get(schema_uri) } pub fn get_resource_schemas(&self, resource_type: &str) -> Vec<&SchemaDefinition> { if let Some(schema_uris) = self.resource_schemas.get(resource_type) { schema_uris.iter() .filter_map(|uri| self.schemas.get(uri)) .collect() } else { Vec::new() } } pub fn validate_resource( &self, resource: &serde_json::Value, schemas: &[String], ) -> Result<(), ValidationError> { for schema_uri in schemas { if let Some(schema) = self.get_schema(schema_uri) { self.validate_against_schema(resource, schema)?; } else { return Err(ValidationError::UnknownSchema(schema_uri.clone())); } } Ok(()) } fn register_core_schemas(&mut self) { // Register User schema let user_schema = self.create_user_schema(); self.schemas.insert(user_schema.id.clone(), user_schema); // Register Group schema let group_schema = self.create_group_schema(); self.schemas.insert(group_schema.id.clone(), group_schema); // Register Enterprise User extension let enterprise_schema = self.create_enterprise_user_schema(); self.schemas.insert(enterprise_schema.id.clone(), enterprise_schema); // Map resource types to schemas self.resource_schemas.insert( "User".to_string(), vec![ "urn:ietf:params:scim:schemas:core:2.0:User".to_string(), "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User".to_string(), ] ); self.resource_schemas.insert( "Group".to_string(), vec!["urn:ietf:params:scim:schemas:core:2.0:Group".to_string()] ); } } }
Working with Schemas
Retrieving Schema Information
#![allow(unused)] fn main() { use scim_server::ScimServer; async fn get_user_schema(server: &ScimServer) -> Result<SchemaDefinition, ScimError> { let registry = server.schema_registry(); let schema = registry.get_schema("urn:ietf:params:scim:schemas:core:2.0:User") .ok_or(ScimError::SchemaNotFound)?; Ok(schema.clone()) } async fn list_all_schemas(server: &ScimServer) -> Vec<SchemaDefinition> { let registry = server.schema_registry(); registry.list_schemas() } }
Validating Resources
#![allow(unused)] fn main() { use scim_server::models::User; use serde_json; async fn validate_user( registry: &SchemaRegistry, user: &User, ) -> Result<(), ValidationError> { let user_json = serde_json::to_value(user)?; registry.validate_resource( &user_json, &user.schemas, ) } }
Schema Versioning
Version Management
Schemas should be versioned to handle evolution over time:
#![allow(unused)] fn main() { pub struct SchemaVersion { pub major: u32, pub minor: u32, pub patch: u32, } impl SchemaVersion { pub fn new(major: u32, minor: u32, patch: u32) -> Self { Self { major, minor, patch } } pub fn is_compatible_with(&self, other: &SchemaVersion) -> bool { // Same major version is compatible self.major == other.major } } // Schema URI with version // urn:company:schemas:extension:employee:1.0:User }
Migration Support
#![allow(unused)] fn main() { pub trait SchemaMigration { fn migrate(&self, from: &SchemaVersion, to: &SchemaVersion, data: &mut serde_json::Value) -> Result<(), MigrationError>; fn supports_migration(&self, from: &SchemaVersion, to: &SchemaVersion) -> bool; } pub struct SchemaEvolutionManager { migrations: Vec<Box<dyn SchemaMigration>>, } impl SchemaEvolutionManager { pub fn migrate_data( &self, data: &mut serde_json::Value, from_version: &SchemaVersion, to_version: &SchemaVersion, ) -> Result<(), MigrationError> { for migration in &self.migrations { if migration.supports_migration(from_version, to_version) { migration.migrate(from_version, to_version, data)?; break; } } Ok(()) } } }
Error Handling
Schema-Related Errors
#![allow(unused)] fn main() { #[derive(Debug, thiserror::Error)] pub enum SchemaError { #[error("Schema validation failed: {0}")] ValidationFailed(String), #[error("Unknown schema: {0}")] UnknownSchema(String), #[error("Schema conflict: {0}")] SchemaConflict(String), #[error("Invalid attribute definition: {0}")] InvalidAttribute(String), #[error("Schema version incompatible: {0}")] VersionIncompatible(String), } }
Best Practices
Schema Design Guidelines
- Use meaningful names: Attribute names should be descriptive and follow camelCase convention
- Choose appropriate types: Select the most specific type that fits your data
- Set proper constraints: Use
required,uniqueness, andmutabilityappropriately - Document thoroughly: Provide clear descriptions for schemas and attributes
- Version strategically: Plan for schema evolution from the beginning
Performance Considerations
- Index unique attributes: Ensure database indexes exist for attributes with uniqueness constraints
- Minimize complex attributes: Deeply nested structures can impact performance
- Cache schema definitions: Avoid repeated schema lookups during validation
- Batch validation: Validate multiple resources together when possible
Next Steps
- Custom Resources - Learn to create entirely new resource types
- Extensions - Add custom attributes to existing resources
- Validation - Implement custom validation rules for schemas