Why You Need SCIM Server
If you're building multi-tenanted software and identity integration gaps are preventing enterprise adoption, this library is for you.
The Enterprise Authentication Trap
You've built brilliant software. Your core functionality is solid, and enterprises are showing interest. Then potential users ask: "How do we provision and manage users from our Okta directory?"
Suddenly, you realise that whilst you've focussed on building your core product, you've left identity and access management as an afterthought. Research shows that identity integration gaps prevent 75-80% of enterprise software adoption, with user provisioning being one of the most frequently requested features that creates insurmountable barriers for enterprise users.
What is User Provisioning (and Why It Matters)
User provisioning is the automated process that allows enterprise identity providers like Okta, Azure Entra, or Google Workspace to:
- Automatically create user accounts in your SaaS application when employees join
- Update user information when roles change
- Immediately deactivate accounts when employees leave
- Synchronise group memberships and permissions
For enterprises, this isn't just convenientβit's essential for security, compliance, and operational efficiency. Without it, IT teams must manually manage hundreds or thousands of user accounts across dozens of SaaS applications. For multi-tenanted platforms, this becomes even more critical as each customer organisation expects seamless integration with their existing identity infrastructure.
The Technical Challenges
Here are some of the technical challenges that developers implementing SCIM provisioning face:
- Provider Fragmentation: Identity providers interpret SCIM differentlyβemail handling, user deactivation, and custom attributes work differently across Okta, Azure, and Google
- Protocol Compliance: SCIM 2.0 has strict requirements with 10 common implementation pitfalls that cause enterprise integration failures
- Hidden Development Costs: Homegrown SSO/SCIM solutions are expensive to develop and maintain, requiring significant resources and specialist expertise.
- Ongoing Maintenance: Security incidents, provider-specific bugs, and manual customer onboarding create continuous overhead
- Protocol Complexity: The SCIM 2.0 protocol provides has many "optional" features that homegrown developers rarely implement because of their complexity, until they need them. Extensible schemas with custom attributes require run-time evaluation that developers often don't factor into their designs at the outset then requiring a big refactor to accommodate.
Many developers underestimate this complexity and spend months debugging provider-specific edge cases, dealing with "more deviation than standard" implementations, and handling enterprise customers who discover integration issues in production.
From Adoption Barrier to Enabler
Instead of preventing enterprise adoption due to identity integration gaps, software using SCIM Server turns user provisioning into an enabler. What was once a technical barrier becomes a seamless integration experience that enterprise organisations expect and value.
The SCIM Server library transforms user provisioning from a complex engineering project into a solved problem:
- π‘οΈ Type Safety: Leverage Rust's type system to prevent the runtime errors that plague custom provisioning implementations
- π’ Multi-Tenancy: Built-in support for multiple customer organisationsβessential for SaaS platforms
- β‘ Performance: Async-first design handles enterprise-scale provisioning operations efficiently
- π Framework Flexibility: Works with your existing web framework (Axum, Warp, Actix, or custom)
- π Standards Compliance: Full SCIM 2.0 implementation that works with all major identity providers
- π Production Ready: ETag-based concurrency control, comprehensive validation, and robust error handling
Time & Cost Savings
Instead of facing the typical 3-6 month development timeline and $3.5M+ costs that industry data shows for homegrown solutions, focus on your application:
Building From Scratch | Using SCIM Server |
---|---|
β 3-6 months learning SCIM protocol complexities | β Start building immediately with working components |
β $3.5M+ development and maintenance costs over 3 years | β Fraction of the cost with proven components |
β Debugging provider-specific implementation differences | β Handle Okta, Azure, Google variations automatically |
β Building multi-tenant isolation from scratch | β Multi-tenant context and isolation built-in |
β Lost enterprise deals due to auth requirements | β Enterprise-ready identity provisioning components |
Result: Avoid the 75-80% of enterprise deals that stall on authentication by having production-ready SCIM components instead of months of custom development.
Who Should Use This?
This library is designed for Rust developers who need to:
- Add enterprise customer support to SaaS applications requiring SCIM provisioning
- Build identity management tools that integrate with multiple identity providers
- Create AI agents that need to manage user accounts and permissions
- Develop custom identity solutions with specific business requirements
- Integrate existing systems with enterprise identity infrastructure
Ready to explore what the library offers? See the Library Introduction to understand the technical components and capabilities.
References
Enterprise authentication challenges and statistics sourced from: Gupta, "Enterprise Authentication: The Hidden SaaS Growth Blocker", 2024; WorkOS "Build vs Buy" analysis, 2024; WorkOS ROI comparison, 2024.
SCIM implementation pitfalls from: Traxion "10 Most Common Pitfalls for SCIM 2.0 Compliant API Implementations" based on testing 40-50 SCIM implementations.
Provider-specific differences documented in: WorkOS "SCIM Challenges", 2024.
Library Introduction
SCIM Server is a comprehensive Rust library that implements the SCIM 2.0 (System for Cross-domain Identity Management) protocolβthe industry standard for user provisioning. Instead of building custom user management APIs from scratch (which typically takes 3-6 developer months), SCIM Server provides a type-safe, high-performance foundation that gets you from zero to enterprise-ready user provisioning in weeks, not months.
What is SCIM Server?
SCIM Server is a Rust library that provides all the essential components for building SCIM 2.0-compliant systems. Instead of implementing SCIM from scratch, you get proven building blocks that handle the complex parts while letting you focus on your application logic.
The library uses the SCIM 2.0 protocol as a framework to standardize identity data validation and processing. You compose the components you needβfrom simple single-tenant systems to complex multi-tenant platforms with custom schemas and AI integration.
What You Get
Ready-to-Use Components
StandardResourceProvider
: Complete SCIM resource operations for typical use casesInMemoryStorage
andSqliteStorage
: Development and testing storage backends- Schema Registry: Pre-loaded with RFC 7643 User and Group schemas
- ETag Versioning: Automatic concurrency control for production deployments
Extension Points
ResourceProvider
trait: Implement for custom business logic and data modelsStorageProvider
trait: Connect to any database or storage system- Custom Value Objects: Type-safe handling of domain-specific attributes
- Multi-Tenant Context: Built-in tenant isolation and context management
Enterprise Features
- Protocol Compliance: All the RFC 7643/7644 complexity handled correctly
- Schema Extensions: Add custom attributes while maintaining SCIM compatibility
- AI Integration: Model Context Protocol support for AI agent interactions
- Production Ready: Structured logging, error handling, and performance optimizations
Need to understand why SCIM Server is essential for enterprise adoption? See Why You Need SCIM Server for the business case and problem context.
How to Use This Guide
The guide is organized into progressive sections:
- Getting Started: Quick setup and basic usage
- Core Concepts: Understanding the fundamental ideas
- Tutorials: Step-by-step guides for common scenarios
- How-To Guides: Solutions for specific problems
- Advanced Topics: Deep dives into complex scenarios
- Reference: Technical specifications and details
Learning Path
New to SCIM? Start with the Architecture Overview to understand the standard.
Ready to code? Jump to Your First SCIM Server for hands-on experience.
Building production systems? Read through Installation and the Configuration Guide.
What You'll Learn
By the end of this guide, you'll understand how to:
- Compose SCIM Server components for your specific requirements
- Implement the
ResourceProvider
trait for your application's data model - Create custom schema extensions and value objects
- Build multi-tenant systems using the provided context components
- Integrate SCIM components with web frameworks and AI tools
- Deploy production systems using the concurrency control and observability components
Getting Help
- Examples: Check the examples directory for working code
- API Documentation: See docs.rs/scim-server for detailed API reference
- Issues: Report bugs or ask questions on GitHub Issues
Let's get started! π
Installation
This guide will get you up and running with the SCIM server library in under 5 minutes.
Prerequisites
- Rust 1.75 or later - Install Rust
To verify your installation:
rustc --version
cargo --version
Adding the Dependency
Add to your Cargo.toml
:
[dependencies]
scim-server = "=0.5.3"
tokio = { version = "1.0", features = ["full"] }
serde_json = "1.0"
Note: The library is under active development. Pin to exact versions for stability. Breaking changes are signaled by minor version increments until v1.0.
Verification
Create a simple test to verify the installation works:
use scim_server::{ ScimServer, // Core SCIM server - see API docs providers::StandardResourceProvider, // Standard resource provider implementation storage::InMemoryStorage, // In-memory storage for development RequestContext // Request context for operations }; use serde_json::json; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { // See: https://docs.rs/scim-server/latest/scim_server/storage/struct.InMemoryStorage.html let storage = InMemoryStorage::new(); // See: https://docs.rs/scim-server/latest/scim_server/providers/struct.StandardResourceProvider.html let provider = StandardResourceProvider::new(storage); // See: https://docs.rs/scim-server/latest/scim_server/struct.ScimServer.html let server = ScimServer::new(provider)?; // See: https://docs.rs/scim-server/latest/scim_server/struct.RequestContext.html let context = RequestContext::new("test".to_string()); let user_data = json!({ "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "userName": "john.doe", "active": true }); let user = server.create_resource("User", user_data, &context).await?; let retrieved = server.get_resource("User", user.get_id().unwrap(), &context).await?; assert_eq!(retrieved.get_attribute("active").unwrap(), &json!(true)); Ok(()) }
Run with:
cargo run
If this runs without errors, your installation is working correctly!
Next Steps
Once installation is complete, proceed to:
- Your First SCIM Server - Build a complete working implementation
- Configuration Guide - Learn about storage backends and advanced setup
- API Reference - Complete API documentation on docs.rs
For production deployments, see the Configuration Guide for information about storage backends, multi-tenant setup, and scaling considerations.
Your First SCIM Server
Learn to build a working SCIM server in 10 minutes using this library. This guide demonstrates the core ScimServer
API and common patterns.
Quick Start
1. Create a New Project
cargo new my-scim-server
cd my-scim-server
2. Add Dependencies
[dependencies]
scim-server = "0.5.3"
tokio = { version = "1.0", features = ["full"] }
serde_json = "1.0"
3. Basic Server (20 lines)
use scim_server::{ ScimServer, // Core SCIM server orchestration providers::StandardResourceProvider, // Ready-to-use resource provider storage::InMemoryStorage, // Development storage backend resource_handlers::create_user_resource_handler, // Schema-aware resource handlers multi_tenant::ScimOperation, // Available SCIM operations RequestContext, // Request tracking and tenant context }; use serde_json::json; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { // Create storage, provider, and SCIM server let storage = InMemoryStorage::new(); let provider = StandardResourceProvider::new(storage); let mut server = ScimServer::new(provider)?; // Register User resource type with schema validation let user_schema = server.get_schema_by_id("urn:ietf:params:scim:schemas:core:2.0:User")?.clone(); let user_handler = create_user_resource_handler(user_schema); server.register_resource_type("User", user_handler, vec![ScimOperation::Create])?; // Create request context and user data let context = RequestContext::new("demo".to_string()); let user_data = json!({ "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "userName": "john.doe", "emails": [{"value": "john@example.com", "primary": true}], "active": true }); let user_json = server.create_resource_with_refs("User", user_data, &context).await?; println!("Created user: {}", user_json["userName"]); Ok(()) }
4. Run It
cargo run
# Output: Created user: john.doe
Core Operations
Setup
For the following examples, we'll use this server and context setup:
#![allow(unused)] fn main() { use scim_server::{ ScimServer, // Main SCIM server - see API docs providers::StandardResourceProvider, // Standard resource provider implementation storage::InMemoryStorage, // In-memory storage for development resource_handlers::{create_user_resource_handler, create_group_resource_handler}, multi_tenant::ScimOperation, // SCIM operation types RequestContext, // Request context for operations }; use serde_json::json; // Create storage, provider, and SCIM server // See: https://docs.rs/scim-server/latest/scim_server/storage/struct.InMemoryStorage.html let storage = InMemoryStorage::new(); // See: https://docs.rs/scim-server/latest/scim_server/providers/struct.StandardResourceProvider.html let provider = StandardResourceProvider::new(storage); // See: https://docs.rs/scim-server/latest/scim_server/struct.ScimServer.html let mut server = ScimServer::new(provider)?; // Register User and Group resource types with schema validation let user_schema = server.get_schema_by_id("urn:ietf:params:scim:schemas:core:2.0:User")?.clone(); let user_handler = create_user_resource_handler(user_schema); server.register_resource_type("User", user_handler, vec![ScimOperation::Create, ScimOperation::Read, ScimOperation::Update, ScimOperation::Delete])?; let group_schema = server.get_schema_by_id("urn:ietf:params:scim:schemas:core:2.0:Group")?.clone(); let group_handler = create_group_resource_handler(group_schema); server.register_resource_type("Group", group_handler, vec![ScimOperation::Create, ScimOperation::Read, ScimOperation::Update, ScimOperation::Delete])?; // Single-tenant RequestContext tracks each operation for logging let context = RequestContext::new("demo".to_string()); }
All the following examples will use these server
and context
variables.
Creating Resources
#![allow(unused)] fn main() { // Use JSON to define user attributes following SCIM 2.0 schema let user_data = json!({ "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "userName": "alice.smith", "name": { "givenName": "Alice", "familyName": "Smith" }, "emails": [{"value": "alice@company.com", "primary": true}], "active": true }); // Create the user - server handles validation, $ref fields, and metadata let user_json = server.create_resource_with_refs("User", user_data, &context).await?; let user_id = user_json["id"].as_str().unwrap(); // Get the auto-generated unique ID }
Reading Resources
#![allow(unused)] fn main() { // Get user by ID - returns SCIM-compliant JSON with proper $ref fields let retrieved_user = server.get_resource("User", &user_id, &context).await?; println!("Found: {}", retrieved_user["userName"]); // Search by specific attribute value - useful for username lookups let search_results = server.search_resources("User", "userName", &json!("alice.smith"), &context).await?; if !search_results.is_empty() { println!("Search found: {}", search_results[0]["userName"]); } }
Updating Resources
#![allow(unused)] fn main() { // Updates require the full resource data, including the ID and schemas let update_data = json!({ "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "id": user_id, "userName": "alice.smith", "name": { "givenName": "Alice", "familyName": "Johnson" // Changed surname }, "emails": [{"value": "alice@company.com", "primary": true}], "active": false // Deactivated }); // Update replaces the entire resource with new data, maintains SCIM compliance let updated_user = server.update_resource("User", &user_id, update_data, &context).await?; }
Listing and Searching
#![allow(unused)] fn main() { // List all users with proper SCIM compliance let all_users = server.list_resources("User", &context).await?; println!("Total users: {}", all_users.len()); // Check existence let exists = server.resource_exists("User", &user_id, &context).await?; println!("User exists: {}", exists); }
Validation and Error Handling
#![allow(unused)] fn main() { // The server automatically validates data against SCIM schemas let invalid_user = json!({ "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "userName": "", // Empty username - violates SCIM requirements "emails": [{"value": "not-an-email"}], // Invalid email format }); // Always handle validation errors gracefully match server.create_resource_with_refs("User", invalid_user, &context).await { Ok(user_json) => println!("User created: {}", user_json["id"]), Err(e) => println!("Validation failed: {}", e), // Detailed error message } }
Deleting Resources
#![allow(unused)] fn main() { // Delete a resource by ID server.delete_resource("User", &user_id, &context).await?; // Verify deletion let exists = server.resource_exists("User", &user_id, &context).await?; println!("User still exists: {}", exists); // Should be false }
Working with Groups
#![allow(unused)] fn main() { // Groups can contain users as members - useful for access control // Create a group (assuming you have a user_id from previous examples) let group_data = json!({ "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"], "displayName": "Engineering Team", // Required: human-readable name "members": [ // Optional: list of member references { "value": user_id, // Reference to the user's ID "type": "User" // Type of the referenced resource // Note: $ref field will be automatically generated by the server } ] }); // Create group with full SCIM compliance - server will inject proper $ref fields let group_json = server.create_resource_with_refs("Group", group_data, &context).await?; println!("Created group: {}", group_json["displayName"]); println!("Member $ref: {}", group_json["members"][0]["$ref"]); // Auto-generated! }
Multi-Tenant Support
For multi-tenant scenarios, you create explicit tenant contexts instead of using the default single-tenant setup. See the Multi-Tenant Architecture guide for detailed patterns.
#![allow(unused)] fn main() { // Import TenantContext for multi-tenant operations // See: https://docs.rs/scim-server/latest/scim_server/struct.ScimServerBuilder.html use scim_server::{ScimServerBuilder, TenantStrategy, multi_tenant::TenantContext}; // Create multi-tenant server with proper configuration let storage = InMemoryStorage::new(); let provider = StandardResourceProvider::new(storage); let mut server = ScimServerBuilder::new(provider) .with_base_url("https://api.company.com") .with_tenant_strategy(TenantStrategy::PathBased) .build()?; // Register resource types (same as before) let user_schema = server.get_schema_by_id("urn:ietf:params:scim:schemas:core:2.0:User")?.clone(); let user_handler = create_user_resource_handler(user_schema); server.register_resource_type("User", user_handler, vec![ScimOperation::Create])?; // Multi-tenant contexts - each gets isolated data space // See: https://docs.rs/scim-server/latest/scim_server/struct.TenantContext.html let tenant_a = TenantContext::new("company-a".to_string(), "client-123".to_string()); let tenant_a_context = RequestContext::with_tenant("req-a".to_string(), tenant_a); let tenant_b = TenantContext::new("company-b".to_string(), "client-456".to_string()); let tenant_b_context = RequestContext::with_tenant("req-b".to_string(), tenant_b); // Same server, different tenants - data is completely isolated let user_data = json!({ "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "userName": "john.doe", "emails": [{"value": "john@company.com", "primary": true}] }); server.create_resource_with_refs("User", user_data.clone(), &tenant_a_context).await?; server.create_resource_with_refs("User", user_data, &tenant_b_context).await?; // Each tenant sees only their own data let tenant_a_users = server.list_resources("User", &tenant_a_context).await?; let tenant_b_users = server.list_resources("User", &tenant_b_context).await?; println!("Company A users: {}", tenant_a_users.len()); println!("Company B users: {}", tenant_b_users.len()); }
Server Information and Capabilities
#![allow(unused)] fn main() { // Get server information and capabilities // See: https://docs.rs/scim-server/latest/scim_server/struct.ScimServer.html#method.get_server_info let server_info = server.get_server_info(); println!("Supported resource types: {:?}", server_info.supported_resource_types); println!("SCIM version: {}", server_info.scim_version); println!("Server capabilities: {:?}", server_info.capabilities); }
Next Steps
- Configuration Guide - Advanced server setup and storage backends
- Multi-Tenant Architecture - Advanced tenant isolation and management
- Resource Providers - Custom business logic and data models
- Storage Providers - Database integration patterns
Complete Examples
See the examples directory for full working implementations:
- basic_usage.rs - Complete CRUD operations
- group_example.rs - Group management with members
- multi_tenant_example.rs - Tenant isolation patterns
Running Examples
# Run any example to see it in action
cargo run --example basic_usage
cargo run --example group_example
Key Concepts
ScimServer
- Main interface providing full SCIM 2.0 complianceStandardResourceProvider
- Storage abstraction layerInMemoryStorage
- Simple storage backend for developmentRequestContext
- Request tracking and tenant isolation- Resource Handlers - Schema validation and business logic
- Resource Types - "User", "Group", or custom types registered with schemas
- JSON Data - All resource data uses
serde_json::Value
- Auto-generated Fields - Server automatically adds
$ref
,meta.location
, and other SCIM compliance fields
You now have a working SCIM server! The examples above demonstrate all core functionality needed for SCIM 2.0 compliant implementations.
Configuration Guide
This guide walks you through configuring a SCIM server application from the ground up. By the end of this guide, you'll understand the essential components needed to build a working SCIM server and the configuration options available at each stage.
What This Library Does (And Doesn't Do)
The scim-server
library provides the core SCIM protocol implementation, resource management, and storage abstraction. It handles:
- SCIM 2.0 protocol compliance - Resource validation, schema management, and SCIM operations
- Multi-tenant support - Tenant isolation and context management
- Resource lifecycle - Create, read, update, delete, and list operations
- Schema validation - Built-in User and Group schemas with extension support
- Concurrency control - ETag-based optimistic locking
What the library does NOT provide:
- HTTP server and routing - You need a web framework like Axum, Actix-web, or Warp
- Authentication and authorization - Implement JWT, OAuth2, or API key validation yourself
- Database connections - Choose and configure your own database or storage solution
- Process/thread management - Handle async runtimes and server lifecycle in your application
- Logging configuration - Set up structured logging with your preferred framework
- Deployment infrastructure - Docker, Kubernetes, or cloud deployment is your responsibility
Architecture Overview
The SCIM server follows a layered architecture where each component builds upon the previous one:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Your Application β
β (HTTP routes, auth, logging) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β SCIM Server β
β (Protocol, validation, operations) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Resource Provider β
β (Business logic, SCIM semantics) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Storage Provider β
β (Data persistence, queries) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
You configure these components from the bottom up, with each layer depending on the one below it.
Configuration Steps
Stage 1: Storage Provider (Foundation)
Purpose: The storage provider is the foundation layer that handles all data persistence and retrieval operations for SCIM resources. It provides a protocol-agnostic interface that abstracts away the specific storage implementation details, allowing the higher layers to work with any storage backend. This layer is responsible for storing, retrieving, updating, and deleting JSON resource data, as well as supporting queries and searches across resources within tenant boundaries.
Default Options:
InMemoryStorage::new()
- For development and testingSqliteStorage::new()
- For file-based persistence (requiressqlite
feature)
Configuration:
#![allow(unused)] fn main() { use scim_server::storage::InMemoryStorage; // Development/testing storage let storage = InMemoryStorage::new(); // Or for file-based storage // let storage = SqliteStorage::new().await?; }
When to implement custom storage:
- Database integration (PostgreSQL, MySQL, MongoDB)
- Cloud storage (DynamoDB, CosmosDB)
- Distributed systems (Redis, Cassandra)
- Search integration (Elasticsearch)
- User backend integrations (AWS Cognito User Pools, Microsoft Entra External ID)
- Legacy system integration (existing user directories, LDAP systems)
Custom storage example:
#![allow(unused)] fn main() { // Custom storage implementing StorageProvider trait // See: https://docs.rs/scim-server/latest/scim_server/storage/trait.StorageProvider.html struct DatabaseStorage { pool: sqlx::PgPool, } impl StorageProvider for DatabaseStorage { type Error = sqlx::Error; async fn put(&self, key: StorageKey, data: Value) -> Result<Value, Self::Error> { // Your database logic here } // ... implement other required methods } }
Stage 2: Resource Provider (Business Logic)
Purpose: The resource provider acts as the business logic layer that implements SCIM 2.0 protocol semantics and organizational rules on top of the storage layer. It handles resource validation, enforces SCIM compliance, manages resource relationships (like group memberships), implements concurrency control through ETags, and provides tenant-aware operations. This layer transforms generic storage operations into SCIM-compliant resource management, ensuring that all operations follow the SCIM specification while allowing for custom business logic integration.
Default Option:
StandardResourceProvider::new(storage)
- Recommended for most use cases
Configuration:
#![allow(unused)] fn main() { use scim_server::providers::StandardResourceProvider; // See: https://docs.rs/scim-server/latest/scim_server/providers/struct.StandardResourceProvider.html let provider = StandardResourceProvider::new(storage); }
When to implement custom providers:
- Complex business validation rules
- External system integration (LDAP, Active Directory)
- Custom security requirements (field-level encryption)
- Workflow integration or approval processes
Custom provider pattern:
#![allow(unused)] fn main() { // Custom provider implementing ResourceProvider trait // See: https://docs.rs/scim-server/latest/scim_server/trait.ResourceProvider.html struct EnterpriseProvider<S: StorageProvider> { standard: StandardResourceProvider<S>, ldap_client: LdapClient, } impl<S: StorageProvider> ResourceProvider for EnterpriseProvider<S> { async fn create_resource(&self, resource_type: &str, data: Value, context: &RequestContext) -> Result<VersionedResource, Self::Error> { // Custom validation self.validate_with_ldap(&data).await?; // Delegate to standard provider self.standard.create_resource(resource_type, data, context).await } } }
Stage 3: SCIM Server Configuration (Protocol Layer)
Purpose: The SCIM server represents the protocol layer that orchestrates all SCIM operations and provides the main API surface for your application. It manages resource type registration, schema validation against registered schemas, URL generation for resource references ($ref fields), multi-tenant URL strategies, and coordinates between different resource providers. This layer handles the SCIM protocol lifecycle including resource creation, updates, patches, deletions, and search operations while maintaining protocol compliance and providing proper error responses.
Creation Options:
Option A - Simple (Single tenant, default settings):
#![allow(unused)] fn main() { use scim_server::ScimServer; // See: https://docs.rs/scim-server/latest/scim_server/struct.ScimServer.html let server = ScimServer::new(provider)?; }
Option B - Builder Pattern (Recommended for production):
#![allow(unused)] fn main() { use scim_server::{ScimServerBuilder, TenantStrategy}; // See: https://docs.rs/scim-server/latest/scim_server/struct.ScimServerBuilder.html let server = ScimServerBuilder::new(provider) .with_base_url("https://api.company.com") .with_tenant_strategy(TenantStrategy::PathBased) .with_scim_version("v2") .build()?; }
Builder Configuration Options:
-
Base URL: Root URL for $ref generation
"https://api.company.com"
- Production HTTPS"http://localhost:8080"
- Development"mcp://scim"
- AI agent integration
-
Tenant Strategy: How tenant information appears in URLs
TenantStrategy::SingleTenant
- No tenant in URLs:/v2/Users/123
TenantStrategy::Subdomain
- Subdomain:https://tenant.api.com/v2/Users/123
TenantStrategy::PathBased
- Path:/tenant/v2/Users/123
-
SCIM Version: Protocol version in URLs (default: "v2")
Stage 4: Resource Type Registration
Purpose: Resource type registration defines what types of resources your SCIM server can handle and which operations are supported for each type. This stage connects SCIM schemas (which define the structure and validation rules) with resource handlers (which provide the processing logic) and declares which SCIM operations (Create, Read, Update, Delete, List, Search) are available for each resource type. Without this registration, the SCIM server cannot process requests for specific resource types, making this a critical configuration step that determines your server's capabilities.
Process:
- Get schema from server's built-in schema registry
- Create resource handler from schema
- Register with supported operations
#![allow(unused)] fn main() { use scim_server::{resource_handlers::{create_user_resource_handler, create_group_resource_handler}, ScimOperation}; // Register User resource type // See: https://docs.rs/scim-server/latest/scim_server/struct.ScimServer.html#method.get_schema_by_id let user_schema = server.get_schema_by_id("urn:ietf:params:scim:schemas:core:2.0:User")?.clone(); // See: https://docs.rs/scim-server/latest/scim_server/resource_handlers/fn.create_user_resource_handler.html let user_handler = create_user_resource_handler(user_schema); // See: https://docs.rs/scim-server/latest/scim_server/struct.ScimServer.html#method.register_resource_type server.register_resource_type("User", user_handler, vec![ ScimOperation::Create, ScimOperation::Read, ScimOperation::Update, ScimOperation::Delete, ScimOperation::List, ])?; // Register Group resource type let group_schema = server.get_schema_by_id("urn:ietf:params:scim:schemas:core:2.0:Group")?.clone(); // See: https://docs.rs/scim-server/latest/scim_server/resource_handlers/fn.create_group_resource_handler.html let group_handler = create_group_resource_handler(group_schema); server.register_resource_type("Group", group_handler, vec![ ScimOperation::Create, ScimOperation::Read, ])?; }
Built-in Resource Types:
- User - Individual user accounts with standard SCIM User schema
- Group - User collections with standard SCIM Group schema
Custom Resource Types: Create custom schemas and handlers for organization-specific resources like Roles, Permissions, or Devices.
Stage 5: Request Context Configuration
Purpose: Request contexts provide essential tracking and tenant isolation capabilities for every SCIM operation. The RequestContext
carries a unique request identifier for logging and debugging purposes, while the optional TenantContext
provides tenant isolation in multi-tenant deployments. These contexts flow through every layer of the system, ensuring that operations are properly attributed, logged, and isolated to the correct tenant boundaries. The context system enables audit trails, tenant-specific customizations, and secure multi-tenant operations.
Context Types:
Single-tenant contexts: Single-tenant contexts are used when your application serves only one organization or when you don't need tenant isolation. The RequestContext carries only a request ID for operation tracking.
#![allow(unused)] fn main() { use scim_server::RequestContext; // Simple context with custom request ID for tracking let context = RequestContext::new("request-123".to_string()); // Auto-generated UUID request ID for convenience let context = RequestContext::with_generated_id(); }
Multi-tenant contexts: Multi-tenant contexts include both request tracking and tenant isolation information. The TenantContext contains the tenant identifier (for resource isolation) and client identifier (for API access tracking).
#![allow(unused)] fn main() { use scim_server::{RequestContext, TenantContext}; // Create tenant context with tenant ID and client ID let tenant = TenantContext::new("tenant-id".to_string(), "client-id".to_string()); let context = RequestContext::with_tenant("request-123".to_string(), tenant); // Auto-generated request ID with tenant context let context = RequestContext::with_tenant_generated_id(tenant); }
The tenant ID determines resource isolation boundaries (ensuring tenants can only access their own resources), while the client ID tracks which API client is making requests (useful for API key management and rate limiting).
Stage 6: Multi-Tenant Configuration (Optional)
Purpose: Multi-tenant configuration enables secure resource isolation between different organizations or customer instances within a single SCIM server deployment. This stage defines tenant-specific permissions (what operations each tenant can perform), resource quotas (maximum users/groups per tenant), isolation levels (how strictly tenants are separated), and tenant-specific customizations. Multi-tenancy is essential for SaaS applications, enterprise deployments, and any scenario where multiple distinct organizations need to share the same SCIM infrastructure while maintaining complete data separation.
When to skip: Single-tenant applications where all users share the same namespace.
When to use: SaaS applications, enterprise deployments with multiple organizations.
Tenant Context Configuration:
#![allow(unused)] fn main() { use scim_server::{TenantContext, TenantPermissions, IsolationLevel}; // Configure tenant permissions let permissions = TenantPermissions { can_create: true, can_read: true, can_update: true, can_delete: false, // Read-only deletion policy can_list: true, max_users: Some(1000), max_groups: Some(100), }; // Create tenant with isolation settings let tenant = TenantContext::new("enterprise-corp".to_string(), "client-123".to_string()) .with_isolation_level(IsolationLevel::Strict) .with_permissions(permissions); }
Isolation Levels:
IsolationLevel::Strict
- Complete separation, highest securityIsolationLevel::Standard
- Normal isolation with some resource sharingIsolationLevel::Shared
- Minimal isolation for development/testing
Stage 7: Schema Extensions (Optional)
Purpose: Schema extensions allow you to add custom attributes, validation rules, and resource types beyond the standard SCIM User and Group schemas. This stage is crucial for organizations that need to store additional information like employee IDs, cost centers, custom roles, or industry-specific data fields. Extensions can be defined at the server level (affecting all tenants) or per-tenant (for multi-tenant deployments), enabling flexible customization while maintaining SCIM protocol compliance. This extensibility ensures your SCIM server can adapt to diverse organizational requirements and integrate with existing identity systems.
When to use:
- Industry-specific requirements (healthcare, finance)
- Enterprise attributes (employee ID, cost center)
- Integration with existing systems
Extension Options:
Option A - Custom Schema Files: Create JSON schema files with your custom attributes:
{
"id": "urn:company:params:scim:schemas:extension:2.0:Employee",
"name": "Employee",
"attributes": [
{
"name": "employeeId",
"type": "string",
"required": true,
"uniqueness": "server"
}
]
}
Option B - Runtime Schema Addition:
#![allow(unused)] fn main() { // Load custom schema and add to registry let mut schema_registry = server.schema_registry_mut(); schema_registry.add_schema(custom_schema)?; }
Stage 8: Request/Response Handler Integration
Purpose: The final integration stage connects your configured SCIM server with HTTP frameworks and client applications. This stage involves creating HTTP route handlers that translate REST API requests into SCIM server operations, implementing authentication middleware, handling request/response serialization, and managing error responses. This is where your SCIM server becomes accessible to client applications, identity providers, and administrative tools. The request/response handlers serve as the bridge between the HTTP protocol layer and your SCIM server's internal operations.
Integration Options:
HTTP Framework Integration:
#![allow(unused)] fn main() { // Example with Axum web framework use axum::{extract::Extension, http::StatusCode, response::Json, routing::post, Router}; async fn create_user_handler( Extension(server): Extension<ScimServer<YourProvider>>, Json(user_data): Json<serde_json::Value>, ) -> Result<Json<serde_json::Value>, StatusCode> { let context = RequestContext::with_generated_id(); match server.create_resource("User", user_data, &context).await { Ok(user) => Ok(Json(user.into_json())), Err(_) => Err(StatusCode::BAD_REQUEST), } } let app = Router::new() .route("/scim/v2/Users", post(create_user_handler)) .layer(Extension(server)); }
Authentication Integration:
#![allow(unused)] fn main() { // Middleware for JWT/API key validation async fn auth_middleware(request: Request, next: Next) -> Response { // Extract and validate authentication token let auth_header = request.headers().get("authorization"); if let Some(token) = extract_and_validate_token(auth_header) { // Add authenticated context to request request.extensions_mut().insert(AuthenticatedUser::from(token)); next.run(request).await } else { Response::builder() .status(401) .body("Unauthorized".into()) .unwrap() } } }
What you need to implement:
- HTTP route handlers for each SCIM endpoint (Users, Groups, etc.)
- Authentication and authorization middleware
- Request validation and error handling
- Response formatting and status code management
- CORS policies for web applications
- Rate limiting and request throttling
- Audit logging and monitoring integration
Complete Configuration Example
Here's a minimal but complete configuration for a production-ready SCIM server:
use scim_server::{ ScimServerBuilder, TenantStrategy, RequestContext, providers::StandardResourceProvider, storage::InMemoryStorage, resource_handlers::{create_user_resource_handler, create_group_resource_handler}, ScimOperation, }; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { // Stage 1: Storage Provider let storage = InMemoryStorage::new(); // Stage 2: Resource Provider let provider = StandardResourceProvider::new(storage); // Stage 3: SCIM Server Configuration let mut server = ScimServerBuilder::new(provider) .with_base_url("https://api.company.com") .with_tenant_strategy(TenantStrategy::PathBased) .build()?; // Stage 4: Resource Type Registration let user_schema = server.get_schema_by_id("urn:ietf:params:scim:schemas:core:2.0:User")?.clone(); let user_handler = create_user_resource_handler(user_schema); server.register_resource_type("User", user_handler, vec![ ScimOperation::Create, ScimOperation::Read, ScimOperation::Update, ScimOperation::Delete, ])?; // Stage 5: Request Context (configured per operation) let context = RequestContext::with_generated_id(); // Stage 6: Ready for request/response handler integration // server is now ready to handle SCIM operations through HTTP handlers // Next: create HTTP routes and authentication middleware Ok(()) }
Next Steps
With your SCIM server configured, you can explore more advanced topics:
- Multi-Tenant Architecture - Set up tenant isolation and management
- Operation Handlers - Framework-agnostic request/response handling
- MCP Integration - AI agent support and tool discovery
- Schema Extensions - Custom attributes and validation
- Concurrency Control - Version management and conflict resolution
- Storage Providers - Database integration patterns
For complete working examples, see the examples directory in the repository.
API Reference
For detailed API documentation, see:
- ScimServer API - Main server interface
- ResourceProvider trait - Business logic interface
- StorageProvider trait - Data persistence interface
- Complete API docs - Full library documentation
Setting Up Your MCP Server
This guide shows you how to set up a working Model Context Protocol (MCP) server that exposes your SCIM operations as discoverable tools for AI agents.
See the MCP Integration API documentation for complete details.
What is MCP Integration?
The MCP integration allows AI agents to interact with your SCIM server through a standardized protocol. AI agents can discover available tools (like "create user" or "search users") and execute them with proper validation and error handling.
Key Benefits:
- AI-Friendly Interface - Structured tool discovery and execution
- Multi-Tenant Support - Isolated operations for different clients
- Schema Introspection - AI agents can understand your data model
- Error Handling - Graceful error responses with detailed information
Quick Start
1. Enable the MCP Feature
Add the MCP feature to your Cargo.toml
:
[dependencies]
scim-server = { version = "0.5.3", features = ["mcp"] }
tokio = { version = "1.0", features = ["full"] }
serde_json = "1.0"
env_logger = "0.10" # For logging (recommended)
2. Basic MCP Server (30 lines)
Create a minimal MCP server that exposes SCIM operations:
use scim_server::{ mcp_integration::ScimMcpServer, // MCP server wrapper for SCIM multi_tenant::ScimOperation, // Available SCIM operations providers::StandardResourceProvider, // Standard resource provider resource_handlers::create_user_resource_handler, // Schema-aware handlers scim_server::ScimServer, // Core SCIM server storage::InMemoryStorage, // Development storage }; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { // Initialize logging for debugging env_logger::init(); // Create SCIM server with in-memory storage // See: https://docs.rs/scim-server/latest/scim_server/storage/struct.InMemoryStorage.html let storage = InMemoryStorage::new(); // See: https://docs.rs/scim-server/latest/scim_server/providers/struct.StandardResourceProvider.html let provider = StandardResourceProvider::new(storage); let mut scim_server = ScimServer::new(provider)?; // Register User resource type with full operations let user_schema = scim_server .get_schema_by_id("urn:ietf:params:scim:schemas:core:2.0:User")? .clone(); let user_handler = create_user_resource_handler(user_schema); scim_server.register_resource_type( "User", user_handler, vec![ ScimOperation::Create, ScimOperation::Read, ScimOperation::Update, ScimOperation::Delete, ScimOperation::List, ScimOperation::Search, ], )?; // Create and start MCP server let mcp_server = ScimMcpServer::new(scim_server); println!("π MCP Server starting - listening on stdio"); // This runs until EOF (Ctrl+D) or process termination mcp_server.run_stdio().await?; Ok(()) }
3. Run Your MCP Server
cargo run --features mcp
The server starts and listens on standard input/output for MCP protocol messages.
Testing Your Server
You can test the server by sending JSON-RPC messages directly:
1. Initialize Connection
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{}}}' | your_server
2. Discover Available Tools
echo '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' | your_server
3. Create a User
echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"scim_create_user","arguments":{"user_data":{"schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],"userName":"alice@example.com","active":true}}}}' | your_server
Production-Ready Setup
For production use, you'll want a more comprehensive setup:
use scim_server::{ mcp_integration::{McpServerInfo, ScimMcpServer}, // MCP server wrapper multi_tenant::ScimOperation, // SCIM operation types providers::StandardResourceProvider, // Standard provider resource_handlers::{create_user_resource_handler, create_group_resource_handler}, scim_server::ScimServer, storage::InMemoryStorage, // Replace with your database storage }; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { // Production logging configuration env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")) .init(); // Create SCIM server (use your production storage here) let storage = InMemoryStorage::new(); // Replace with PostgresStorage, etc. let provider = StandardResourceProvider::new(storage); let mut scim_server = ScimServer::new(provider)?; // Register User resource type let user_schema = scim_server .get_schema_by_id("urn:ietf:params:scim:schemas:core:2.0:User")? .clone(); let user_handler = create_user_resource_handler(user_schema); scim_server.register_resource_type( "User", user_handler, vec![ ScimOperation::Create, ScimOperation::Read, ScimOperation::Update, ScimOperation::Delete, ScimOperation::List, ScimOperation::Search, ], )?; // Register Group resource type (if needed) if let Some(group_schema) = scim_server .get_schema_by_id("urn:ietf:params:scim:schemas:core:2.0:Group") { let group_handler = create_group_resource_handler(group_schema.clone()); scim_server.register_resource_type( "Group", group_handler, vec![ ScimOperation::Create, ScimOperation::Read, ScimOperation::Update, ScimOperation::Delete, ScimOperation::List, ScimOperation::Search, ], )?; } // Create MCP server with custom information let server_info = McpServerInfo { name: "Production SCIM Server".to_string(), version: env!("CARGO_PKG_VERSION").to_string(), description: "Enterprise SCIM server with MCP integration".to_string(), supported_resource_types: vec!["User".to_string(), "Group".to_string()], }; let mcp_server = ScimMcpServer::with_info(scim_server, server_info); // Log available tools let tools = mcp_server.get_tools(); log::info!("π§ Available MCP tools: {}", tools.len()); for tool in &tools { if let Some(name) = tool.get("name").and_then(|n| n.as_str()) { log::info!(" β’ {}", name); } } log::info!("π MCP Server ready - listening on stdio"); // Start the server mcp_server.run_stdio().await?; log::info!("β MCP Server shutdown complete"); Ok(()) }
Available MCP Tools
Your MCP server exposes these tools to AI agents:
User Management
scim_create_user
- Create a new userscim_get_user
- Retrieve user by IDscim_update_user
- Update existing userscim_delete_user
- Delete user by IDscim_list_users
- List all users with paginationscim_search_users
- Search users by attributescim_user_exists
- Check if user exists
System Information
scim_get_schemas
- Get all SCIM schemasscim_server_info
- Get server capabilities and info
Multi-Tenant Support
The MCP server supports multi-tenant operations out of the box:
{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "scim_create_user",
"arguments": {
"user_data": {
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName": "bob@tenant-a.com",
"active": true
},
"tenant_id": "tenant-a"
}
}
}
Each tenant's data is completely isolated from others.
Integration with AI Agents
Your MCP server can be used with any MCP-compatible AI agent or framework. The AI agent will:
- Initialize - Establish connection and capabilities
- Discover Tools - Get list of available SCIM operations
- Get Schemas - Understand your data model structure
- Execute Operations - Create, read, update, delete resources
- Handle Errors - Process validation and operation errors gracefully
Error Handling
The MCP server provides structured error responses for AI agents:
{
"jsonrpc": "2.0",
"id": 5,
"error": {
"code": -32000,
"message": "Tool execution failed: Validation error: userName is required"
}
}
Logging and Monitoring
Enable comprehensive logging for production deployments:
#![allow(unused)] fn main() { // In your main function env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")) .format_timestamp_secs() .init(); log::info!("MCP Server starting"); log::debug!("Available tools: {:?}", mcp_server.get_tools().len()); }
Running Examples
The crate includes complete working examples:
# Basic MCP server
cargo run --example mcp_stdio_server --features mcp
# Comprehensive example with demos
cargo run --example mcp_server_example --features mcp
Next Steps
- Storage Backends - Replace InMemoryStorage with PostgreSQL or other databases
- Multi-Tenant Configuration - Advanced tenant management
- Custom Resource Types - Beyond User and Group
- Production Deployment - Scaling and monitoring MCP servers
Complete Working Example
See examples/mcp_stdio_server.rs
for a complete, production-ready MCP server implementation with:
- Comprehensive error handling
- Multi-tenant support
- Full logging configuration
- Tool discovery and execution
- Schema introspection
- Graceful shutdown handling
The MCP integration makes your SCIM server AI-ready with minimal configuration!
Architecture
The SCIM Server follows a clean trait-based architecture with clear separation of concerns designed for maximum composability and extensibility.
Component Architecture
The library is built around composable traits that you implement for your specific needs:
βββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββ
β Client Layer β β SCIM Server β β Resource Layer β
β β β β β β
β β’ MCP AI βββββΆβ β’ Operations βββββΆβ ResourceProviderβ
β β’ Web Frameworkβ β β’ Multi-tenant β β trait β
β β’ Custom β β β’ Type Safety β β β
βββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββ
β β
βΌ βΌ
ββββββββββββββββββββ βββββββββββββββββββ
β Schema System β β Storage Layer β
β β β β
β β’ SchemaRegistry β β StorageProvider β
β β’ Validation β β trait β
β β’ Value Objects β β β’ In-Memory β
β β’ Extensions β β β’ Database β
ββββββββββββββββββββ β β’ Custom β
βββββββββββββββββββ
Layer Responsibilities
Client Layer: Your integration points - compose these components into web endpoints, AI tools, or custom applications.
SCIM Server: Orchestration component that coordinates resource operations using your provider implementations.
Resource Layer: ResourceProvider
trait - implement this interface for your data model, or use the provided StandardResourceProvider
for common scenarios.
Schema System: Schema registry and validation components - extend with custom schemas and value objects.
Storage Layer: StorageProvider
trait - use the provided InMemoryStorage
for development, or connect to any database or custom backend.
Core Traits
ResourceProvider
Your main integration point for SCIM resource operations - see the full API documentation:
#![allow(unused)] fn main() { pub trait ResourceProvider { type Error: std::error::Error + Send + Sync + 'static; async fn create_resource( &self, resource_type: &str, data: Value, context: &RequestContext, ) -> Result<Resource, Self::Error>; async fn get_resource( &self, resource_type: &str, id: &str, context: &RequestContext, ) -> Result<Option<Resource>, Self::Error>; // ... other CRUD operations } }
Implementation Options:
- Use
StandardResourceProvider<S>
with anyStorageProvider
for typical use cases - Implement directly for custom business logic and data models
- Wrap existing services or databases
StorageProvider
Pure data persistence abstraction - see the full API documentation:
#![allow(unused)] fn main() { pub trait StorageProvider: Send + Sync { type Error: std::error::Error + Send + Sync + 'static; async fn put(&self, key: StorageKey, data: Value) -> Result<Value, Self::Error>; async fn get(&self, key: StorageKey) -> Result<Option<Value>, Self::Error>; async fn delete(&self, key: StorageKey) -> Result<bool, Self::Error>; async fn list(&self, prefix: StoragePrefix) -> Result<Vec<Value>, Self::Error>; } }
Implementation Options:
- Use
InMemoryStorage
for development and testing - Implement for your database (PostgreSQL, MongoDB, etc.)
- Connect to cloud storage or external APIs
Value Objects
Type-safe SCIM attribute handling - see the schema documentation:
#![allow(unused)] fn main() { pub trait ValueObject: Debug + Send + Sync { fn attribute_type(&self) -> AttributeType; fn to_json(&self) -> ValidationResult<Value>; fn validate_against_schema(&self, definition: &AttributeDefinition) -> ValidationResult<()>; // ... } pub trait SchemaConstructible: ValueObject + Sized { fn from_schema_and_value( definition: &AttributeDefinition, value: &Value, ) -> ValidationResult<Self>; // ... } }
Extension Points:
- Create custom value objects for domain-specific attributes
- Implement validation logic for business rules
- Support for complex multi-valued attributes
Multi-Tenant Architecture
The library provides several components for multi-tenant systems:
TenantResolver
Maps authentication credentials to tenant context - see the multi-tenant API documentation:
#![allow(unused)] fn main() { pub trait TenantResolver: Send + Sync { type Error: std::error::Error + Send + Sync + 'static; async fn resolve_tenant(&self, credential: &str) -> Result<TenantContext, Self::Error>; } }
RequestContext
Carries tenant and request information through all operations - see the RequestContext API:
#![allow(unused)] fn main() { pub struct RequestContext { pub request_id: String, tenant_context: Option<TenantContext>, } }
Tenant Isolation
- All
ResourceProvider
operations includeRequestContext
- Storage keys automatically include tenant ID
- Schema validation respects tenant-specific extensions
Schema System Architecture
SchemaRegistry
Central registry for SCIM schemas - see the SchemaRegistry API:
- Loads and validates RFC 7643 core schemas
- Supports custom schema extensions
- Provides validation services for all operations
Dynamic Value Objects
- Runtime creation from schema definitions
- Type-safe attribute handling
- Extensible factory pattern for custom types
Extension Model
- Custom resource types beyond User/Group
- Organization-specific attributes
- Maintains SCIM compliance and interoperability
Concurrency Control
ETag-Based Versioning
Built into the core architecture:
- Automatic version generation from resource content
- Conditional operations (If-Match, If-None-Match)
- Conflict detection and resolution
- Production-ready optimistic locking
Version-Aware Operations
All resource operations support conditional execution - see the Resource API:
#![allow(unused)] fn main() { // Conditional update with version check let result = provider.conditional_update( "User", &user_id, updated_data, &expected_version, &context ).await?; match result { ConditionalResult::Success(resource) => // Update succeeded ConditionalResult::PreconditionFailed => // Version conflict } }
Integration Patterns
Web Framework Integration
Components work with any HTTP framework:
- Extract SCIM request details
- Create
RequestContext
with tenant info - Call appropriate
ScimServer
operations - Format responses per SCIM specification
AI Agent Integration
Model Context Protocol (MCP) components:
- Expose SCIM operations as discoverable tools
- Structured schemas for AI understanding
- Error handling designed for AI decision making
- Multi-tenant aware tool descriptions
Custom Client Integration
Direct component usage:
- Implement
ResourceProvider
for your data model - Choose appropriate
StorageProvider
- Configure schema extensions as needed
- Build custom API layer or integration logic
Performance Considerations
Async-First Design
- All I/O operations are async
- Non-blocking concurrent operations
- Efficient resource utilization
Minimal Allocations
- Zero-copy JSON processing where possible
- Efficient value object system
- Smart caching in schema registry
Scalability Features
- Pluggable storage for horizontal scaling
- Multi-tenant isolation for SaaS platforms
- Connection pooling support through storage traits
Operation Handlers
Operation Handlers are the framework-agnostic bridge between transport layers and the SCIM Server core, providing structured request/response handling with built-in concurrency control and comprehensive error management. They abstract SCIM operations into a transport-neutral interface that can be used with HTTP frameworks, MCP protocols, CLI tools, or any other integration pattern.
See the Operation Handler API documentation for complete details.
Value Proposition
Operation Handlers deliver critical integration capabilities:
- Framework Agnostic: Work with any transport layer (HTTP, MCP, CLI, custom protocols)
- Structured Interface: Consistent request/response patterns across all operation types
- Built-in ETag Support: Automatic version control and concurrency management
- Comprehensive Error Handling: Structured error responses with proper SCIM compliance
- Request Tracing: Built-in request ID correlation and operational logging
- Multi-Tenant Aware: Seamless tenant context propagation through operations
- Type-Safe Operations: Strongly typed operation dispatch with compile-time safety
Architecture Overview
Operation Handlers sit between transport layers and the SCIM Server core:
Transport Layer (HTTP/MCP/CLI)
β
ScimOperationHandler (Framework Abstraction)
βββ Request Structuring & Validation
βββ Operation Dispatch & Routing
βββ ETag Version Management
βββ Error Handling & Translation
βββ Response Formatting
β
SCIM Server (Business Logic)
β
Resource Providers & Storage
Core Components
ScimOperationHandler
: Main dispatcher for all SCIM operationsScimOperationRequest
: Structured request wrapper with validationScimOperationResponse
: Consistent response format with metadataOperationMetadata
: Version control, tracing, and operational data- Builder Utilities: Convenient construction helpers for requests
Use Cases
1. HTTP Framework Integration
Building REST APIs with any HTTP framework
#![allow(unused)] fn main() { use scim_server::operation_handler::{ScimOperationHandler, ScimOperationRequest}; use axum::{Json, Path, extract::Query, response::Json as ResponseJson}; // Setup once let handler = ScimOperationHandler::new(scim_server); // HTTP handler functions async fn create_user( Path(resource_type): Path<String>, Json(data): Json<Value> ) -> ResponseJson<Value> { let request = ScimOperationRequest::create(resource_type, data) .with_request_id(uuid::Uuid::new_v4().to_string()); let response = handler.handle_operation(request).await; if response.success { ResponseJson(response.data.unwrap()) } else { // Handle error appropriately ResponseJson(json!({"error": response.error})) } } async fn get_user( Path((resource_type, id)): Path<(String, String)> ) -> ResponseJson<Value> { let request = ScimOperationRequest::get(resource_type, id); let response = handler.handle_operation(request).await; // Handle response... } }
Benefits: Framework independence, consistent error handling, automatic ETag support.
2. MCP Protocol Integration
AI agent tool integration through Model Context Protocol
#![allow(unused)] fn main() { // MCP tool handler async fn handle_scim_tool(tool_name: &str, args: Value) -> McpResult { let request = match tool_name { "create_user" => { ScimOperationRequest::create("User", args["user_data"].clone()) .with_tenant_context(extract_tenant_from_args(&args)?) }, "get_user" => { ScimOperationRequest::get("User", args["user_id"].as_str().unwrap()) }, "update_user" => { ScimOperationRequest::update( "User", args["user_id"].as_str().unwrap(), args["user_data"].clone() ).with_expected_version(args["expected_version"].as_str()) }, _ => return Err(McpError::UnknownTool), }; let response = operation_handler.handle_operation(request).await; McpResult { success: response.success, content: response.data, metadata: response.metadata, } } }
Benefits: Structured AI interactions, automatic version management, consistent tool interface.
3. CLI Tool Development
Command-line identity management tools
#![allow(unused)] fn main() { // CLI command handlers async fn cli_create_user(args: &CreateUserArgs) -> CliResult { let user_data = json!({ "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "userName": args.username, "displayName": args.display_name, "active": true }); let request = ScimOperationRequest::create("User", user_data) .with_tenant_context(args.tenant_context.clone()) .with_request_id(format!("cli-{}", uuid::Uuid::new_v4())); let response = handler.handle_operation(request).await; if response.success { println!("β User created successfully"); if let Some(etag) = response.metadata.additional.get("etag") { println!(" ETag: {}", etag.as_str().unwrap()); } Ok(()) } else { eprintln!("β Failed to create user: {}", response.error.unwrap()); Err(CliError::OperationFailed) } } }
Benefits: Consistent command behavior, built-in error reporting, automatic version tracking.
4. Batch Processing Systems
Bulk identity operations with concurrency control
#![allow(unused)] fn main() { async fn batch_update_users(updates: Vec<UserUpdate>) -> BatchResult { let mut results = Vec::new(); for update in updates { let request = ScimOperationRequest::update("User", &update.id, update.data) .with_expected_version(update.expected_version) .with_request_id(format!("batch-{}", uuid::Uuid::new_v4())); let response = handler.handle_operation(request).await; results.push(BatchUpdateResult { user_id: update.id.clone(), success: response.success, error: response.error, new_version: response.metadata.additional .get("version") .and_then(|v| v.as_str()) .map(String::from), }); } BatchResult { total: updates.len(), succeeded: results.iter().filter(|r| r.success).count(), results } } }
Benefits: Built-in concurrency control, consistent error handling, operation tracing.
5. Custom Protocol Integration
Embedding SCIM in domain-specific protocols
#![allow(unused)] fn main() { // Custom protocol message handler async fn handle_identity_message(msg: IdentityMessage) -> ProtocolResponse { let request = match msg.operation { IdentityOperation::Provision { user_spec } => { ScimOperationRequest::create("User", user_spec.to_scim_json()) .with_tenant_context(msg.tenant_context) }, IdentityOperation::Deprovision { user_id } => { ScimOperationRequest::delete("User", user_id) .with_tenant_context(msg.tenant_context) }, IdentityOperation::Query { criteria } => { ScimOperationRequest::search("User") .with_query(criteria.to_scim_query()) .with_tenant_context(msg.tenant_context) }, }; let response = handler.handle_operation(request).await; ProtocolResponse { correlation_id: msg.correlation_id, success: response.success, payload: response.data, error_details: response.error, } } }
Benefits: Protocol-agnostic operations, consistent behavior patterns, built-in error handling.
Design Patterns
Request Builder Pattern
Fluent API for constructing operation requests:
#![allow(unused)] fn main() { let request = ScimOperationRequest::update("User", "123", user_data) .with_tenant_context(tenant_ctx) .with_request_id("req-456") .with_expected_version("v1.2.3"); }
This provides type-safe request construction with optional parameters.
Structured Response Pattern
Consistent response format across all operations:
#![allow(unused)] fn main() { pub struct ScimOperationResponse { pub success: bool, pub data: Option<Value>, pub error: Option<String>, pub error_code: Option<String>, pub metadata: OperationMetadata, } }
This ensures uniform error handling and metadata access.
Operation Dispatch Pattern
Type-safe operation routing:
#![allow(unused)] fn main() { match request.operation { ScimOperationType::Create => handle_create(handler, request, context).await, ScimOperationType::Update => handle_update(handler, request, context).await, ScimOperationType::Get => handle_get(handler, request, context).await, // ... other operations } }
This provides compile-time guarantees about operation handling.
Metadata Propagation Pattern
Automatic metadata management:
#![allow(unused)] fn main() { pub struct OperationMetadata { pub request_id: String, pub tenant_id: Option<String>, pub resource_count: Option<usize>, pub additional: HashMap<String, Value>, // Includes ETag info } }
This ensures consistent metadata across all operations.
Integration with Other Components
SCIM Server Integration
Operation Handlers orchestrate SCIM Server operations:
- Operation Dispatch: Routes structured requests to appropriate server methods
- Context Management: Ensures proper tenant context flows through operations
- Response Formatting: Converts server responses to structured format
- Error Translation: Maps SCIM errors to transport-appropriate formats
Version Control Integration
Built-in ETag concurrency control:
- Automatic Versioning: All resources include version information in responses
- Conditional Operations: Support for If-Match/If-None-Match semantics
- Conflict Detection: Structured responses for version conflicts
- Version Propagation: Version metadata included in all operation responses
Multi-Tenant Integration
Seamless tenant context handling:
- Context Extraction: Automatically extracts tenant information from requests
- Tenant Validation: Ensures tenant context is properly validated
- Scoped Operations: All operations automatically tenant-scoped
- Tenant Metadata: Tenant information included in operation metadata
Error Handling Integration
Comprehensive error management:
- Structured Errors: SCIM-compliant error responses with proper status codes
- Error Propagation: Consistent error handling across all operation types
- Debug Information: Rich error context for troubleshooting
- Transport Agnostic: Error format suitable for any transport layer
Best Practices
1. Use Request Builders for Complex Operations
#![allow(unused)] fn main() { // Good: Use builder pattern for readable construction let request = ScimOperationRequest::update("User", user_id, user_data) .with_tenant_context(tenant_context) .with_expected_version(current_version) .with_request_id(correlation_id); // Avoid: Manual struct construction let request = ScimOperationRequest { operation: ScimOperationType::Update, resource_type: "User".to_string(), // ... many fields }; }
2. Always Handle Version Information
#![allow(unused)] fn main() { // Good: Check and use version information let response = handler.handle_operation(request).await; if response.success { let new_version = response.metadata.additional .get("version") .and_then(|v| v.as_str()); // Store version for next operation } // Avoid: Ignoring version information // This leads to lost updates and concurrency issues }
3. Implement Proper Error Handling
#![allow(unused)] fn main() { // Good: Handle different error types appropriately match response.error_code.as_deref() { Some("version_conflict") => { // Handle version conflict specifically retry_with_fresh_version().await }, Some("resource_not_found") => { // Handle missing resource return NotFoundError; }, Some(_) | None if !response.success => { // Handle other errors log_error(&response.error); return ServerError; }, _ => { // Success case process_response_data(response.data) } } // Avoid: Generic error handling that loses context if !response.success { return Err("Operation failed"); } }
4. Use Request IDs for Tracing
#![allow(unused)] fn main() { // Good: Consistent request ID usage let request_id = generate_correlation_id(); let request = ScimOperationRequest::create("User", data) .with_request_id(request_id.clone()); let response = handler.handle_operation(request).await; log::info!("Operation {} completed: {}", request_id, response.success); // Avoid: No request correlation // This makes debugging and tracing difficult }
5. Leverage Tenant Context Appropriately
#![allow(unused)] fn main() { // Good: Explicit tenant context handling let request = if let Some(tenant_ctx) = extract_tenant_from_auth(auth_header) { ScimOperationRequest::create("User", data) .with_tenant_context(tenant_ctx) } else { ScimOperationRequest::create("User", data) // Single tenant }; // Avoid: Ignoring tenant context in multi-tenant scenarios // This can lead to cross-tenant data access }
When to Use Operation Handlers
Primary Scenarios
- HTTP Framework Integration: Building REST APIs that expose SCIM endpoints
- Protocol Integration: Adapting SCIM to custom protocols (MCP, GraphQL, gRPC)
- CLI Tools: Building command-line identity management utilities
- Batch Processing: Implementing bulk identity operations
- Testing Frameworks: Creating test harnesses that need structured SCIM operations
Implementation Strategies
Scenario | Approach | Complexity | Benefits |
---|---|---|---|
REST API | Direct handler integration | Low | Framework independence, built-in ETag |
MCP Protocol | Tool handler delegation | Medium | Structured AI interactions |
CLI Tools | Command handler wrapper | Low | Consistent CLI behavior |
Batch Processing | Async handler coordination | Medium | Concurrency control, error handling |
Custom Protocols | Protocol adapter layer | High | Protocol flexibility, SCIM compliance |
Comparison with Direct SCIM Server Usage
Approach | Abstraction | Error Handling | Version Control | Complexity |
---|---|---|---|---|
Operation Handlers | β High | β Structured | β Built-in | Low |
Direct SCIM Server | β οΈ Medium | β οΈ Manual | β οΈ Manual | Medium |
Custom Integration | β Low | β Ad-hoc | β Custom | High |
Operation Handlers provide the optimal balance of abstraction and functionality for most integration scenarios, offering structured operations with built-in best practices.
Framework Examples
Axum Integration
#![allow(unused)] fn main() { async fn axum_create_resource( Path(resource_type): Path<String>, headers: HeaderMap, Json(data): Json<Value> ) -> Result<Json<Value>, StatusCode> { let request = ScimOperationRequest::create(resource_type, data) .with_request_id(extract_request_id(&headers)); let response = handler.handle_operation(request).await; if response.success { Ok(Json(response.data.unwrap())) } else { Err(map_error_to_status(&response)) } } }
Warp Integration
#![allow(unused)] fn main() { fn warp_scim_routes() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { warp::path("Users") .and(warp::post()) .and(warp::body::json()) .and_then(move |data: Value| { let handler = handler.clone(); async move { let request = ScimOperationRequest::create("User", data); let response = handler.handle_operation(request).await; Ok::<_, warp::Rejection>(warp::reply::json(&response.data)) } }) } }
Operation Handlers serve as the foundational integration layer that enables SCIM Server to work seamlessly with any transport mechanism while maintaining consistent behavior, proper error handling, and automatic concurrency control across all integration patterns.
MCP Integration (AI Agent Support)
MCP (Model Context Protocol) Integration enables AI agents to perform identity management operations through a standardized, discoverable tool interface. This integration transforms SCIM operations into structured tools that AI systems can understand, discover, and execute, making identity management accessible to artificial intelligence workflows and automation systems.
Note: MCP Integration is available behind the
mcp
feature flag. Addfeatures = ["mcp"]
to yourCargo.toml
dependency to enable this functionality.
See the MCP Integration API documentation for complete details.
Value Proposition
MCP Integration delivers comprehensive AI-first identity management capabilities:
- AI-Native Interface: Structured tool discovery and execution designed for AI agent workflows
- Schema-Driven Operations: AI agents understand SCIM data structures through JSON Schema definitions
- Automatic Tool Discovery: Dynamic exposure of available operations based on server configuration
- Conversational Identity Management: Natural language to structured SCIM operations translation
- Multi-Tenant AI Support: Tenant-aware operations for enterprise AI deployment scenarios
- Version-Aware AI Operations: Built-in optimistic locking prevents AI-induced data conflicts
- Error-Resilient AI Workflows: Structured error responses enable AI decision making and recovery
Architecture Overview
MCP Integration operates as an AI-agent bridge on top of the Operation Handler layer:
AI Agent (Claude, GPT, Custom)
β
MCP Protocol (JSON-RPC 2.0)
β
ScimMcpServer (AI Agent Bridge)
βββ Tool Discovery & Schema Generation
βββ Parameter Validation & Conversion
βββ AI-Friendly Error Translation
βββ Version Metadata Management
βββ Tenant Context Extraction
β
Operation Handler (Framework Abstraction)
β
SCIM Server (Business Logic)
Core Components
ScimMcpServer
: Main MCP server wrapper exposing SCIM operations as AI tools- Tool Schemas: JSON Schema definitions for AI agent tool discovery
- Tool Handlers: Execution logic for each exposed SCIM operation
- Protocol Layer: MCP JSON-RPC 2.0 protocol implementation
ScimToolResult
: Structured results optimized for AI decision making
Use Cases
1. Conversational HR Assistant
AI-powered employee onboarding and management
#![allow(unused)] fn main() { // Setup MCP server for HR AI assistant let hr_server_info = McpServerInfo { name: "HR Identity Management".to_string(), version: "1.0.0".to_string(), description: "AI-powered employee lifecycle management".to_string(), supported_resource_types: vec!["User".to_string(), "Group".to_string()], }; let mcp_server = ScimMcpServer::with_info(scim_server, hr_server_info); // AI agent discovers available tools let tools = mcp_server.get_tools(); // Returns: create_user, get_user, update_user, delete_user, list_users, etc. // AI agent executes conversational commands: // "Create a new employee John Doe with email john.doe@company.com" let result = mcp_server.execute_tool("scim_create_user", json!({ "user_data": { "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "userName": "john.doe@company.com", "name": { "givenName": "John", "familyName": "Doe" }, "emails": [{ "value": "john.doe@company.com", "primary": true }], "active": true } })).await; // AI receives structured response with raw version for follow-up operations if result.success { let user_id = result.metadata.as_ref() .and_then(|m| m.get("resource_id")) .and_then(|id| id.as_str()); let version = result.metadata.as_ref() .and_then(|m| m.get("version")) .and_then(|v| v.as_str()); // AI can now reference this user and version in subsequent operations } }
Benefits: Natural language HR operations, automatic compliance, conversation history tracking.
2. DevOps Automation Agent
AI-driven development environment provisioning
#![allow(unused)] fn main() { // Multi-tenant development environment management let devops_context = json!({ "tenant_id": "dev-environment-123" }); // AI agent: "Set up development accounts for the new team" let team_members = vec!["alice.dev", "bob.dev", "charlie.dev"]; for username in team_members { let create_result = mcp_server.execute_tool("scim_create_user", json!({ "user_data": { "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "userName": username, "active": true, "emails": [{ "value": format!("{}@dev.company.com", username), "primary": true }] }, "tenant_id": "dev-environment-123" })).await; if !create_result.success { // AI can understand and act on structured errors println!("Failed to create {}: {}", username, create_result.content.get("error").unwrap()); } } // AI agent: "Create development team group and add all developers" let group_result = mcp_server.execute_tool("scim_create_group", json!({ "group_data": { "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"], "displayName": "Development Team", "members": team_user_ids // Collected from previous operations }, "tenant_id": "dev-environment-123" })).await; }
Benefits: Automated environment setup, consistent development team provisioning, AI-driven scaling.
3. Compliance and Audit AI
AI agent for identity governance and compliance monitoring
#![allow(unused)] fn main() { // AI agent performing compliance audit let compliance_server = ScimMcpServer::with_info(scim_server, McpServerInfo { name: "Compliance Monitor".to_string(), description: "AI-powered identity compliance and audit system".to_string(), ..Default::default() }); // AI agent: "Check all inactive users and prepare deprovisioning report" let inactive_users_result = mcp_server.execute_tool("scim_search_users", json!({ "attribute": "active", "value": "false" })).await; if let Some(users) = inactive_users_result.content.as_array() { for user in users { let user_id = user.get("id").and_then(|id| id.as_str()).unwrap(); // AI agent: "Analyze user access patterns and recommend action" let user_details = mcp_server.execute_tool("scim_get_user", json!({ "user_id": user_id })).await; // AI processes user data and determines compliance actions // Based on last login, role, department, etc. } } // AI agent generates compliance report and recommended actions }
Benefits: Automated compliance monitoring, intelligent audit trails, AI-driven governance decisions.
4. Customer Support AI
AI-powered customer identity troubleshooting
#![allow(unused)] fn main() { // Support AI with customer context let support_context = json!({ "tenant_id": "customer-acme-corp" }); // AI agent: "Help customer john.doe@acme.com who can't log in" let user_search_result = mcp_server.execute_tool("scim_search_users", json!({ "attribute": "userName", "value": "john.doe@acme.com", "tenant_id": "customer-acme-corp" })).await; if let Some(user_data) = user_search_result.content.as_array() { if let Some(user) = user_data.first() { let is_active = user.get("active").and_then(|a| a.as_bool()).unwrap_or(false); if !is_active { // AI agent: "User account is inactive, reactivating..." let reactivate_result = mcp_server.execute_tool("scim_update_user", json!({ "user_id": user.get("id").unwrap(), "user_data": { "active": true }, "tenant_id": "customer-acme-corp" })).await; if reactivate_result.success { // AI provides customer with resolution confirmation } } } } }
Benefits: Instant customer issue resolution, multi-tenant support context, automated troubleshooting.
5. Security Response AI
AI agent for automated security incident response
#![allow(unused)] fn main() { // Security AI with emergency response capabilities let security_context = json!({ "tenant_id": "production-environment" }); // AI agent: "Detect compromised user accounts and take protective action" let suspicious_users = vec!["compromised.user1", "compromised.user2"]; for username in suspicious_users { // AI agent: "Immediately disable compromised account" let disable_result = mcp_server.execute_tool("scim_update_user", json!({ "user_id": username, "user_data": { "active": false }, "tenant_id": "production-environment", "expected_version": current_version // Raw version format for MCP })).await; if disable_result.success { // AI logs security action and updates incident response system let audit_metadata = disable_result.metadata.unwrap(); // Security AI can track exactly what actions were taken when } else { // AI escalates if automated response fails alert_security_team(&disable_result.content); } } }
Benefits: Rapid incident response, automated security actions, comprehensive audit trails.
Design Patterns
Tool Discovery Pattern
AI agents discover available operations through structured schemas:
#![allow(unused)] fn main() { pub fn get_tools(&self) -> Vec<Value> { vec![ user_schemas::create_user_tool(), user_schemas::get_user_tool(), user_schemas::update_user_tool(), // ... other tools ] } }
This enables dynamic capability discovery based on server configuration.
Parameter Validation Pattern
JSON Schema validation ensures AI agents provide correct parameters:
{
"name": "scim_create_user",
"inputSchema": {
"type": "object",
"properties": {
"user_data": {
"type": "object",
"properties": {
"schemas": {"type": "array"},
"userName": {"type": "string"}
},
"required": ["schemas", "userName"]
}
},
"required": ["user_data"]
}
}
This provides AI agents with clear parameter requirements and validation rules.
Structured Response Pattern
Consistent response format enables AI decision making:
#![allow(unused)] fn main() { pub struct ScimToolResult { pub success: bool, pub content: Value, // Main data or error information pub metadata: Option<Value>, // Operation context and version info } }
This allows AI agents to understand operation outcomes and plan subsequent actions.
Version Propagation Pattern
AI agents receive raw version information for safe concurrent operations:
#![allow(unused)] fn main() { // Response includes raw version metadata (no HTTP ETag formatting) let metadata = json!({ "operation": "create_user", "resource_id": "123", "version": "abc123def" // Raw version format for MCP }); }
This enables AI agents to perform version-aware operations without conflicts. The MCP integration automatically converts HTTP ETags to raw version strings for consistent programmatic access.
Integration with Other Components
Operation Handler Integration
MCP Integration leverages Operation Handlers for structured processing:
- Request Translation: Converts MCP tool calls to ScimOperationRequest format
- Response Formatting: Transforms operation responses to AI-friendly format
- Error Handling: Provides structured error information for AI decision making
- Version Management: Automatically includes version metadata in responses
SCIM Server Integration
Direct integration with SCIM Server core functionality:
- Dynamic Tool Generation: Available tools reflect registered resource types
- Schema Discovery: AI agents can introspect available schemas and capabilities
- Multi-Tenant Support: Automatic tenant context extraction and validation
- Permission Enforcement: Tenant permissions automatically applied to AI operations
Multi-Tenant Integration
Seamless tenant awareness for enterprise AI deployment:
- Tenant Context Extraction: Automatically extracts tenant information from tool parameters
- Scoped Operations: All AI operations automatically tenant-scoped
- Tenant Validation: Ensures AI agents can only access authorized tenants
- Cross-Tenant Prevention: Prevents AI agents from accidentally crossing tenant boundaries
Best Practices
1. Design AI-Friendly Tool Schemas
#![allow(unused)] fn main() { // Good: Clear, descriptive schemas with validation pub fn create_user_tool() -> Value { json!({ "name": "scim_create_user", "description": "Create a new user in the SCIM server", "inputSchema": { "type": "object", "properties": { "user_data": { "description": "User data conforming to SCIM User schema", "properties": { "userName": { "type": "string", "description": "Unique identifier for the user" } } } } } }) } // Avoid: Vague schemas without clear validation // AI agents need precise parameter requirements }
2. Provide Rich Error Context for AI Agents
#![allow(unused)] fn main() { // Good: Structured error responses AI agents can understand ScimToolResult { success: false, content: json!({ "error": "User already exists", "error_code": "duplicate_username", "suggested_action": "try_different_username" }), metadata: Some(json!({ "operation": "create_user", "conflict_field": "userName" })) } // Avoid: Generic error messages ScimToolResult { success: false, content: json!({"error": "Failed"}), // Not actionable for AI metadata: None } }
3. Include Version Information for AI Concurrency
#![allow(unused)] fn main() { // Good: Always include raw version metadata let mut metadata = json!({ "operation": "update_user", "resource_id": user_id }); // Convert ETag to raw version for MCP clients if let Some(etag) = response.metadata.additional.get("etag") { if let Some(raw_version) = etag_to_raw_version(etag) { metadata["version"] = json!(raw_version); } } // This enables AI agents to perform safe concurrent operations }
4. Use Tenant Context Consistently
#![allow(unused)] fn main() { // Good: Extract and validate tenant context from AI requests let tenant_context = arguments .get("tenant_id") .and_then(|t| t.as_str()) .map(|id| TenantContext::new(id.to_string(), "ai-agent".to_string())); let request = ScimOperationRequest::create("User", user_data) .with_tenant_context(tenant_context); // Avoid: Ignoring tenant context in AI operations // This can lead to cross-tenant data access }
5. Design for AI Conversation Flows
#![allow(unused)] fn main() { // Good: Operations return metadata for follow-up actions ScimToolResult { success: true, content: user_json, metadata: Some(json!({ "operation": "create_user", "resource_id": "user-123", "next_actions": ["add_to_group", "set_permissions", "send_welcome"] })) } // This helps AI agents plan multi-step workflows }
When to Use MCP Integration
Primary Scenarios
- AI-Powered HR Systems: Conversational employee lifecycle management
- DevOps Automation: AI-driven environment and user provisioning
- Compliance Monitoring: Automated identity governance and audit
- Customer Support: AI-powered identity troubleshooting and resolution
- Security Response: Automated incident response and threat mitigation
Implementation Strategies
Scenario | AI Agent Type | Complexity | Benefits |
---|---|---|---|
HR Assistant | Conversational AI (Claude, GPT) | Low | Natural language HR operations |
DevOps Automation | Workflow AI (custom agents) | Medium | Automated provisioning at scale |
Compliance Monitor | Analytics AI (specialized) | Medium | Continuous governance monitoring |
Security Response | Response AI (real-time) | High | Instant threat mitigation |
Customer Support | Support AI (chat-based) | Low | 24/7 identity issue resolution |
Comparison with Traditional Integration Approaches
Approach | AI Accessibility | Discovery | Validation | Automation |
---|---|---|---|---|
MCP Integration | β Native | β Automatic | β JSON Schema | β Conversational |
REST API | β οΈ Manual | β Static | β οΈ Manual | β οΈ Scripted |
GraphQL | β οΈ Schema-based | β οΈ Introspection | β οΈ Type System | β οΈ Query-based |
Custom Protocol | β Requires Training | β Manual | β Custom | β Programmatic |
MCP Integration provides the optimal path for AI agents to perform identity management operations with native understanding, automatic discovery, and conversational interaction patterns.
Version Handling in MCP vs HTTP
The MCP integration uses raw version strings instead of HTTP ETags for better programmatic access by AI agents:
HTTP Integration
- Uses HTTP ETag format:
W/"abc123def"
- Suitable for web browsers and HTTP clients
- Standard HTTP conditional request headers
MCP Integration
- Uses raw version format:
"abc123def"
- Better for JSON-RPC and programmatic access
- AI agents work directly with version strings
- Automatic conversion from ETags to raw format
The MCP handlers automatically convert between formats using the etag_to_raw_version()
utility function, ensuring AI agents always receive consistent raw version strings regardless of the internal representation.
AI Agent Capabilities Enabled
Schema Understanding
- AI agents can introspect available SCIM schemas
- Automatic validation ensures compliance with SCIM 2.0
- Dynamic tool discovery based on server configuration
Conversational Operations
- Natural language requests translated to structured SCIM operations
- AI agents understand operation outcomes and can plan follow-up actions
- Multi-step workflows handled through conversation context
Error Recovery
- Structured error responses enable AI decision making
- Suggested actions help AI agents resolve issues automatically
- Retry logic with raw version awareness prevents infinite loops
Multi-Tenant Awareness
- AI agents understand tenant boundaries and permissions
- Automatic tenant context validation prevents cross-tenant access
- Enterprise deployment ready with proper isolation
MCP Integration transforms SCIM Server into an AI-native identity management platform, enabling artificial intelligence to perform sophisticated identity operations through natural, discoverable, and safe interaction patterns. This creates new possibilities for automated identity governance, conversational HR systems, and intelligent security response capabilities.
Feature Flag Usage
To enable MCP Integration, add the feature flag to your Cargo.toml
:
[dependencies]
scim-server = { version = "0.5.3", features = ["mcp"] }
The MCP integration includes:
- Tool discovery and execution
- Raw version handling optimized for AI agents
- Multi-tenant support for enterprise AI deployment
- Structured error responses for AI decision making
SCIM Server
The ScimServer
is the central orchestration layer of the SCIM Server library, providing a complete, dynamic SCIM 2.0 protocol implementation that can handle any resource type registered at runtime. It serves as the primary interface between your application and the SCIM protocol, eliminating hard-coded resource types and enabling truly schema-driven identity management.
See the ScimServer API documentation for complete details.
Value Proposition
The SCIM Server module delivers comprehensive identity management capabilities:
- Dynamic Resource Management: Register any resource type at runtime without code changes
- Complete SCIM 2.0 Compliance: Full implementation of SCIM protocol semantics and behaviors
- Multi-Tenant Architecture: Built-in tenant isolation with flexible URL generation strategies
- Schema-Driven Operations: Automatic validation and processing based on SCIM schemas
- Pluggable Storage: Storage-agnostic design works with any backend implementation
- Production Ready: Comprehensive error handling, logging, concurrency control, and observability
- Zero Configuration: Works out-of-the-box with sensible defaults while remaining highly configurable
Architecture Overview
The SCIM Server operates as the orchestration hub in the library's layered architecture:
SCIM Server (Orchestration Layer)
βββ Resource Registration & Validation
βββ Schema Management & Discovery
βββ Operation Routing & Authorization
βββ Multi-Tenant URL Generation
βββ Concurrency & Version Control
βββ Provider Abstraction
β
Resource Provider (Business Logic)
β
Storage Provider (Data Persistence)
Core Components
ScimServer
Struct: The main server instance with pluggable providersScimServerBuilder
: Fluent configuration API for server setup- Resource Registration: Runtime registration of resource types and operations
- Schema Management: Automatic schema validation and discovery
- Operation Router: Dynamic dispatch to appropriate handlers
- URL Generation: Multi-tenant aware endpoint URL creation
Use Cases
1. Single-Tenant Identity Server
Simple identity management for single organizations
#![allow(unused)] fn main() { use scim_server::{ScimServer, ScimServerBuilder}; use scim_server::providers::StandardResourceProvider; use scim_server::storage::InMemoryStorage; use scim_server::resource::{RequestContext, ScimOperation}; // Setup server with provider // See: https://docs.rs/scim-server/latest/scim_server/storage/struct.InMemoryStorage.html let storage = InMemoryStorage::new(); // See: https://docs.rs/scim-server/latest/scim_server/providers/struct.StandardResourceProvider.html let provider = StandardResourceProvider::new(storage); // See: https://docs.rs/scim-server/latest/scim_server/struct.ScimServer.html let mut server = ScimServer::new(provider)?; // Register User resource type // See: https://docs.rs/scim-server/latest/scim_server/struct.ScimServer.html#method.register_resource_type server.register_resource_type( "User", user_handler, vec![ScimOperation::Create, ScimOperation::Read, ScimOperation::Update, ScimOperation::Delete] )?; // Create user through SCIM server // See: https://docs.rs/scim-server/latest/scim_server/struct.RequestContext.html let context = RequestContext::new("request-123".to_string()); let user_data = json!({ "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "userName": "alice@company.com", "displayName": "Alice Smith", "emails": [{"value": "alice@company.com", "primary": true}] }); // See: https://docs.rs/scim-server/latest/scim_server/struct.ScimServer.html#method.create_resource let created_user = server.create_resource("User", user_data, &context).await?; }
Benefits: Automatic schema validation, metadata management, standardized error handling.
2. Multi-Tenant SaaS Platform
Identity management for multiple customer organizations
#![allow(unused)] fn main() { use scim_server::{ScimServerBuilder, TenantStrategy}; use scim_server::resource::{RequestContext, TenantContext, TenantPermissions}; // See: https://docs.rs/scim-server/latest/scim_server/struct.ScimServerBuilder.html // Configure multi-tenant server let mut server = ScimServerBuilder::new(provider) .with_base_url("https://api.company.com") .with_tenant_strategy(TenantStrategy::PathBased) .build()?; // Register resource types server.register_resource_type("User", user_handler, user_operations)?; server.register_resource_type("Group", group_handler, group_operations)?; // Tenant-specific operations let tenant_permissions = TenantPermissions { max_users: Some(1000), max_groups: Some(50), allowed_operations: vec!["create".into(), "read".into(), "update".into()], }; let tenant_context = TenantContext { tenant_id: "customer-123".to_string(), client_id: "scim-client-1".to_string(), permissions: tenant_permissions, }; let context = RequestContext::with_tenant_generated_id(tenant_context); // Operations automatically scoped to tenant let user = server.create_resource("User", user_data, &context).await?; }
Benefits: Automatic tenant isolation, resource limits, tenant-specific URL generation.
3. Custom Resource Types
Managing application-specific identity resources
#![allow(unused)] fn main() { // Register custom resource type at runtime // See: https://docs.rs/scim-server/latest/scim_server/schema/struct.Schema.html let application_schema = Schema { id: "urn:example:schemas:Application".to_string(), name: "Application".to_string(), description: "Custom application resource".to_string(), attributes: vec![ // Define custom attributes using AttributeDefinition // See: https://docs.rs/scim-server/latest/scim_server/schema/struct.AttributeDefinition.html create_attribute("displayName", AttributeType::String, false, true, false), create_attribute("version", AttributeType::String, false, false, false), create_attribute("permissions", AttributeType::Complex, true, false, false), ], }; // Create resource handler for the custom schema // See: https://docs.rs/scim-server/latest/scim_server/resource_handlers/index.html let app_handler = ResourceHandler { resource_type: "Application".to_string(), schema: application_schema, endpoint: "/Applications".to_string(), }; // Register with the server // See: https://docs.rs/scim-server/latest/scim_server/struct.ScimServer.html#method.register_resource_type server.register_resource_type( "Application", app_handler, vec![ScimOperation::Create, ScimOperation::Read, ScimOperation::List] )?; // Use custom resource type let app_data = json!({ "schemas": ["urn:example:schemas:Application"], "displayName": "My Application", "version": "1.2.3", "permissions": [ {"name": "read", "scope": "user"}, {"name": "write", "scope": "admin"} ] }); // See: https://docs.rs/scim-server/latest/scim_server/struct.ScimServer.html#method.create_resource let application = server.create_resource("Application", app_data, &context).await?; }
Benefits: No code changes for new resource types, automatic schema validation, consistent API.
4. Schema Discovery and Introspection
Dynamic discovery of server capabilities
#![allow(unused)] fn main() { // Automatic capability discovery // See: https://docs.rs/scim-server/latest/scim_server/struct.ScimServer.html#method.get_server_info let server_info = server.get_server_info(); println!("Supported resource types: {:?}", server_info.supported_resource_types); println!("SCIM version: {}", server_info.scim_version); // SCIM ServiceProviderConfig generation // See: https://docs.rs/scim-server/latest/scim_server/schema_discovery/index.html let service_config = server.get_service_provider_config()?; println!("Authentication schemes: {:?}", service_config.authentication_schemes); println!("Bulk operations: {:?}", service_config.bulk); // Schema introspection // See: https://docs.rs/scim-server/latest/scim_server/struct.ScimServer.html#method.get_all_schemas let all_schemas = server.get_all_schemas(); for schema in all_schemas { println!("Schema: {} - {}", schema.id, schema.description); } // Resource type specific schema // See: https://docs.rs/scim-server/latest/scim_server/struct.ScimServer.html#method.get_schema_by_id let user_schema = server.get_schema_by_id("urn:ietf:params:scim:schemas:core:2.0:User")?; println!("User schema attributes: {}", user_schema.attributes.len()); }
Benefits: Automatic capability advertisement, standards-compliant discovery, runtime introspection.
5. Advanced URL Generation
Flexible endpoint URL generation for different deployment patterns
#![allow(unused)] fn main() { // Subdomain-based tenant isolation // See: https://docs.rs/scim-server/latest/scim_server/struct.ScimServerBuilder.html#method.with_tenant_strategy let server = ScimServerBuilder::new(provider) .with_base_url("https://scim.company.com") .with_tenant_strategy(TenantStrategy::Subdomain) .build()?; // Generates: https://tenant123.scim.company.com/v2/Users/user456 let ref_url = server.generate_ref_url(Some("tenant123"), "Users", "user456")?; // Path-based tenant isolation let server = ScimServerBuilder::new(provider) .with_base_url("https://api.company.com") .with_tenant_strategy(TenantStrategy::PathBased) .build()?; // Generates: https://api.company.com/tenant123/v2/Users/user456 let ref_url = server.generate_ref_url(Some("tenant123"), "Users", "user456")?; // Single tenant mode let server = ScimServerBuilder::new(provider) .with_base_url("https://identity.company.com") .build()?; // Generates: https://identity.company.com/v2/Users/user456 let ref_url = server.generate_ref_url(None, "Users", "user456")?; }
Benefits: Flexible deployment patterns, proper SCIM $ref field generation, tenant-aware URLs.
Design Patterns
Builder Pattern for Configuration
The SCIM Server uses the builder pattern for flexible configuration with ScimServerBuilder
:
#![allow(unused)] fn main() { // See: https://docs.rs/scim-server/latest/scim_server/struct.ScimServerBuilder.html pub struct ScimServerBuilder<P> { provider: P, config: ScimServerConfig, } impl<P: ResourceProvider> ScimServerBuilder<P> { pub fn new(provider: P) -> Self; pub fn with_base_url(self, base_url: impl Into<String>) -> Self; pub fn with_tenant_strategy(self, strategy: TenantStrategy) -> Self; pub fn with_scim_version(self, version: impl Into<String>) -> Self; pub fn build(self) -> Result<ScimServer<P>, ScimError>; } }
This allows for fluent, type-safe configuration while maintaining defaults.
Dynamic Resource Registration
Resources are registered at runtime without compile-time dependencies:
#![allow(unused)] fn main() { pub fn register_resource_type( &mut self, resource_type: &str, handler: ResourceHandler, operations: Vec<ScimOperation>, ) -> Result<(), ScimError> }
This enables:
- Plugin architectures
- Configuration-driven resource types
- Runtime schema evolution
- Multi-version support
Provider Abstraction
Clean abstraction over ResourceProvider
:
#![allow(unused)] fn main() { // See: https://docs.rs/scim-server/latest/scim_server/struct.ScimServer.html pub struct ScimServer<P> { provider: P, // ... } // See: https://docs.rs/scim-server/latest/scim_server/trait.ResourceProvider.html impl<P: ResourceProvider> ScimServer<P> { // Operations delegated to provider } }
This enables:
- Pluggable storage backends
- Custom business logic
- Testing with mock providers
- Incremental migration strategies
Integration with Other Components
Resource Integration
The SCIM Server works seamlessly with the Resource system:
- Type Safety: Core attributes use validated value objects
- Flexibility: Extended attributes remain as JSON
- Serialization: Automatic $ref field injection for SCIM compliance
- Metadata: Automatic timestamp and version management
Resource Provider Integration
The server orchestrates provider operations:
- Operation Dispatch: Routes operations to appropriate provider methods
- Context Passing: Ensures request context flows through all operations
- Error Translation: Converts provider errors to SCIM-compliant responses
- Concurrency: Manages version-aware operations for conflict prevention
Storage Provider Integration
Through the Resource Provider layer:
- Storage Agnostic: Works with any storage implementation
- Transaction Support: Leverages provider transaction capabilities
- Bulk Operations: Coordinates multi-resource operations
- Query Translation: Converts SCIM queries to storage-specific formats
Schema Integration
Deep integration with the schema system:
- Automatic Validation: All operations validated against registered schemas
- Schema Discovery: Runtime introspection of available schemas
- Extension Support: Handles custom schema extensions transparently
- Compliance Checking: Ensures SCIM 2.0 specification adherence
Error Handling
The SCIM Server provides comprehensive error handling:
Structured Error Types
All server operations return structured errors with proper SCIM compliance:
#![allow(unused)] fn main() { pub enum ScimError { UnsupportedResourceType(String), UnsupportedOperation { resource_type: String, operation: ScimOperation }, SchemaValidation { schema_id: String, message: String }, InvalidRequest { message: String }, ResourceNotFound { resource_type: String, id: String }, ConflictError { message: String }, // ... other variants } }
Operation-Specific Error Handling
Each operation handles errors appropriately:
- Create: Schema validation, uniqueness conflicts, tenant limits
- Read: Resource not found, authorization failures
- Update: Version conflicts, schema validation, immutable field protection
- Delete: Resource not found, referential integrity
- List/Search: Query validation, pagination errors
Provider Error Translation
Provider errors are automatically translated to SCIM-compliant responses:
#![allow(unused)] fn main() { let result = self .provider .create_resource(resource_type, data, context) .await .map_err(|e| ScimError::ProviderError(e.to_string())); }
Best Practices
1. Use Builder Pattern for Configuration
Always use the ScimServerBuilder
for server setup:
#![allow(unused)] fn main() { // Good: Explicit configuration // See: https://docs.rs/scim-server/latest/scim_server/struct.ScimServerBuilder.html let server = ScimServerBuilder::new(provider) .with_base_url("https://api.company.com") .with_tenant_strategy(TenantStrategy::PathBased) .build()?; // Avoid: Default constructor for production let server = ScimServer::new(provider)?; // Uses localhost defaults }
2. Register All Required Operations
Be explicit about supported operations using register_resource_type
:
#![allow(unused)] fn main() { // Good: Explicit operation support // See: https://docs.rs/scim-server/latest/scim_server/struct.ScimServer.html#method.register_resource_type server.register_resource_type( "User", user_handler, vec![ScimOperation::Create, ScimOperation::Read, ScimOperation::Update] )?; // Avoid: Registering unsupported operations server.register_resource_type("User", user_handler, vec![ScimOperation::Patch])?; // Not implemented }
3. Handle Multi-Tenancy Consistently
Choose a TenantStrategy
and use it consistently:
#![allow(unused)] fn main() { // Good: Consistent tenant strategy // See: https://docs.rs/scim-server/latest/scim_server/struct.ScimServerBuilder.html#method.with_tenant_strategy let server = ScimServerBuilder::new(provider) .with_tenant_strategy(TenantStrategy::PathBased) .build()?; // All operations automatically handle tenant isolation // See: https://docs.rs/scim-server/latest/scim_server/struct.ScimServer.html#method.create_resource let user = server.create_resource("User", data, &tenant_context).await?; // Avoid: Manual tenant handling // Let the server handle tenant isolation automatically }
4. Leverage Schema Validation
Trust the automatic schema validation:
#![allow(unused)] fn main() { // Good: Let server validate automatically // See: https://docs.rs/scim-server/latest/scim_server/struct.ScimServer.html#method.create_resource let result = server.create_resource("User", user_data, &context).await; match result { Ok(user) => process_user(user), // See: https://docs.rs/scim-server/latest/scim_server/enum.ScimError.html#variant.SchemaValidation Err(ScimError::SchemaValidation { message, .. }) => handle_validation_error(message), Err(e) => handle_other_error(e), } // Avoid: Manual validation before server operations // The server already provides comprehensive validation }
5. Use Proper Error Handling
Handle different ScimError
types appropriately:
#![allow(unused)] fn main() { // Good: Structured error handling // See: https://docs.rs/scim-server/latest/scim_server/struct.ScimServer.html#method.get_resource match server.get_resource("User", id, &context).await { Ok(Some(user)) => Ok(user), Ok(None) => Err(HttpError::NotFound), // See: https://docs.rs/scim-server/latest/scim_server/enum.ScimError.html Err(ScimError::UnsupportedResourceType(_)) => Err(HttpError::BadRequest), Err(ScimError::ProviderError(_)) => Err(HttpError::InternalServerError), Err(e) => Err(HttpError::from(e)), } // Avoid: Generic error handling // Loses important context for proper HTTP responses }
When to Use SCIM Server Directly
Primary Use Cases
- HTTP Server Implementation: Building REST APIs that expose SCIM endpoints
- Application Integration: Embedding SCIM capabilities into existing applications
- Identity Bridges: Creating adapters between different identity systems
- Testing Frameworks: Building test harnesses for SCIM compliance
- Custom Protocols: Implementing SCIM over non-HTTP transports
- MCP Integration: Exposing SCIM operations to AI agents
Implementation Strategies
Scenario | Approach | Complexity |
---|---|---|
Simple REST API | Use with HTTP framework | Low |
Multi-tenant SaaS | Builder with tenant strategy | Medium |
Custom Resources | Runtime registration | Medium |
Protocol Bridge | Custom resource provider | High |
Embedded Identity | Direct server integration | Medium |
Comparison with Alternative Approaches
Approach | Flexibility | Compliance | Performance | Complexity |
---|---|---|---|---|
SCIM Server | β Very High | β Complete | β High | Medium |
Hard-coded Resources | β Low | β οΈ Partial | β Very High | Low |
Generic REST Framework | β High | β Manual | β High | High |
Identity Provider SDK | β οΈ Medium | β High | β οΈ Medium | Low |
The SCIM Server provides the optimal balance of flexibility, compliance, and performance for identity management scenarios, offering complete SCIM 2.0 implementation while remaining adaptable to diverse deployment requirements.
Relationship to HTTP Layer
While the ScimServer
handles protocol semantics, it's designed to work with any HTTP framework:
- Framework Agnostic: No dependencies on specific HTTP libraries
- Clean Separation: HTTP concerns handled separately from SCIM logic
- Easy Integration: Simple async interface maps directly to HTTP handlers
- Standard Responses: Returns structured data suitable for JSON serialization
- Operation Handlers: Framework-agnostic bridge layer available
For integration patterns and examples, see the Operation Handlers guide and the examples directory in the repository.
This design enables the SCIM Server to serve as the core for various deployment scenarios, from embedded applications to high-performance web services, while maintaining full SCIM 2.0 compliance and providing the flexibility needed for real-world identity management systems.
Resources
Resources are the foundational data structures in SCIM that represent identity objects like users, groups, and custom entities. The SCIM Server library implements a hybrid resource design that combines type safety for core attributes with JSON flexibility for extensions.
See the Resource API documentation for complete details.
Value Proposition
The Resource system in SCIM Server provides several key benefits:
- Type Safety: Core SCIM attributes use validated value objects that make invalid states unrepresentable
- Extensibility: Extended attributes remain as flexible JSON, preserving SCIM's extension capabilities
- Performance: Compile-time guarantees reduce runtime validation overhead
- Developer Experience: Rich type information and validation errors guide correct usage
- Interoperability: Full compliance with SCIM 2.0 specification while adding safety
Architecture Overview
Resources follow a hybrid design pattern:
Resource
βββ Type-Safe Core Attributes (Value Objects)
β βββ ResourceId (validated)
β βββ UserName (validated)
β βββ EmailAddress (validated)
β βββ SchemaUri (validated)
βββ Extended Attributes (JSON Map)
βββ Custom fields
βββ Enterprise extensions
βββ Third-party extensions
Core Validated Attributes
The following attributes use type-safe value objects with compile-time validation:
- Resource Identity:
ResourceId
,ExternalId
,SchemaUri
- User Attributes:
UserName
,Name
,EmailAddress
,PhoneNumber
,Address
- Group Attributes:
GroupMembers
(see Referential Integrity for relationship management) - Metadata:
Meta
(timestamps, versions, resource type)
Extended Attributes
All other attributes are stored as JSON in the attributes
map, providing:
- Full SCIM schema flexibility
- Support for enterprise extensions
- Custom attribute definitions
- Complex nested structures
Use Cases
1. Standard SCIM Operations
Creating a User with Core Attributes
#![allow(unused)] fn main() { use scim_server::resource::Resource; use serde_json::json; // Resource automatically validates core attributes let user_data = json!({ "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "userName": "john.doe", "name": { "givenName": "John", "familyName": "Doe" }, "emails": [{ "value": "john.doe@example.com", "primary": true }] }); let resource = Resource::from_json("User".to_string(), user_data)?; }
Benefits: UserName and email validation happens at creation time, preventing invalid data from entering the system.
2. Enterprise Extensions
Adding Custom Attributes
#![allow(unused)] fn main() { let enterprise_user = json!({ "schemas": [ "urn:ietf:params:scim:schemas:core:2.0:User", "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" ], "userName": "jane.smith", "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": { "employeeNumber": "12345", "department": "Engineering", "manager": { "value": "boss@example.com", "displayName": "Boss Person" } } }); let resource = Resource::from_json("User".to_string(), enterprise_user)?; }
Benefits: Core attributes remain type-safe while enterprise extensions use JSON flexibility.
3. Custom Resource Types
Defining Application-Specific Resources
#![allow(unused)] fn main() { let application_resource = json!({ "schemas": ["urn:example:schemas:Application"], "id": "app-123", "displayName": "My Application", "version": "1.2.3", "permissions": ["read", "write", "admin"] }); let resource = Resource::from_json("Application".to_string(), application_resource)?; }
Benefits: Resources aren't limited to Users and Groups - any JSON structure can be managed.
4. Multi-Valued Attributes
Handling Complex Attribute Collections
#![allow(unused)] fn main() { let user_with_multiple_emails = json!({ "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "userName": "multi.user", "emails": [ { "value": "work@example.com", "type": "work", "primary": true }, { "value": "personal@example.com", "type": "home", "primary": false } ] }); let resource = Resource::from_json("User".to_string(), user_with_multiple_emails)?; // Type-safe access to primary email if let Some(primary_email) = resource.get_primary_email() { println!("Primary email: {}", primary_email); } }
Benefits: Multi-valued attributes get proper validation while maintaining SCIM semantics.
Design Patterns
Value Object Pattern
Core attributes use the value object pattern for domain modeling:
#![allow(unused)] fn main() { // UserName is a value object with validation pub struct UserName(String); impl UserName { pub fn new(value: String) -> Result<Self, ValidationError> { if value.trim().is_empty() { return Err(ValidationError::EmptyField("userName".to_string())); } if value.len() > 256 { return Err(ValidationError::TooLong { field: "userName".to_string(), max: 256, actual: value.len(), }); } Ok(UserName(value)) } } }
This ensures invalid usernames cannot be constructed at compile time.
Hybrid Serialization
Resources serialize to standard SCIM JSON while maintaining internal type safety:
#![allow(unused)] fn main() { let resource = Resource::from_json("User".to_string(), user_data)?; let scim_json = resource.to_json()?; // Standard SCIM format }
When to Use Resources Directly
Application Scenarios
- Custom SCIM Servers: Building domain-specific identity management
- Data Transformation: Converting between identity formats
- Validation Services: Ensuring SCIM data integrity
- Testing Frameworks: Generating valid test data
Integration Points
Resources integrate with other SCIM Server components:
- Storage Providers: Resources serialize to JSON for storage
- Resource Providers: Business logic operates on Resources
- HTTP Handlers: Resources convert to/from HTTP payloads
- Schema Validation: Resources respect SCIM schema definitions
Best Practices
1. Use Value Objects for Critical Data
Wrap important business identifiers in value objects:
#![allow(unused)] fn main() { // Good: Type-safe, validated let user_id = ResourceId::new(id_string)?; // Avoid: Stringly-typed, no validation let user_id = id_string; }
2. Preserve JSON for Flexibility
Keep non-critical attributes as JSON for extensibility:
#![allow(unused)] fn main() { // Good: Allows schema evolution resource.set_attribute("customField", json!("value")); // Avoid: Over-constraining extensions struct CustomField(String); // Too rigid for evolving schemas }
3. Handle Validation Errors Gracefully
Resource creation can fail with detailed error information:
#![allow(unused)] fn main() { match Resource::from_json("User".to_string(), data) { Ok(resource) => process_resource(resource), Err(ValidationError::EmptyField(field)) => { return_validation_error(&field); }, Err(e) => log_and_handle_error(e), } }
4. Leverage Type Safety in Business Logic
Use typed accessors for core attributes:
#![allow(unused)] fn main() { // Type-safe access with proper error handling if let Some(username) = resource.get_username() { validate_username_policy(username); } // Avoid raw JSON access for core attributes let username = resource.get_attribute("userName"); // Loses type safety }
Comparison with Alternative Approaches
Approach | Type Safety | Flexibility | Performance | Complexity |
---|---|---|---|---|
Hybrid Design | β High (core) | β High (extensions) | β High | Medium |
Full Value Objects | β Very High | β Low | β Very High | High |
Pure JSON | β None | β Very High | β οΈ Medium | Low |
Schema-Only | β οΈ Runtime | β High | β οΈ Medium | Medium |
The hybrid approach provides the best balance for SCIM use cases, offering safety where it matters most while preserving the protocol's inherent flexibility.
Resource Providers
Resource Providers form the business logic layer of the SCIM Server architecture, implementing SCIM protocol semantics while remaining agnostic to storage implementation details. They bridge the gap between HTTP requests and data persistence, handling validation, metadata management, concurrency control, and multi-tenancy.
See the ResourceProvider API documentation for complete details.
Value Proposition
Resource Providers deliver several critical capabilities:
- SCIM Protocol Compliance: Full implementation of SCIM 2.0 semantics and behaviors
- Business Logic Separation: Clean separation between protocol logic and storage concerns
- Multi-Tenancy Support: Built-in tenant isolation and resource limits
- Concurrency Control: Optimistic locking with version-aware operations
- Pluggable Architecture: Storage-agnostic design enables diverse backends
- Production Ready: Comprehensive error handling, logging, and observability
Architecture Overview
Resource Providers operate as the orchestration layer in the SCIM Server stack:
HTTP Layer
β
Resource Provider (Business Logic)
βββ SCIM Protocol Logic
βββ Validation & Metadata
βββ Concurrency Control
βββ Multi-Tenancy
βββ Error Handling
β
Storage Provider (Data Persistence)
Key Components
ResourceProvider
Trait: Unified interface for all SCIM operationsStandardResourceProvider
: Production-ready implementation- Helper Traits: Composable functionality for custom providers
- Context Management: Request scoping and tenant isolation
Core Interface
The ResourceProvider
trait defines the contract for SCIM operations:
#![allow(unused)] fn main() { pub trait ResourceProvider { type Error: std::error::Error + Send + Sync + 'static; // Core CRUD operations async fn create_resource(&self, resource_type: &str, data: Value, context: &RequestContext) -> Result<VersionedResource, Self::Error>; async fn get_resource(&self, resource_type: &str, id: &str, context: &RequestContext) -> Result<Option<VersionedResource>, Self::Error>; async fn update_resource(&self, resource_type: &str, id: &str, data: Value, expected_version: Option<&RawVersion>, context: &RequestContext) -> Result<VersionedResource, Self::Error>; async fn delete_resource(&self, resource_type: &str, id: &str, expected_version: Option<&RawVersion>, context: &RequestContext) -> Result<(), Self::Error>; // Query operations async fn list_resources(&self, resource_type: &str, query: Option<&ListQuery>, context: &RequestContext) -> Result<Vec<VersionedResource>, Self::Error>; async fn find_resources_by_attribute(&self, resource_type: &str, attribute_name: &str, attribute_value: &str, context: &RequestContext) -> Result<Vec<VersionedResource>, Self::Error>; // Advanced operations async fn patch_resource(&self, resource_type: &str, id: &str, patch_request: &Value, expected_version: Option<&RawVersion>, context: &RequestContext) -> Result<VersionedResource, Self::Error>; async fn resource_exists(&self, resource_type: &str, id: &str, context: &RequestContext) -> Result<bool, Self::Error>; } }
Use Cases
1. Single-Tenant SCIM Server
Simple identity management for single organizations
#![allow(unused)] fn main() { use scim_server::providers::StandardResourceProvider; use scim_server::storage::InMemoryStorage; use scim_server::resource::RequestContext; // Setup let storage = InMemoryStorage::new(); let provider = StandardResourceProvider::new(storage); // Single-tenant context (no tenant isolation) let context = RequestContext::with_generated_id(); // Create user let user_data = json!({ "userName": "alice@company.com", "displayName": "Alice Smith", "emails": [{"value": "alice@company.com", "primary": true}] }); let user = provider.create_resource("User", user_data, &context).await?; println!("Created user: {}", user.resource().get_id().unwrap()); }
Benefits: Simplified setup, automatic metadata management, built-in validation.
2. Multi-Tenant SaaS Platform
Identity management for multiple customer organizations
#![allow(unused)] fn main() { use scim_server::resource::{RequestContext, TenantContext, TenantPermissions}; // Configure tenant with resource limits let permissions = TenantPermissions { max_users: Some(1000), max_groups: Some(50), allowed_operations: vec!["create".into(), "read".into(), "update".into()], }; let tenant_context = TenantContext { tenant_id: "customer-123".to_string(), client_id: "scim-client-1".to_string(), permissions, }; let context = RequestContext::with_tenant_generated_id(tenant_context); // Operations are automatically scoped to this tenant let user = provider.create_resource("User", user_data, &context).await?; // This user only exists within "customer-123" tenant let retrieved = provider.get_resource("User", &user.resource().get_id().unwrap(), &context).await?; }
Benefits: Automatic tenant isolation, resource limits, per-tenant permissions.
3. Version-Aware Operations
Preventing lost updates in concurrent environments
#![allow(unused)] fn main() { // Get current resource with version let user = provider.get_resource("User", "123", &context).await?.unwrap(); let current_version = user.version(); // Modify user data let mut updated_data = user.resource().to_json()?; updated_data["displayName"] = json!("Updated Name"); // Conditional update - only succeeds if version matches match provider.update_resource("User", "123", updated_data, Some(current_version), &context).await { Ok(updated_user) => println!("Update successful"), Err(ProviderError::PreconditionFailed { .. }) => { println!("Resource was modified by another process"); // Handle conflict resolution } } }
Benefits: Prevents lost updates, enables conflict detection, maintains data consistency.
4. Custom Business Logic
Implementing domain-specific validation and processing
#![allow(unused)] fn main() { use scim_server::providers::ResourceProvider; pub struct CustomResourceProvider<S: StorageProvider> { standard: StandardResourceProvider<S>, audit_logger: AuditLogger, } impl<S: StorageProvider> ResourceProvider for CustomResourceProvider<S> { type Error = ProviderError; async fn create_resource(&self, resource_type: &str, mut data: Value, context: &RequestContext) -> Result<VersionedResource, Self::Error> { // Custom validation if resource_type == "User" { self.validate_company_email(&data)?; self.assign_department(&mut data, context)?; } // Delegate to standard implementation let resource = self.standard.create_resource(resource_type, data, context).await?; // Custom post-processing self.audit_logger.log_creation(&resource, context).await; self.send_welcome_email(&resource).await; Ok(resource) } // ... other methods delegate to standard provider ... } }
Benefits: Extend standard behavior, add custom validation, integrate with external systems.
Implementation Patterns
1. Delegating Provider Pattern
Build on top of StandardResourceProvider
for custom logic:
#![allow(unused)] fn main() { pub struct EnterpriseProvider<S> { standard: StandardResourceProvider<S>, ldap_sync: LdapSync, compliance_checker: ComplianceChecker, } impl<S: StorageProvider> EnterpriseProvider<S> { // Override specific operations while delegating others async fn create_user_with_compliance(&self, data: Value, context: &RequestContext) -> Result<VersionedResource, ProviderError> { // Pre-creation compliance check self.compliance_checker.validate_user_data(&data)?; // Standard creation let user = self.standard.create_resource("User", data, context).await?; // Post-creation sync self.ldap_sync.sync_user(&user).await?; Ok(user) } } }
2. Middleware Provider Pattern
Chain multiple providers for cross-cutting concerns:
#![allow(unused)] fn main() { pub struct LoggingProvider<P> { inner: P, logger: Logger, } impl<P: ResourceProvider> ResourceProvider for LoggingProvider<P> { type Error = P::Error; async fn create_resource(&self, resource_type: &str, data: Value, context: &RequestContext) -> Result<VersionedResource, Self::Error> { let start = Instant::now(); self.logger.info("Creating {} resource", resource_type); let result = self.inner.create_resource(resource_type, data, context).await; self.logger.info("Create operation completed in {:?}", start.elapsed()); result } } }
3. Storage-Agnostic Provider
Work with any storage backend:
#![allow(unused)] fn main() { // Works with in-memory storage for testing let memory_provider = StandardResourceProvider::new(InMemoryStorage::new()); // Works with SQLite for persistence let sqlite_provider = StandardResourceProvider::new(SqliteStorage::new("users.db")?); // Works with custom storage implementations let custom_provider = StandardResourceProvider::new(MyCustomStorage::new()); }
Helper Traits
Resource Providers compose functionality through helper traits:
ScimMetadataManager
Handles SCIM metadata (timestamps, versions, locations):
#![allow(unused)] fn main() { use scim_server::providers::helpers::ScimMetadataManager; // Automatically implemented for providers impl<S> ScimMetadataManager for StandardResourceProvider<S> { fn add_creation_metadata(&self, resource: &mut Resource, base_url: &str) -> Result<(), String>; fn update_modification_metadata(&self, resource: &mut Resource) -> Result<(), String>; } }
MultiTenantProvider
Manages tenant isolation and resource limits:
#![allow(unused)] fn main() { use scim_server::providers::helpers::MultiTenantProvider; // Provides tenant-aware ID generation and validation impl<S> MultiTenantProvider for StandardResourceProvider<S> { fn effective_tenant_id(&self, context: &RequestContext) -> String; fn generate_tenant_resource_id(&self, tenant_id: &str, resource_type: &str) -> String; } }
ScimPatchOperations
Implements SCIM PATCH semantics:
#![allow(unused)] fn main() { use scim_server::providers::helpers::ScimPatchOperations; // Handles complex PATCH operations impl<S> ScimPatchOperations for StandardResourceProvider<S> { fn apply_patch_operation(&self, data: &mut Value, operation: &Value) -> Result<(), ProviderError>; } }
Best Practices
1. Use Standard Provider as Base
Start with StandardResourceProvider
and extend as needed:
#![allow(unused)] fn main() { // Good: Build on proven foundation let provider = StandardResourceProvider::new(storage); // Avoid: Implementing from scratch unless necessary struct FullCustomProvider; // Requires implementing all SCIM logic }
2. Delegate Storage Concerns
Keep providers focused on business logic:
#![allow(unused)] fn main() { // Good: Provider handles SCIM logic, storage handles persistence let result = self.storage.put(key, processed_data).await?; // Avoid: Provider handling storage implementation details let result = self.write_to_database_with_connection_pooling(data).await?; }
3. Handle Errors Appropriately
Use structured error types for better error handling:
#![allow(unused)] fn main() { // Good: Specific error types enable proper HTTP status codes return Err(ProviderError::DuplicateAttribute { resource_type: "User".to_string(), attribute: "userName".to_string(), // ... }); // Avoid: Generic errors lose important context return Err("duplicate username".into()); }
4. Leverage Context Information
Use RequestContext
for operation scoping:
#![allow(unused)] fn main() { // Good: Context-aware operations let tenant_id = context.tenant_id().unwrap_or("default"); context.validate_operation("create")?; // Avoid: Hardcoded assumptions let tenant_id = "default"; // Breaks multi-tenancy }
When to Implement Custom Providers
Scenarios for Custom Implementation
- Complex Business Rules: Domain-specific validation beyond SCIM
- External System Integration: Real-time sync with HR systems, directories
- Compliance Requirements: Audit logging, data residency, encryption
- Performance Optimization: Caching, batching, specialized queries
- Legacy System Integration: Adapting existing identity stores
Implementation Strategies
Requirement | Approach | Complexity |
---|---|---|
Simple Extensions | Delegate to Standard | Low |
Custom Validation | Override Specific Methods | Medium |
External Integration | Middleware Pattern | Medium |
Full Custom Logic | Implement from Trait | High |
The Resource Provider layer is where SCIM Server's flexibility shines, allowing you to implement exactly the business logic your application requires while leveraging battle-tested infrastructure for storage, HTTP handling, and protocol compliance.
Storage Providers
Storage Providers form the data persistence layer of the SCIM Server architecture, providing a clean abstraction between SCIM protocol logic and data storage implementation. They handle pure data operations on JSON resources while remaining completely agnostic to SCIM semantics.
See the StorageProvider API documentation for complete details.
Value Proposition
Storage Providers deliver focused data persistence capabilities:
- Clean Separation: Pure data operations isolated from SCIM business logic
- Storage Agnostic: Unified interface works with any storage backend
- Simple Operations: Focused on PUT/GET/DELETE with minimal complexity
- Tenant Isolation: Built-in support for multi-tenant data organization
- Performance Optimized: Direct storage operations without protocol overhead
- Pluggable Backends: Easy to swap storage implementations
Architecture Overview
Storage Providers operate at the lowest level of the SCIM Server stack:
Resource Provider (Business Logic)
β
Storage Provider (Data Persistence)
βββ PUT/GET/DELETE Operations
βββ JSON Document Storage
βββ Tenant Key Organization
βββ Basic Querying & Filtering
βββ Backend Implementation
β (examples)
βββ InMemoryStorage
βββ SqliteStorage
βββ CustomStorage
Design Philosophy
The storage layer follows a fundamental principle: at the storage level, CREATE and UPDATE are the same operation. You're simply putting data at a location. The distinction between "create" vs "update" is business logic that belongs in the Resource Provider layer.
This design provides several benefits:
- Simplicity: Fewer operations to implement and understand
- Consistency: Same operation semantics regardless of whether data exists
- Performance: No need to check existence before operations
- Flexibility: Storage backends can optimize PUT operations as needed
Core Interface
The StorageProvider
trait defines the contract for data persistence:
#![allow(unused)] fn main() { pub trait StorageProvider: Send + Sync { type Error: std::error::Error + Send + Sync + 'static; // Core data operations async fn put(&self, key: StorageKey, data: Value) -> Result<Value, Self::Error>; async fn get(&self, key: StorageKey) -> Result<Option<Value>, Self::Error>; async fn delete(&self, key: StorageKey) -> Result<bool, Self::Error>; // Query operations async fn list(&self, prefix: StoragePrefix, start_index: usize, count: usize) -> Result<Vec<(StorageKey, Value)>, Self::Error>; async fn find_by_attribute(&self, prefix: StoragePrefix, attribute_name: &str, attribute_value: &str) -> Result<Vec<(StorageKey, Value)>, Self::Error>; // Utility operations async fn exists(&self, key: StorageKey) -> Result<bool, Self::Error>; async fn count(&self, prefix: StoragePrefix) -> Result<usize, Self::Error>; async fn clear(&self) -> Result<(), Self::Error>; // Discovery operations async fn list_tenants(&self) -> Result<Vec<String>, Self::Error>; async fn list_resource_types(&self, tenant_id: &str) -> Result<Vec<String>, Self::Error>; async fn list_all_resource_types(&self) -> Result<Vec<String>, Self::Error>; // Statistics async fn stats(&self) -> Result<StorageStats, Self::Error>; } }
Key Organization
Storage uses hierarchical keys for tenant and resource type isolation:
#![allow(unused)] fn main() { pub struct StorageKey { tenant_id: String, // "tenant-123" resource_type: String, // "User" or "Group" resource_id: String, // "user-456" } // Examples: // tenant-123/User/user-456 // tenant-123/Group/group-789 // default/User/admin-user }
Available Implementations
1. InMemoryStorage
Perfect for development, testing, and proof-of-concepts
#![allow(unused)] fn main() { use scim_server::storage::InMemoryStorage; let storage = InMemoryStorage::new(); // Benefits: // - Instant startup // - No external dependencies // - Perfect for unit tests // - High performance // Limitations: // - Data lost on restart // - Memory usage grows with data // - Single-process only }
Use Cases:
- Unit and integration testing
- Development environments
- Demos and prototypes
- Temporary data scenarios
2. SqliteStorage
Production-ready persistence with zero configuration
#![allow(unused)] fn main() { use scim_server::storage::SqliteStorage; let storage = SqliteStorage::new("scim_data.db").await?; // Benefits: // - File-based persistence // - No server setup required // - ACID transactions // - Excellent performance // - Cross-platform // Limitations: // - Single-writer concurrency // - File-system dependent // - Size limitations for very large datasets }
Use Cases:
- Single-server deployments
- Small to medium scale applications
- Desktop applications
- Edge computing scenarios
3. Custom Storage Implementations
Extend to any backend you need
#![allow(unused)] fn main() { use scim_server::storage::StorageProvider; pub struct RedisStorage { client: redis::Client, } impl StorageProvider for RedisStorage { type Error = redis::RedisError; async fn put(&self, key: StorageKey, data: Value) -> Result<Value, Self::Error> { let redis_key = format!("{}/{}/{}", key.tenant_id(), key.resource_type(), key.resource_id()); let json_string = data.to_string(); self.client.set(&redis_key, &json_string).await?; Ok(data) // Return what was stored } // ... implement other methods } }
Use Cases
1. Development and Testing
Rapid iteration with in-memory storage
#![allow(unused)] fn main() { use scim_server::storage::InMemoryStorage; use scim_server::providers::StandardResourceProvider; #[tokio::test] async fn test_user_operations() { // Setup - instant, no external dependencies let storage = InMemoryStorage::new(); let provider = StandardResourceProvider::new(storage); // Test operations let context = RequestContext::with_generated_id(); let user = provider.create_resource("User", user_data, &context).await?; // Clean slate for each test provider.clear().await; assert_eq!(provider.get_stats().await.total_resources, 0); } }
Benefits: Fast test execution, isolated test environments, no cleanup required.
2. Single-Server Production
Persistent storage without infrastructure complexity
use scim_server::storage::SqliteStorage; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { // Production-ready storage with single file let storage = SqliteStorage::new("/var/lib/scim/users.db").await?; let provider = StandardResourceProvider::new(storage); // Data persists across application restarts let server = ScimServer::new(provider); server.run("0.0.0.0:8080").await?; Ok(()) }
Benefits: Data persistence, ACID guarantees, simple deployment.
3. Distributed Systems
Custom storage for scalability
#![allow(unused)] fn main() { pub struct CassandraStorage { session: Arc<Session>, keyspace: String, } impl StorageProvider for CassandraStorage { async fn put(&self, key: StorageKey, data: Value) -> Result<Value, Self::Error> { let cql = "INSERT INTO resources (tenant_id, resource_type, resource_id, data) VALUES (?, ?, ?, ?)"; self.session.query(cql, ( key.tenant_id(), key.resource_type(), key.resource_id(), data.to_string() )).await?; Ok(data) } async fn get(&self, key: StorageKey) -> Result<Option<Value>, Self::Error> { let cql = "SELECT data FROM resources WHERE tenant_id = ? AND resource_type = ? AND resource_id = ?"; match self.session.query(cql, (key.tenant_id(), key.resource_type(), key.resource_id())).await? { Some(row) => { let json_str: String = row.get("data")?; Ok(Some(serde_json::from_str(&json_str)?)) } None => Ok(None) } } } }
Benefits: Horizontal scalability, high availability, geographic distribution.
4. Hybrid Storage Strategies
Different backends for different use cases
#![allow(unused)] fn main() { pub struct HybridStorage { hot_storage: InMemoryStorage, // Recently accessed data cold_storage: SqliteStorage, // Persistent storage } impl StorageProvider for HybridStorage { async fn get(&self, key: StorageKey) -> Result<Option<Value>, Self::Error> { // Try hot storage first if let Some(data) = self.hot_storage.get(key.clone()).await? { return Ok(Some(data)); } // Fall back to cold storage and warm cache if let Some(data) = self.cold_storage.get(key.clone()).await? { self.hot_storage.put(key, data.clone()).await?; return Ok(Some(data)); } Ok(None) } } }
Benefits: Performance optimization, cost efficiency, flexible data lifecycle.
Data Organization Patterns
Tenant Isolation
Storage automatically isolates data by tenant:
Storage Layout:
βββ tenant-a/
β βββ User/
β β βββ user-1 β {"id": "user-1", "userName": "alice", ...}
β β βββ user-2 β {"id": "user-2", "userName": "bob", ...}
β βββ Group/
β βββ group-1 β {"id": "group-1", "displayName": "Admins", ...}
βββ tenant-b/
β βββ User/
β βββ user-3 β {"id": "user-3", "userName": "charlie", ...}
βββ default/
βββ User/
βββ admin β {"id": "admin", "userName": "admin", ...}
Efficient Querying
Storage providers optimize common query patterns:
#![allow(unused)] fn main() { // List all users in a tenant let prefix = StorageKey::prefix("tenant-123", "User"); let users = storage.list(prefix, 0, 100).await?; // Find user by username let matches = storage.find_by_attribute(prefix, "userName", "alice").await?; // Count resources for capacity planning let user_count = storage.count(prefix).await?; }
Performance Considerations
1. Storage Selection by Scale
Scale | Recommended Storage | Reasoning |
---|---|---|
< 1K resources | InMemoryStorage | Maximum performance, simple setup |
1K - 100K resources | SqliteStorage | Balanced performance, persistence |
100K+ resources | Custom (Postgres/Cassandra) | Scalability, advanced features |
2. Key Design Impact
Efficient key structure enables fast operations:
#![allow(unused)] fn main() { // Good: Hierarchical keys enable prefix operations let key = StorageKey::new("tenant-123", "User", "user-456"); // Good: Batch operations on prefixes let prefix = StorageKey::prefix("tenant-123", "User"); let all_users = storage.list(prefix, 0, usize::MAX).await?; }
3. JSON Storage Optimization
Storage providers work with JSON documents:
#![allow(unused)] fn main() { // Storage receives fully-formed JSON let user_json = json!({ "id": "user-123", "userName": "alice", "meta": { "resourceType": "User", "created": "2023-01-01T00:00:00Z", "version": "v1" } }); // Storage doesn't parse or validate - just stores storage.put(key, user_json).await?; }
Best Practices
1. Choose Storage Based on Requirements
#![allow(unused)] fn main() { // Development: Fast iteration, no persistence needed let storage = InMemoryStorage::new(); // Production: Small scale, simple deployment let storage = SqliteStorage::new("app.db").await?; // Enterprise: High scale, distributed let storage = CustomDistributedStorage::new().await?; }
2. Handle Errors Appropriately
#![allow(unused)] fn main() { // Good: Specific error handling match storage.get(key).await { Ok(Some(data)) => process_data(data), Ok(None) => handle_not_found(), Err(e) => log_storage_error(e), } // Avoid: Ignoring storage errors let data = storage.get(key).await.unwrap(); // Can panic! }
3. Use Efficient Queries
#![allow(unused)] fn main() { // Good: Use prefix queries for lists let prefix = StorageKey::prefix(tenant_id, resource_type); let resources = storage.list(prefix, start, count).await?; // Avoid: Individual gets for list operations for id in resource_ids { let key = StorageKey::new(tenant_id, resource_type, id); let resource = storage.get(key).await?; // N+1 queries! } }
4. Monitor Storage Performance
#![allow(unused)] fn main() { // Get insights into storage usage let stats = storage.stats().await?; println!("Tenants: {}, Resources: {}", stats.tenant_count, stats.total_resources); // Use for capacity planning and optimization if stats.total_resources > 10000 { consider_scaling_storage(); } }
Integration Patterns
Factory Pattern
Create storage based on configuration:
#![allow(unused)] fn main() { pub fn create_storage(config: &StorageConfig) -> Box<dyn StorageProvider> { match config.storage_type { StorageType::Memory => Box::new(InMemoryStorage::new()), StorageType::Sqlite { path } => Box::new(SqliteStorage::new(path).await.unwrap()), StorageType::Custom { .. } => Box::new(CustomStorage::new(config)), } } }
Migration Support
Move between storage backends:
#![allow(unused)] fn main() { pub async fn migrate_storage<F, T>(from: F, to: T) -> Result<(), Box<dyn std::error::Error>> where F: StorageProvider, T: StorageProvider, { let tenants = from.list_tenants().await?; for tenant_id in tenants { let resource_types = from.list_resource_types(&tenant_id).await?; for resource_type in resource_types { let prefix = StorageKey::prefix(&tenant_id, &resource_type); let resources = from.list(prefix, 0, usize::MAX).await?; for (key, data) in resources { to.put(key, data).await?; } } } Ok(()) } }
The Storage Provider layer enables SCIM Server to work with any data backend while maintaining clean separation between storage concerns and SCIM protocol logic. Whether you need the simplicity of in-memory storage for testing or the scalability of distributed databases for production, the unified interface makes it seamless to switch between implementations.
Understanding SCIM Schemas
SCIM (System for Cross-domain Identity Management) uses a sophisticated schema system to define how identity data is structured, validated, and extended across HTTP REST operations. This chapter explores the schema-centric aspects of SCIM and how they're implemented in the SCIM Server library.
See the Schema API documentation for complete details.
SCIM Protocol Background
SCIM is defined by two key Internet Engineering Task Force (IETF) Request for Comments (RFC) specifications:
- RFC 7643: System for Cross-domain Identity Management (SCIM): Core Schema - Defines the core schema and extension model for representing users and groups, published September 2015
- RFC 7644: System for Cross-domain Identity Management (SCIM): Protocol - Specifies the REST API protocol for provisioning and managing identity data, published September 2015
These RFCs establish SCIM 2.0 as the industry standard for identity provisioning, providing a specification for automated user lifecycle management between identity providers (like Okta, Azure AD) and service providers (applications). The schema system defined in RFC 7643 forms the foundation for all SCIM operations, ensuring consistent data representation while allowing for extensibility to meet specific organizational requirements.
What Are SCIM Schemas?
SCIM schemas define the structure and constraints for identity resources like Users and Groups across HTTP REST operations. They serve multiple purposes:
- Data Structure Definition: Define what attributes a resource can have
- Validation Rules: Specify required fields, data types, and constraints
- HTTP Operation Context: Guide validation and processing for GET, POST, PUT, PATCH, DELETE
- Extensibility Framework: Allow custom attributes while maintaining interoperability
- API Contract: Provide a machine-readable description of resource formats
- Meta Components: Define service provider capabilities and resource types
Schema Structure Progression
SCIM uses a layered approach to schema definition, progressing from meta-schemas to concrete resource schemas.
1. Schema for Schemas (Meta-Schema)
At the foundation is the meta-schema that defines how schemas themselves are structured. This is defined in RFC 7643 Section 7 and includes attributes like:
{
"id": "urn:ietf:params:scim:schemas:core:2.0:Schema",
"name": "Schema",
"description": "Specifies the schema that describes a SCIM schema",
"attributes": [
{
"name": "id",
"type": "string",
"multiValued": false,
"description": "The unique URI of the schema",
"required": true,
"mutability": "readOnly"
}
// ... more meta-attributes
]
}
This meta-schema ensures consistency across all SCIM schema definitions and enables programmatic schema discovery and validation.
2. Core Resource Schemas
SCIM defines two core resource schemas that all compliant implementations must support:
User Schema (urn:ietf:params:scim:schemas:core:2.0:User
)
The User schema defines standard attributes for representing people in identity systems:
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "urn:ietf:params:scim:schemas:core:2.0:User",
"name": "User",
"description": "User Account",
"attributes": [
{
"name": "userName",
"type": "string",
"multiValued": false,
"description": "Unique identifier for the User",
"required": true,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "server"
},
{
"name": "name",
"type": "complex",
"multiValued": false,
"description": "The components of the user's real name",
"required": false,
"subAttributes": [
{
"name": "formatted",
"type": "string",
"multiValued": false,
"description": "The full name",
"required": false
},
{
"name": "familyName",
"type": "string",
"multiValued": false,
"description": "The family name",
"required": false
}
// ... more name components
]
}
// ... more user attributes
]
}
Key User Attributes:
userName
: Unique identifier (required)name
: Complex type with formatted, family, and given namesemails
: Multi-valued array of email addressesactive
: Boolean indicating account statusgroups
: Multi-valued references to group memberships
Group Schema (urn:ietf:params:scim:schemas:core:2.0:Group
)
The Group schema defines attributes for representing collections of users:
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"id": "urn:ietf:params:scim:schemas:core:2.0:Group",
"name": "Group",
"description": "Group",
"attributes": [
{
"name": "displayName",
"type": "string",
"multiValued": false,
"description": "A human-readable name for the Group",
"required": true,
"mutability": "readWrite"
},
{
"name": "members",
"type": "complex",
"multiValued": true,
"description": "A list of members of the Group",
"required": false,
"subAttributes": [
{
"name": "value",
"type": "string",
"multiValued": false,
"description": "Identifier of the member",
"mutability": "immutable"
},
{
"name": "$ref",
"type": "reference",
"referenceTypes": ["User", "Group"],
"multiValued": false,
"description": "The URI of the member resource"
}
]
}
]
}
Key Group Attributes:
displayName
: Human-readable group name (required)members
: Multi-valued complex attribute containing user/group references
3. Schema Specialization and Extensions
SCIM's extensibility model allows organizations to add custom attributes while maintaining core compatibility.
Enterprise User Extension
RFC 7643 defines a standard extension for enterprise environments:
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User",
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
],
"userName": "john.doe",
"emails": [{"value": "john@example.com", "primary": true}],
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
"employeeNumber": "12345",
"department": "Engineering",
"manager": {
"value": "26118915-6090-4610-87e4-49d8ca9f808d",
"$ref": "../Users/26118915-6090-4610-87e4-49d8ca9f808d",
"displayName": "Jane Smith"
},
"organization": "Acme Corp"
}
}
Custom Schema Extensions
Organizations can define completely custom schemas using proper URN namespacing:
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User",
"urn:example:params:scim:schemas:extension:acme:2.0:User"
],
"userName": "alice.engineer",
"urn:example:params:scim:schemas:extension:acme:2.0:User": {
"securityClearance": "SECRET",
"projectAssignments": [
{
"projectId": "PROJ-001",
"role": "Lead Developer",
"startDate": "2024-01-15"
}
],
"skills": ["rust", "scim", "identity-management"]
}
}
Schema Processing in SCIM Operations
SCIM schemas drive all protocol operations, providing structure and validation rules that ensure consistent data handling across different identity providers and service providers.
HTTP REST Operations and Schema Processing
SCIM schemas are deeply integrated with HTTP REST operations. Each SCIM command has specific schema processing requirements:
Schema-Aware HTTP Operations
GET Operations - Schema-Driven Response Formation
// GET /Users/{id} - Schema determines response structure
let user = provider.get_resource("User", &user_id, &context).await?;
// Schema controls:
// - Which attributes are returned by default
// - Attribute mutability affects response inclusion
// - Extension schemas determine namespace organization
// - "returned" attribute property: "always", "never", "default", "request"
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "2819c223-7f76-453a-919d-413861904646",
"userName": "bjensen@example.com", // returned: "default"
"meta": { // Always included per spec
"resourceType": "User",
"created": "2010-01-23T04:56:22Z",
"lastModified": "2011-05-13T04:42:34Z",
"version": "W/\"3694e05e9dff591\"",
"location": "https://example.com/v2/Users/2819c223..."
}
}
POST Operations - Schema Validation on Creation
// POST /Users - Complete resource creation with schema validation
let create_request = json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName": "newuser@example.com", // Required per schema
"emails": [{"value": "newuser@example.com", "primary": true}]
});
// Schema validation includes:
// - Required attribute presence check
// - Data type validation
// - Uniqueness constraint enforcement
// - Mutability rules ("readOnly" attributes rejected)
// - Extension schema validation
let user = provider.create_resource("User", create_request, &context).await?;
PUT Operations - Complete Resource Replacement
// PUT /Users/{id} - Schema ensures complete resource validity
let replacement_data = json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName": "bjensen@example.com", // Must include all required fields
"emails": [{"value": "bjensen@newdomain.com", "primary": true}],
"active": true
});
// Schema processing for PUT:
// - Validates complete resource structure
// - Ensures all required attributes present
// - Respects "immutable" and "readOnly" constraints
// - Processes all registered extension schemas
PATCH Operations - Partial Updates with Schema Context
use scim_server::patch::{PatchOperation, PatchOp};
// PATCH /Users/{id} - Schema-aware partial modifications
let patch_ops = vec![
PatchOperation {
op: PatchOp::Replace,
path: Some("emails[primary eq true].value".to_string()),
value: Some(json!("newemail@example.com")),
},
PatchOperation {
op: PatchOp::Add,
path: Some("urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department".to_string()),
value: Some(json!("Engineering")),
}
];
// Schema validation for PATCH:
// - Path expression validation against schema structure
// - Target attribute mutability checking
// - Extension schema awareness for namespaced paths
// - Multi-valued attribute handling per schema rules
DELETE Operations - Schema-Informed Cleanup
// DELETE /Users/{id} - Schema guides deletion processing
// Schema-informed deletion:
// - Validates deletion permissions based on mutability constraints
// - Processes extension schema cleanup requirements
// - Determines soft delete vs hard delete approach
SCIM Meta Components and Schema Integration
SCIM defines several meta-schemas that describe the service provider's capabilities and resource structures:
Service Provider Configuration Schema
The ServiceProviderConfig resource describes server capabilities:
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
"documentationUri": "https://example.com/help/scim.html",
"patch": {
"supported": true
},
"bulk": {
"supported": true,
"maxOperations": 1000,
"maxPayloadSize": 1048576
},
"filter": {
"supported": true,
"maxResults": 200
},
"changePassword": {
"supported": false
},
"sort": {
"supported": true
},
"etag": {
"supported": true
},
"authenticationSchemes": [
{
"type": "oauthbearertoken",
"name": "OAuth Bearer Token",
"description": "Authentication scheme using the OAuth Bearer Token",
"specUri": "http://www.rfc-editor.org/info/rfc6750",
"documentationUri": "https://example.com/help/oauth.html"
}
]
}
Resource Type Meta-Schema
Resource types describe available SCIM resources and their schemas:
// GET /ResourceTypes/User returns:
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"],
"id": "User",
"name": "User",
"endpoint": "/Users",
"description": "User Account",
"schema": "urn:ietf:params:scim:schemas:core:2.0:User",
"schemaExtensions": [
{
"schema": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User",
"required": false
},
{
"schema": "urn:example:params:scim:schemas:extension:acme:2.0:User",
"required": true
}
],
"meta": {
"location": "https://example.com/v2/ResourceTypes/User",
"resourceType": "ResourceType"
}
}
Schema Meta-Attributes
Every SCIM resource includes meta-attributes that support HTTP operations:
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "2819c223-7f76-453a-919d-413861904646",
"userName": "bjensen@example.com",
"meta": {
"resourceType": "User", // Schema-derived type
"created": "2010-01-23T04:56:22Z", // Creation timestamp
"lastModified": "2011-05-13T04:42:34Z", // Modification tracking
"location": "https://example.com/v2/Users/2819c223-7f76-453a-919d-413861904646",
"version": "W/\"3694e05e9dff591\"" // ETag for concurrency control
}
}
// Meta attributes enable:
// - HTTP caching with ETags
// - Conditional operations (If-Match, If-None-Match)
// - Audit trail capabilities
// - Resource location for HATEOAS compliance
Schema Processing in SCIM Server Implementation
The SCIM Server library integrates schema processing throughout the HTTP request lifecycle:
Request Processing Pipeline
use scim_server::{ScimServer, RequestContext};
let server = ScimServer::new(provider)?;
// 1. HTTP Request β Schema Identification
let schemas = extract_schemas_from_request(&request_body)?;
// 2. Schema Validation β Resource Processing
let validation_context = ValidationContext {
schemas: &schemas,
operation: HttpOperation::Post,
resource_type: "User",
};
// 3. Operation Execution with Schema Constraints
let result = match request.method() {
"GET" => server.get_with_schema_filtering(&resource_type, &id, &query_params, &context).await?,
"POST" => server.create_with_schema_validation(&resource_type, &request_body, &context).await?,
"PUT" => server.replace_with_schema_validation(&resource_type, &id, &request_body, &context).await?,
"PATCH" => server.patch_with_schema_awareness(&resource_type, &id, &patch_ops, &context).await?,
"DELETE" => server.delete_with_schema_cleanup(&resource_type, &id, &context).await?,
};
Schema-Driven Query Processing
SCIM query parameters interact directly with schema definitions:
GET /Users?attributes=userName,emails&filter=active eq true
Schema processing for queries:
- Validates attribute names against schema definitions
- Handles extension schema attributes in projections
- Enforces "returned" attribute constraints
- Processes complex attribute path expressions
- Validates filter expressions against attribute types
Auto Schema Discovery
SCIM Server provides automatic schema discovery capabilities that integrate with the SCIM protocol's introspection endpoints:
Schema Endpoint Implementation
SCIM servers provide schema introspection endpoints:
GET /Schemas
returns all registered schemasGET /Schemas/{schema_id}
returns specific schema details- Enables automatic schema discovery for clients and tools
Resource Type Discovery
SCIM servers support resource type introspection as defined in RFC 7644:
GET /ResourceTypes
returns supported resource types, including:
- Resource endpoint paths
- Associated schema URNs
- Available schema extensions
- Extension requirement status
Dynamic Data Validation
The schema system enables sophisticated validation that goes beyond simple type checking:
Multi-Level Validation
SCIM validation occurs at multiple levels:
- HTTP Method Validation: Operation-specific constraints
- Syntax Validation: JSON structure and basic type checking
- Schema Validation: Compliance with schema definitions
- Business Rule Validation: Custom validation logic
The validation process ensures that data conforms to schema requirements before processing, returning appropriate HTTP status codes for different error types.
Operation-Specific Validation
Each HTTP operation has specific validation requirements based on schema attribute properties:
- POST (Create): All required attributes must be present
- PUT (Replace): Complete resource validation, immutable attributes cannot change
- PATCH (Update): Path validation, readOnly attributes cannot be targeted
- GET (Read): Response filtering based on "returned" attribute property
HTTP Status Code Mapping
Schema validation errors map to specific HTTP status codes:
- 400 Bad Request: Invalid values, missing required attributes, mutability violations
- 409 Conflict: Uniqueness constraint violations
- 412 Precondition Failed: ETag version mismatches
Working with Standard Data Definitions
SCIM defines standard data formats and constraints that the library enforces:
Attribute Types and Constraints
Type | Description | Validation Rules |
---|---|---|
string | Text data | Length limits, case sensitivity, uniqueness |
boolean | True/false values | Must be valid JSON boolean |
decimal | Numeric data | Precision and scale constraints |
integer | Whole numbers | Range validation |
dateTime | ISO 8601 timestamps | Format and timezone validation |
binary | Base64-encoded data | Encoding validation |
reference | Resource references | Referential integrity checks |
complex | Nested objects | Sub-attribute validation |
Multi-Valued Attributes
SCIM supports multi-valued attributes with sophisticated handling:
{
"emails": [
{
"value": "primary@example.com",
"type": "work",
"primary": true
},
{
"value": "secondary@example.com",
"type": "personal",
"primary": false
}
]
}
Multi-Value Rules:
- At most one
primary
value allowed - Type values from canonical list (if specified)
- Duplicate detection and handling
- Order preservation for client expectations
Reference Attributes
References to other SCIM resources are handled with full integrity checking:
{
"groups": [
{
"value": "e9e30dba-f08f-4109-8486-d5c6a331660a",
"$ref": "https://example.com/scim/v2/Groups/e9e30dba-f08f-4109-8486-d5c6a331660a",
"display": "Administrators"
}
]
}
// The library validates:
// - Reference type matches schema constraints
// - Tenant isolation for multi-tenant systems
HTTP Content Negotiation and Schema Processing
SCIM servers use HTTP headers to negotiate schema processing:
Content-Type and Schema Validation
SCIM requests must include proper Content-Type headers and schema declarations:
POST /Users HTTP/1.1
Content-Type: application/scim+json
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName": "newuser@example.com"
}
Servers validate:
- Content-Type header matches SCIM specification (
application/scim+json
) - schemas array matches Content-Type expectations
- Resource structure conforms to declared schemas
ETag and Schema Versioning
The SCIM protocol uses ETags (entity tags) for optimistic concurrency control, preventing lost updates when multiple clients modify the same resource. Each SCIM resource includes a meta.version
field containing an ETag value that changes whenever the resource is modified. Clients use HTTP conditional headers (If-Match
, If-None-Match
) with these ETags to ensure they're operating on the expected version of a resource.
For implementation details and practical usage patterns, see the Concurrency Control in SCIM Operations chapter and Schema Mechanisms in SCIM Server.
Schema Extensibility Patterns
The SCIM Server supports several patterns for extending schemas while maintaining interoperability:
Additive Extensions
Add new attributes without modifying core schemas:
// Core user data remains unchanged
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User",
"urn:example:params:scim:schemas:extension:acme:2.0:User"
],
"userName": "alice.engineer",
"emails": [{"value": "alice@acme.com", "primary": true}],
// Extension data in separate namespace
"urn:example:params:scim:schemas:extension:acme:2.0:User": {
"department": "R&D",
"clearanceLevel": "SECRET",
"projects": ["moonshot", "widget-2.0"]
}
}
Schema Composition
Combine multiple extensions for complex scenarios:
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User",
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User",
"urn:example:params:scim:schemas:extension:security:2.0:User",
"urn:example:params:scim:schemas:extension:hr:2.0:User"
],
"userName": "bob.manager",
// Each extension provides its own attributes
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
"employeeNumber": "E12345"
},
"urn:example:params:scim:schemas:extension:security:2.0:User": {
"lastSecurityReview": "2024-01-15T10:30:00Z"
},
"urn:example:params:scim:schemas:extension:hr:2.0:User": {
"performanceRating": "exceeds-expectations"
}
}
Best Practices for Schema Design
When designing custom schemas for use with SCIM Server:
1. Follow Naming Conventions
- Use proper URN namespacing:
urn:example:params:scim:schemas:extension:company:version:Type
- Choose descriptive attribute names that clearly indicate purpose
- Use camelCase for attribute names to match SCIM conventions
2. Design for Interoperability
- Minimize custom types - prefer standard SCIM types when possible
- Document extensions clearly for integration partners
- Provide sensible defaults and make attributes optional when appropriate
3. Consider Performance Implications
- Avoid deeply nested complex attributes that are expensive to validate
- Use appropriate uniqueness constraints to leverage database indexes
- Consider query patterns when designing multi-valued attributes
4. Plan for Evolution
- Design extensible schemas that can accommodate future requirements
- Use semantic versioning for schema URNs
- Maintain backwards compatibility when modifying existing schemas
Integration with AI Systems
The SCIM Server's schema system is designed to work seamlessly with AI agents through the Model Context Protocol (MCP):
Schema-Aware AI Tools
SCIM schemas enable AI integration by providing structured descriptions of available operations and data formats. AI systems can discover schema capabilities and generate compliant requests automatically.
Schema information that benefits AI systems includes:
- Required and optional attributes for each resource type
- Validation rules and data format constraints
- Available HTTP operations and their requirements
- Extension schemas and custom attribute definitions
For implementation details, see Schema Mechanisms in SCIM Server and the AI Integration Guide.
Conclusion
Conclusion
SCIM schemas provide a powerful foundation for identity data management that balances standardization with extensibility across HTTP REST operations. Understanding these protocol concepts enables you to:
- Design compliant SCIM resources that work with enterprise identity providers
- Implement proper validation across all HTTP operations
- Create extensible systems using schema extension mechanisms
- Build interoperable solutions that follow RFC specifications
- Support diverse client requirements through schema discovery
Key takeaways include:
- Schema Structure: Schemas define both data structure and operational behavior
- HTTP Integration: Each REST operation has specific schema processing requirements
- Extensibility: Extension schemas enable customization while maintaining compatibility
- Validation: Multi-layered validation ensures data integrity and compliance
- Discovery: Schema endpoints enable dynamic client capabilities
For practical implementation of these concepts using the SCIM Server library, see Schema Mechanisms in SCIM Server.
The next chapter will explore hands-on implementation patterns and real-world usage scenarios.
Schema Mechanisms in SCIM Server
This chapter explores how the SCIM Server library implements the schema concepts defined in the SCIM protocol. While Understanding SCIM Schemas covers the protocol specifications, this chapter focuses on the conceptual mechanisms that make schema processing practical and type-safe in Rust applications.
See the Schema API documentation for complete details.
The SCIM Server library transforms the abstract schema definitions from RFC 7643 into concrete, composable components that provide compile-time safety, runtime validation, and seamless integration with Rust's type system.
Schema Registry
The Schema Registry serves as the central schema management system within SCIM Server. It acts as a knowledge base that holds all schema definitions and provides validation services throughout the request lifecycle.
Core Concept: Rather than parsing schemas repeatedly or maintaining scattered validation logic, the Schema Registry centralizes all schema knowledge in a single, queryable component. It comes pre-loaded with RFC 7643 core schemas (User, Group, Enterprise User extension) and supports dynamic registration of custom schemas at runtime.
The registry operates as a validation oracleβwhen any component needs to understand attribute constraints, validate data structures, or determine response formatting, it queries the registry. This creates a single source of truth for schema behavior across the entire system.
Integration Points: The registry integrates with every major operationβresource creation validates against registered schemas, query processing checks attribute names, and response formatting respects schema-defined visibility rules.
For detailed API reference, see the SchemaRegistry documentation.
Value Objects
Value Objects provide compile-time type safety for SCIM attributes by wrapping primitive values in domain-specific types. This mechanism prevents common errors like assigning invalid email addresses or constructing malformed names.
Core Concept: Instead of working with raw JSON values that can contain any data, Value Objects create typed wrappers that enforce validation at construction time. An Email
value object can only be created with a valid email string, and a UserName
can only contain characters that meet SCIM requirements.
This approach leverages Rust's ownership system to make invalid states unrepresentable. Once you have a DisplayName
value object, you know it contains valid display name dataβno runtime checks needed. The type system becomes your validation mechanism.
Schema Integration: Value objects understand their corresponding schema definitions. They know their attribute type, validation rules, and serialization requirements. When converting to JSON for API responses, they automatically apply schema-defined formatting and constraints.
Extensibility: The value object system supports both pre-built types for common SCIM attributes and custom value objects for organization-specific extensions. The factory pattern allows dynamic creation while maintaining type safety.
For implementation details, see the ValueObject trait documentation.
Dynamic Schema Construction
Dynamic Schema Construction addresses the challenge of working with schemas that are not known at compile timeβsuch as tenant-specific extensions or runtime-configured resource types.
Core Concept: While value objects provide compile-time safety for known schemas, dynamic construction enables runtime flexibility for unknown or variable schemas. The system can examine a schema definition at runtime and create appropriate value objects and validation logic on demand.
This mechanism uses a factory pattern where schema definitions drive object creation. Given a schema's attribute definition and a JSON value, the system can construct the appropriate typed representation without prior knowledge of the specific schema structure.
Schema-Driven Behavior: The construction process respects all schema constraintsβrequired attributes, data types, multi-valued rules, and custom validation logic. The resulting objects behave identically to compile-time created value objects, maintaining consistency across static and dynamic scenarios.
Use Cases: This enables multi-tenant systems where each tenant may have custom schemas, AI integration where schemas are discovered at runtime, and administrative tools that work with arbitrary SCIM resource types.
For advanced usage patterns, see the SchemaConstructible trait documentation.
Validation Pipeline
The Validation Pipeline orchestrates multi-layered validation that progresses from basic syntax checking to complex business rule enforcement. This mechanism ensures that only valid, schema-compliant data enters your system.
Core Concept: Rather than ad-hoc validation scattered throughout the codebase, the pipeline provides a structured, configurable validation process. Each layer builds on the previous oneβsyntax validation ensures basic JSON correctness, schema validation checks SCIM compliance, and business validation enforces organizational rules.
The pipeline integrates with HTTP operations, applying operation-specific validation rules. A POST request validates required attributes, while a PATCH request validates path expressions and mutability constraints.
Validation Context: The pipeline operates within a context that includes the target schema, HTTP operation type, tenant information, and existing resource state. This context enables sophisticated validation logic that considers the complete request environment.
Error Handling: Validation failures produce structured errors with appropriate HTTP status codes and detailed messages. The pipeline can collect multiple errors in a single pass, providing comprehensive feedback rather than stopping at the first issue.
For error handling strategies and custom validation rules, see the Validation Guide.
Auto Schema Discovery
Auto Schema Discovery provides SCIM-compliant endpoints that expose available schemas and resource types to clients and tools. This mechanism enables runtime introspection of server capabilities.
Core Concept: The discovery system automatically generates schema and resource type information from the registered schemas in the Schema Registry. Clients can query /Schemas
and /ResourceTypes
endpoints to understand what resources are available and how they're structured.
This creates a self-documenting API where tools and AI agents can discover capabilities dynamically rather than requiring pre-configured knowledge of the server's schema support.
Standards Compliance: The discovery endpoints conform to RFC 7644 specifications, ensuring compatibility with standard SCIM clients and identity providers. The generated responses include all required metadata for proper client integration.
For endpoint configuration and custom resource type registration, see the REST API Guide.
AI Integration
AI Integration makes SCIM operations accessible to artificial intelligence agents through structured, schema-aware tool descriptions. This mechanism transforms SCIM Server capabilities into AI-consumable formats.
Core Concept: The integration generates Model Context Protocol (MCP) tool descriptions that include schema constraints, validation rules, and example usage patterns. AI agents receive structured information about what operations are available and how to use them correctly.
Schema awareness ensures that AI tools understand not just the API surface but the data validation requirements, making them more likely to generate valid requests and handle errors appropriately.
Dynamic Capabilities: The AI integration reflects the current server configuration, including custom schemas and tenant-specific extensions. As schemas are added or modified, the AI tools automatically update to reflect new capabilities.
For AI agent configuration and custom tool creation, see the AI Integration Guide.
Component Relationships
These mechanisms work together to create a cohesive schema processing system:
- Schema Registry provides the authoritative schema definitions
- Value Objects implement type-safe attribute handling based on registry schemas
- Dynamic Construction creates value objects from registry definitions at runtime
- Validation Pipeline uses registry schemas to enforce compliance
- Auto Discovery exposes registry contents through SCIM endpoints
- AI Integration translates registry capabilities into agent-readable formats
This architecture ensures that schema knowledge flows consistently throughout the system, from initial registration through final API responses.
Extensibility and Customization
Each mechanism supports extension while maintaining SCIM compliance:
- Custom Schemas integrate seamlessly with the registry system
- Domain-Specific Value Objects extend the type safety model
- Business Validation Rules plug into the validation pipeline
- Tenant-Specific Behavior works across all mechanisms
- Custom AI Tools can be generated from schema definitions
The key principle is additive customizationβyou extend capabilities without modifying core behavior, ensuring that standard SCIM operations continue to work while supporting organization-specific requirements.
Production Considerations
These mechanisms are designed for production deployment:
- Performance: Schema processing is optimized for minimal runtime overhead
- Memory Efficiency: Schema definitions are shared across requests and tenants
- Thread Safety: All mechanisms support concurrent access without locking
- Error Recovery: Validation failures don't impact server stability
- Observability: Schema processing integrates with structured logging
For deployment and monitoring guidance, see the Production Deployment Guide.
Next Steps
Understanding these schema mechanisms prepares you for implementing SCIM Server in your applications:
- Getting Started: Begin with the First SCIM Server tutorial
- Implementation: Explore How-To Guides for specific scenarios
- Advanced Usage: Review Advanced Topics for complex deployments
- API Reference: Consult API Documentation for detailed interfaces
These conceptual mechanisms become practical tools through hands-on implementation and real-world usage patterns.
For understanding how schema versioning enables concurrency control in multi-client scenarios, see Concurrency Control in SCIM Operations.
Concurrency Control in SCIM Operations
SCIM operations often involve multiple clients accessing and modifying the same identity resources simultaneously. Without proper concurrency control, this can lead to data corruption, lost updates, and inconsistent system state. This chapter explores how the SCIM Server library provides automatic concurrency protection through version-based optimistic locking.
See the Resource API documentation and concurrency control methods for complete details.
The Concurrency Challenge
Consider a common scenario: two HR administrators are simultaneously updating the same user record. Admin A is adding the user to a new department group, while Admin B is updating the user's job title. Without concurrency control, the last update winsβpotentially causing one administrator's changes to be silently lost.
This problem becomes more acute in modern distributed systems where identity data is managed across multiple applications, each making updates through SCIM APIs. Traditional database locking mechanisms are insufficient because SCIM operates over HTTP, where connections are short-lived and stateless.
The Lost Update Problem: When multiple clients read a resource, modify it, and write it back, the client that writes last unknowingly overwrites changes made by other clients. This is particularly dangerous for identity data where access rights, group memberships, and security attributes must remain consistent.
When Concurrency Control Matters
Understanding when to enable concurrency protection is crucial for system design and performance.
Multi-Client Scenarios (Concurrency Control Required)
Enterprise Identity Management: Multiple identity providers (HR systems, Active Directory, Okta) synchronizing user data with your application. Each system may attempt to update user attributes simultaneously based on their internal schedules.
Administrative Interfaces: Multiple administrators managing users through web interfaces. Without concurrency control, one administrator can unknowingly overwrite changes made by another, leading to confusion and data loss.
Automated Provisioning: Identity lifecycle management systems that automatically create, update, and deactivate users based on business rules. These systems often operate concurrently and need protection against conflicting updates.
Integration Scenarios: Applications integrating with multiple identity sources (LDAP, Azure AD, Google Workspace) where changes from different sources may arrive simultaneously.
Single-Client Scenarios (Concurrency Control Optional)
Dedicated Integration: A single HR system that is the sole source of truth for user data. Since only one client makes updates, there's no risk of concurrent modification conflicts.
Batch Processing: Scheduled data synchronization where updates are processed sequentially by a single system during maintenance windows.
Development and Testing: Development environments where only one developer or test suite is making changes.
Read-Heavy Workloads: Applications that primarily read identity data with infrequent updates from a single source.
HTTP ETag vs MCP Version Handling
The SCIM Server library supports two distinct version management approaches, each optimized for different integration patterns.
HTTP ETag Integration
Protocol Context: HTTP ETags are part of the HTTP 1.1 specification (RFC 7232) and provide standard web caching and conditional request mechanisms. They're familiar to web developers and integrate seamlessly with existing HTTP infrastructure.
Format: ETags appear as HTTP headers with quoted strings: ETag: W/"abc123def"
. The W/
prefix indicates a "weak" ETag, meaning it represents semantic equivalence rather than byte-for-byte identity.
Usage Pattern: Web applications and REST clients naturally work with ETags through standard HTTP headers:
GET /Users/123
Response: ETag: W/"v1.2.3"
PUT /Users/123
If-Match: W/"v1.2.3"
Integration Benefits: Works automatically with HTTP caches, proxies, and standard web frameworks. No special client-side handling neededβjust standard HTTP conditional request headers.
MCP Version Handling
Protocol Context: Model Context Protocol (MCP) operates over JSON-RPC rather than HTTP, making traditional ETags inappropriate. MCP versions use raw string identifiers that are more suitable for programmatic access.
Format: Raw version strings without HTTP formatting: "abc123def"
. No quotes, no W/
prefix, just the opaque version identifier.
Usage Pattern: JSON-RPC clients work directly with version strings in request/response payloads:
{
"method": "scim_update_user",
"params": {
"user_data": {...},
"expected_version": "abc123def"
}
}
Integration Benefits: Cleaner for programmatic access, especially in AI agents and automated tools that work with JSON data structures rather than HTTP headers.
Architecture: Raw Types with Format Safety
The SCIM Server library uses a sophisticated type system to prevent version format confusion while maintaining compatibility with both HTTP and MCP protocols.
Internal Raw Representation
Core Concept: All versions are stored internally as raw, opaque strings. This provides a canonical representation that's independent of the client protocol being used.
Benefits: Storage systems, databases, and internal processing logic work with a single, consistent version format. No protocol-specific encoding in your data layer. With content-based versioning, your storage layer doesn't need to store versions at allβthey can be computed on-demand from resource content.
Content-Based Generation: Versions can be generated from resource content using SHA-256 hashing, ensuring deterministic versioning across different systems and eliminating the need for centralized version counters.
Type-Safe Format Conversion
Phantom Types: The library uses Rust's phantom type system to distinguish between different version formats at compile time:
RawVersion
: Internal canonical format ("abc123def")HttpVersion
: HTTP ETag format ("W/"abc123def"")
Compile-Time Safety: The type system prevents accidentally mixing formats. You cannot pass an HTTP ETag where a raw version is expectedβthe compiler catches these errors before deployment.
Automatic Conversions: Standard Rust traits handle conversions between formats:
#![allow(unused)] fn main() { let raw_version = RawVersion::from_hash("abc123"); let http_version = HttpVersion::from(raw_version); // Conversion let etag_header = http_version.to_string(); // "W/\"abc123\"" }
Cross-Format Equality: Versions with the same underlying content are equal regardless of format, enabling seamless comparison between HTTP and MCP clients working on the same data.
Implementation Patterns
Automatic Version Generation
Content-Based Versioning: The library automatically generates versions from resource content, ensuring that any change to user data results in a new version. This eliminates the need for manual version management in your application code.
Provider Integration: Resource providers automatically handle version computation and comparison. Your storage layer can optionally store versions for performance, or rely on pure content-based computation, while protocol handlers manage format conversion transparently.
Version Storage Trade-offs
Pure Content-Based Versioning: Your storage layer doesn't need to store versions at all. Versions are computed on-demand from resource content using SHA-256 hashing. This approach eliminates version storage complexity entirely and ensures versions are always accurate, even if resources are modified outside the SCIM API.
Hybrid Approach with Meta Storage: Versions can be cached in the resource's meta field for performance optimization. The system first checks for a stored version, then falls back to content-based computation if none exists. This provides the best of both worldsβperformance when versions are cached, accuracy when they're not.
Storage Benefits: For high-throughput scenarios with frequent version checks, storing versions in the meta field avoids repeated SHA-256 computation. The version is updated automatically whenever the resource changes.
Stateless Benefits: Pure content-based versioning works well for scenarios where resources might be modified by external systems, batch processes, or direct database operations. The version always reflects the current resource state without requiring version synchronization.
Conditional Operations
Built-In Protection: All update and delete operations include conditional variants that check versions before making changes. This protection is automaticβno additional code required in your business logic.
Graceful Conflict Handling: When version mismatches occur, the library provides detailed conflict information including expected vs. current versions and human-readable error messages for client display.
Flexible Response: Your application can choose to retry with updated versions, merge changes intelligently, or present conflicts to users for manual resolution.
Best Practices for Concurrency Design
Choose Based on Client Count
Single Client: If your SCIM server serves only one client system, concurrency control adds overhead without benefits. Use simple operations without version checking.
Multiple Clients: Enable concurrency control when multiple systems update the same resources. The performance overhead is minimal compared to the data integrity benefits.
Version Management Strategy
Let the Library Handle It: Use automatic content-based versioning rather than implementing your own version schemes. The library's approach is proven, consistent, and integrates with both HTTP and MCP protocols.
Version Storage Options: You can either store versions in your database for performance, or use pure content-based versioning where versions are computed on-demand from resource content. For high-throughput scenarios, storing versions avoids repeated computation. For simple deployments, content-based computation eliminates version storage complexity entirely.
Error Handling Design
Expect Version Conflicts: Design your client applications to handle version mismatch errors gracefully. Provide clear user feedback and options for conflict resolution.
Implement Retry Logic: For automated systems, implement exponential backoff retry with fresh version retrieval when conflicts occur.
Integration with Modern Identity Systems
Cloud Identity Providers
Multi-Provider Scenarios: Modern enterprises often integrate multiple identity providers (Azure AD, Okta, Google Workspace) with SCIM endpoints. Concurrency control prevents conflicts when these systems synchronize user data simultaneously.
Eventual Consistency: Concurrency control provides immediate consistency for SCIM operations while allowing eventual consistency patterns in your broader identity architecture.
AI and Automation
AI Agent Integration: AI systems making identity management decisions benefit from MCP version handling, which uses raw version strings (e.g., "abc123def"
) instead of HTTP ETag format for cleaner programmatic access. The MCP integration automatically converts between formats.
Automated Compliance: Compliance systems that automatically update user attributes based on policy changes need concurrency protection to avoid overwriting simultaneous manual administrative changes.
Performance Considerations
Minimal Overhead
Lightweight Versioning: Version computation uses SHA-256 hashing of JSON content, which is fast and deterministic. For content-based versioning, the computational overhead is minimal compared to database operations. You can eliminate version storage entirely and compute versions on-demand, or cache versions in the resource meta field for optimal performance.
No Database Locking: Optimistic concurrency control avoids database locks, maintaining high throughput for read operations and simple updates.
Scalability Benefits
Horizontal Scaling: Version-based concurrency control works across multiple server instances without coordination, enabling horizontal scaling of SCIM endpoints.
Caching Compatibility: HTTP ETag integration works seamlessly with web caches and CDNs, potentially reducing server load for read-heavy workloads.
Conclusion
Concurrency control in SCIM operations provides essential data integrity protection for multi-client scenarios while remaining optional for simple single-client integrations. The SCIM Server library's version-based approach offers automatic protection with minimal performance overhead, type-safe format handling, and seamless integration with both HTTP and MCP protocols.
By understanding when concurrency control is needed and how the library's type-safe architecture prevents common version handling errors, you can build robust identity management systems that maintain data consistency even under concurrent access patterns.
The key insight is matching your concurrency strategy to your integration pattern: use the protection when you need it, skip it when you don't, and let the library handle the complex details of version management and format conversion automatically.
Multi-Tenant Architecture
The Multi-Tenant Architecture in SCIM Server provides complete isolation and management of identity resources across multiple customer organizations within a single deployment. It enables Software-as-a-Service (SaaS) providers to serve multiple tenants while ensuring strict data isolation, flexible URL generation, and tenant-specific configuration management.
See the Multi-Tenant API documentation for complete details.
Value Proposition
The Multi-Tenant Architecture delivers comprehensive multi-tenancy capabilities:
- Complete Tenant Isolation: Strict separation of data, operations, and configurations between tenants
- Flexible URL Strategies: Multiple deployment patterns for tenant-specific endpoints
- Scalable Authentication: Credential-based tenant resolution with pluggable authentication
- Resource Limits & Permissions: Granular control over tenant capabilities and quotas
- Zero Configuration Overhead: Automatic tenant handling with sensible defaults
- SCIM Compliance: Full SCIM 2.0 compliance maintained across all tenant scenarios
- Production Ready: Comprehensive security, audit trails, and operational monitoring
Architecture Overview
The Multi-Tenant Architecture operates as a cross-cutting concern throughout the SCIM Server stack:
Multi-Tenant Architecture (Cross-Cutting Layer)
βββ Tenant Resolution & Authentication
βββ Request Context & Isolation
βββ URL Generation & Routing
βββ Resource Scoping & Limits
βββ Configuration Management
β
Applied to All Layers:
βββ SCIM Server (Tenant-Aware Operations)
βββ Resource Providers (Tenant Isolation)
βββ Storage Providers (Tenant Scoping)
βββ Schema Management (Tenant Extensions)
Core Components
TenantContext
: Complete tenant identity and permissionsRequestContext
: Tenant-aware request handling with automatic scopingTenantResolver
: Authentication credential to tenant mappingTenantStrategy
: Flexible URL generation patterns- Multi-Tenant Provider: Storage-level tenant isolation helpers
ScimTenantConfiguration
: SCIM-specific tenant settings
Use Cases
1. SaaS Identity Provider
Multi-customer identity management platform
#![allow(unused)] fn main() { use scim_server::{ScimServerBuilder, TenantStrategy}; use scim_server::multi_tenant::{StaticTenantResolver, ScimTenantConfiguration}; use scim_server::resource::{TenantContext, TenantPermissions, RequestContext}; // Configure multi-tenant server with subdomain strategy let server = ScimServerBuilder::new(provider) .with_base_url("https://identity.company.com") .with_tenant_strategy(TenantStrategy::Subdomain) .build()?; // Set up tenant resolver for authentication let mut resolver = StaticTenantResolver::new(); // Add customer tenants with specific limits let acme_permissions = TenantPermissions { can_create: true, can_read: true, can_update: true, can_delete: false, // Restrict deletion for this customer max_users: Some(1000), max_groups: Some(50), ..Default::default() }; let acme_context = TenantContext::new("acme-corp".to_string(), "scim-client-1".to_string()) .with_permissions(acme_permissions); resolver.add_tenant("api-key-acme-123", acme_context).await; // Enterprise customer with higher limits let enterprise_permissions = TenantPermissions { max_users: Some(10000), max_groups: Some(500), ..Default::default() }; let enterprise_context = TenantContext::new("enterprise".to_string(), "scim-client-ent".to_string()) .with_permissions(enterprise_permissions); resolver.add_tenant("api-key-enterprise-456", enterprise_context).await; // Operations automatically scoped to tenant let tenant_context = resolver.resolve_tenant("api-key-acme-123").await?; let context = RequestContext::with_tenant_generated_id(tenant_context); // This user only exists within "acme-corp" tenant let user = server.create_resource("User", user_data, &context).await?; }
Benefits: Complete customer isolation, flexible billing models, tenant-specific limits.
2. Enterprise Multi-Division Identity
Single enterprise with multiple business divisions
#![allow(unused)] fn main() { // Path-based tenant strategy for internal divisions let server = ScimServerBuilder::new(provider) .with_base_url("https://hr.enterprise.com") .with_tenant_strategy(TenantStrategy::PathBased) .build()?; // Configure division-specific contexts let hr_context = TenantContext::new("hr-division".to_string(), "hr-client".to_string()) .with_isolation_level(IsolationLevel::Standard); let engineering_context = TenantContext::new("engineering".to_string(), "eng-client".to_string()) .with_isolation_level(IsolationLevel::Strict); let sales_context = TenantContext::new("sales".to_string(), "sales-client".to_string()) .with_isolation_level(IsolationLevel::Shared); // Division-specific operations with different isolation levels let hr_request = RequestContext::with_tenant_generated_id(hr_context); let engineering_request = RequestContext::with_tenant_generated_id(engineering_context); let sales_request = RequestContext::with_tenant_generated_id(sales_context); // URLs generated: https://hr.enterprise.com/hr-division/v2/Users/123 // https://hr.enterprise.com/engineering/v2/Users/456 // https://hr.enterprise.com/sales/v2/Users/789 }
Benefits: Division autonomy, shared corporate policies, centralized management.
3. Development Environment Isolation
Separate environments for development, staging, and production
#![allow(unused)] fn main() { // Single server handling multiple environments as tenants let server = ScimServerBuilder::new(provider) .with_base_url("https://scim-dev.company.com") .with_tenant_strategy(TenantStrategy::PathBased) .build()?; // Development environment with relaxed permissions let dev_permissions = TenantPermissions { can_create: true, can_update: true, can_delete: true, // Allow deletion in dev max_users: Some(100), // Smaller limits for dev max_groups: Some(10), ..Default::default() }; let dev_context = TenantContext::new("development".to_string(), "dev-client".to_string()) .with_permissions(dev_permissions) .with_isolation_level(IsolationLevel::Shared); // Allow cross-team access // Staging environment with production-like restrictions let staging_permissions = TenantPermissions { can_delete: false, // Restrict deletion in staging max_users: Some(500), max_groups: Some(25), ..Default::default() }; let staging_context = TenantContext::new("staging".to_string(), "staging-client".to_string()) .with_permissions(staging_permissions) .with_isolation_level(IsolationLevel::Standard); // Production environment with strict limits let prod_permissions = TenantPermissions { can_delete: false, // No deletion in production max_users: Some(5000), max_groups: Some(100), ..Default::default() }; let prod_context = TenantContext::new("production".to_string(), "prod-client".to_string()) .with_permissions(prod_permissions) .with_isolation_level(IsolationLevel::Strict); }
Benefits: Environment isolation, development flexibility, production safety.
4. Geographic Data Residency
Regional tenant isolation for compliance
#![allow(unused)] fn main() { // Region-specific tenant configurations let eu_server = ScimServerBuilder::new(eu_provider) .with_base_url("https://eu.identity.company.com") .with_tenant_strategy(TenantStrategy::Subdomain) .build()?; let us_server = ScimServerBuilder::new(us_provider) .with_base_url("https://us.identity.company.com") .with_tenant_strategy(TenantStrategy::Subdomain) .build()?; // EU tenant with GDPR compliance settings let eu_context = TenantContext::new("customer-eu".to_string(), "eu-client".to_string()) .with_isolation_level(IsolationLevel::Strict); // US tenant with different compliance requirements let us_context = TenantContext::new("customer-us".to_string(), "us-client".to_string()) .with_isolation_level(IsolationLevel::Standard); // Data automatically scoped to appropriate region and compliance rules }
Benefits: Regulatory compliance, data residency, regional performance.
5. Customer White-Label Solutions
Tenant-specific branding and configuration
#![allow(unused)] fn main() { // Configure tenant-specific SCIM settings let white_label_config = ScimTenantConfiguration::builder("customer-brand".to_string()) .with_endpoint_path("/api/scim/v2") .with_scim_rate_limit(200, Duration::from_secs(60)) .with_scim_client("brand-client-1", "custom-api-key") .enable_scim_audit_log() .with_custom_schema_extensions(vec![ ScimSchemaExtension::new("urn:customer:schema:Brand", brand_attributes) ]) .build()?; // Server with customer-specific subdomain let server = ScimServerBuilder::new(provider) .with_base_url("https://identity.customer.com") .with_tenant_strategy(TenantStrategy::SingleTenant) // Customer gets dedicated subdomain .build()?; // Customer-specific extensions and branding automatically applied }
Benefits: Brand consistency, customer-specific features, dedicated endpoints.
Design Patterns
Tenant Resolution Pattern
Authentication credentials map to tenant contexts:
#![allow(unused)] fn main() { pub trait TenantResolver: Send + Sync { type Error: std::error::Error + Send + Sync + 'static; async fn resolve_tenant(&self, credential: &str) -> Result<TenantContext, Self::Error>; async fn validate_tenant(&self, tenant_id: &str) -> Result<bool, Self::Error>; async fn get_all_tenants(&self) -> Result<Vec<String>, Self::Error>; } }
This pattern enables:
- Pluggable authentication strategies
- Credential-to-tenant mapping
- Dynamic tenant discovery
- Authentication audit trails
Context Propagation Pattern
Request contexts carry tenant information through all operations:
#![allow(unused)] fn main() { pub struct RequestContext { pub request_id: String, tenant_context: Option<TenantContext>, } impl RequestContext { pub fn with_tenant_generated_id(tenant_context: TenantContext) -> Self; pub fn tenant_id(&self) -> Option<&str>; pub fn is_multi_tenant(&self) -> bool; pub fn can_perform_operation(&self, operation: &str) -> bool; } }
This ensures:
- Automatic tenant scoping
- Permission validation
- Audit trail continuity
- Consistent isolation
URL Generation Strategy Pattern
Flexible tenant URL patterns:
#![allow(unused)] fn main() { pub enum TenantStrategy { SingleTenant, // https://api.com/v2/Users/123 Subdomain, // https://tenant.api.com/v2/Users/123 PathBased, // https://api.com/tenant/v2/Users/123 } pub fn generate_ref_url(&self, tenant_id: Option<&str>, resource_type: &str, resource_id: &str) -> Result<String, ScimError>; }
This provides:
- Deployment flexibility
- Customer preferences accommodation
- Migration path support
- SCIM $ref compliance
Storage Isolation Pattern
Multi-tenant providers ensure data separation:
#![allow(unused)] fn main() { pub trait MultiTenantProvider: ResourceProvider { fn effective_tenant_id(&self, context: &RequestContext) -> String; fn tenant_scoped_key(&self, tenant_id: &str, resource_type: &str, resource_id: &str) -> String; fn tenant_scoped_prefix(&self, tenant_id: &str, resource_type: &str) -> String; } }
This guarantees:
- Data isolation at storage level
- Consistent key generation
- Tenant-scoped queries
- Cross-tenant access prevention
Integration with Other Components
SCIM Server Integration
The SCIM Server provides multi-tenant orchestration:
- Automatic Tenant Handling: All operations automatically scoped to tenant context
- URL Generation: Server configuration drives tenant-aware URL generation
- Permission Enforcement: Tenant permissions validated before operations
- Resource Limits: Tenant quotas enforced during resource creation
Resource Provider Integration
Resource Providers implement tenant isolation:
- Context Propagation: Tenant context flows through all provider operations
- Scoped Operations: All CRUD operations automatically tenant-scoped
- Limit Enforcement: Resource limits checked during creation operations
- Audit Integration: Tenant information included in all audit logs
Storage Provider Integration
Storage layer ensures tenant data separation:
- Key Prefixing: All storage keys include tenant identifiers
- Query Scoping: List and search operations automatically scoped to tenant
- Batch Operations: Multi-resource operations maintain tenant boundaries
- Migration Support: Tenant data can be moved between storage backends
Resource Integration
Resources maintain tenant awareness:
- Metadata Injection: Tenant information included in resource metadata
- Reference Generation: $ref fields use tenant-specific URLs
- Version Control: Tenant-scoped version management
- Schema Extensions: Tenant-specific schema customizations supported
Security Considerations
Isolation Levels
Three levels of tenant isolation:
#![allow(unused)] fn main() { pub enum IsolationLevel { Strict, // Complete separation, no shared resources Standard, // Shared infrastructure, separate data Shared, // Some resources may be shared between tenants } }
Each level provides different security and resource sharing characteristics.
Permission Management
Granular tenant permissions:
#![allow(unused)] fn main() { pub struct TenantPermissions { pub can_create: bool, pub can_read: bool, pub can_update: bool, pub can_delete: bool, pub can_list: bool, pub max_users: Option<usize>, pub max_groups: Option<usize>, } }
Enables fine-grained control over tenant capabilities.
Credential Security
- Secure Resolution: TenantResolver implementations should use secure credential storage
- Rate Limiting: Built-in rate limiting prevents authentication attacks
- Audit Logging: All authentication attempts logged for security monitoring
- Token Validation: Support for various authentication schemes (API keys, JWT, OAuth)
Best Practices
1. Choose Appropriate Tenant Strategy
Select the tenant strategy based on deployment requirements:
#![allow(unused)] fn main() { // Good: Match strategy to deployment model let saas_server = ScimServerBuilder::new(provider) .with_tenant_strategy(TenantStrategy::Subdomain) // Clear tenant separation .build()?; let enterprise_server = ScimServerBuilder::new(provider) .with_tenant_strategy(TenantStrategy::PathBased) // Internal division structure .build()?; // Avoid: Using single tenant for multi-customer SaaS let wrong_server = ScimServerBuilder::new(provider) .with_tenant_strategy(TenantStrategy::SingleTenant) // No tenant isolation .build()?; }
2. Implement Robust Tenant Resolution
Use secure and efficient tenant resolution:
#![allow(unused)] fn main() { // Good: Database-backed with caching struct DatabaseTenantResolver { db: DatabasePool, cache: Arc<RwLock<HashMap<String, TenantContext>>>, } impl TenantResolver for DatabaseTenantResolver { async fn resolve_tenant(&self, credential: &str) -> Result<TenantContext, Self::Error> { // Check cache first if let Some(context) = self.cache.read().await.get(credential) { return Ok(context.clone()); } // Query database with secure credential comparison let context = self.db.get_tenant_by_credential(credential).await?; // Cache result self.cache.write().await.insert(credential.to_string(), context.clone()); Ok(context) } } // Avoid: Hardcoded credentials in production let static_resolver = StaticTenantResolver::new(); // Only for testing/examples }
3. Set Appropriate Resource Limits
Define realistic tenant resource limits:
#![allow(unused)] fn main() { // Good: Tiered limits based on customer plan let basic_permissions = TenantPermissions { max_users: Some(100), max_groups: Some(10), can_delete: false, // Prevent accidental data loss ..Default::default() }; let enterprise_permissions = TenantPermissions { max_users: Some(10000), max_groups: Some(500), can_delete: true, // Full capabilities for enterprise ..Default::default() }; // Avoid: Unlimited resources without business justification let dangerous_permissions = TenantPermissions { max_users: None, // Could lead to resource exhaustion max_groups: None, ..Default::default() }; }
4. Use Proper Isolation Levels
Choose isolation level based on security requirements:
#![allow(unused)] fn main() { // Good: Strict isolation for sensitive data let healthcare_context = TenantContext::new("hospital".to_string(), "health-client".to_string()) .with_isolation_level(IsolationLevel::Strict); // HIPAA compliance let internal_context = TenantContext::new("internal".to_string(), "internal-client".to_string()) .with_isolation_level(IsolationLevel::Standard); // Standard business use let dev_context = TenantContext::new("development".to_string(), "dev-client".to_string()) .with_isolation_level(IsolationLevel::Shared); // Development flexibility // Avoid: One-size-fits-all isolation // Different tenants have different security requirements }
5. Handle Multi-Tenant Errors Gracefully
Provide clear error messages for tenant issues:
#![allow(unused)] fn main() { // Good: Specific error handling match server.create_resource("User", data, &context).await { Ok(user) => Ok(user), Err(ScimError::TenantLimitExceeded { limit, current }) => { HttpResponse::PaymentRequired() .json(json!({ "error": "tenant_limit_exceeded", "message": format!("User limit of {} exceeded (current: {})", limit, current) })) }, Err(ScimError::TenantNotFound { tenant_id }) => { HttpResponse::Unauthorized() .json(json!({ "error": "invalid_tenant", "message": "Tenant not found or inactive" })) }, Err(e) => handle_other_errors(e), } // Avoid: Generic error handling that loses tenant context }
When to Use Multi-Tenant Architecture
Primary Scenarios
- Software-as-a-Service (SaaS): Multiple customers sharing infrastructure
- Enterprise Divisions: Large organizations with multiple business units
- Development Environments: Separate dev/staging/production environments
- Geographic Regions: Compliance-driven data residency requirements
- White-Label Solutions: Customer-specific branding and configuration
Implementation Strategies
Scenario | Strategy | Complexity | Isolation |
---|---|---|---|
SaaS Multi-Customer | Subdomain | Medium | High |
Enterprise Divisions | Path-Based | Low | Medium |
Environment Separation | Path-Based | Low | High |
Geographic Regions | Separate Deployments | High | Very High |
White-Label | Single Tenant per Domain | Medium | Very High |
Comparison with Alternative Approaches
Approach | Isolation | Scalability | Complexity | Compliance |
---|---|---|---|---|
Multi-Tenant Architecture | β Complete | β High | Medium | β Excellent |
Separate Deployments | β Perfect | β οΈ Limited | High | β Excellent |
Database Schemas | β οΈ Good | β High | Low | β οΈ Good |
Application Logic Only | β Poor | β High | Low | β Poor |
The Multi-Tenant Architecture provides the optimal balance of isolation, scalability, and operational simplicity for identity management scenarios requiring tenant separation.
Migration and Evolution
Single to Multi-Tenant Migration
The architecture supports gradual migration:
- Start Single-Tenant: Begin with
TenantStrategy::SingleTenant
- Add Default Tenant: Migrate existing data to "default" tenant context
- Enable Multi-Tenancy: Switch to
TenantStrategy::PathBased
orTenantStrategy::Subdomain
- Add New Tenants: Register additional tenants without affecting existing data
Tenant Strategy Evolution
Tenant strategies can evolve as requirements change:
- Development β Production: Move from
PathBased
toSubdomain
for customer isolation - Single β Multi: Enable multi-tenancy without breaking existing integrations
- Subdomain β Custom: Transition to customer-specific domains as business grows
The Multi-Tenant Architecture in SCIM Server provides enterprise-grade multi-tenancy that scales from development environments to global SaaS platforms, ensuring complete tenant isolation while maintaining operational simplicity and SCIM compliance.
Referential Integrity
Referential integrity in SCIM systems involves managing data dependencies between Users and Groups while maintaining consistency across multiple identity providers and clients. This page outlines the SCIM server's principled approach to handling these challenges.
Value Proposition
The SCIM server's referential integrity stance provides:
- Protocol Compliance: Strict adherence to SCIM 2.0 specifications for group membership management
- Client Authority: Clear delineation of responsibilities between server and client systems
- Scalable Architecture: Design that works across single and multi-client environments
- Operational Clarity: Well-defined boundaries for what the server does and doesn't manage
- Integration Flexibility: Support for diverse IdP ecosystems without imposing rigid constraints
The Referential Integrity Challenge
Single Source of Truth Complexity
In traditional identity management, a single Identity Provider (IdP) like Active Directory serves as the authoritative source for user and group data. However, modern SCIM deployments often involve:
- Multiple SCIM Clients: Different systems provisioning to the same SCIM server
- Federated Identity Sources: Various HR systems, directories, and applications
- Hybrid Environments: Mix of cloud and on-premises identity sources
- Temporal Inconsistencies: Different provisioning schedules and update cycles
Data Dependency Scenarios
Scenario 1: User Deletion
βββββββββββββββββββ βββββββββββββββββββ
β IdP-A deletes β β Group still β
β User "john.doe" βββββΆβ references β
β β β deleted user β
βββββββββββββββββββ βββββββββββββββββββ
Scenario 2: Group Membership Changes
βββββββββββββββββββ βββββββββββββββββββ
β IdP-A removes β β IdP-B adds same β
β user from Group βββββΆβ user to Group β
β simultaneously β β simultaneously β
βββββββββββββββββββ βββββββββββββββββββ
Scenario 3: Nested Group Dependencies
βββββββββββββββββββ βββββββββββββββββββ
β Parent Group β β Child Group β
β references βββββΆβ gets deleted β
β Child Group β β by different β
β β β client β
βββββββββββββββββββ βββββββββββββββββββ
SCIM 2.0 Protocol Foundation
Group-Centric Membership Model
The SCIM 2.0 specification establishes clear principles for referential integrity:
Groups Resource as Authority
- Group membership is managed through the
Group
resource'smembers
attribute - The
members
attribute contains authoritative member references - All membership changes must flow through Group resource operations
User Groups as Read-Only
- The User resource's
groups
attribute is read-only - User groups are derived from Group memberships, not directly managed
- Clients cannot modify user group memberships via User resource operations
Example: Proper Group Membership Management
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"id": "engineering-team",
"displayName": "Engineering Team",
"members": [
{
"value": "user-123",
"$ref": "https://example.com/v2/Users/user-123",
"type": "User",
"display": "John Doe"
},
{
"value": "user-456",
"$ref": "https://example.com/v2/Users/user-456",
"type": "User",
"display": "Jane Smith"
}
]
}
Multi-Valued Attribute Semantics
SCIM's multi-valued attribute design supports referential relationships:
- Resource References:
$ref
fields provide URI-based resource linking - Type Indicators:
type
field distinguishes User vs Group members - Display Names: Human-readable references for operational clarity
- Value Fields: Canonical resource identifiers
SCIM Server's Principled Stance
Core Philosophy: Client Authority
The SCIM server adopts a client authority model where:
- SCIM Clients (IdPs) maintain authoritative control over referential integrity
- The server validates protocol compliance, not business logic consistency
- Cross-resource relationship enforcement is the client's responsibility
- The server provides mechanisms, clients provide policies
What the Server Does
Protocol Validation
- Validates that member references use proper ResourceId format
- Ensures Group.members follows SCIM multi-valued attribute structure
- Verifies that member types ("User", "Group") are recognized values
- Maintains consistent JSON schema compliance
Resource Operation Support
- Processes Group resource CREATE/UPDATE/DELETE operations
- Handles member addition/removal through Group resource modifications
- Supports PATCH operations for incremental membership changes
- Provides proper HTTP status codes and error responses
Version Management
- Tracks resource versions for optimistic concurrency control
- Enables conditional operations to prevent lost updates
- Maintains version consistency across resource modifications
What the Server Does NOT Do
Cross-Resource Validation
- Does not verify that referenced User resources exist
- Does not prevent creation of groups with non-existent members
- Does not validate that User.groups matches Group.members
Cascading Operations
- Does not automatically remove users from groups when users are deleted
- Does not cascade group deletions to remove nested group references
- Does not synchronize User.groups when Group.members changes
Multi-Client Coordination
- Does not resolve conflicts between competing SCIM clients
- Does not enforce "last writer wins" or other conflict resolution policies
- Does not maintain client priority or authorization hierarchies
Client Responsibilities
Single-Client Environments
When one SCIM client manages all resources:
Consistency Maintenance
- Ensure User deletions trigger Group membership cleanup
- Maintain User.groups derivation from Group.members relationships
- Handle nested group dependencies appropriately
Error Recovery
- Implement retry logic for failed referential operations
- Provide reconciliation processes for inconsistent states
- Monitor for orphaned references and clean them up
Multi-Client Environments
When multiple SCIM clients operate against the same server:
External Coordination Required
- Implement client-side coordination mechanisms outside SCIM
- Use external message queues, databases, or orchestration systems
- Establish client priority and conflict resolution policies
Common Coordination Patterns
Pattern 1: Master-Slave Hierarchy
βββββββββββββββββββ βββββββββββββββββββ
β Primary IdP β β Secondary IdPs β
β (HR System) βββββΆβ defer to β
β authoritative β β primary for β
β for users β β conflicts β
βββββββββββββββββββ βββββββββββββββββββ
Pattern 2: Functional Separation
βββββββββββββββββββ βββββββββββββββββββ
β IdP-A manages β β IdP-B manages β
β User lifecycle β β Group β
β (create/delete) β β memberships β
βββββββββββββββββββ βββββββββββββββββββ
Pattern 3: External Orchestrator
βββββββββββββββββββ βββββββββββββββββββ
β Identity β β Multiple IdPs β
β Orchestrator βββββΆβ coordinate β
β coordinates β β through β
β all changes β β orchestrator β
βββββββββββββββββββ βββββββββββββββββββ
Implementation in SCIM Server
Group Members Architecture
The server's GroupMembers
value object properly models SCIM group membership:
#![allow(unused)] fn main() { // Type-safe group member representation pub struct GroupMember { value: ResourceId, // Required: member resource ID display: Option<String>, // Optional: human-readable name member_type: Option<String>, // Optional: "User" or "Group" } // Collection type with validation pub type GroupMembers = MultiValuedAttribute<GroupMember>; }
Design Benefits
- Compile-time validation of member reference format
- Support for both User and Group members (nested groups)
- SCIM-compliant JSON serialization with
$ref
fields - Integration with the server's value object architecture
Resource Provider Integration
ResourceProvider implementations handle group operations:
#![allow(unused)] fn main() { // Group creation with members async fn create_resource( &self, resource_type: "Group", data: group_with_members_json, context: &RequestContext, ) -> Result<VersionedResource, Self::Error> // Group membership updates async fn update_resource( &self, resource_type: "Group", id: "engineering-team", data: updated_members_json, expected_version: Some(version), context: &RequestContext, ) -> Result<VersionedResource, Self::Error> }
Implementation Boundaries
- Validates JSON structure and member format
- Does not verify referenced users exist in storage
- Processes membership changes as requested by client
- Returns appropriate errors for malformed requests only
Industry IdP Behavior Patterns
Okta
- Single Source Model: Okta maintains authoritative user/group data
- Push-Based Sync: Pushes complete group memberships to SCIM servers
- Consistency Expectation: Expects SCIM server to store what's pushed
- Conflict Handling: Limited multi-client coordination capabilities
Microsoft Entra (Azure AD)
- Directory Authority: Azure AD as single source of truth
- Group-Centric Operations: Manages membership through Group resources
- Telemetry Support: Provides detailed error reporting for reconciliation
- Enterprise Features: Advanced conflict detection and reporting
Ping Identity
- Flexible Architecture: Supports complex multi-source scenarios
- Directory Integration: Can act as both SCIM client and server
- Custom Reconciliation: Allows scripted consistency checks
- Enterprise Coordination: Advanced tools for multi-client environments
Common Pattern: Server Delegation
All major IdPs expect the SCIM server to:
- Store the data as provided by the client
- Validate protocol compliance, not business logic
- Provide mechanisms for the IdP to maintain consistency
- Report errors for malformed requests, not referential issues
Operational Considerations
Monitoring and Observability
Recommended Metrics
- Group membership operation rates
- Member reference validation errors
- Resource not found errors (indicating potential orphans)
- Multi-client operation overlaps
Alerting Strategies
- High rates of referential errors may indicate client coordination issues
- Sudden spikes in group operations may indicate bulk synchronization
- Monitor for patterns indicating multiple clients modifying same resources
Diagnostic Capabilities
Optional Enhancement: Consistency Reports While not enforcing referential integrity, the server could provide optional diagnostic endpoints:
GET /v2/Diagnostics/ReferentialConsistency?tenantId=acme-corp
{
"orphanedGroupMembers": [
{
"groupId": "engineering-team",
"memberId": "deleted-user-123",
"issue": "Member references non-existent User resource"
}
],
"inconsistentUserGroups": [
{
"userId": "john.doe",
"issue": "User.groups does not match Group.members references"
}
]
}
Benefits
- Helps clients identify and resolve consistency issues
- Provides operational visibility without enforcing constraints
- Supports client-side reconciliation processes
Best Practices for SCIM Server Operators
1. Document Client Responsibilities Clearly
Provide explicit documentation to IdP integration teams about:
- Who is responsible for referential integrity (they are)
- What the server validates (protocol format) vs. doesn't (business consistency)
- Recommended patterns for multi-client coordination
- Error codes that indicate client-side issues to resolve
2. Design for Protocol Compliance
Focus server implementation on being an excellent SCIM protocol implementation:
- Strict adherence to SCIM 2.0 specifications
- Comprehensive support for Group resource operations
- Robust error handling with appropriate HTTP status codes
- Clear API documentation and examples
3. Support Client Success
Provide tools and capabilities that help clients maintain consistency:
- Version-based concurrency control to prevent lost updates
- Detailed error messages for malformed requests
- Optional diagnostic capabilities for troubleshooting
- Clear examples of proper Group membership management
4. Avoid Over-Engineering
Resist the temptation to solve identity management problems beyond SCIM's scope:
- Don't build custom conflict resolution algorithms
- Don't impose business logic constraints on clients
- Don't attempt to coordinate between multiple clients
- Focus on being the best possible SCIM server, not an identity management system
Comparison with Alternative Approaches
Approach | Protocol Compliance | Operational Complexity | Client Flexibility | Scalability |
---|---|---|---|---|
Client Authority (Recommended) | β High | β Low | β High | β High |
Server-Enforced Integrity | β οΈ Medium | β High | β Low | β οΈ Medium |
Hybrid Validation | β οΈ Medium | β οΈ Medium | β οΈ Medium | β οΈ Medium |
No Validation | β Low | β Low | β High | β High |
The Client Authority model provides the best balance of SCIM compliance, operational simplicity, and real-world scalability.
Future Considerations
SCIM Protocol Evolution
The SCIM working group continues to evolve the specification. Future versions may include:
- Enhanced referential integrity guidance
- Standardized multi-client coordination patterns
- Improved error reporting for consistency issues
- Optional server-side validation capabilities
Integration Ecosystem Maturity
As the SCIM ecosystem matures:
- IdPs are developing better coordination mechanisms
- Industry patterns for multi-client scenarios are emerging
- Standardized orchestration tools are becoming available
- Best practices for complex identity topologies are evolving
Conclusion
The SCIM server's referential integrity stance prioritizes protocol compliance over data policing. By clearly delineating responsibilitiesβwith clients maintaining authoritative control over data consistency and the server providing robust protocol implementationβthis approach scales across diverse deployment scenarios while remaining true to SCIM's fundamental design principles.
This client authority model acknowledges that referential integrity is fundamentally an identity management challenge, not a protocol challenge. The server's role is to be an excellent SCIM protocol implementation that enables clients to build sophisticated identity management solutions, rather than attempting to solve those challenges directly.
For organizations deploying SCIM servers, this approach provides a clear operational model: invest in robust client-side identity management processes and use the SCIM server as a reliable, compliant protocol gateway that faithfully executes the identity operations you design.
Identity Provider Idiosyncrasies
SCIM implementations by different identity providers (IdPs) frequently introduce their own unique idiosyncrasies that deviate from the standard SCIM 2.0 specification. Understanding these variations is crucial for building robust SCIM integrations that can handle real-world deployments across diverse identity provider ecosystems.
See the SCIM Server Integration Guide for practical implementation approaches.
Value Proposition
Understanding IdP idiosyncrasies enables:
- Robust Integrations: Handle real-world SCIM variations without breaking
- Faster Onboarding: Anticipate common integration challenges during customer setup
- Better Error Handling: Provide meaningful feedback for provider-specific issues
- Strategic Planning: Make informed decisions about which IdPs to prioritize
- Maintenance Efficiency: Categorize and address similar issues systematically
- Customer Success: Reduce integration friction and support burden
Architecture Overview
IdP idiosyncrasies manifest across multiple layers of SCIM operations:
SCIM Integration Stack (IdP Variations Impact All Layers)
βββ Protocol Layer (HTTP/REST Variations)
βββ Schema Layer (Attribute & Extension Differences)
βββ Resource Layer (User/Group Handling Variations)
βββ Operation Layer (CRUD Operation Differences)
βββ Data Layer (Storage & Synchronization Issues)
β
Common Idiosyncrasy Categories:
βββ Attribute Schema Variations
βββ User Identification & Uniqueness
βββ Group Handling & Fragmentation
βββ User Lifecycle & Status Changes
βββ Request Processing & Scaling
βββ Onboarding & Provisioning Logic
Common IdP Idiosyncrasies
Attribute Schema Variations
Different IdPs extend, modify, or interpret the standard SCIM schema in unique ways:
Custom Attributes and Extensions
- Inconsistent Naming: Some use
surname
while others uselastName
orlast_name
- Data Type Variations: Phone numbers as strings vs. structured objects
- Extension Prefixes: Azure Entra ID requires specific schema extension URNs
- Nesting Differences: Okta flattens complex attributes, while others maintain hierarchy
Schema Processing Challenges
- Required vs. Optional: IdPs may require fields that are optional in the spec
- Custom Field Limits: Rippling restricts the number of custom attributes allowed
- Validation Rules: Different regex patterns or length constraints for the same field types
#![allow(unused)] fn main() { // Example: Handling attribute name variations match idp_provider { "okta" => user.last_name = value, "azure" => user.surname = value, "google" => user.family_name = value, _ => user.name.family_name = value, // SCIM standard } }
User Identification and Uniqueness
IdPs vary significantly in how they handle user identity and unique identifiers:
Identifier Strategy Differences
- External ID Usage: Some consistently use
externalId
, others preferuserName
or custom IDs - Uniqueness Scope: Global uniqueness vs. tenant-scoped uniqueness interpretations
- Identifier Stability: Whether identifiers change during user lifecycle events
Common Conflicts
- HTTP 409 Errors: Duplication conflicts due to inconsistent identifier handling
- Orphaned Resources: Users created with one identifier strategy, updated with another
- Case Sensitivity: Some IdPs treat identifiers as case-sensitive, others don't
Group Handling and Fragmentation
Group management approaches vary dramatically across providers:
Membership Management
- Update Strategies: Add/remove individual members vs. full membership replacement
- Role Promotions: How group membership changes during organizational role changes
- Nested Groups: Support for hierarchical group structures varies widely
Group Lifecycle Issues
- Deletion Prerequisites: Some require manual user removal before group deletion
- Membership Synchronization: Timing issues between user and group operations
- Group Fragmentation: Partial updates leading to inconsistent group states
These variations highlight the importance of understanding Referential Integrity principles when working with different IdP implementations.
#![allow(unused)] fn main() { // Example: Provider-specific group deletion logic async fn delete_group(&self, group_id: &str, provider: &IdpProvider) -> Result<()> { match provider { IdpProvider::AzureAD => { // Azure requires removing all members first self.remove_all_members(group_id).await?; self.delete_empty_group(group_id).await } IdpProvider::Okta => { // Okta handles member cleanup automatically self.delete_group_direct(group_id).await } _ => self.delete_group_standard(group_id).await, } } }
User Lifecycle and Status Changes
IdPs implement user status management differently:
Status Change Variations
- Deactivation Methods: Delete vs. suspend vs. set
active: false
- Reactivation Process: Some allow reactivation, others require recreation
- Status Semantics: Different meanings for "inactive," "suspended," and "disabled"
Permission Synchronization
- Access Rights: How quickly access changes propagate across systems
- Audit Trails: Variations in lifecycle event logging and reporting
- Compliance Requirements: Different retention policies for deactivated users
Request Processing and Scaling
Large-scale deployments reveal significant processing differences:
Bulk Operations
- Batch Size Limits: Varying limits on bulk user provisioning requests
- Rate Limiting: Different throttling strategies and recovery mechanisms
- Error Handling: Partial failure handling in bulk operations
Performance Characteristics
- Timeout Behavior: Request timeout handling varies significantly
- Retry Strategies: Different backoff algorithms and retry limits
- Concurrent Requests: Varying levels of parallel operation support
Onboarding and Provisioning Logic
New customer integration reveals provider-specific requirements:
Configuration Requirements
- Attribute Mapping: Custom field mapping from IdP schema to application model
- Endpoint Configuration: Provider-specific URL patterns and authentication methods
- Feature Negotiation: Determining which SCIM features are actually supported
Integration Complexity
- Authentication Variations: OAuth, bearer tokens, mutual TLS, API keys
- Webhook Support: Event notification capabilities and formats vary
- Error Reporting: Different error message formats and diagnostic information
Classification Framework
The following table categorizes common idiosyncrasies by their functional impact:
Category | Typical Variations | Implementation Impact | Mitigation Strategy |
---|---|---|---|
Attribute Schema | Custom attributes, naming inconsistencies, nesting differences | Requires mapping logic, interoperability risk | Schema transformation layers, attribute dictionaries |
User Identification | externalId vs other identifiers, duplication handling | Identity conflicts, HTTP 409 errors | Flexible identifier resolution, conflict detection |
Group Management | Membership updates, deletion prerequisites, role handling | Group fragmentation, manual cleanup required | Provider-specific group handlers, state validation |
Lifecycle Status | Deactivation methods, reactivation support, status semantics | Security gaps, access control inconsistencies | Unified status mapping, audit trail normalization |
Request Processing | Bulk limits, rate limiting, timeout behavior | Performance bottlenecks, missed operations | Adaptive batching, provider-aware retry logic |
Onboarding Logic | Attribute mapping, authentication, configuration | Time-consuming setup, error-prone integration | Configuration templates, automated discovery |
Best Practices for Handling Idiosyncrasies
1. Design for Variation from Day One
#![allow(unused)] fn main() { trait IdpAdapter { async fn create_user(&self, user: ScimUser) -> Result<ScimUser>; async fn update_user(&self, id: &str, user: ScimUser) -> Result<ScimUser>; async fn delete_user(&self, id: &str) -> Result<()>; // Provider-specific customization points fn normalize_attributes(&self, user: &mut ScimUser); fn handle_identifier_conflicts(&self, conflict: IdentifierConflict) -> Resolution; } }
2. Implement Comprehensive Testing
- Provider-Specific Test Suites: Separate test scenarios for each major IdP
- Real-World Data: Use actual IdP export data in testing
- Regression Detection: Automated detection of provider behavior changes
3. Build Adaptive Configuration
- Runtime Discovery: Detect provider capabilities automatically where possible
- Feature Flags: Enable/disable functionality based on provider support
- Graceful Degradation: Fallback behavior for unsupported operations
4. Maintain Provider Profiles
- Documentation: Detailed profiles of known idiosyncrasies per provider
- Version Tracking: Monitor provider API changes and behavioral updates
- Community Knowledge: Leverage shared experience across integration teams
Integration with SCIM Server
The SCIM Server library's architecture provides a solid foundation for handling IdP idiosyncrasies through its flexible design patterns. Current capabilities enable library users to craft their own logic for managing provider variations, while future versions will provide comprehensive built-in support for common identity providers.
Current Capabilities
SCIM Server's existing architecture offers several key features that facilitate handling IdP idiosyncrasies:
Schema Extensions and Custom Attributes
The library's schema system supports provider-specific extensions and custom attributes:
#![allow(unused)] fn main() { // Handle provider-specific schema extensions let azure_extension = SchemaExtension::new() .with_urn("urn:ietf:params:scim:schemas:extension:enterprise:2.0:User") .with_attributes(azure_custom_attributes); server_builder.add_schema_extension(azure_extension); }
Field Mapping in Storage Providers
Storage providers can implement field mapping logic to handle attribute name variations and data transformations:
#![allow(unused)] fn main() { #[async_trait] impl StorageProvider for IdpAwareStorageProvider { async fn create_user(&self, user: ScimUser, context: &RequestContext) -> Result<ScimUser> { // Apply provider-specific field mapping let mapped_user = self.map_attributes_for_provider(&user, &context.idp_type)?; self.inner_storage.create_user(mapped_user, context).await } } }
Flexible Resource Provider Architecture
The resource provider pattern allows complete customization of SCIM operations while maintaining standard interfaces:
#![allow(unused)] fn main() { use scim_server::prelude::*; #[derive(Debug)] pub struct IdpAdaptedProvider { base_provider: Box<dyn ResourceProvider>, idp_config: IdpConfiguration, } #[async_trait] impl ResourceProvider for IdpAdaptedProvider { async fn create_user(&self, user: ScimUser, context: &RequestContext) -> Result<ScimUser> { // Apply IdP-specific preprocessing let adapted_user = self.adapt_for_provider(user, &self.idp_config)?; // Use base provider with adapted data self.base_provider.create_user(adapted_user, context).await } } }
Operation Handler Customization
Operation handlers can be customized to implement provider-specific business logic:
#![allow(unused)] fn main() { let operation_handler = OperationHandler::new() .with_pre_create_hook(validate_provider_requirements) .with_post_update_hook(sync_provider_specific_fields) .with_delete_validation(check_provider_deletion_rules); }
Future Roadmap: Comprehensive IdP Support
Future versions of SCIM Server (to be implemented prior to the version 1.0.0 release), will provide users with a much more comprehensive approach to managing idiosyncrasies for the major identity providers:
Provider Profiles
Built-in profiles for major IdPs with pre-configured handling of known idiosyncrasies:
#![allow(unused)] fn main() { // Future API - Provider profiles with built-in idiosyncrasy handling let server = ScimServer::builder() .with_provider_profile(IdpProfile::AzureAD { handle_extension_attributes: true, require_external_id: true, group_deletion_strategy: GroupDeletionStrategy::RemoveMembersFirst, }) .with_provider_profile(IdpProfile::Okta { flatten_complex_attributes: true, bulk_operation_limits: BulkLimits::new(100, Duration::from_secs(30)), }) .build(); }
Adaptive Configuration via Client Context
Provider-specific configuration will be automatically applied through the request context:
#![allow(unused)] fn main() { // Future API - Adaptive configuration based on client context #[derive(Debug)] pub struct ClientContext { pub idp_type: IdentityProvider, pub idp_version: Option<String>, pub supported_features: ProviderCapabilities, pub adaptation_rules: AdaptationProfile, } // Automatic adaptation based on client context impl RequestContext { pub fn adapt_for_provider(&self, resource: ScimResource) -> Result<ScimResource> { self.client_context .adaptation_rules .apply_transformations(resource) } } }
Principled Idiosyncrasy Management Tools
Structured tools for managing provider-specific behaviors in a consistent way:
#![allow(unused)] fn main() { // Future API - Structured idiosyncrasy management pub struct IdiosyncracyManager { attribute_mapper: AttributeMapper, lifecycle_adapter: LifecycleAdapter, validation_rules: ValidationRuleSet, error_translator: ErrorTranslator, } impl IdiosyncracyManager { pub fn for_provider(provider: IdentityProvider) -> Self { // Load pre-configured rules for known providers Self::load_provider_profile(provider) } pub async fn process_request<T>(&self, request: T, context: &RequestContext) -> Result<T> { // Apply all necessary transformations automatically self.attribute_mapper.transform(request, context) } } }
Automatic Feature Detection and Fallback
Smart detection of provider capabilities with graceful degradation:
#![allow(unused)] fn main() { // Future API - Automatic capability detection pub struct ProviderCapabilities { pub supports_bulk_operations: bool, pub max_bulk_size: Option<usize>, pub supports_patch_operations: bool, pub custom_attributes: Vec<AttributeDefinition>, pub lifecycle_behaviors: LifecycleBehaviorSet, } // Automatic fallback for unsupported features impl ScimServer { async fn auto_detect_capabilities(&self, provider_endpoint: &str) -> ProviderCapabilities { // Probe provider capabilities and configure accordingly } } }
Migration Path
The future enhancements will be designed to be backward compatible with existing custom implementations:
- Gradual Adoption: Existing custom logic can be gradually replaced with built-in provider profiles
- Override Capability: Built-in profiles can be customized or overridden for specific use cases
- Fallback Support: Custom implementations remain fully supported alongside built-in profiles
- Configuration Migration: Tools to migrate existing custom configurations to new provider profile format
Monitoring and Observability
Track idiosyncrasy impact in production:
Metrics to Monitor
- Integration Success Rates: Per-provider success/failure ratios
- Error Categories: Classification of failures by idiosyncrasy type
- Performance Variations: Response times and throughput per provider
- Configuration Drift: Detection of provider behavior changes
Alerting Strategies
- Provider-Specific Thresholds: Different alert levels for known problematic areas
- Trend Analysis: Detect degrading integration health over time
- Automatic Fallbacks: Circuit breakers for provider-specific failures
Future Considerations
Industry Standardization Efforts
- SCIM 2.1 Developments: Potential improvements to address common variations. The development of SCIM 2.1 is ongoing with active drafts addressing protocol enhancements such as more complex filtering, better bulk operation support, and improved synchronization mechanisms. Industry and community efforts focus on expanding capabilities while maintaining interoperability, but no finalized SCIM 2.1 standard is yet published as of mid-2025.
- Provider Collaboration: Working with IdPs to reduce unnecessary deviations
- Best Practice Guidelines: Industry-wide adoption of consistent implementations
SCIM Server Evolution
- Provider Profile Expansion: Additional built-in support for more identity providers
- Enhanced Client Context: Richer provider detection and adaptive configuration
- Automated Testing: Provider-specific test suites and validation tools
- Configuration Simplification: Reduced setup complexity through intelligent defaults
Conclusion
Identity Provider idiosyncrasies are an inevitable reality in SCIM integrations. By understanding common patterns, implementing adaptive architectures, and building comprehensive testing strategies, you can create robust integrations that handle real-world complexity while maintaining the benefits of standardized identity provisioning.
The key is to design for variation from the beginning, rather than treating idiosyncrasies as edge cases to be handled later. With proper planning and the right architectural patterns, these variations become manageable challenges rather than integration blockers.
Request Lifecycle & Context Management
This deep dive explores how requests flow through the SCIM server architecture from HTTP entry point to storage operations, with particular focus on context propagation, tenant resolution, and the integration points between components.
Overview
Understanding the request lifecycle is fundamental to working with SCIM Server, as it shows how all the individual components work together to process SCIM operations. This end-to-end view helps you make informed decisions about where to implement custom logic, how to handle errors, and where performance optimizations matter most.
Complete Request Flow
HTTP Request
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 1. HTTP Integration Layer (Your Web Framework) β
β β’ Extract SCIM request details (method, path, headers, body) β
β β’ Handle authentication (API keys, OAuth, etc.) β
β β’ Create initial request context β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ScimOperationRequest
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 2. Operation Handler Layer β
β β’ ScimOperationHandler processes structured SCIM request β
β β’ Validates SCIM protocol compliance β
β β’ Extracts operation metadata β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Validated Operation + RequestContext
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 3. Tenant Resolution & Context Enhancement β
β β’ TenantResolver maps credentials β TenantContext β
β β’ RequestContext enhanced with tenant information β
β β’ Permissions and isolation levels applied β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Tenant-Aware RequestContext
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 4. SCIM Server Orchestration β
β β’ Route operation to appropriate method β
β β’ Apply schema validation β
β β’ Handle concurrency control (ETag processing) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Validated Operation + Enhanced Context
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 5. Resource Provider Business Logic β
β β’ Apply business rules and transformations β
β β’ Handle resource-specific validation β
β β’ Coordinate with storage operations β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Storage Operations + Context
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 6. Storage Provider Data Persistence β
β β’ Apply tenant-scoped storage keys β
β β’ Perform actual data operations (CRUD) β
β β’ Handle storage-level errors and retries β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Storage Results
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 7. Response Assembly & Error Handling β
β β’ Convert storage results to SCIM resources β
β β’ Apply response transformations β
β β’ Handle errors with appropriate SCIM error responses β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ScimOperationResponse
HTTP Response (SCIM-compliant JSON)
Context Propagation Architecture
The RequestContext
is the backbone of the request lifecycle, carrying essential information through every layer of the system.
RequestContext Structure
#![allow(unused)] fn main() { pub struct RequestContext { pub request_id: String, // Unique identifier for tracing tenant_context: Option<TenantContext>, // Multi-tenant information } impl RequestContext { // Single-tenant constructor pub fn new(request_id: String) -> Self // Multi-tenant constructor pub fn with_tenant_generated_id(tenant_context: TenantContext) -> Self // Context queries pub fn tenant_id(&self) -> Option<&str> pub fn is_multi_tenant(&self) -> bool pub fn can_perform_operation(&self, operation: &str) -> bool } }
Context Creation Patterns
Single-Tenant Context
#![allow(unused)] fn main() { // Simple single-tenant setup let context = RequestContext::new("req-12345".to_string()); }
Multi-Tenant Context with Tenant Resolution
#![allow(unused)] fn main() { // Resolve tenant from authentication let tenant_context = tenant_resolver .resolve_tenant(api_key) .await?; let context = RequestContext::with_tenant_generated_id(tenant_context); }
Context with Custom Request ID
#![allow(unused)] fn main() { // Use correlation ID from HTTP headers let request_id = extract_correlation_id(&headers) .unwrap_or_else(|| generate_request_id()); let mut context = RequestContext::new(request_id); if let Some(tenant) = resolved_tenant { context = RequestContext::with_tenant(request_id, tenant); } }
Integration Layer Patterns
Web Framework Integration
The HTTP integration layer is where you connect SCIM Server to your web framework. Here's how different frameworks typically integrate:
Axum Integration Pattern
#![allow(unused)] fn main() { use axum::{extract::Path, http::HeaderMap, Json}; use scim_server::{ScimServer, RequestContext, ScimOperationHandler}; async fn scim_operation_handler( Path((tenant_id, resource_type, resource_id)): Path<(String, String, Option<String>)>, headers: HeaderMap, Json(body): Json<Value>, Extension(server): Extension<Arc<ScimServer<MyProvider>>>, Extension(tenant_resolver): Extension<Arc<dyn TenantResolver>>, ) -> Result<Json<Value>, ScimError> { // 1. Create operation request from HTTP details let operation_request = ScimOperationRequest::from_http( method, &path, headers, body )?; // 2. Resolve tenant context let api_key = extract_api_key(&headers)?; let tenant_context = tenant_resolver.resolve_tenant(&api_key).await?; let request_context = RequestContext::with_tenant_generated_id(tenant_context); // 3. Process through operation handler let operation_handler = ScimOperationHandler::new(&server); let response = operation_handler .handle_operation(operation_request, &request_context) .await?; // 4. Convert to HTTP response Ok(Json(response.into_json())) } }
Actix-Web Integration Pattern
#![allow(unused)] fn main() { use actix_web::{web, HttpRequest, HttpResponse, Result}; async fn scim_handler( req: HttpRequest, path: web::Path<(String, String)>, body: web::Json<Value>, server: web::Data<ScimServer<MyProvider>>, ) -> Result<HttpResponse> { // Similar pattern but with Actix-specific extractors let (tenant_id, resource_type) = path.into_inner(); // Extract and create context let context = create_request_context(&req, &tenant_id).await?; // Process operation let response = process_scim_operation( &server, &req.method(), &req.uri().path(), body.into_inner(), &context ).await?; Ok(HttpResponse::Ok().json(response)) } }
Operation Handler Integration
The ScimOperationHandler
provides a framework-agnostic way to process SCIM operations:
#![allow(unused)] fn main() { use scim_server::{ScimOperationHandler, ScimOperationRequest, ScimOperationResponse}; // Create operation handler let handler = ScimOperationHandler::new(&scim_server); // Process different operation types match operation_request.operation_type() { ScimOperation::Create => { let response = handler.handle_create( &operation_request.resource_type, operation_request.resource_data, &request_context ).await?; }, ScimOperation::GetById => { let response = handler.handle_get( &operation_request.resource_type, &operation_request.resource_id.unwrap(), &request_context ).await?; }, // ... other operations } }
Tenant Resolution Integration Points
Multi-tenant applications need to resolve tenant context early in the request lifecycle:
Database-Backed Tenant Resolution
#![allow(unused)] fn main() { use scim_server::multi_tenant::TenantResolver; pub struct DatabaseTenantResolver { db_pool: PgPool, cache: Arc<RwLock<HashMap<String, TenantContext>>>, } impl TenantResolver for DatabaseTenantResolver { type Error = DatabaseError; async fn resolve_tenant(&self, credential: &str) -> Result<TenantContext, Self::Error> { // 1. Check cache first if let Some(tenant) = self.cache.read().unwrap().get(credential) { return Ok(tenant.clone()); } // 2. Query database let tenant_record = sqlx::query_as!( TenantRecord, "SELECT tenant_id, client_id, permissions FROM tenants WHERE api_key = $1", credential ) .fetch_optional(&self.db_pool) .await? .ok_or(DatabaseError::TenantNotFound)?; // 3. Build tenant context let tenant_context = TenantContext::new( tenant_record.tenant_id, tenant_record.client_id ).with_permissions(tenant_record.permissions); // 4. Cache for future requests self.cache.write().unwrap() .insert(credential.to_string(), tenant_context.clone()); Ok(tenant_context) } } }
JWT-Based Tenant Resolution
#![allow(unused)] fn main() { pub struct JwtTenantResolver { jwt_secret: String, } impl TenantResolver for JwtTenantResolver { type Error = JwtError; async fn resolve_tenant(&self, token: &str) -> Result<TenantContext, Self::Error> { // 1. Validate and decode JWT let claims: TenantClaims = jsonwebtoken::decode( token, &DecodingKey::from_secret(self.jwt_secret.as_ref()), &Validation::default() )?.claims; // 2. Extract tenant information from claims let permissions = TenantPermissions { can_create: claims.permissions.contains("create"), can_update: claims.permissions.contains("update"), max_users: claims.limits.max_users, max_groups: claims.limits.max_groups, ..Default::default() }; // 3. Build tenant context Ok(TenantContext::new(claims.tenant_id, claims.client_id) .with_permissions(permissions) .with_isolation_level(claims.isolation_level)) } } }
Error Handling Patterns
Errors can occur at any stage of the request lifecycle. The SCIM Server provides structured error handling:
Error Propagation Flow
Storage Error β Resource Provider Error β SCIM Error β HTTP Error Response
Examples:
β’ Database connection failure β StorageError β ScimError::InternalError β 500
β’ Tenant not found β ResolverError β ScimError::Unauthorized β 401
β’ Resource not found β ResourceError β ScimError::NotFound β 404
β’ Validation failure β ValidationError β ScimError::BadRequest β 400
β’ Version conflict β ConcurrencyError β ScimError::PreconditionFailed β 412
Custom Error Handling
#![allow(unused)] fn main() { // Custom error mapper for your web framework impl From<ScimError> for HttpResponse { fn from(error: ScimError) -> Self { let (status_code, scim_error_response) = match error { ScimError::NotFound(msg) => ( StatusCode::NOT_FOUND, ScimErrorResponse::not_found(&msg) ), ScimError::TenantNotFound(tenant_id) => ( StatusCode::UNAUTHORIZED, ScimErrorResponse::unauthorized(&format!("Invalid tenant: {}", tenant_id)) ), ScimError::ValidationError(details) => ( StatusCode::BAD_REQUEST, ScimErrorResponse::bad_request_with_details(details) ), _ => ( StatusCode::INTERNAL_SERVER_ERROR, ScimErrorResponse::internal_error() ), }; HttpResponse::build(status_code).json(scim_error_response) } } }
Performance Considerations
Context Creation Optimization
#![allow(unused)] fn main() { // Avoid unnecessary tenant resolution for public endpoints pub async fn optimized_context_creation( api_key: Option<&str>, tenant_resolver: &Arc<dyn TenantResolver>, request_id: String, ) -> Result<RequestContext, ScimError> { match api_key { Some(key) => { // Only resolve tenant when needed let tenant_context = tenant_resolver.resolve_tenant(key).await?; Ok(RequestContext::with_tenant(request_id, tenant_context)) }, None => { // Single-tenant or public operation Ok(RequestContext::new(request_id)) } } } }
Async Best Practices
#![allow(unused)] fn main() { // Concurrent operations where possible pub async fn batch_operation_handler( operations: Vec<ScimOperationRequest>, context: &RequestContext, server: &ScimServer<impl ResourceProvider>, ) -> Vec<Result<ScimOperationResponse, ScimError>> { // Process operations concurrently let futures = operations.into_iter().map(|op| { let handler = ScimOperationHandler::new(server); handler.handle_operation(op, context) }); futures::future::join_all(futures).await } }
Debugging and Observability
Request Tracing
#![allow(unused)] fn main() { use tracing::{info, error, span, Level}; pub async fn traced_operation_handler( operation: ScimOperationRequest, context: &RequestContext, server: &ScimServer<impl ResourceProvider>, ) -> Result<ScimOperationResponse, ScimError> { let span = span!( Level::INFO, "scim_operation", request_id = %context.request_id, tenant_id = %context.tenant_id().unwrap_or("single-tenant"), operation_type = %operation.operation_type(), resource_type = %operation.resource_type ); async move { info!("Processing SCIM operation"); let result = ScimOperationHandler::new(server) .handle_operation(operation, context) .await; match &result { Ok(response) => info!( status = "success", resource_id = ?response.resource_id() ), Err(error) => error!( status = "error", error = %error, error_type = ?std::mem::discriminant(error) ), } result }.instrument(span).await } }
Integration Testing Patterns
End-to-End Request Testing
#![allow(unused)] fn main() { #[tokio::test] async fn test_complete_request_lifecycle() { // Setup let storage = InMemoryStorage::new(); let provider = StandardResourceProvider::new(storage); let server = ScimServer::new(provider).unwrap(); let tenant_resolver = StaticTenantResolver::new(); // Add test tenant let tenant_context = TenantContext::new("test-tenant".into(), "test-client".into()); tenant_resolver.add_tenant("test-api-key", tenant_context).await; // Simulate HTTP request let operation_request = ScimOperationRequest::create( "User", json!({ "userName": "test.user", "displayName": "Test User" }) ); // Resolve tenant (simulating middleware) let resolved_tenant = tenant_resolver.resolve_tenant("test-api-key").await.unwrap(); let context = RequestContext::with_tenant_generated_id(resolved_tenant); // Process operation let handler = ScimOperationHandler::new(&server); let response = handler.handle_operation(operation_request, &context).await.unwrap(); // Verify response assert!(response.is_success()); assert!(response.resource_id().is_some()); } }
Related Topics
- Multi-Tenant Architecture Patterns - Deep dive into tenant isolation strategies
- Resource Provider Architecture - Business logic layer implementation patterns
- Operation Handlers - Framework-agnostic request processing
- Multi-Tenant Architecture - Core multi-tenancy concepts
Next Steps
Now that you understand how requests flow through the system:
- Implement your HTTP integration layer using the patterns shown above
- Set up tenant resolution if building a multi-tenant system
- Add proper error handling and observability for production use
- Consider Resource Provider architecture for your business logic needs
Multi-Tenant Architecture Patterns
This deep dive explores end-to-end multi-tenant patterns in SCIM Server, from authentication and tenant resolution through storage isolation and URL generation strategies. It provides practical guidance for implementing robust multi-tenant SCIM systems that scale.
Overview
Multi-tenant architecture in SCIM Server involves several interconnected patterns that work together to provide complete tenant isolation. This document shows how these patterns combine to create production-ready multi-tenant systems.
Core Multi-Tenant Flow:
Client Request β Authentication β Tenant Resolution β Context Propagation β
Storage Isolation β Response Generation β Tenant-Specific URLs
Complete Multi-Tenant Request Flow
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 1. Authentication & Credential Extraction β
β β’ Extract API key, JWT token, or subdomain β
β β’ Validate credential format and signature β
β β’ Handle authentication failures β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Validated Credential
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 2. Tenant Resolution β
β β’ Map credential β TenantContext β
β β’ Load tenant permissions and limits β
β β’ Apply isolation level configuration β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β TenantContext
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 3. Request Context Enhancement β
β β’ Create RequestContext with tenant information β
β β’ Validate operation permissions β
β β’ Set up request tracing and audit context β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Tenant-Aware RequestContext
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 4. Storage Key Scoping β
β β’ Apply tenant prefix to all storage keys β
β β’ Enforce isolation level constraints β
β β’ Handle cross-tenant reference validation β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Tenant-Scoped Storage Operations
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 5. Resource Operations with Tenant Enforcement β
β β’ Apply tenant-specific resource limits β
β β’ Validate cross-tenant resource references β
β β’ Enforce tenant-specific schema extensions β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Tenant-Compliant Resources
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 6. Response Assembly with Tenant URLs β
β β’ Generate tenant-specific resource URLs β
β β’ Apply tenant branding and response customization β
β β’ Include tenant-aware pagination and filtering β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Tenant-Specific SCIM Response
Tenant Resolution Patterns
Pattern 1: API Key-Based Resolution
Most common for API-driven integrations:
#![allow(unused)] fn main() { use scim_server::multi_tenant::{TenantResolver, TenantContext}; use std::collections::HashMap; use tokio::sync::RwLock; pub struct ApiKeyTenantResolver { // In production: use database or distributed cache tenant_mappings: RwLock<HashMap<String, TenantContext>>, default_permissions: TenantPermissions, } impl ApiKeyTenantResolver { pub fn new() -> Self { Self { tenant_mappings: RwLock::new(HashMap::new()), default_permissions: TenantPermissions::default(), } } pub async fn register_tenant( &self, api_key: String, tenant_id: String, client_id: String, custom_permissions: Option<TenantPermissions>, ) -> Result<(), TenantError> { let permissions = custom_permissions.unwrap_or_else(|| self.default_permissions.clone()); let tenant_context = TenantContext::new(tenant_id, client_id) .with_permissions(permissions) .with_isolation_level(IsolationLevel::Standard); self.tenant_mappings.write().await .insert(api_key, tenant_context); Ok(()) } } impl TenantResolver for ApiKeyTenantResolver { type Error = TenantResolutionError; async fn resolve_tenant(&self, api_key: &str) -> Result<TenantContext, Self::Error> { self.tenant_mappings.read().await .get(api_key) .cloned() .ok_or(TenantResolutionError::TenantNotFound(api_key.to_string())) } async fn validate_tenant(&self, tenant_context: &TenantContext) -> Result<bool, Self::Error> { // Additional validation logic Ok(tenant_context.is_active() && !tenant_context.is_suspended()) } } }
Pattern 2: JWT-Based Resolution with Claims
Ideal for OAuth2/OpenID Connect integrations:
#![allow(unused)] fn main() { use jsonwebtoken::{decode, DecodingKey, Validation, Algorithm}; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] pub struct TenantClaims { pub sub: String, // Subject (user ID) pub tenant_id: String, // Tenant identifier pub client_id: String, // Client application ID pub scopes: Vec<String>, // Granted scopes pub resource_limits: ResourceLimits, pub exp: usize, // Expiration time pub iat: usize, // Issued at } #[derive(Debug, Serialize, Deserialize)] pub struct ResourceLimits { pub max_users: Option<usize>, pub max_groups: Option<usize>, pub allowed_operations: Vec<String>, } pub struct JwtTenantResolver { decoding_key: DecodingKey, validation: Validation, } impl JwtTenantResolver { pub fn new(jwt_secret: &str) -> Self { let mut validation = Validation::new(Algorithm::HS256); validation.validate_exp = true; validation.validate_nbf = true; Self { decoding_key: DecodingKey::from_secret(jwt_secret.as_ref()), validation, } } } impl TenantResolver for JwtTenantResolver { type Error = JwtTenantError; async fn resolve_tenant(&self, token: &str) -> Result<TenantContext, Self::Error> { // Decode and validate JWT let token_data = decode::<TenantClaims>( token, &self.decoding_key, &self.validation )?; let claims = token_data.claims; // Convert JWT claims to tenant permissions let permissions = TenantPermissions { can_create: claims.scopes.contains(&"scim:create".to_string()), can_read: claims.scopes.contains(&"scim:read".to_string()), can_update: claims.scopes.contains(&"scim:update".to_string()), can_delete: claims.scopes.contains(&"scim:delete".to_string()), can_list: claims.scopes.contains(&"scim:list".to_string()), max_users: claims.resource_limits.max_users, max_groups: claims.resource_limits.max_groups, }; // Build tenant context Ok(TenantContext::new(claims.tenant_id, claims.client_id) .with_permissions(permissions) .with_isolation_level(IsolationLevel::Standard) .with_metadata("subject", claims.sub)) } } }
Pattern 3: Database-Backed Resolution with Caching
Production-ready pattern for large-scale deployments:
#![allow(unused)] fn main() { use sqlx::{PgPool, Row}; use std::time::{Duration, Instant}; pub struct DatabaseTenantResolver { db_pool: PgPool, cache: Arc<RwLock<TenantCache>>, cache_ttl: Duration, } #[derive(Clone)] struct CachedTenant { tenant_context: TenantContext, cached_at: Instant, } type TenantCache = HashMap<String, CachedTenant>; impl DatabaseTenantResolver { pub fn new(db_pool: PgPool, cache_ttl: Duration) -> Self { Self { db_pool, cache: Arc::new(RwLock::new(HashMap::new())), cache_ttl, } } async fn fetch_tenant_from_db(&self, api_key: &str) -> Result<TenantContext, DatabaseError> { let row = sqlx::query!( r#" SELECT t.tenant_id, t.client_id, t.isolation_level, t.is_active, tp.can_create, tp.can_read, tp.can_update, tp.can_delete, tp.can_list, tp.max_users, tp.max_groups FROM tenants t JOIN tenant_permissions tp ON t.tenant_id = tp.tenant_id WHERE t.api_key = $1 AND t.is_active = true "#, api_key ) .fetch_optional(&self.db_pool) .await? .ok_or(DatabaseError::TenantNotFound)?; let permissions = TenantPermissions { can_create: row.can_create, can_read: row.can_read, can_update: row.can_update, can_delete: row.can_delete, can_list: row.can_list, max_users: row.max_users.map(|n| n as usize), max_groups: row.max_groups.map(|n| n as usize), }; let isolation_level = match row.isolation_level.as_str() { "strict" => IsolationLevel::Strict, "shared" => IsolationLevel::Shared, _ => IsolationLevel::Standard, }; Ok(TenantContext::new(row.tenant_id, row.client_id) .with_permissions(permissions) .with_isolation_level(isolation_level)) } fn is_cache_valid(&self, cached_tenant: &CachedTenant) -> bool { cached_tenant.cached_at.elapsed() < self.cache_ttl } } impl TenantResolver for DatabaseTenantResolver { type Error = DatabaseTenantError; async fn resolve_tenant(&self, api_key: &str) -> Result<TenantContext, Self::Error> { // 1. Check cache first { let cache = self.cache.read().await; if let Some(cached) = cache.get(api_key) { if self.is_cache_valid(cached) { return Ok(cached.tenant_context.clone()); } } } // 2. Fetch from database let tenant_context = self.fetch_tenant_from_db(api_key).await?; // 3. Update cache { let mut cache = self.cache.write().await; cache.insert(api_key.to_string(), CachedTenant { tenant_context: tenant_context.clone(), cached_at: Instant::now(), }); } Ok(tenant_context) } } }
URL Generation Strategies
Different deployment patterns require different URL generation approaches:
Strategy 1: Subdomain-Based Tenancy
#![allow(unused)] fn main() { use scim_server::TenantStrategy; pub struct SubdomainTenantStrategy { base_domain: String, use_https: bool, } impl SubdomainTenantStrategy { pub fn new(base_domain: String, use_https: bool) -> Self { Self { base_domain, use_https } } pub fn generate_tenant_base_url(&self, tenant_id: &str) -> String { let protocol = if self.use_https { "https" } else { "http" }; format!("{}://{}.{}", protocol, tenant_id, self.base_domain) } pub fn generate_resource_url( &self, tenant_id: &str, resource_type: &str, resource_id: &str, ) -> String { format!( "{}/scim/v2/{}/{}", self.generate_tenant_base_url(tenant_id), resource_type, resource_id ) } pub fn extract_tenant_from_host(&self, host: &str) -> Option<String> { if let Some(subdomain) = host.strip_suffix(&format!(".{}", self.base_domain)) { if !subdomain.contains('.') { return Some(subdomain.to_string()); } } None } } // Usage in HTTP handler async fn extract_tenant_from_request( req: &HttpRequest, strategy: &SubdomainTenantStrategy, ) -> Result<String, TenantExtractionError> { let host = req.headers() .get("host") .and_then(|h| h.to_str().ok()) .ok_or(TenantExtractionError::MissingHost)?; strategy.extract_tenant_from_host(host) .ok_or(TenantExtractionError::InvalidSubdomain) } }
Strategy 2: Path-Based Tenancy
#![allow(unused)] fn main() { pub struct PathBasedTenantStrategy { base_url: String, } impl PathBasedTenantStrategy { pub fn new(base_url: String) -> Self { Self { base_url } } pub fn generate_resource_url( &self, tenant_id: &str, resource_type: &str, resource_id: &str, ) -> String { format!( "{}/tenants/{}/scim/v2/{}/{}", self.base_url, tenant_id, resource_type, resource_id ) } pub fn extract_tenant_from_path(&self, path: &str) -> Option<String> { // Path format: /tenants/{tenant_id}/scim/v2/... let parts: Vec<&str> = path.split('/').collect(); if parts.len() >= 3 && parts[1] == "tenants" { Some(parts[2].to_string()) } else { None } } } // Usage in routing pub fn setup_tenant_routes(app: &mut App) { app.route( "/tenants/{tenant_id}/scim/v2/{resource_type}", web::post().to(create_resource_handler) ) .route( "/tenants/{tenant_id}/scim/v2/{resource_type}/{resource_id}", web::get().to(get_resource_handler) ); } }
Strategy 3: Header-Based Tenancy
Useful for API gateways and proxy scenarios:
#![allow(unused)] fn main() { pub struct HeaderBasedTenantStrategy; impl HeaderBasedTenantStrategy { pub fn extract_tenant_from_headers( headers: &HeaderMap, ) -> Result<String, TenantExtractionError> { // Try different header names in order of preference let header_names = ["x-tenant-id", "x-client-id", "tenant"]; for header_name in &header_names { if let Some(header_value) = headers.get(*header_name) { if let Ok(tenant_id) = header_value.to_str() { if !tenant_id.is_empty() { return Ok(tenant_id.to_string()); } } } } Err(TenantExtractionError::MissingTenantHeader) } pub fn generate_resource_url( &self, base_url: &str, resource_type: &str, resource_id: &str, ) -> String { // Headers don't affect URLs in this strategy format!("{}/scim/v2/{}/{}", base_url, resource_type, resource_id) } } }
Storage Isolation Patterns
Strict Isolation Pattern
Complete separation with no shared data:
#![allow(unused)] fn main() { use scim_server::storage::{StorageProvider, StorageKey, StoragePrefix}; pub struct StrictIsolationProvider<S: StorageProvider> { inner_storage: S, } impl<S: StorageProvider> StrictIsolationProvider<S> { pub fn new(inner_storage: S) -> Self { Self { inner_storage } } fn tenant_scoped_key(&self, tenant_id: &str, original_key: &StorageKey) -> StorageKey { StorageKey::new(format!("tenant:{}:{}", tenant_id, original_key.as_str())) } fn tenant_scoped_prefix(&self, tenant_id: &str, original_prefix: &StoragePrefix) -> StoragePrefix { StoragePrefix::new(format!("tenant:{}:{}", tenant_id, original_prefix.as_str())) } } impl<S: StorageProvider> StorageProvider for StrictIsolationProvider<S> { type Error = S::Error; async fn put( &self, key: StorageKey, data: Value, context: &RequestContext, ) -> Result<Value, Self::Error> { let tenant_id = context.tenant_id() .ok_or_else(|| StorageError::TenantRequired)?; let scoped_key = self.tenant_scoped_key(tenant_id, &key); self.inner_storage.put(scoped_key, data, context).await } async fn get( &self, key: StorageKey, context: &RequestContext, ) -> Result<Option<Value>, Self::Error> { let tenant_id = context.tenant_id() .ok_or_else(|| StorageError::TenantRequired)?; let scoped_key = self.tenant_scoped_key(tenant_id, &key); self.inner_storage.get(scoped_key, context).await } async fn list( &self, prefix: StoragePrefix, context: &RequestContext, ) -> Result<Vec<Value>, Self::Error> { let tenant_id = context.tenant_id() .ok_or_else(|| StorageError::TenantRequired)?; let scoped_prefix = self.tenant_scoped_prefix(tenant_id, &prefix); let results = self.inner_storage.list(scoped_prefix, context).await?; // Additional validation to ensure no cross-tenant data leakage let expected_prefix = format!("tenant:{}:", tenant_id); let filtered_results: Vec<Value> = results .into_iter() .filter(|item| { if let Some(id) = item.get("id").and_then(|v| v.as_str()) { id.starts_with(&expected_prefix) } else { false } }) .collect(); Ok(filtered_results) } async fn delete( &self, key: StorageKey, context: &RequestContext, ) -> Result<bool, Self::Error> { let tenant_id = context.tenant_id() .ok_or_else(|| StorageError::TenantRequired)?; let scoped_key = self.tenant_scoped_key(tenant_id, &key); self.inner_storage.delete(scoped_key, context).await } } }
Standard Isolation with Shared Resources
Allows some shared data while maintaining tenant boundaries:
#![allow(unused)] fn main() { pub struct StandardIsolationProvider<S: StorageProvider> { inner_storage: S, shared_prefixes: HashSet<String>, } impl<S: StorageProvider> StandardIsolationProvider<S> { pub fn new(inner_storage: S) -> Self { let mut shared_prefixes = HashSet::new(); shared_prefixes.insert("schema:".to_string()); shared_prefixes.insert("config:".to_string()); Self { inner_storage, shared_prefixes, } } fn should_scope_key(&self, key: &StorageKey) -> bool { !self.shared_prefixes.iter() .any(|prefix| key.as_str().starts_with(prefix)) } fn apply_tenant_scoping( &self, key: StorageKey, context: &RequestContext, ) -> StorageKey { if self.should_scope_key(&key) { if let Some(tenant_id) = context.tenant_id() { StorageKey::new(format!("tenant:{}:{}", tenant_id, key.as_str())) } else { key } } else { key } } } impl<S: StorageProvider> StorageProvider for StandardIsolationProvider<S> { type Error = S::Error; async fn put( &self, key: StorageKey, data: Value, context: &RequestContext, ) -> Result<Value, Self::Error> { let scoped_key = self.apply_tenant_scoping(key, context); self.inner_storage.put(scoped_key, data, context).await } // Similar implementations for get, list, delete... } }
Permission Enforcement Patterns
Resource Limit Enforcement
#![allow(unused)] fn main() { use scim_server::multi_tenant::TenantValidator; pub struct ResourceLimitValidator; impl TenantValidator for ResourceLimitValidator { async fn validate_create_operation( &self, resource_type: &str, context: &RequestContext, storage: &impl StorageProvider, ) -> Result<(), ValidationError> { let tenant_context = context.tenant_context() .ok_or(ValidationError::TenantRequired)?; match resource_type { "User" => { if let Some(max_users) = tenant_context.permissions.max_users { let current_count = self.count_resources("User", context, storage).await?; if current_count >= max_users { return Err(ValidationError::ResourceLimitExceeded { resource_type: "User".to_string(), current: current_count, limit: max_users, }); } } }, "Group" => { if let Some(max_groups) = tenant_context.permissions.max_groups { let current_count = self.count_resources("Group", context, storage).await?; if current_count >= max_groups { return Err(ValidationError::ResourceLimitExceeded { resource_type: "Group".to_string(), current: current_count, limit: max_groups, }); } } }, _ => { // Custom resource type validation } } Ok(()) } async fn count_resources( &self, resource_type: &str, context: &RequestContext, storage: &impl StorageProvider, ) -> Result<usize, ValidationError> { let prefix = StoragePrefix::new(format!("{}:", resource_type.to_lowercase())); let resources = storage.list(prefix, context).await?; Ok(resources.len()) } } }
Operation Permission Enforcement
#![allow(unused)] fn main() { pub fn validate_operation_permission( operation: ScimOperation, context: &RequestContext, ) -> Result<(), PermissionError> { let tenant_context = context.tenant_context() .ok_or(PermissionError::TenantRequired)?; let has_permission = match operation { ScimOperation::Create => tenant_context.permissions.can_create, ScimOperation::GetById | ScimOperation::List => tenant_context.permissions.can_read, ScimOperation::Update => tenant_context.permissions.can_update, ScimOperation::Delete => tenant_context.permissions.can_delete, }; if !has_permission { return Err(PermissionError::OperationNotAllowed { operation, tenant_id: tenant_context.tenant_id.clone(), }); } Ok(()) } }
Complete Integration Example
Here's how all patterns work together in a production setup:
#![allow(unused)] fn main() { use axum::{extract::Path, http::HeaderMap, Extension, Json}; use scim_server::{ScimServer, RequestContext, ScimOperationHandler}; pub async fn multi_tenant_scim_handler( Path(path_params): Path<HashMap<String, String>>, headers: HeaderMap, Json(body): Json<Value>, Extension(server): Extension<Arc<ScimServer<MultiTenantProvider>>>, Extension(tenant_resolver): Extension<Arc<DatabaseTenantResolver>>, Extension(url_strategy): Extension<Arc<SubdomainTenantStrategy>>, ) -> Result<Json<Value>, ScimError> { // 1. Extract tenant identifier using configured strategy let tenant_id = match url_strategy.extract_tenant_from_host( headers.get("host").and_then(|h| h.to_str().ok()).unwrap_or("") ) { Some(id) => id, None => return Err(ScimError::TenantNotFound("Unable to determine tenant".into())), }; // 2. Extract API key for authentication let api_key = headers.get("authorization") .and_then(|h| h.to_str().ok()) .and_then(|s| s.strip_prefix("Bearer ")) .ok_or(ScimError::Unauthorized("Missing or invalid API key".into()))?; // 3. Resolve tenant context let tenant_context = tenant_resolver.resolve_tenant(api_key).await .map_err(|e| ScimError::TenantResolutionFailed(e.to_string()))?; // 4. Validate tenant ID matches resolved context if tenant_context.tenant_id != tenant_id { return Err(ScimError::TenantMismatch { requested: tenant_id, resolved: tenant_context.tenant_id, }); } // 5. Create request context let request_context = RequestContext::with_tenant_generated_id(tenant_context); // 6. Build SCIM operation request let operation_request = ScimOperationRequest::from_http_parts( &path_params, &headers, body, )?; // 7. Validate operation permissions validate_operation_permission( operation_request.operation_type(), &request_context, )?; // 8. Process operation let handler = ScimOperationHandler::new(&server); let response = handler .handle_operation(operation_request, &request_context) .await?; // 9. Apply tenant-specific URL generation to response let mut response_json = response.into_json(); if let Some(location) = response_json.get_mut("meta").and_then(|m| m.get_mut("location")) { if let Some(resource_id) = response_json.get("id").and_then(|v| v.as_str()) { let tenant_url = url_strategy.generate_resource_url( &tenant_id, &operation_request.resource_type, resource_id, ); *location = Value::String(tenant_url); } } Ok(Json(response_json)) } }
Testing Multi-Tenant Patterns
Integration Test Setup
#![allow(unused)] fn main() { #[tokio::test] async fn test_complete_multi_tenant_flow() { // Setup multi-tenant infrastructure let storage = InMemoryStorage::new(); let isolated_storage = StrictIsolationProvider::new(storage); let provider = StandardResourceProvider::new(isolated_storage); let server = ScimServer::new(provider).unwrap(); // Setup tenant resolver let tenant_resolver = StaticTenantResolver::new(); // Add test tenants let tenant_a = TenantContext::new("tenant-a".into(), "client-a".into()) .with_permissions(TenantPermissions { max_users: Some(10), ..Default::default() }); let tenant_b = TenantContext::new("tenant-b".into(), "client-b".into()); tenant_resolver.add_tenant("api-key-a", tenant_a).await; tenant_resolver.add_tenant("api-key-b", tenant_b).await; // Test tenant isolation let context_a = RequestContext::with_tenant_generated_id( tenant_resolver.resolve_tenant("api-key-a").await.unwrap() ); let context_b = RequestContext::with_tenant_generated_id( tenant_resolver.resolve_tenant("api-key-b").await.unwrap() ); // Create resources in different tenants let handler = ScimOperationHandler::new(&server); let user_a = handler.handle_create( "User", json!({"userName": "user.a", "displayName": "User A"}), &context_a, ).await.unwrap(); let user_b = handler.handle_create( "User", json!({"userName": "user.b", "displayName": "User B"}), &context_b, ).await.unwrap(); // Verify isolation - tenant A cannot see tenant B's user let tenant_a_users = handler.handle_list("User", &ListQuery::default(), &context_a) .await.unwrap(); assert_eq!(tenant_a_users.resources.len(), 1); assert_eq!(tenant_a_users.resources[0].get("userName").unwrap(), "user.a"); let tenant_b_users = handler.handle_list("User", &ListQuery::default(), &context_b) .await.unwrap(); assert_eq!(tenant_b_users.resources.len(), 1); assert_eq!(tenant_b_users.resources[0].get("userName").unwrap(), "user.b"); // Test resource limit enforcement for tenant A let mut create_futures = Vec::new(); for i in 1..15 { // Try to create more than the limit of 10 let user_data = json!({ "userName": format!("user.a.{}", i), "displayName": format!("User A {}", i) }); create_futures.push(handler.handle_create("User", user_data, &context_a)); } let results = futures::future::join_all(create_futures).await; let successful_creates = results.into_iter().filter(|r| r.is_ok()).count(); // Should only allow 9 more users (10 total - 1 already created) assert_eq!(successful_creates, 9); } }
Migration Patterns
Single-Tenant to Multi-Tenant Migration
#![allow(unused)] fn main() { pub struct TenantMigrationService<S: StorageProvider> { storage: S, default_tenant_id: String, } impl<S: StorageProvider> TenantMigrationService<S> { pub async fn migrate_to_multi_tenant( &self, default_client_id: String, ) -> Result<MigrationReport, MigrationError> { let mut report = MigrationReport::new(); // 1. Create default tenant context let default_tenant = TenantContext::new( self.default_tenant_id.clone(), default_client_id, ); // 2. Migrate existing users let users = self.storage .list(StoragePrefix::new("user:"), &RequestContext::migration()) .await?; for user in users { let old_key = StorageKey::from_resource(&user)?; let new_key = StorageKey::new(format!( "tenant:{}:{}", self.default_tenant_id, old_key.as_str() )); // Copy to new location self.storage.put(new_key, user.clone(), &RequestContext::migration()).await?; report.users_migrated += 1; } // 3. Migrate existing groups let groups = self.storage .list(StoragePrefix::new("group:"), &RequestContext::migration()) .await?; for group in groups { let old_key = StorageKey::from_resource(&group)?; let new_key = StorageKey::new(format!( "tenant:{}:{}", self.default_tenant_id, old_key.as_str() )); self.storage.put(new_key, group.clone(), &RequestContext::migration()).await?; report.groups_migrated += 1; } // 4. Clean up old data (optional, use with caution) if report.cleanup_old_data { self.cleanup_pre_migration_data().await?; } Ok(report) } } #[derive(Debug)] pub struct MigrationReport { pub users_migrated: usize, pub groups_migrated: usize, pub errors: Vec<MigrationError>, pub cleanup_old_data: bool, } }
Production Considerations
Monitoring and Observability
#![allow(unused)] fn main() { use tracing::{info, warn, error, span, Level}; use metrics::{counter, histogram, gauge}; pub struct MultiTenantMetrics; impl MultiTenantMetrics { pub fn record_tenant_operation( &self, tenant_id: &str, operation: &str, resource_type: &str, duration: Duration, success: bool, ) { // Record operation metrics counter!("scim_operations_total") .with_tag("tenant_id", tenant_id) .with_tag("operation", operation) .with_tag("resource_type", resource_type) .with_tag("success", success.to_string()) .increment(1); histogram!("scim_operation_duration_seconds") .with_tag("tenant_id", tenant_id) .with_tag("operation", operation) .record(duration.as_secs_f64()); } pub fn record_tenant_resource_count( &self, tenant_id: &str, resource_type: &str, count: usize, ) { gauge!("scim_tenant_resources") .with_tag("tenant_id", tenant_id) .with_tag("resource_type", resource_type) .set(count as f64); } pub fn record_tenant_limit_approaching( &self, tenant_id: &str, resource_type: &str, current: usize, limit: usize, ) { let utilization = (current as f64 / limit as f64) * 100.0; gauge!("scim_tenant_limit_utilization") .with_tag("tenant_id", tenant_id) .with_tag("resource_type", resource_type) .set(utilization); if utilization > 80.0 { warn!( tenant_id = %tenant_id, resource_type = %resource_type, current = current, limit = limit, utilization = %format!("{:.1}%", utilization), "Tenant approaching resource limit" ); } } } }
Health Checks and Diagnostics
#![allow(unused)] fn main() { pub struct MultiTenantHealthCheck<S: StorageProvider> { storage: S, tenant_resolver: Arc<dyn TenantResolver>, } impl<S: StorageProvider> MultiTenantHealthCheck<S> { pub async fn check_tenant_health( &self, tenant_id: &str, ) -> Result<TenantHealthReport, HealthCheckError> { let mut report = TenantHealthReport::new(tenant_id.to_string()); // Check tenant resolution let test_credential = format!("health-check-{}", tenant_id); match self.tenant_resolver.resolve_tenant(&test_credential).await { Ok(_) => report.tenant_resolution = HealthStatus::Healthy, Err(e) => { report.tenant_resolution = HealthStatus::Unhealthy(e.to_string()); report.overall_health = HealthStatus::Degraded; } } // Check storage connectivity for tenant let test_context = RequestContext::with_tenant_id(tenant_id.to_string()); let test_key = StorageKey::new("health-check"); let test_data = json!({"health": "check", "timestamp": chrono::Utc::now()}); match self.storage.put(test_key.clone(), test_data, &test_context).await { Ok(_) => { report.storage_write = HealthStatus::Healthy; // Test read match self.storage.get(test_key.clone(), &test_context).await { Ok(Some(_)) => report.storage_read = HealthStatus::Healthy, Ok(None) => report.storage_read = HealthStatus::Unhealthy("Data not found".into()), Err(e) => report.storage_read = HealthStatus::Unhealthy(e.to_string()), } // Cleanup let _ = self.storage.delete(test_key, &test_context).await; }, Err(e) => { report.storage_write = HealthStatus::Unhealthy(e.to_string()); report.overall_health = HealthStatus::Unhealthy("Storage write failed".into()); } } Ok(report) } } #[derive(Debug)] pub struct TenantHealthReport { pub tenant_id: String, pub overall_health: HealthStatus, pub tenant_resolution: HealthStatus, pub storage_read: HealthStatus, pub storage_write: HealthStatus, pub checked_at: chrono::DateTime<chrono::Utc>, } #[derive(Debug)] pub enum HealthStatus { Healthy, Degraded, Unhealthy(String), } }
Best Practices Summary
Configuration Management
- Use environment-specific tenant configurations for different deployment stages
- Implement tenant configuration hot-reloading for operational flexibility
- Validate tenant configurations at startup to catch errors early
Security Considerations
- Always validate tenant boundaries in storage operations
- Use strong isolation levels for sensitive data
- Implement audit logging for all multi-tenant operations
- Regularly rotate tenant credentials and API keys
Performance Optimization
- Cache tenant resolution results with appropriate TTL
- Use connection pooling for database-backed tenant resolvers
- Implement pagination for large tenant lists
- Monitor tenant-specific resource usage patterns
Operational Excellence
- Implement comprehensive health checks for all tenant components
- Use structured logging with tenant context for debugging
- Set up alerts for tenant limit violations
- Plan for tenant migration and evolution scenarios
Related Topics
- Request Lifecycle & Context Management - How tenant context flows through requests
- Resource Provider Architecture - Implementing tenant-aware business logic
- Multi-Tenant Architecture - Core concepts and components
- Multi-Tenant Server Example - Complete implementation example
Next Steps
Now that you understand multi-tenant architecture patterns:
- Choose your tenant strategy (subdomain, path-based, or header-based)
- Implement tenant resolution for your authentication system
- Configure storage isolation based on your security requirements
- Set up monitoring and health checks for production deployment
- Plan tenant migration strategies for future scalability needs
Resource Provider Architecture
This deep dive explores the Resource Provider architecture in SCIM Server, covering when to use StandardResourceProvider versus custom implementations, design patterns for business logic integration, and strategies for connecting to existing data models and external systems.
Overview
The ResourceProvider
trait is the primary integration point for implementing SCIM business logic. It abstracts away SCIM protocol details while giving you full control over how resources are created, validated, stored, and retrieved. Understanding this architecture is crucial for building production SCIM systems that integrate with your existing infrastructure.
Key Architectural Decision: Choose between StandardResourceProvider
(composition-based) or custom ResourceProvider
implementation (direct control).
ResourceProvider Architecture Overview
SCIM Server
β SCIM Operations
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ResourceProvider Trait β
β β
β βββββββββββββββββββββββ βββββββββββββββββββββββββββββββββββββββ β
β β StandardResource β β Custom ResourceProvider β β
β β Provider<S> β β Implementation β β
β β β β β β
β β β’ Schema validation β β β’ Direct business logic control β β
β β β’ Generic CRUD ops β β β’ Custom validation rules β β
β β β’ Storage agnostic β β β’ Integration with existing APIs β β
β β β’ ETag support β β β’ Custom error handling β β
β β β’ Multi-tenant β β β’ Performance optimizations β β
β βββββββββββββββββββββββ βββββββββββββββββββββββββββββββββββββββ β
β β β β
β βββββββββββββββββββββββ βββββββββββββββββββββββββββββββββββββββ β
β β StorageProvider β β Your Business Layer β β
β β β’ InMemoryStorage β β β’ Database DAOs β β
β β β’ Database adapters β β β’ External APIs β β
β β β’ Custom backends β β β’ Message queues β β
β βββββββββββββββββββββββ β β’ Legacy systems β β
β βββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Decision Matrix: StandardResourceProvider vs Custom
Use StandardResourceProvider When:
- β Standard SCIM compliance is your primary goal
- β Simple data models that map well to SCIM schemas
- β Storage abstraction meets your needs
- β Built-in features (validation, ETag, multi-tenant) are sufficient
- β Rapid prototyping or getting started quickly
Use Custom ResourceProvider When:
- β Complex business logic requires custom validation or processing
- β Existing data models don't map cleanly to SCIM
- β Performance requirements need specialized optimizations
- β External system integration requires custom API calls
- β Advanced error handling or audit requirements
- β Custom resource types beyond standard User/Group
StandardResourceProvider Deep Dive
Basic Setup Pattern
#![allow(unused)] fn main() { use scim_server::providers::StandardResourceProvider; use scim_server::storage::InMemoryStorage; use scim_server::ScimServer; // Simple in-memory setup let storage = InMemoryStorage::new(); let provider = StandardResourceProvider::new(storage); let server = ScimServer::new(provider)?; }
Database Integration Pattern
#![allow(unused)] fn main() { use scim_server::storage::StorageProvider; use sqlx::{PgPool, Row}; pub struct PostgresStorageProvider { pool: PgPool, } impl StorageProvider for PostgresStorageProvider { type Error = sqlx::Error; async fn put( &self, key: StorageKey, data: Value, context: &RequestContext, ) -> Result<Value, Self::Error> { // Add metadata for versioning and tenant isolation let mut enriched_data = data; enriched_data["meta"] = json!({ "version": generate_version(&enriched_data), "created": chrono::Utc::now(), "lastModified": chrono::Utc::now(), "resourceType": extract_resource_type(&key), "location": generate_location(&key, context), }); // Apply tenant scoping if needed let table_name = if context.is_multi_tenant() { format!("scim_resources_{}", context.tenant_id().unwrap()) } else { "scim_resources".to_string() }; sqlx::query(&format!( "INSERT INTO {} (id, resource_type, data, version, created_at) VALUES ($1, $2, $3, $4, NOW()) ON CONFLICT (id) DO UPDATE SET data = $3, version = $4, updated_at = NOW()", table_name )) .bind(key.as_str()) .bind(extract_resource_type(&key)) .bind(&enriched_data) .bind(enriched_data["meta"]["version"].as_str()) .execute(&self.pool) .await?; Ok(enriched_data) } async fn get( &self, key: StorageKey, context: &RequestContext, ) -> Result<Option<Value>, Self::Error> { let table_name = if context.is_multi_tenant() { format!("scim_resources_{}", context.tenant_id().unwrap()) } else { "scim_resources".to_string() }; let row = sqlx::query(&format!("SELECT data FROM {} WHERE id = $1", table_name)) .bind(key.as_str()) .fetch_optional(&self.pool) .await?; Ok(row.map(|r| r.get("data"))) } async fn list( &self, prefix: StoragePrefix, context: &RequestContext, ) -> Result<Vec<Value>, Self::Error> { let table_name = if context.is_multi_tenant() { format!("scim_resources_{}", context.tenant_id().unwrap()) } else { "scim_resources".to_string() }; let resource_type = prefix.as_str().trim_end_matches(':'); let rows = sqlx::query(&format!( "SELECT data FROM {} WHERE resource_type = $1 ORDER BY created_at", table_name )) .bind(resource_type) .fetch_all(&self.pool) .await?; Ok(rows.into_iter().map(|r| r.get("data")).collect()) } async fn delete( &self, key: StorageKey, context: &RequestContext, ) -> Result<bool, Self::Error> { let table_name = if context.is_multi_tenant() { format!("scim_resources_{}", context.tenant_id().unwrap()) } else { "scim_resources".to_string() }; let result = sqlx::query(&format!("DELETE FROM {} WHERE id = $1", table_name)) .bind(key.as_str()) .execute(&self.pool) .await?; Ok(result.rows_affected() > 0) } } // Usage with StandardResourceProvider let postgres_storage = PostgresStorageProvider::new(db_pool); let provider = StandardResourceProvider::new(postgres_storage); let server = ScimServer::new(provider)?; }
Extending StandardResourceProvider with Custom Validation
#![allow(unused)] fn main() { use scim_server::providers::StandardResourceProvider; use scim_server::schema::{ValidationResult, ValidationError}; pub struct ValidatingResourceProvider<S: StorageProvider> { inner: StandardResourceProvider<S>, business_validator: BusinessRuleValidator, } impl<S: StorageProvider> ValidatingResourceProvider<S> { pub fn new(storage: S, validator: BusinessRuleValidator) -> Self { Self { inner: StandardResourceProvider::new(storage), business_validator: validator, } } async fn validate_business_rules( &self, resource_type: &str, data: &Value, context: &RequestContext, ) -> ValidationResult<()> { match resource_type { "User" => self.business_validator.validate_user(data, context).await, "Group" => self.business_validator.validate_group(data, context).await, _ => Ok(()), // No custom validation for other types } } } impl<S: StorageProvider> ResourceProvider for ValidatingResourceProvider<S> { type Error = StandardProviderError<S::Error>; async fn create_resource( &self, resource_type: &str, data: Value, context: &RequestContext, ) -> Result<Resource, Self::Error> { // Apply custom business rule validation before delegating self.validate_business_rules(resource_type, &data, context) .await .map_err(|e| StandardProviderError::ValidationError(e))?; // Delegate to StandardResourceProvider self.inner.create_resource(resource_type, data, context).await } async fn update_resource( &self, resource_type: &str, id: &str, data: Value, context: &RequestContext, ) -> Result<Resource, Self::Error> { // Custom validation before update self.validate_business_rules(resource_type, &data, context) .await .map_err(|e| StandardProviderError::ValidationError(e))?; self.inner.update_resource(resource_type, id, data, context).await } // Delegate other methods to inner provider async fn get_resource( &self, resource_type: &str, id: &str, context: &RequestContext, ) -> Result<Option<Resource>, Self::Error> { self.inner.get_resource(resource_type, id, context).await } // ... other methods } pub struct BusinessRuleValidator { // External dependencies for validation user_service: Arc<dyn UserService>, policy_engine: Arc<dyn PolicyEngine>, } impl BusinessRuleValidator { async fn validate_user(&self, data: &Value, context: &RequestContext) -> ValidationResult<()> { // Custom user validation logic if let Some(email) = data.get("emails").and_then(|e| e.as_array()) { for email_obj in email { if let Some(email_value) = email_obj.get("value").and_then(|v| v.as_str()) { // Check if email is already in use if self.user_service.email_exists(email_value).await? { return Err(ValidationError::custom( "email", "Email address already in use", )); } // Validate against company policy if !self.policy_engine.validate_email_domain(email_value, context).await? { return Err(ValidationError::custom( "email", "Email domain not allowed for this tenant", )); } } } } Ok(()) } } }
Custom ResourceProvider Patterns
Direct Database Integration
#![allow(unused)] fn main() { use scim_server::{ResourceProvider, Resource, RequestContext}; use uuid::Uuid; pub struct DirectDatabaseProvider { db_pool: PgPool, user_mapper: UserMapper, group_mapper: GroupMapper, } impl ResourceProvider for DirectDatabaseProvider { type Error = DatabaseProviderError; async fn create_resource( &self, resource_type: &str, data: Value, context: &RequestContext, ) -> Result<Resource, Self::Error> { match resource_type { "User" => self.create_user(data, context).await, "Group" => self.create_group(data, context).await, _ => Err(DatabaseProviderError::UnsupportedResourceType(resource_type.to_string())), } } // ... other trait methods } impl DirectDatabaseProvider { async fn create_user( &self, scim_data: Value, context: &RequestContext, ) -> Result<Resource, DatabaseProviderError> { // 1. Convert SCIM data to internal user model let user_model = self.user_mapper.scim_to_internal(&scim_data)?; // 2. Apply tenant scoping let tenant_scoped_user = if let Some(tenant_id) = context.tenant_id() { user_model.with_tenant_id(tenant_id.to_string()) } else { user_model }; // 3. Begin database transaction let mut tx = self.db_pool.begin().await?; // 4. Insert user record let user_id = Uuid::new_v4(); sqlx::query!( r#" INSERT INTO users ( id, tenant_id, username, email, first_name, last_name, active, created_at, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW()) "#, user_id, tenant_scoped_user.tenant_id, tenant_scoped_user.username, tenant_scoped_user.email, tenant_scoped_user.first_name, tenant_scoped_user.last_name, tenant_scoped_user.active, ) .execute(&mut *tx) .await?; // 5. Handle multi-valued attributes (emails, phone numbers) if let Some(emails) = scim_data.get("emails").and_then(|v| v.as_array()) { for (idx, email_obj) in emails.iter().enumerate() { if let Some(email) = email_obj.get("value").and_then(|v| v.as_str()) { let is_primary = email_obj.get("primary") .and_then(|v| v.as_bool()) .unwrap_or(idx == 0); let email_type = email_obj.get("type") .and_then(|v| v.as_str()) .unwrap_or("work"); sqlx::query!( "INSERT INTO user_emails (user_id, email, email_type, is_primary) VALUES ($1, $2, $3, $4)", user_id, email, email_type, is_primary, ) .execute(&mut *tx) .await?; } } } // 6. Commit transaction tx.commit().await?; // 7. Fetch complete user data let created_user = self.get_user_by_id(&user_id.to_string(), context).await? .ok_or(DatabaseProviderError::UserNotFound)?; // 8. Convert back to SCIM resource let scim_resource = self.user_mapper.internal_to_scim(created_user)?; Ok(Resource::new(scim_resource)) } async fn get_user_by_id( &self, id: &str, context: &RequestContext, ) -> Result<Option<InternalUser>, DatabaseProviderError> { let user_uuid = Uuid::parse_str(id)?; // Build tenant-aware query let tenant_filter = if let Some(tenant_id) = context.tenant_id() { format!("AND tenant_id = '{}'", tenant_id) } else { "AND tenant_id IS NULL".to_string() }; let user_row = sqlx::query(&format!( "SELECT * FROM users WHERE id = $1 {}", tenant_filter )) .bind(user_uuid) .fetch_optional(&self.db_pool) .await?; match user_row { Some(row) => { // Fetch associated emails let emails = sqlx::query!( "SELECT email, email_type, is_primary FROM user_emails WHERE user_id = $1", user_uuid ) .fetch_all(&self.db_pool) .await?; Ok(Some(InternalUser { id: row.get("id"), tenant_id: row.get("tenant_id"), username: row.get("username"), first_name: row.get("first_name"), last_name: row.get("last_name"), active: row.get("active"), emails: emails.into_iter().map(|e| UserEmail { value: e.email, email_type: e.email_type, primary: e.is_primary, }).collect(), created_at: row.get("created_at"), updated_at: row.get("updated_at"), })) }, None => Ok(None), } } } }
External API Integration Pattern
#![allow(unused)] fn main() { use reqwest::Client; use serde::{Deserialize, Serialize}; pub struct ExternalApiProvider { http_client: Client, api_base_url: String, api_token: String, scim_mapper: ScimApiMapper, } impl ResourceProvider for ExternalApiProvider { type Error = ApiProviderError; async fn create_resource( &self, resource_type: &str, data: Value, context: &RequestContext, ) -> Result<Resource, Self::Error> { match resource_type { "User" => self.create_user_via_api(data, context).await, "Group" => self.create_group_via_api(data, context).await, _ => Err(ApiProviderError::UnsupportedResourceType(resource_type.to_string())), } } async fn get_resource( &self, resource_type: &str, id: &str, context: &RequestContext, ) -> Result<Option<Resource>, Self::Error> { let api_endpoint = match resource_type { "User" => format!("{}/api/v1/users/{}", self.api_base_url, id), "Group" => format!("{}/api/v1/groups/{}", self.api_base_url, id), _ => return Err(ApiProviderError::UnsupportedResourceType(resource_type.to_string())), }; let response = self.http_client .get(&api_endpoint) .header("Authorization", format!("Bearer {}", self.api_token)) .header("X-Tenant-ID", context.tenant_id().unwrap_or("default")) .send() .await?; match response.status() { reqwest::StatusCode::OK => { let api_data: Value = response.json().await?; let scim_resource = self.scim_mapper.api_to_scim(resource_type, api_data)?; Ok(Some(Resource::new(scim_resource))) }, reqwest::StatusCode::NOT_FOUND => Ok(None), status => Err(ApiProviderError::ApiError(status, response.text().await?)), } } // ... other methods } impl ExternalApiProvider { async fn create_user_via_api( &self, scim_data: Value, context: &RequestContext, ) -> Result<Resource, ApiProviderError> { // 1. Convert SCIM to external API format let api_payload = self.scim_mapper.scim_to_api("User", scim_data)?; // 2. Add tenant context to API call let mut enriched_payload = api_payload; if let Some(tenant_id) = context.tenant_id() { enriched_payload["tenant_id"] = json!(tenant_id); } // 3. Make API call let response = self.http_client .post(&format!("{}/api/v1/users", self.api_base_url)) .header("Authorization", format!("Bearer {}", self.api_token)) .header("Content-Type", "application/json") .json(&enriched_payload) .send() .await?; match response.status() { reqwest::StatusCode::CREATED => { let created_user: Value = response.json().await?; let scim_resource = self.scim_mapper.api_to_scim("User", created_user)?; Ok(Resource::new(scim_resource)) }, status => { let error_body = response.text().await?; Err(ApiProviderError::ApiError(status, error_body)) } } } } #[derive(Debug)] pub struct ScimApiMapper { // Configuration for field mappings user_field_mappings: HashMap<String, String>, group_field_mappings: HashMap<String, String>, } impl ScimApiMapper { pub fn new() -> Self { let mut user_mappings = HashMap::new(); user_mappings.insert("userName".to_string(), "username".to_string()); user_mappings.insert("displayName".to_string(), "display_name".to_string()); user_mappings.insert("name.givenName".to_string(), "first_name".to_string()); user_mappings.insert("name.familyName".to_string(), "last_name".to_string()); Self { user_field_mappings: user_mappings, group_field_mappings: HashMap::new(), } } pub fn scim_to_api(&self, resource_type: &str, scim_data: Value) -> Result<Value, MappingError> { match resource_type { "User" => self.map_user_scim_to_api(scim_data), "Group" => self.map_group_scim_to_api(scim_data), _ => Err(MappingError::UnsupportedResourceType(resource_type.to_string())), } } fn map_user_scim_to_api(&self, scim_data: Value) -> Result<Value, MappingError> { let mut api_data = json!({}); // Map simple fields for (scim_field, api_field) in &self.user_field_mappings { if let Some(value) = scim_data.get(scim_field) { api_data[api_field] = value.clone(); } } // Handle complex fields like emails if let Some(emails) = scim_data.get("emails").and_then(|v| v.as_array()) { if let Some(primary_email) = emails.iter().find(|e| { e.get("primary").and_then(|p| p.as_bool()).unwrap_or(false) }) { if let Some(email_value) = primary_email.get("value") { api_data["email"] = email_value.clone(); } } } Ok(api_data) } pub fn api_to_scim(&self, resource_type: &str, api_data: Value) -> Result<Value, MappingError> { match resource_type { "User" => self.map_user_api_to_scim(api_data), "Group" => self.map_group_api_to_scim(api_data), _ => Err(MappingError::UnsupportedResourceType(resource_type.to_string())), } } fn map_user_api_to_scim(&self, api_data: Value) -> Result<Value, MappingError> { let mut scim_data = json!({ "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], }); // Reverse mapping from API to SCIM for (scim_field, api_field) in &self.user_field_mappings { if let Some(value) = api_data.get(api_field) { scim_data[scim_field] = value.clone(); } } // Handle complex fields if let Some(email) = api_data.get("email").and_then(|v| v.as_str()) { scim_data["emails"] = json!([{ "value": email, "type": "work", "primary": true }]); } // Add metadata scim_data["meta"] = json!({ "resourceType": "User", "created": api_data.get("created_at").unwrap_or(&json!("")), "lastModified": api_data.get("updated_at").unwrap_or(&json!("")), }); Ok(scim_data) } } }
Hybrid Provider Pattern
Sometimes you need different strategies for different resource types:
#![allow(unused)] fn main() { pub struct HybridResourceProvider { user_provider: Box<dyn ResourceProvider<Error = Box<dyn std::error::Error + Send + Sync>>>, group_provider: Box<dyn ResourceProvider<Error = Box<dyn std::error::Error + Send + Sync>>>, default_provider: StandardResourceProvider<InMemoryStorage>, } impl ResourceProvider for HybridResourceProvider { type Error = HybridProviderError; async fn create_resource( &self, resource_type: &str, data: Value, context: &RequestContext, ) -> Result<Resource, Self::Error> { match resource_type { "User" => { self.user_provider .create_resource(resource_type, data, context) .await .map_err(|e| HybridProviderError::UserProviderError(e)) }, "Group" => { self.group_provider .create_resource(resource_type, data, context) .await .map_err(|e| HybridProviderError::GroupProviderError(e)) }, _ => { // Fall back to default provider for other resource types self.default_provider .create_resource(resource_type, data, context) .await .map_err(|e| HybridProviderError::DefaultProviderError(e)) } } } // Similar delegation for other methods... } }
Performance Optimization Patterns
Caching Layer Pattern
#![allow(unused)] fn main() { use std::time::{Duration, Instant}; use tokio::sync::RwLock; pub struct CachingResourceProvider<P: ResourceProvider> { inner_provider: P, cache: Arc<RwLock<ResourceCache>>, cache_ttl: Duration, } struct ResourceCache { resources: HashMap<String, CachedResource>, list_cache: HashMap<String, CachedList>, } struct CachedResource { resource: Resource, cached_at: Instant, version: String, } struct CachedList { resources: Vec<Resource>, cached_at: Instant, } impl<P: ResourceProvider> CachingResourceProvider<P> { pub fn new(inner_provider: P, cache_ttl: Duration) -> Self { Self { inner_provider, cache: Arc::new(RwLock::new(ResourceCache { resources: HashMap::new(), list_cache: HashMap::new(), })), cache_ttl, } } fn cache_key(&self, resource_type: &str, id: &str, context: &RequestContext) -> String { if let Some(tenant_id) = context.tenant_id() { format!("{}:{}:{}", tenant_id, resource_type, id) } else { format!("{}:{}", resource_type, id) } } fn list_cache_key(&self, resource_type: &str, context: &RequestContext) -> String { if let Some(tenant_id) = context.tenant_id() { format!("{}:{}:list", tenant_id, resource_type) } else { format!("{}:list", resource_type) } } async fn is_cache_valid(&self, cached_at: Instant) -> bool { cached_at.elapsed() < self.cache_ttl } async fn invalidate_related_cache(&self, resource_type: &str, context: &RequestContext) { let mut cache = self.cache.write().await; // Invalidate list cache for this resource type let list_key = self.list_cache_key(resource_type, context); cache.list_cache.remove(&list_key); // For group operations, also invalidate user caches (due to membership changes) if resource_type == "Group" { cache.list_cache.remove(&self.list_cache_key("User", context)); } } } impl<P: ResourceProvider> ResourceProvider for CachingResourceProvider<P> { type Error = P::Error; async fn get_resource( &self, resource_type: &str, id: &str, context: &RequestContext, ) -> Result<Option<Resource>, Self::Error> { let cache_key = self.cache_key(resource_type, id, context); // Check cache first { let cache = self.cache.read().await; if let Some(cached) = cache.resources.get(&cache_key) { if self.is_cache_valid(cached.cached_at).await { return Ok(Some(cached.resource.clone())); } } } // Cache miss or expired - fetch from inner provider let resource = self.inner_provider.get_resource(resource_type, id, context).await?; // Update cache if let Some(ref res) = resource { let mut cache = self.cache.write().await; cache.resources.insert(cache_key, CachedResource { resource: res.clone(), cached_at: Instant::now(), version: res.version().unwrap_or_default(), }); } Ok(resource) } async fn create_resource( &self, resource_type: &str, data: Value, context: &RequestContext, ) -> Result<Resource, Self::Error> { let resource = self.inner_provider.create_resource(resource_type, data, context).await?; // Invalidate related caches self.invalidate_related_cache(resource_type, context).await; // Cache the new resource let cache_key = self.cache_key(resource_type, resource.id(), context); let mut cache = self.cache.write().await; cache.resources.insert(cache_key, CachedResource { resource: resource.clone(), cached_at: Instant::now(), version: resource.version().unwrap_or_default(), }); Ok(resource) } async fn update_resource( &self, resource_type: &str, id: &str, data: Value, context: &RequestContext, ) -> Result<Resource, Self::Error> { let resource = self.inner_provider.update_resource(resource_type, id, data, context).await?; // Update cache let cache_key = self.cache_key(resource_type, id, context); let mut cache = self.cache.write().await; cache.resources.insert(cache_key, CachedResource { resource: resource.clone(), cached_at: Instant::now(), version: resource.version().unwrap_or_default(), }); // Invalidate list caches self.invalidate_related_cache(resource_type, context).await; Ok(resource) } async fn delete_resource( &self, resource_type: &str, id: &str, context: &RequestContext, ) -> Result<bool, Self::Error> { let deleted = self.inner_provider.delete_resource(resource_type, id, context).await?; if deleted { // Remove from cache let cache_key = self.cache_key(resource_type, id, context); let mut cache = self.cache.write().await; cache.resources.remove(&cache_key); // Invalidate list caches self.invalidate_related_cache(resource_type, context).await; } Ok(deleted) } } }
Connection Pooling Pattern
#![allow(unused)] fn main() { use deadpool_postgres::{Config, Pool, Runtime}; use tokio_postgres::NoTls; pub struct PooledDatabaseProvider { pool: Pool, max_connections: usize, } impl PooledDatabaseProvider { pub fn new(database_url: &str, max_connections: usize) -> Result<Self, PoolError> { let mut cfg = Config::new(); cfg.url = Some(database_url.to_string()); cfg.pool = Some(deadpool_postgres::PoolConfig::new(max_connections)); let pool = cfg.create_pool(Some(Runtime::Tokio1), NoTls)?; Ok(Self { pool, max_connections, }) } pub async fn health_check(&self) -> Result<(), HealthCheckError> { let client = self.pool.get().await?; client.execute("SELECT 1", &[]).await?; Ok(()) } pub async fn get_pool_status(&self) -> PoolStatus { let status = self.pool.status(); PoolStatus { size: status.size, available: status.available, waiting: status.waiting, max_size: self.max_connections, } } } }
Error Handling Patterns
Comprehensive Error Mapping
#![allow(unused)] fn main() { use scim_server::{ScimError, ScimResult}; use thiserror::Error; #[derive(Error, Debug)] pub enum CustomProviderError { #[error("Database error: {0}")] DatabaseError(#[from] sqlx::Error), #[error("External API error: {status} - {message}")] ApiError { status: reqwest::StatusCode, message: String }, #[error("Business rule violation: {rule} - {details}")] BusinessRuleViolation { rule: String, details: String }, #[error("Resource not found: {resource_type} with id {id}")] ResourceNotFound { resource_type: String, id: String }, #[error("Tenant limit exceeded: {resource_type} ({current}/{limit})")] TenantLimitExceeded { resource_type: String, current: usize, limit: usize }, #[error("Invalid data format: {field} - {reason}")] InvalidDataFormat { field: String, reason: String }, #[error("Concurrent modification detected for {resource_type} {id}")] ConcurrentModification { resource_type: String, id: String }, } impl From<CustomProviderError> for ScimError { fn from(error: CustomProviderError) -> Self { match error { CustomProviderError::ResourceNotFound { resource_type, id } => { ScimError::NotFound(format!("{} {} not found", resource_type, id)) }, CustomProviderError::TenantLimitExceeded { resource_type, current, limit } => { ScimError::BadRequest(format!( "Tenant limit exceeded for {}: {}/{}", resource_type, current, limit )) }, CustomProviderError::BusinessRuleViolation { rule, details } => { ScimError::BadRequest(format!("Business rule '{}' violated: {}", rule, details)) }, CustomProviderError::ConcurrentModification { resource_type, id } => { ScimError::PreconditionFailed(format!( "Resource {} {} was modified by another request", resource_type, id )) }, CustomProviderError::InvalidDataFormat { field, reason } => { ScimError::BadRequest(format!("Invalid {}: {}", field, reason)) }, CustomProviderError::DatabaseError(db_error) => { // Log the detailed database error but return generic error to client tracing::error!(error = %db_error, "Database operation failed"); ScimError::InternalError("Database operation failed".to_string()) }, CustomProviderError::ApiError { status, message } => { tracing::error!(status = %status, message = %message, "External API error"); match status { reqwest::StatusCode::NOT_FOUND => ScimError::NotFound("Resource not found in external system".to_string()), reqwest::StatusCode::CONFLICT => ScimError::Conflict("Resource conflict in external system".to_string()), _ => ScimError::InternalError("External system error".to_string()), } }, } } } }
Testing Patterns
Mock Provider for Testing
#![allow(unused)] fn main() { use async_trait::async_trait; use std::collections::HashMap; use std::sync::{Arc, Mutex}; pub struct MockResourceProvider { resources: Arc<Mutex<HashMap<String, Value>>>, fail_on_create: bool, simulate_latency: Option<Duration>, } impl MockResourceProvider { pub fn new() -> Self { Self { resources: Arc::new(Mutex::new(HashMap::new())), fail_on_create: false, simulate_latency: None, } } pub fn with_failure_simulation(mut self, fail_on_create: bool) -> Self { self.fail_on_create = fail_on_create; self } pub fn with_latency_simulation(mut self, latency: Duration) -> Self { self.simulate_latency = Some(latency); self } pub fn preset_resource(&self, resource_type: &str, id: &str, data: Value) { let key = format!("{}:{}", resource_type, id); self.resources.lock().unwrap().insert(key, data); } pub fn resource_count(&self, resource_type: &str) -> usize { let resources = self.resources.lock().unwrap(); resources.keys().filter(|k| k.starts_with(&format!("{}:", resource_type))).count() } } #[async_trait] impl ResourceProvider for MockResourceProvider { type Error = MockProviderError; async fn create_resource( &self, resource_type: &str, mut data: Value, _context: &RequestContext, ) -> Result<Resource, Self::Error> { if let Some(latency) = self.simulate_latency { tokio::time::sleep(latency).await; } if self.fail_on_create { return Err(MockProviderError::SimulatedFailure); } let id = uuid::Uuid::new_v4().to_string(); data["id"] = json!(id); data["meta"] = json!({ "resourceType": resource_type, "created": chrono::Utc::now().to_rfc3339(), "lastModified": chrono::Utc::now().to_rfc3339(), "version": format!("W/\"{}\"", uuid::Uuid::new_v4()), }); let key = format!("{}:{}", resource_type, id); self.resources.lock().unwrap().insert(key, data.clone()); Ok(Resource::new(data)) } async fn get_resource( &self, resource_type: &str, id: &str, _context: &RequestContext, ) -> Result<Option<Resource>, Self::Error> { if let Some(latency) = self.simulate_latency { tokio::time::sleep(latency).await; } let key = format!("{}:{}", resource_type, id); let resources = self.resources.lock().unwrap(); Ok(resources.get(&key).map(|data| Resource::new(data.clone()))) } // ... other methods } #[derive(Error, Debug)] pub enum MockProviderError { #[error("Simulated failure for testing")] SimulatedFailure, } }
Integration Test Patterns
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; use scim_server::{ScimServer, RequestContext}; use serde_json::json; #[tokio::test] async fn test_custom_provider_integration() { let provider = MockResourceProvider::new(); let server = ScimServer::new(provider).unwrap(); let context = RequestContext::new("test-request".to_string()); // Test resource creation let user_data = json!({ "userName": "test.user", "displayName": "Test User", "emails": [{ "value": "test@example.com", "primary": true }] }); let created_user = server .create_resource("User", user_data.clone(), &context) .await .expect("Failed to create user"); assert!(created_user.id().is_some()); assert_eq!(created_user.get("userName").unwrap(), "test.user"); // Test resource retrieval let retrieved_user = server .get_resource("User", created_user.id().unwrap(), &context) .await .expect("Failed to get user") .expect("User not found"); assert_eq!(retrieved_user.get("userName").unwrap(), "test.user"); } #[tokio::test] async fn test_multi_tenant_provider_isolation() { let provider = MockResourceProvider::new(); let server = ScimServer::new(provider).unwrap(); // Create contexts for different tenants let tenant_a_context = RequestContext::with_tenant_generated_id( TenantContext::new("tenant-a".to_string(), "client-a".to_string()) ); let tenant_b_context = RequestContext::with_tenant_generated_id( TenantContext::new("tenant-b".to_string(), "client-b".to_string()) ); // Create user in tenant A let user_data = json!({ "userName": "user.a", "displayName": "User A" }); let user_a = server .create_resource("User", user_data, &tenant_a_context) .await .expect("Failed to create user for tenant A"); // Try to access from tenant B - should not be found let not_found = server .get_resource("User", user_a.id().unwrap(), &tenant_b_context) .await .expect("Operation should succeed but return None"); assert!(not_found.is_none(), "Tenant isolation violated"); // But should be accessible from tenant A let found = server .get_resource("User", user_a.id().unwrap(), &tenant_a_context) .await .expect("Should be able to access own tenant's resources") .expect("User should exist in tenant A"); assert_eq!(found.get("userName").unwrap(), "user.a"); } #[tokio::test] async fn test_error_handling() { let provider = MockResourceProvider::new().with_failure_simulation(true); let server = ScimServer::new(provider).unwrap(); let context = RequestContext::new("test-request".to_string()); let user_data = json!({ "userName": "test.user", "displayName": "Test User" }); let result = server .create_resource("User", user_data, &context) .await; assert!(result.is_err(), "Expected simulated failure"); match result.unwrap_err() { ScimError::InternalError(_) => { // Expected error type }, other => panic!("Unexpected error type: {:?}", other), } } } }
Best Practices Summary
Architecture Decisions
- Start with StandardResourceProvider for rapid development and standard compliance
- Move to custom ResourceProvider when you need specialized business logic
- Use hybrid approaches for different resource types with different requirements
- Consider caching layers for performance-critical applications
- Implement comprehensive error handling with proper error mapping
Performance Considerations
- Use connection pooling for database providers
- Implement caching for frequently accessed resources
- Optimize database queries with proper indexing and query patterns
- Consider async operations for external API integrations
- Monitor resource provider performance with metrics and tracing
Security and Multi-Tenancy
- Always validate tenant boundaries in custom providers
- Apply tenant scoping at the storage level
- Implement proper permission checking before resource operations
- Use secure credential handling for external API integrations
- Audit all resource operations for compliance requirements
Related Topics
- Request Lifecycle & Context Management - How requests flow through resource providers
- Multi-Tenant Architecture Patterns - Tenant isolation in resource providers
- Storage Providers - Understanding the storage abstraction layer
- Resource Providers - Core concepts and interfaces
Next Steps
Now that you understand Resource Provider architecture:
- Evaluate your requirements using the decision matrix
- Choose your provider strategy (Standard, Custom, or Hybrid)
- Implement your data integration layer following the patterns
- Add proper error handling and logging for production readiness
- Set up performance monitoring and optimization strategies
Authentication & Authorization Strategies
This deep dive explores authentication and authorization patterns in SCIM Server, covering different authentication strategies, role-based access control (RBAC), compile-time vs runtime security patterns, and integration with existing identity systems.
Overview
Authentication and authorization in SCIM Server involves multiple layers working together to provide secure access control. This document shows how to implement robust security patterns that scale from simple API key authentication to complex enterprise identity integration.
Core Security Flow:
Client Request β Authentication β Authorization β Tenant Resolution β
Resource Access Control β Operation Execution β Audit Logging
Authentication Architecture Overview
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Authentication Layer β
β β
β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββββββββββββββββββ β
β β API Key Auth β β JWT/OAuth2 β β Custom Authentication β β
β β β β β β β β
β β β’ Simple setup β β β’ Standards β β β’ Legacy system integration β β
β β β’ High perf β β compliant β β β’ Custom protocols β β
β β β’ Tenant scoped β β β’ Token based β β β’ Advanced security β β
β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Authorization Layer β
β β
β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββββββββββββββββββ β
β β Permission-Basedβ β Role-Based β β Attribute-Based (ABAC) β β
β β (Simple) β β (RBAC) β β (Advanced) β β
β β β β β β β β
β β β’ Resource ops β β β’ Role β β β’ Context-aware decisions β β
β β β’ CRUD perms β β hierarchies β β β’ Policy engine integration β β
β β β’ Tenant limits β β β’ Dynamic perms β β β’ Fine-grained control β β
β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Access Control Enforcement β
β β’ Resource-level filtering β’ Operation validation β’ Audit logging β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Authentication Strategies
Strategy 1: API Key Authentication
Best for machine-to-machine integrations and simple setups:
#![allow(unused)] fn main() { use scim_server::auth::{AuthenticationProvider, AuthenticationResult, Credential}; use std::collections::HashMap; use tokio::sync::RwLock; pub struct ApiKeyAuthProvider { api_keys: RwLock<HashMap<String, ApiKeyInfo>>, key_validator: KeyValidator, } #[derive(Clone)] pub struct ApiKeyInfo { pub key_id: String, pub tenant_id: String, pub client_id: String, pub permissions: Vec<String>, pub expires_at: Option<chrono::DateTime<chrono::Utc>>, pub rate_limit: Option<RateLimit>, pub created_at: chrono::DateTime<chrono::Utc>, pub last_used_at: Option<chrono::DateTime<chrono::Utc>>, } #[derive(Clone)] pub struct RateLimit { pub requests_per_minute: u32, pub burst_capacity: u32, } impl ApiKeyAuthProvider { pub fn new(key_validator: KeyValidator) -> Self { Self { api_keys: RwLock::new(HashMap::new()), key_validator, } } pub async fn create_api_key( &self, tenant_id: String, client_id: String, permissions: Vec<String>, expires_at: Option<chrono::DateTime<chrono::Utc>>, ) -> Result<String, ApiKeyError> { let api_key = self.key_validator.generate_secure_key()?; let key_info = ApiKeyInfo { key_id: uuid::Uuid::new_v4().to_string(), tenant_id, client_id, permissions, expires_at, rate_limit: Some(RateLimit { requests_per_minute: 1000, burst_capacity: 100, }), created_at: chrono::Utc::now(), last_used_at: None, }; self.api_keys.write().await.insert(api_key.clone(), key_info); Ok(api_key) } pub async fn revoke_api_key(&self, api_key: &str) -> Result<bool, ApiKeyError> { Ok(self.api_keys.write().await.remove(api_key).is_some()) } pub async fn rotate_api_key(&self, old_key: &str) -> Result<String, ApiKeyError> { let mut keys = self.api_keys.write().await; if let Some(mut key_info) = keys.remove(old_key) { let new_key = self.key_validator.generate_secure_key()?; key_info.created_at = chrono::Utc::now(); key_info.last_used_at = None; keys.insert(new_key.clone(), key_info); Ok(new_key) } else { Err(ApiKeyError::KeyNotFound) } } } impl AuthenticationProvider for ApiKeyAuthProvider { type Error = ApiKeyAuthError; async fn authenticate(&self, credential: &Credential) -> Result<AuthenticationResult, Self::Error> { let api_key = match credential { Credential::ApiKey(key) => key, Credential::BearerToken(token) => { // Support Bearer token format: "Bearer api_key_here" token.strip_prefix("Bearer ").unwrap_or(token) }, _ => return Err(ApiKeyAuthError::UnsupportedCredentialType), }; let mut keys = self.api_keys.write().await; let key_info = keys.get_mut(api_key) .ok_or(ApiKeyAuthError::InvalidApiKey)?; // Check expiration if let Some(expires_at) = key_info.expires_at { if chrono::Utc::now() > expires_at { keys.remove(api_key); return Err(ApiKeyAuthError::ApiKeyExpired); } } // Update last used timestamp key_info.last_used_at = Some(chrono::Utc::now()); // Build authentication result Ok(AuthenticationResult { authenticated: true, tenant_id: Some(key_info.tenant_id.clone()), client_id: key_info.client_id.clone(), subject: key_info.key_id.clone(), permissions: key_info.permissions.clone(), metadata: HashMap::from([ ("auth_method".to_string(), "api_key".to_string()), ("key_id".to_string(), key_info.key_id.clone()), ]), }) } async fn validate_permissions( &self, auth_result: &AuthenticationResult, required_permission: &str, ) -> Result<bool, Self::Error> { Ok(auth_result.permissions.contains(&required_permission.to_string()) || auth_result.permissions.contains(&"*".to_string())) } } pub struct KeyValidator { key_length: usize, key_prefix: Option<String>, } impl KeyValidator { pub fn new(key_length: usize, key_prefix: Option<String>) -> Self { Self { key_length, key_prefix } } pub fn generate_secure_key(&self) -> Result<String, ApiKeyError> { use rand::Rng; const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; let mut rng = rand::thread_rng(); let key: String = (0..self.key_length) .map(|_| { let idx = rng.gen_range(0..CHARSET.len()); CHARSET[idx] as char }) .collect(); match &self.key_prefix { Some(prefix) => Ok(format!("{}_{}", prefix, key)), None => Ok(key), } } } // Usage example async fn setup_api_key_auth() -> Result<ApiKeyAuthProvider, Box<dyn std::error::Error>> { let key_validator = KeyValidator::new(32, Some("sk".to_string())); let auth_provider = ApiKeyAuthProvider::new(key_validator); // Create API key for tenant let api_key = auth_provider.create_api_key( "tenant-123".to_string(), "client-app".to_string(), vec!["scim:read".to_string(), "scim:write".to_string()], Some(chrono::Utc::now() + chrono::Duration::days(90)), // 90 day expiry ).await?; println!("Generated API key: {}", api_key); Ok(auth_provider) } }
Strategy 2: JWT/OAuth2 Authentication
Standards-compliant token-based authentication:
#![allow(unused)] fn main() { use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation, Algorithm}; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] pub struct ScimClaims { pub sub: String, // Subject (user/client ID) pub iss: String, // Issuer pub aud: String, // Audience pub exp: usize, // Expiration time pub iat: usize, // Issued at pub jti: String, // JWT ID pub tenant_id: Option<String>, // Tenant identifier pub client_id: String, // Client application pub scope: String, // OAuth2 scopes pub permissions: Vec<String>, // SCIM-specific permissions } pub struct JwtAuthProvider { decoding_key: DecodingKey, encoding_key: Option<EncodingKey>, validation: Validation, issuer: String, audience: String, } impl JwtAuthProvider { pub fn new( secret: &str, issuer: String, audience: String, algorithm: Algorithm, ) -> Self { let mut validation = Validation::new(algorithm); validation.set_issuer(&[issuer.clone()]); validation.set_audience(&[audience.clone()]); validation.validate_exp = true; validation.validate_nbf = true; Self { decoding_key: DecodingKey::from_secret(secret.as_ref()), encoding_key: Some(EncodingKey::from_secret(secret.as_ref())), validation, issuer, audience, } } pub fn from_public_key( public_key_pem: &str, issuer: String, audience: String, algorithm: Algorithm, ) -> Result<Self, JwtAuthError> { let mut validation = Validation::new(algorithm); validation.set_issuer(&[issuer.clone()]); validation.set_audience(&[audience.clone()]); Ok(Self { decoding_key: DecodingKey::from_rsa_pem(public_key_pem.as_bytes())?, encoding_key: None, validation, issuer, audience, }) } pub fn create_token(&self, claims: ScimClaims) -> Result<String, JwtAuthError> { let encoding_key = self.encoding_key.as_ref() .ok_or(JwtAuthError::TokenCreationNotSupported)?; encode(&Header::default(), &claims, encoding_key) .map_err(JwtAuthError::from) } fn parse_scopes_to_permissions(scope: &str) -> Vec<String> { scope.split_whitespace() .filter_map(|s| { match s { "scim:read" => Some("scim:read".to_string()), "scim:write" => Some("scim:write".to_string()), "scim:admin" => Some("*".to_string()), _ if s.starts_with("scim:") => Some(s.to_string()), _ => None, } }) .collect() } } impl AuthenticationProvider for JwtAuthProvider { type Error = JwtAuthError; async fn authenticate(&self, credential: &Credential) -> Result<AuthenticationResult, Self::Error> { let token = match credential { Credential::BearerToken(token) => { token.strip_prefix("Bearer ").unwrap_or(token) }, Credential::JwtToken(token) => token, _ => return Err(JwtAuthError::UnsupportedCredentialType), }; let token_data = decode::<ScimClaims>(token, &self.decoding_key, &self.validation)?; let claims = token_data.claims; // Convert OAuth2 scopes to SCIM permissions let mut permissions = Self::parse_scopes_to_permissions(&claims.scope); permissions.extend(claims.permissions); Ok(AuthenticationResult { authenticated: true, tenant_id: claims.tenant_id, client_id: claims.client_id, subject: claims.sub, permissions, metadata: HashMap::from([ ("auth_method".to_string(), "jwt".to_string()), ("jti".to_string(), claims.jti), ("iss".to_string(), claims.iss), ("scope".to_string(), claims.scope), ]), }) } async fn validate_permissions( &self, auth_result: &AuthenticationResult, required_permission: &str, ) -> Result<bool, Self::Error> { // Check explicit permission or wildcard Ok(auth_result.permissions.contains(&required_permission.to_string()) || auth_result.permissions.contains(&"*".to_string())) } } // OAuth2 integration example pub struct OAuth2Integration { jwt_provider: JwtAuthProvider, oauth_client: oauth2::basic::BasicClient, } impl OAuth2Integration { pub async fn exchange_authorization_code( &self, code: String, redirect_uri: String, ) -> Result<String, OAuth2Error> { use oauth2::{AuthorizationCode, RedirectUrl, TokenResponse}; let token_result = self.oauth_client .exchange_code(AuthorizationCode::new(code)) .set_redirect_uri(RedirectUrl::new(redirect_uri)?) .request_async(oauth2::reqwest::async_http_client) .await?; let access_token = token_result.access_token().secret(); // Convert OAuth2 access token to SCIM JWT let claims = ScimClaims { sub: "user-from-oauth".to_string(), // Extract from OAuth2 user info iss: self.jwt_provider.issuer.clone(), aud: self.jwt_provider.audience.clone(), exp: (chrono::Utc::now() + chrono::Duration::hours(1)).timestamp() as usize, iat: chrono::Utc::now().timestamp() as usize, jti: uuid::Uuid::new_v4().to_string(), tenant_id: Some("extracted-from-oauth".to_string()), client_id: "oauth-client".to_string(), scope: token_result.scopes() .map(|scopes| scopes.iter().map(|s| s.to_string()).collect::<Vec<_>>().join(" ")) .unwrap_or_default(), permissions: vec!["scim:read".to_string(), "scim:write".to_string()], }; self.jwt_provider.create_token(claims) } } }
Strategy 3: Custom Enterprise Authentication
Integration with existing enterprise identity systems:
#![allow(unused)] fn main() { use ldap3::{LdapConn, Scope, SearchEntry}; use async_trait::async_trait; pub struct LdapAuthProvider { ldap_url: String, base_dn: String, bind_dn: String, bind_password: String, user_search_filter: String, group_search_filter: String, connection_pool: deadpool::managed::Pool<LdapConnectionManager>, } pub struct LdapConnectionManager { ldap_url: String, bind_dn: String, bind_password: String, } impl LdapAuthProvider { pub async fn new( ldap_url: String, base_dn: String, bind_dn: String, bind_password: String, pool_size: usize, ) -> Result<Self, LdapAuthError> { let manager = LdapConnectionManager { ldap_url: ldap_url.clone(), bind_dn: bind_dn.clone(), bind_password: bind_password.clone(), }; let pool_config = deadpool::managed::PoolConfig::new(pool_size); let connection_pool = deadpool::managed::Pool::builder(manager) .config(pool_config) .build()?; Ok(Self { ldap_url, base_dn, bind_dn, bind_password, user_search_filter: "(uid={})".to_string(), group_search_filter: "(member={})".to_string(), connection_pool, }) } async fn authenticate_user( &self, username: &str, password: &str, ) -> Result<LdapUserInfo, LdapAuthError> { let conn = self.connection_pool.get().await?; // Search for user let user_filter = self.user_search_filter.replace("{}", username); let (rs, _res) = conn.search( &self.base_dn, Scope::Subtree, &user_filter, vec!["dn", "uid", "mail", "displayName", "memberOf"] ).await?; let user_entry = rs.into_iter() .next() .ok_or(LdapAuthError::UserNotFound)?; let user_entry = SearchEntry::construct(user_entry); let user_dn = user_entry.dn.clone(); // Authenticate with user credentials let mut user_conn = LdapConn::new(&self.ldap_url)?; user_conn.simple_bind(&user_dn, password).await?; // Extract user information let user_info = LdapUserInfo { dn: user_dn, username: user_entry.attrs.get("uid") .and_then(|v| v.first()) .unwrap_or(&username) .clone(), email: user_entry.attrs.get("mail") .and_then(|v| v.first()) .cloned(), display_name: user_entry.attrs.get("displayName") .and_then(|v| v.first()) .cloned(), groups: user_entry.attrs.get("memberOf") .map(|groups| groups.clone()) .unwrap_or_default(), }; Ok(user_info) } async fn map_groups_to_permissions(&self, groups: &[String]) -> Vec<String> { let mut permissions = Vec::new(); for group in groups { match group.as_str() { g if g.contains("scim-admins") => permissions.push("*".to_string()), g if g.contains("scim-users") => { permissions.push("scim:read".to_string()); permissions.push("scim:write".to_string()); }, g if g.contains("scim-readonly") => permissions.push("scim:read".to_string()), _ => {} // No SCIM permissions for other groups } } if permissions.is_empty() { permissions.push("scim:read".to_string()); // Default permission } permissions } } #[async_trait] impl AuthenticationProvider for LdapAuthProvider { type Error = LdapAuthError; async fn authenticate(&self, credential: &Credential) -> Result<AuthenticationResult, Self::Error> { let (username, password) = match credential { Credential::UsernamePassword { username, password } => (username, password), Credential::BasicAuth(encoded) => { let decoded = base64::decode(encoded)?; let auth_str = String::from_utf8(decoded)?; let parts: Vec<&str> = auth_str.splitn(2, ':').collect(); if parts.len() != 2 { return Err(LdapAuthError::InvalidBasicAuth); } (parts[0], parts[1]) }, _ => return Err(LdapAuthError::UnsupportedCredentialType), }; let user_info = self.authenticate_user(username, password).await?; let permissions = self.map_groups_to_permissions(&user_info.groups).await; // Extract tenant from user's organizational unit or group let tenant_id = user_info.groups.iter() .find(|g| g.contains("ou=")) .and_then(|g| g.split("ou=").nth(1)) .and_then(|ou| ou.split(",").next()) .map(|s| s.to_string()); Ok(AuthenticationResult { authenticated: true, tenant_id, client_id: "ldap-auth".to_string(), subject: user_info.username.clone(), permissions, metadata: HashMap::from([ ("auth_method".to_string(), "ldap".to_string()), ("user_dn".to_string(), user_info.dn), ("email".to_string(), user_info.email.unwrap_or_default()), ("display_name".to_string(), user_info.display_name.unwrap_or_default()), ]), }) } async fn validate_permissions( &self, auth_result: &AuthenticationResult, required_permission: &str, ) -> Result<bool, Self::Error> { Ok(auth_result.permissions.contains(&required_permission.to_string()) || auth_result.permissions.contains(&"*".to_string())) } } #[derive(Debug, Clone)] struct LdapUserInfo { dn: String, username: String, email: Option<String>, display_name: Option<String>, groups: Vec<String>, } }
Authorization Patterns
Role-Based Access Control (RBAC)
#![allow(unused)] fn main() { use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Role { pub name: String, pub description: String, pub permissions: HashSet<Permission>, pub parent_roles: Vec<String>, pub resource_constraints: Vec<ResourceConstraint>, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum Permission { // SCIM resource permissions CreateUser, ReadUser, UpdateUser, DeleteUser, ListUsers, CreateGroup, ReadGroup, UpdateGroup, DeleteGroup, ListGroups, // Administrative permissions ManageSchema, ManageTenants, ViewMetrics, ManageApiKeys, // Wildcard permissions All, AllUsers, AllGroups, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ResourceConstraint { pub resource_type: String, pub constraint_type: ConstraintType, pub constraint_value: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ConstraintType { AttributeEquals, AttributeContains, AttributeStartsWith, TenantEquals, GroupMember, } pub struct RbacAuthProvider { roles: RwLock<HashMap<String, Role>>, user_roles: RwLock<HashMap<String, Vec<String>>>, role_hierarchy: RwLock<HashMap<String, HashSet<String>>>, } impl RbacAuthProvider { pub fn new() -> Self { let mut provider = Self { roles: RwLock::new(HashMap::new()), user_roles: RwLock::new(HashMap::new()), role_hierarchy: RwLock::new(HashMap::new()), }; // Initialize default roles provider.setup_default_roles(); provider } fn setup_default_roles(&mut self) { let roles = vec![ Role { name: "scim_admin".to_string(), description: "Full SCIM administration access".to_string(), permissions: [Permission::All].into_iter().collect(), parent_roles: vec![], resource_constraints: vec![], }, Role { name: "user_manager".to_string(), description: "User resource management".to_string(), permissions: [ Permission::CreateUser, Permission::ReadUser, Permission::UpdateUser, Permission::DeleteUser, Permission::ListUsers ].into_iter().collect(), parent_roles: vec![], resource_constraints: vec![], }, Role { name: "group_manager".to_string(), description: "Group resource management".to_string(), permissions: [ Permission::CreateGroup, Permission::ReadGroup, Permission::UpdateGroup, Permission::DeleteGroup, Permission::ListGroups, Permission::ReadUser, Permission::ListUsers // Needed for group membership ].into_iter().collect(), parent_roles: vec![], resource_constraints: vec![], }, Role { name: "readonly".to_string(), description: "Read-only access to all resources".to_string(), permissions: [ Permission::ReadUser, Permission::ListUsers, Permission::ReadGroup, Permission::ListGroups ].into_iter().collect(), parent_roles: vec![], resource_constraints: vec![], }, Role { name: "tenant_admin".to_string(), description: "Administrative access within tenant".to_string(), permissions: [Permission::AllUsers, Permission::AllGroups].into_iter().collect(), parent_roles: vec![], resource_constraints: vec![ ResourceConstraint { resource_type: "*".to_string(), constraint_type: ConstraintType::TenantEquals, constraint_value: "${user.tenant_id}".to_string(), } ], }, ]; let mut role_map = self.roles.get_mut().unwrap(); for role in roles { role_map.insert(role.name.clone(), role); } } pub async fn assign_role_to_user(&self, user_id: &str, role_name: &str) -> Result<(), RbacError> { // Verify role exists { let roles = self.roles.read().await; if !roles.contains_key(role_name) { return Err(RbacError::RoleNotFound(role_name.to_string())); } } let mut user_roles = self.user_roles.write().await; user_roles.entry(user_id.to_string()) .or_insert_with(Vec::new) .push(role_name.to_string()); Ok(()) } pub async fn get_effective_permissions( &self, user_id: &str, context: &RequestContext, ) -> Result<HashSet<Permission>, RbacError> { let user_roles_guard = self.user_roles.read().await; let user_roles = user_roles_guard.get(user_id) .ok_or(RbacError::UserNotFound(user_id.to_string()))?; let roles_guard = self.roles.read().await; let mut effective_permissions = HashSet::new(); for role_name in user_roles { if let Some(role) = roles_guard.get(role_name) { // Check if role's resource constraints are satisfied if self.check_resource_constraints(&role.resource_constraints, context).await? { effective_permissions.extend(role.permissions.iter().cloned()); // Include permissions from parent roles effective_permissions.extend( self.get_inherited_permissions(&role.parent_roles, context, &roles_guard).await? ); } } } Ok(effective_permissions) } async fn check_resource_constraints( &self, constraints: &[ResourceConstraint], context: &RequestContext, ) -> Result<bool, RbacError> { for constraint in constraints { match &constraint.constraint_type { ConstraintType::TenantEquals => { let expected_tenant = constraint.constraint_value .replace("${user.tenant_id}", context.tenant_id().unwrap_or("")); if context.tenant_id() != Some(&expected_tenant) { return Ok(false); } }, ConstraintType::AttributeEquals => { // Implementation depends on where user attributes are stored // This is a placeholder for more complex attribute-based constraints }, _ => {} // Other constraint types } } Ok(true) } async fn get_inherited_permissions( &self, parent_roles: &[String], context: &RequestContext, roles: &HashMap<String, Role>, ) -> Result<HashSet<Permission>, RbacError> { let mut inherited = HashSet::new(); for parent_role_name in parent_roles { if let Some(parent_role) = roles.get(parent_role_name) { if self.check_resource_constraints(&parent_role.resource_constraints, context).await? { inherited.extend(parent_role.permissions.iter().cloned()); // Recursive inheritance inherited.extend( self.get_inherited_permissions(&parent_role.parent_roles, context, roles).await? ); } } } Ok(inherited) } } impl AuthenticationProvider for RbacAuthProvider { type Error = RbacAuthError; async fn authenticate(&self, credential: &Credential) -> Result<AuthenticationResult, Self::Error> { // RBAC provider typically works as a wrapper around another auth provider // This is a simplified example match credential { Credential::UserId(user_id) => { // For demo purposes - in practice this would delegate to another provider Ok(AuthenticationResult { authenticated: true, tenant_id: None, client_id: "rbac-system".to_string(), subject: user_id.clone(), permissions: vec![], // Will be populated by authorization check metadata: HashMap::from([ ("auth_method".to_string(), "rbac".to_string()), ]), }) }, _ => Err(RbacAuthError::UnsupportedCredentialType), } } async fn authorize_operation( &self, auth_result: &AuthenticationResult, operation: &str, resource_type: &str, context: &RequestContext, ) -> Result<bool, Self::Error> { let effective_permissions = self.get_effective_permissions(&auth_result.subject, context).await?; let required_permission = match (operation, resource_type) { ("create", "User") => Permission::CreateUser, ("read", "User") => Permission::ReadUser, ("update", "User") => Permission::UpdateUser, ("delete", "User") => Permission::DeleteUser, ("list", "User") => Permission::ListUsers, ("create", "Group") => Permission::CreateGroup, ("read", "Group") => Permission::ReadGroup, ("update", "Group") => Permission::UpdateGroup, ("delete", "Group") => Permission::DeleteGroup, ("list", "Group") => Permission::ListGroups, _ => return Ok(false), }; Ok(effective_permissions.contains(&required_permission) || effective_permissions.contains(&Permission::All) || (resource_type == "User" && effective_permissions.contains(&Permission::AllUsers)) || (resource_type == "Group" && effective_permissions.contains(&Permission::AllGroups))) } } }
Attribute-Based Access Control (ABAC)
#![allow(unused)] fn main() { use serde_json::{Value, json}; pub struct AbacPolicyEngine { policies: RwLock<Vec<AbacPolicy>>, attribute_provider: Arc<dyn AttributeProvider>, policy_evaluator: PolicyEvaluator, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AbacPolicy { pub id: String, pub name: String, pub description: String, pub target: PolicyTarget, pub condition: PolicyCondition, pub effect: PolicyEffect, pub priority: i32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PolicyTarget { pub subjects: Vec<String>, pub resources: Vec<String>, pub actions: Vec<String>, pub environment: Vec<String>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub enum PolicyCondition { Always, Never, AttributeMatch { attribute: String, operator: String, value: Value }, And(Vec<PolicyCondition>), Or(Vec<PolicyCondition>), Not(Box<PolicyCondition>), } #[derive(Debug, Clone, Serialize, Deserialize)] pub enum PolicyEffect { Permit, Deny, } pub trait AttributeProvider: Send + Sync { async fn get_subject_attributes(&self, subject_id: &str) -> Result<HashMap<String, Value>, AttributeError>; async fn get_resource_attributes(&self, resource_type: &str, resource_id: &str) -> Result<HashMap<String, Value>, AttributeError>; async fn get_environment_attributes(&self, context: &RequestContext) -> Result<HashMap<String, Value>, AttributeError>; } impl AbacPolicyEngine { pub fn new(attribute_provider: Arc<dyn AttributeProvider>) -> Self { Self { policies: RwLock::new(Vec::new()), attribute_provider, policy_evaluator: PolicyEvaluator::new(), } } pub async fn add_policy(&self, policy: AbacPolicy) { let mut policies = self.policies.write().await; policies.push(policy); policies.sort_by(|a, b| b.priority.cmp(&a.priority)); // Higher priority first } pub async fn evaluate_authorization( &self, subject_id: &str, resource_type: &str, resource_id: Option<&str>, action: &str, context: &RequestContext, ) -> Result<bool, AbacError> { // Gather attributes let subject_attrs = self.attribute_provider.get_subject_attributes(subject_id).await?; let resource_attrs = if let Some(rid) = resource_id { self.attribute_provider.get_resource_attributes(resource_type, rid).await? } else { HashMap::new() }; let env_attrs = self.attribute_provider.get_environment_attributes(context).await?; let evaluation_context = EvaluationContext { subject_id: subject_id.to_string(), resource_type: resource_type.to_string(), resource_id: resource_id.map(|s| s.to_string()), action: action.to_string(), subject_attributes: subject_attrs, resource_attributes: resource_attrs, environment_attributes: env_attrs, }; let policies = self.policies.read().await; // Evaluate policies in priority order for policy in policies.iter() { if self.policy_matches_target(policy, &evaluation_context)? { match self.policy_evaluator.evaluate_condition(&policy.condition, &evaluation_context).await? { true => { return Ok(matches!(policy.effect, PolicyEffect::Permit)); }, false => continue, } } } // Default deny Ok(false) } fn policy_matches_target(&self, policy: &AbacPolicy, context: &EvaluationContext) -> Result<bool, AbacError> { // Check if policy target matches the request context let target_matches = (policy.target.subjects.is_empty() || policy.target.subjects.contains(&context.subject_id) || policy.target.subjects.contains(&"*".to_string())) && (policy.target.resources.is_empty() || policy.target.resources.contains(&context.resource_type) || policy.target.resources.contains(&"*".to_string())) && (policy.target.actions.is_empty() || policy.target.actions.contains(&context.action) || policy.target.actions.contains(&"*".to_string())); Ok(target_matches) } } #[derive(Debug)] struct EvaluationContext { subject_id: String, resource_type: String, resource_id: Option<String>, action: String, subject_attributes: HashMap<String, Value>, resource_attributes: HashMap<String, Value>, environment_attributes: HashMap<String, Value>, } struct PolicyEvaluator; impl PolicyEvaluator { fn new() -> Self { Self } async fn evaluate_condition( &self, condition: &PolicyCondition, context: &EvaluationContext, ) -> Result<bool, AbacError> { match condition { PolicyCondition::Always => Ok(true), PolicyCondition::Never => Ok(false), PolicyCondition::AttributeMatch { attribute, operator, value } => { self.evaluate_attribute_match(attribute, operator, value, context).await }, PolicyCondition::And(conditions) => { for cond in conditions { if !self.evaluate_condition(cond, context).await? { return Ok(false); } } Ok(true) }, PolicyCondition::Or(conditions) => { for cond in conditions { if self.evaluate_condition(cond, context).await? { return Ok(true); } } Ok(false) }, PolicyCondition::Not(condition) => { Ok(!self.evaluate_condition(condition, context).await?) }, } } async fn evaluate_attribute_match( &self, attribute: &str, operator: &str, expected_value: &Value, context: &EvaluationContext, ) -> Result<bool, AbacError> { let actual_value = self.get_attribute_value(attribute, context)?; match operator { "equals" => Ok(actual_value == *expected_value), "not_equals" => Ok(actual_value != *expected_value), "contains" => { if let (Value::String(actual), Value::String(expected)) = (&actual_value, expected_value) { Ok(actual.contains(expected)) } else { Ok(false) } }, "starts_with" => { if let (Value::String(actual), Value::String(expected)) = (&actual_value, expected_value) { Ok(actual.starts_with(expected)) } else { Ok(false) } }, "greater_than" => { if let (Value::Number(actual), Value::Number(expected)) = (&actual_value, expected_value) { Ok(actual.as_f64().unwrap_or(0.0) > expected.as_f64().unwrap_or(0.0)) } else { Ok(false) } }, "in" => { if let Value::Array(expected_array) = expected_value { Ok(expected_array.contains(&actual_value)) } else { Ok(false) } }, _ => Err(AbacError::UnsupportedOperator(operator.to_string())), } } fn get_attribute_value(&self, attribute: &str, context: &EvaluationContext) -> Result<Value, AbacError> { let parts: Vec<&str> = attribute.split('.').collect(); match parts.get(0) { Some(&"subject") => { context.subject_attributes.get(parts.get(1).unwrap_or(&"")) .cloned() .ok_or_else(|| AbacError::AttributeNotFound(attribute.to_string())) }, Some(&"resource") => { context.resource_attributes.get(parts.get(1).unwrap_or(&"")) .cloned() .ok_or_else(|| AbacError::AttributeNotFound(attribute.to_string())) }, Some(&"environment") => { context.environment_attributes.get(parts.get(1).unwrap_or(&"")) .cloned() .ok_or_else(|| AbacError::AttributeNotFound(attribute.to_string())) }, _ => Err(AbacError::InvalidAttributePath(attribute.to_string())), } } } // Example ABAC policy setup async fn setup_abac_policies() -> Result<AbacPolicyEngine, Box<dyn std::error::Error>> { let attribute_provider = Arc::new(DatabaseAttributeProvider::new(/* db connection */)); let policy_engine = AbacPolicyEngine::new(attribute_provider); // Policy 1: Managers can manage users in their department policy_engine.add_policy(AbacPolicy { id: "manager-dept-access".to_string(), name: "Manager Department Access".to_string(), description: "Managers can manage users in their department".to_string(), target: PolicyTarget { subjects: vec!["*".to_string()], resources: vec!["User".to_string()], actions: vec!["create".to_string(), "update".to_string(), "delete".to_string()], environment: vec![], }, condition: PolicyCondition::And(vec![ PolicyCondition::AttributeMatch { attribute: "subject.role".to_string(), operator: "equals".to_string(), value: json!("manager"), }, PolicyCondition::AttributeMatch { attribute: "subject.department".to_string(), operator: "equals".to_string(), value: json!("${resource.department}"), }, ]), effect: PolicyEffect::Permit, priority: 100, }).await; // Policy 2: Business hours restriction policy_engine.add_policy(AbacPolicy { id: "business-hours-only".to_string(), name: "Business Hours Only".to_string(), description: "Some operations only allowed during business hours".to_string(), target: PolicyTarget { subjects: vec!["*".to_string()], resources: vec!["*".to_string()], actions: vec!["delete".to_string()], environment: vec![], }, condition: PolicyCondition::And(vec![ PolicyCondition::AttributeMatch { attribute: "environment.time_of_day".to_string(), operator: "greater_than".to_string(), value: json!(9), // 9 AM }, PolicyCondition::AttributeMatch { attribute: "environment.time_of_day".to_string(), operator: "less_than".to_string(), value: json!(17), // 5 PM }, PolicyCondition::AttributeMatch { attribute: "environment.day_of_week".to_string(), operator: "in".to_string(), value: json!(["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]), }, ]), effect: PolicyEffect::Permit, priority: 50, }).await; Ok(policy_engine) } }
Compile-Time vs Runtime Security
Compile-Time Security Patterns
#![allow(unused)] fn main() { use std::marker::PhantomData; // Type-safe permission system pub struct Authenticated; pub struct Unauthenticated; pub struct Authorized<P>(PhantomData<P>); pub struct Unauthorized; // Permission types pub struct ReadPermission; pub struct WritePermission; pub struct AdminPermission; // Context with type-level authentication state pub struct TypedRequestContext<A, Z> { inner: RequestContext, _auth_state: PhantomData<A>, _authz_state: PhantomData<Z>, } impl TypedRequestContext<Unauthenticated, Unauthorized> { pub fn new(request_id: String) -> Self { Self { inner: RequestContext::new(request_id), _auth_state: PhantomData, _authz_state: PhantomData, } } pub fn authenticate<P: AuthenticationProvider>( self, provider: &P, credential: &Credential, ) -> impl Future<Output = Result<TypedRequestContext<Authenticated, Unauthorized>, P::Error>> { async move { provider.authenticate(credential).await?; Ok(TypedRequestContext { inner: self.inner, _auth_state: PhantomData, _authz_state: PhantomData, }) } } } impl TypedRequestContext<Authenticated, Unauthorized> { pub fn authorize<P>( self, _permission_proof: P, ) -> TypedRequestContext<Authenticated, Authorized<P>> { TypedRequestContext { inner: self.inner, _auth_state: PhantomData, _authz_state: PhantomData, } } } // Only authorized contexts can perform operations impl<P> TypedRequestContext<Authenticated, Authorized<P>> { pub fn into_inner(self) -> RequestContext { self.inner } } // Permission proofs - only created when authorization succeeds pub struct PermissionProof<P> { _permission: PhantomData<P>, } impl<P> PermissionProof<P> { // Private constructor - only created by authorization system fn new() -> Self { Self { _permission: PhantomData } } } // Type-safe SCIM server operations pub struct TypeSafeScimServer<R: ResourceProvider> { inner: ScimServer<R>, } impl<R: ResourceProvider> TypeSafeScimServer<R> { pub fn new(provider: R) -> Result<Self, ScimServerError> { Ok(Self { inner: ScimServer::new(provider)?, }) } // Only accept authorized contexts pub async fn create_resource<P>( &self, resource_type: &str, data: Value, context: TypedRequestContext<Authenticated, Authorized<WritePermission>>, ) -> Result<Resource, ScimError> { self.inner.create_resource(resource_type, data, &context.into_inner()).await } pub async fn get_resource<P>( &self, resource_type: &str, id: &str, context: TypedRequestContext<Authenticated, Authorized<ReadPermission>>, ) -> Result<Option<Resource>, ScimError> { self.inner.get_resource(resource_type, id, &context.into_inner()).await } pub async fn delete_resource<P>( &self, resource_type: &str, id: &str, context: TypedRequestContext<Authenticated, Authorized<AdminPermission>>, ) -> Result<bool, ScimError> { self.inner.delete_resource(resource_type, id, &context.into_inner()).await } } // Authorization service that produces permission proofs pub struct TypeSafeAuthorizationService<A: AuthenticationProvider> { auth_provider: A, rbac_engine: RbacAuthProvider, } impl<A: AuthenticationProvider> TypeSafeAuthorizationService<A> { pub async fn check_read_permission( &self, auth_result: &AuthenticationResult, resource_type: &str, context: &RequestContext, ) -> Result<PermissionProof<ReadPermission>, AuthorizationError> { let required_permission = format!("read_{}", resource_type.to_lowercase()); if self.auth_provider.validate_permissions(auth_result, &required_permission).await? { Ok(PermissionProof::new()) } else { Err(AuthorizationError::InsufficientPermissions) } } pub async fn check_write_permission( &self, auth_result: &AuthenticationResult, resource_type: &str, context: &RequestContext, ) -> Result<PermissionProof<WritePermission>, AuthorizationError> { let required_permission = format!("write_{}", resource_type.to_lowercase()); if self.auth_provider.validate_permissions(auth_result, &required_permission).await? { Ok(PermissionProof::new()) } else { Err(AuthorizationError::InsufficientPermissions) } } pub async fn check_admin_permission( &self, auth_result: &AuthenticationResult, context: &RequestContext, ) -> Result<PermissionProof<AdminPermission>, AuthorizationError> { if self.auth_provider.validate_permissions(auth_result, "*").await? { Ok(PermissionProof::new()) } else { Err(AuthorizationError::InsufficientPermissions) } } } // Usage example async fn compile_time_safe_example() -> Result<(), Box<dyn std::error::Error>> { let provider = StandardResourceProvider::new(InMemoryStorage::new()); let server = TypeSafeScimServer::new(provider)?; let auth_service = TypeSafeAuthorizationService::new(/* auth provider */); // Create unauthenticated context let context = TypedRequestContext::new("req-123".to_string()); // Authenticate - compile error if credential is invalid let auth_context = context.authenticate(&auth_service.auth_provider, &credential).await?; // Try to authorize for read permission let read_proof = auth_service.check_read_permission(&auth_result, "User", &context).await?; let authorized_context = auth_context.authorize(read_proof); // This compiles - we have read permission let user = server.get_resource("User", "123", authorized_context).await?; // This would be a compile error - we don't have write permission // server.create_resource("User", user_data, authorized_context).await?; // β Compile error Ok(()) } }
Integration Patterns
Middleware Integration
#![allow(unused)] fn main() { use axum::{extract::Request, middleware::Next, response::Response}; pub async fn auth_middleware( mut request: Request, next: Next, ) -> Result<Response, AuthMiddlewareError> { // Extract authentication credential let credential = extract_credential_from_request(&request)?; // Get authentication provider from request extensions let auth_provider = request.extensions() .get::<Arc<dyn AuthenticationProvider>>() .ok_or(AuthMiddlewareError::MissingAuthProvider)?; // Authenticate let auth_result = auth_provider.authenticate(&credential).await .map_err(AuthMiddlewareError::AuthenticationFailed)?; if !auth_result.authenticated { return Err(AuthMiddlewareError::Unauthenticated); } // Add authentication result to request extensions request.extensions_mut().insert(auth_result); Ok(next.run(request).await) } pub async fn authz_middleware( mut request: Request, next: Next, ) -> Result<Response, AuthzMiddlewareError> { let auth_result = request.extensions() .get::<AuthenticationResult>() .ok_or(AuthzMiddlewareError::MissingAuthResult)?; // Extract operation details from request let (operation, resource_type) = extract_operation_details(&request)?; // Get authorization provider let authz_provider = request.extensions() .get::<Arc<dyn AuthorizationProvider>>() .ok_or(AuthzMiddlewareError::MissingAuthzProvider)?; // Check authorization let authorized = authz_provider.authorize_operation( auth_result, &operation, &resource_type, &RequestContext::from_request(&request), ).await .map_err(AuthzMiddlewareError::AuthorizationFailed)?; if !authorized { return Err(AuthzMiddlewareError::Forbidden); } Ok(next.run(request).await) } fn extract_credential_from_request(request: &Request) -> Result<Credential, CredentialExtractionError> { // Try Authorization header first if let Some(auth_header) = request.headers().get("authorization") { if let Ok(auth_str) = auth_header.to_str() { if auth_str.starts_with("Bearer ") { return Ok(Credential::BearerToken(auth_str[7..].to_string())); } else if auth_str.starts_with("Basic ") { return Ok(Credential::BasicAuth(auth_str[6..].to_string())); } } } // Try API key header if let Some(api_key) = request.headers().get("x-api-key") { if let Ok(key_str) = api_key.to_str() { return Ok(Credential::ApiKey(key_str.to_string())); } } Err(CredentialExtractionError::NoCredentialFound) } }
Best Practices Summary
Security Implementation Guidelines
-
Layer Security Appropriately
- Authentication verifies identity
- Authorization controls access
- Audit logging tracks all operations
-
Choose the Right Strategy
- API keys for simple machine-to-machine
- JWT/OAuth2 for standards compliance
- Custom auth for legacy integration
- RBAC for role-based organizations
- ABAC for fine-grained control
-
Implement Defense in Depth
- Multiple authentication factors
- Authorization at multiple layers
- Rate limiting and DDoS protection
- Input validation and sanitization
-
Use Compile-Time Safety When Possible
- Type-safe permission systems
- Phantom types for state tracking
- Zero-cost abstractions
-
Monitor and Audit
- Log all authentication attempts
- Track authorization decisions
- Monitor for unusual patterns
- Implement alerting for security events
Related Topics
- Request Lifecycle & Context Management - How authentication integrates with request flow
- Multi-Tenant Architecture Patterns - Tenant-scoped authentication and authorization
- Compile-Time Auth Example - Practical compile-time security patterns
- Role-Based Access Control Example - RBAC implementation details
Next Steps
Now that you understand authentication and authorization strategies:
- Choose your authentication strategy based on your integration requirements
- Implement appropriate authorization (permissions, RBAC, or ABAC)
- Set up proper middleware integration for your web framework
- Add comprehensive audit logging for security monitoring
- Consider compile-time safety patterns for critical applications
Schema System Architecture
This deep dive explores the schema system architecture in SCIM Server, covering how schemas are registered, validated, and extended, plus patterns for creating custom value objects and integrating with dynamic schema requirements.
Overview
The schema system in SCIM Server provides the foundation for data validation, serialization, and extensibility. It combines SCIM 2.0 compliance with flexible extension mechanisms, allowing you to maintain standards compliance while supporting custom attributes and resource types.
Core Schema Flow:
Schema Definition β Registration β Validation β Value Object Creation β
Extension Support β Dynamic Schema Discovery
Schema System Architecture Overview
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Schema Registry β
β β
β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββββββββββββββββββ β
β β Core SCIM β β Extension β β Custom Resource β β
β β Schemas β β Schemas β β Schemas β β
β β β β β β β β
β β β’ User β β β’ Enterprise β β β’ Organization β β
β β β’ Group β β User β β β’ Application β β
β β β’ Schema β β β’ Custom attrs β β β’ Custom types β β
β β β’ ServiceConfig β β β’ Tenant exts β β β’ Domain specific β β
β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Value Object System β
β β
β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββββββββββββββββββ β
β β Static Value β β Dynamic Value β β Custom Value β β
β β Objects β β Objects β β Objects β β
β β β β β β β β
β β β’ Compile-time β β β’ Runtime β β β’ Domain-specific validation β β
β β validation β β creation β β β’ Complex business rules β β
β β β’ Type safety β β β’ Schema-driven β β β’ Custom serialization β β
β β β’ Performance β β β’ Flexible β β β’ Integration adapters β β
β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Validation & Extension Engine β
β β’ Schema validation β’ Extension loading β’ Dynamic discovery β’ Caching β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Schema Registry Architecture
Core Schema Registry
#![allow(unused)] fn main() { use scim_server::schema::{Schema, SchemaRegistry, AttributeDefinition, AttributeType}; use serde_json::{Value, json}; use std::collections::HashMap; use std::sync::{Arc, RwLock}; pub struct ExtendedSchemaRegistry { core_registry: SchemaRegistry, extension_schemas: RwLock<HashMap<String, Schema>>, custom_validators: RwLock<HashMap<String, Box<dyn CustomValidator>>>, schema_cache: RwLock<HashMap<String, CachedSchema>>, tenant_extensions: RwLock<HashMap<String, Vec<String>>>, } #[derive(Clone)] struct CachedSchema { schema: Schema, cached_at: std::time::Instant, compiled_validator: Option<CompiledValidator>, } pub trait CustomValidator: Send + Sync { fn validate(&self, value: &Value, context: &ValidationContext) -> ValidationResult<()>; fn attribute_type(&self) -> AttributeType; } impl ExtendedSchemaRegistry { pub fn new() -> Result<Self, SchemaError> { let core_registry = SchemaRegistry::default(); Ok(Self { core_registry, extension_schemas: RwLock::new(HashMap::new()), custom_validators: RwLock::new(HashMap::new()), schema_cache: RwLock::new(HashMap::new()), tenant_extensions: RwLock::new(HashMap::new()), }) } pub fn register_extension_schema(&self, schema: Schema) -> Result<(), SchemaError> { let schema_id = schema.id().to_string(); // Validate schema format self.validate_schema_format(&schema)?; // Check for conflicts with existing schemas self.check_schema_conflicts(&schema)?; // Register the schema let mut extensions = self.extension_schemas.write().unwrap(); extensions.insert(schema_id.clone(), schema.clone()); // Clear related caches self.invalidate_cache(&schema_id); // Notify listeners of schema registration self.notify_schema_registered(&schema); Ok(()) } pub fn register_custom_validator<V: CustomValidator + 'static>( &self, attribute_name: &str, validator: V, ) -> Result<(), SchemaError> { let mut validators = self.custom_validators.write().unwrap(); validators.insert(attribute_name.to_string(), Box::new(validator)); Ok(()) } pub fn add_tenant_extension( &self, tenant_id: &str, schema_id: &str, ) -> Result<(), SchemaError> { // Verify schema exists if !self.schema_exists(schema_id)? { return Err(SchemaError::SchemaNotFound(schema_id.to_string())); } let mut tenant_extensions = self.tenant_extensions.write().unwrap(); tenant_extensions.entry(tenant_id.to_string()) .or_insert_with(Vec::new) .push(schema_id.to_string()); Ok(()) } pub fn get_effective_schema( &self, base_schema_id: &str, tenant_id: Option<&str>, ) -> Result<CompositeSchema, SchemaError> { let cache_key = format!("{}:{}", base_schema_id, tenant_id.unwrap_or("global")); // Check cache first { let cache = self.schema_cache.read().unwrap(); if let Some(cached) = cache.get(&cache_key) { if cached.cached_at.elapsed() < std::time::Duration::from_mins(5) { return Ok(CompositeSchema::from_cached(&cached.schema)); } } } // Build composite schema let mut composite = CompositeSchema::new(); // Add base schema let base_schema = self.core_registry.get_schema(base_schema_id)?; composite.add_schema(base_schema.clone()); // Add tenant-specific extensions if let Some(tenant_id) = tenant_id { let tenant_extensions = self.tenant_extensions.read().unwrap(); if let Some(extension_ids) = tenant_extensions.get(tenant_id) { for extension_id in extension_ids { let extension_schema = self.get_extension_schema(extension_id)?; composite.add_extension(extension_schema); } } } // Cache the result let compiled_schema = composite.compile()?; { let mut cache = self.schema_cache.write().unwrap(); cache.insert(cache_key, CachedSchema { schema: compiled_schema.clone(), cached_at: std::time::Instant::now(), compiled_validator: Some(compiled_schema.create_validator()?), }); } Ok(composite) } pub fn validate_resource( &self, resource_type: &str, resource_data: &Value, tenant_id: Option<&str>, ) -> ValidationResult<ValidatedResource> { let composite_schema = self.get_effective_schema(resource_type, tenant_id)?; // Perform comprehensive validation let validation_context = ValidationContext { resource_type: resource_type.to_string(), tenant_id: tenant_id.map(|s| s.to_string()), schema_registry: self, custom_context: HashMap::new(), }; composite_schema.validate(resource_data, &validation_context) } fn validate_schema_format(&self, schema: &Schema) -> Result<(), SchemaError> { // Validate required fields if schema.id().is_empty() { return Err(SchemaError::InvalidSchema("Schema ID cannot be empty".into())); } if schema.name().is_empty() { return Err(SchemaError::InvalidSchema("Schema name cannot be empty".into())); } // Validate attributes for attribute in schema.attributes() { self.validate_attribute_definition(attribute)?; } Ok(()) } fn validate_attribute_definition(&self, attr: &AttributeDefinition) -> Result<(), SchemaError> { // Check attribute name format if !attr.name().chars().all(|c| c.is_alphanumeric() || c == '_' || c == '.') { return Err(SchemaError::InvalidAttributeName(attr.name().to_string())); } // Validate attribute type consistency match attr.attribute_type() { AttributeType::Complex => { if attr.sub_attributes().is_empty() { return Err(SchemaError::InvalidSchema( format!("Complex attribute '{}' must have sub-attributes", attr.name()) )); } // Recursively validate sub-attributes for sub_attr in attr.sub_attributes() { self.validate_attribute_definition(sub_attr)?; } }, AttributeType::Reference => { if attr.reference_types().is_empty() { return Err(SchemaError::InvalidSchema( format!("Reference attribute '{}' must specify reference types", attr.name()) )); } }, _ => {} // Other types are valid as-is } Ok(()) } fn check_schema_conflicts(&self, new_schema: &Schema) -> Result<(), SchemaError> { // Check for ID conflicts if self.core_registry.has_schema(new_schema.id())? { return Err(SchemaError::SchemaConflict( format!("Schema ID '{}' already exists in core registry", new_schema.id()) )); } let extensions = self.extension_schemas.read().unwrap(); if extensions.contains_key(new_schema.id()) { return Err(SchemaError::SchemaConflict( format!("Schema ID '{}' already exists in extensions", new_schema.id()) )); } // Check for attribute conflicts in same namespace self.check_attribute_conflicts(new_schema)?; Ok(()) } fn check_attribute_conflicts(&self, schema: &Schema) -> Result<(), SchemaError> { let mut seen_attributes = HashMap::new(); for attribute in schema.attributes() { let attr_name = attribute.name(); if let Some(existing_type) = seen_attributes.get(attr_name) { if existing_type != &attribute.attribute_type() { return Err(SchemaError::AttributeConflict( format!( "Attribute '{}' defined with conflicting types: {:?} vs {:?}", attr_name, existing_type, attribute.attribute_type() ) )); } } seen_attributes.insert(attr_name.to_string(), attribute.attribute_type()); } Ok(()) } } #[derive(Debug)] pub struct CompositeSchema { base_schema: Option<Schema>, extensions: Vec<Schema>, merged_attributes: HashMap<String, AttributeDefinition>, } impl CompositeSchema { pub fn new() -> Self { Self { base_schema: None, extensions: Vec::new(), merged_attributes: HashMap::new(), } } pub fn add_schema(&mut self, schema: Schema) { self.base_schema = Some(schema); self.rebuild_attribute_map(); } pub fn add_extension(&mut self, extension: Schema) { self.extensions.push(extension); self.rebuild_attribute_map(); } fn rebuild_attribute_map(&mut self) { self.merged_attributes.clear(); // Add base schema attributes if let Some(ref base) = self.base_schema { for attr in base.attributes() { self.merged_attributes.insert(attr.name().to_string(), attr.clone()); } } // Add extension attributes (extensions can override base attributes) for extension in &self.extensions { for attr in extension.attributes() { self.merged_attributes.insert(attr.name().to_string(), attr.clone()); } } } pub fn validate( &self, data: &Value, context: &ValidationContext, ) -> ValidationResult<ValidatedResource> { let mut validated_resource = ValidatedResource::new(); // Validate each field in the data if let Value::Object(obj) = data { for (field_name, field_value) in obj { if let Some(attr_def) = self.merged_attributes.get(field_name) { let validated_value = self.validate_attribute( attr_def, field_value, context, )?; validated_resource.add_attribute(field_name.clone(), validated_value); } else if !self.is_core_attribute(field_name) { return Err(ValidationError::UnknownAttribute(field_name.clone())); } } } // Check required attributes self.validate_required_attributes(data, context)?; Ok(validated_resource) } fn validate_attribute( &self, attr_def: &AttributeDefinition, value: &Value, context: &ValidationContext, ) -> ValidationResult<ValidatedValue> { // Check multiplicity if attr_def.is_multi_valued() { if !value.is_array() { return Err(ValidationError::InvalidType { attribute: attr_def.name().to_string(), expected: "array".to_string(), actual: value_type_name(value).to_string(), }); } let array = value.as_array().unwrap(); let mut validated_items = Vec::new(); for item in array { validated_items.push(self.validate_single_value(attr_def, item, context)?); } Ok(ValidatedValue::MultiValue(validated_items)) } else { let validated = self.validate_single_value(attr_def, value, context)?; Ok(ValidatedValue::SingleValue(Box::new(validated))) } } fn validate_single_value( &self, attr_def: &AttributeDefinition, value: &Value, context: &ValidationContext, ) -> ValidationResult<ValidatedValue> { match attr_def.attribute_type() { AttributeType::String => { let str_value = value.as_str() .ok_or_else(|| ValidationError::InvalidType { attribute: attr_def.name().to_string(), expected: "string".to_string(), actual: value_type_name(value).to_string(), })?; // Apply string constraints if let Some(max_length) = attr_def.max_length() { if str_value.len() > max_length { return Err(ValidationError::StringTooLong { attribute: attr_def.name().to_string(), max_length, actual_length: str_value.len(), }); } } // Apply custom validation if let Some(pattern) = attr_def.pattern() { if !pattern.is_match(str_value) { return Err(ValidationError::PatternMismatch { attribute: attr_def.name().to_string(), pattern: pattern.to_string(), value: str_value.to_string(), }); } } Ok(ValidatedValue::String(str_value.to_string())) }, AttributeType::Integer => { let int_value = value.as_i64() .ok_or_else(|| ValidationError::InvalidType { attribute: attr_def.name().to_string(), expected: "integer".to_string(), actual: value_type_name(value).to_string(), })?; // Apply numeric constraints if let Some(min) = attr_def.min_value() { if int_value < min { return Err(ValidationError::ValueTooSmall { attribute: attr_def.name().to_string(), min_value: min, actual_value: int_value, }); } } if let Some(max) = attr_def.max_value() { if int_value > max { return Err(ValidationError::ValueTooLarge { attribute: attr_def.name().to_string(), max_value: max, actual_value: int_value, }); } } Ok(ValidatedValue::Integer(int_value)) }, AttributeType::Boolean => { let bool_value = value.as_bool() .ok_or_else(|| ValidationError::InvalidType { attribute: attr_def.name().to_string(), expected: "boolean".to_string(), actual: value_type_name(value).to_string(), })?; Ok(ValidatedValue::Boolean(bool_value)) }, AttributeType::DateTime => { let date_str = value.as_str() .ok_or_else(|| ValidationError::InvalidType { attribute: attr_def.name().to_string(), expected: "string (ISO 8601 datetime)".to_string(), actual: value_type_name(value).to_string(), })?; let parsed_date = chrono::DateTime::parse_from_rfc3339(date_str) .map_err(|_| ValidationError::InvalidDateTime { attribute: attr_def.name().to_string(), value: date_str.to_string(), })?; Ok(ValidatedValue::DateTime(parsed_date.with_timezone(&chrono::Utc))) }, AttributeType::Complex => { if !value.is_object() { return Err(ValidationError::InvalidType { attribute: attr_def.name().to_string(), expected: "object".to_string(), actual: value_type_name(value).to_string(), }); } let mut validated_complex = HashMap::new(); let obj = value.as_object().unwrap(); // Validate each sub-attribute for sub_attr in attr_def.sub_attributes() { if let Some(sub_value) = obj.get(sub_attr.name()) { let validated_sub = self.validate_single_value(sub_attr, sub_value, context)?; validated_complex.insert(sub_attr.name().to_string(), validated_sub); } else if sub_attr.is_required() { return Err(ValidationError::MissingRequiredAttribute { parent: attr_def.name().to_string(), attribute: sub_attr.name().to_string(), }); } } Ok(ValidatedValue::Complex(validated_complex)) }, AttributeType::Reference => { let ref_str = value.as_str() .ok_or_else(|| ValidationError::InvalidType { attribute: attr_def.name().to_string(), expected: "string (reference)".to_string(), actual: value_type_name(value).to_string(), })?; // Validate reference format and type let reference = ResourceReference::parse(ref_str) .map_err(|_| ValidationError::InvalidReference { attribute: attr_def.name().to_string(), value: ref_str.to_string(), })?; // Check if reference type is allowed if !attr_def.reference_types().contains(&reference.resource_type) { return Err(ValidationError::InvalidReferenceType { attribute: attr_def.name().to_string(), allowed_types: attr_def.reference_types().clone(), actual_type: reference.resource_type, }); } Ok(ValidatedValue::Reference(reference)) }, } } fn validate_required_attributes( &self, data: &Value, _context: &ValidationContext, ) -> ValidationResult<()> { if let Value::Object(obj) = data { for attr_def in self.merged_attributes.values() { if attr_def.is_required() && !obj.contains_key(attr_def.name()) { return Err(ValidationError::MissingRequiredAttribute { parent: "root".to_string(), attribute: attr_def.name().to_string(), }); } } } Ok(()) } fn is_core_attribute(&self, name: &str) -> bool { matches!(name, "id" | "schemas" | "meta" | "externalId") } } }
Value Object System
Static Value Objects
#![allow(unused)] fn main() { use serde::{Deserialize, Serialize}; use std::fmt; // Compile-time validated value objects #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Email { value: String, email_type: Option<String>, primary: Option<bool>, display: Option<String>, } impl Email { pub fn new(value: String) -> ValidationResult<Self> { Self::validate_email_format(&value)?; Ok(Self { value, email_type: None, primary: None, display: None, }) } pub fn with_type(mut self, email_type: String) -> Self { self.email_type = Some(email_type); self } pub fn with_primary(mut self, primary: bool) -> Self { self.primary = Some(primary); self } pub fn with_display(mut self, display: String) -> Self { self.display = Some(display); self } fn validate_email_format(email: &str) -> ValidationResult<()> { if !email.contains('@') { return Err(ValidationError::InvalidEmailFormat(email.to_string())); } let parts: Vec<&str> = email.split('@').collect(); if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() { return Err(ValidationError::InvalidEmailFormat(email.to_string())); } // More comprehensive email validation if parts[1].contains("..") || parts[0].contains("..") { return Err(ValidationError::InvalidEmailFormat(email.to_string())); } Ok(()) } pub fn value(&self) -> &str { &self.value } pub fn email_type(&self) -> Option<&str> { self.email_type.as_deref() } pub fn is_primary(&self) -> bool { self.primary.unwrap_or(false) } } impl fmt::Display for Email { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.value) } } impl ValueObject for Email { fn attribute_type(&self) -> AttributeType { AttributeType::Complex } fn to_json(&self) -> ValidationResult<Value> { Ok(json!({ "value": self.value, "type": self.email_type.as_deref().unwrap_or("work"), "primary": self.primary.unwrap_or(false), "display": self.display.as_deref().unwrap_or(&self.value) })) } fn validate_against_schema(&self, definition: &AttributeDefinition) -> ValidationResult<()> { // Email-specific schema validation if definition.attribute_type() != AttributeType::Complex { return Err(ValidationError::TypeMismatch { expected: AttributeType::Complex, actual: definition.attribute_type(), }); } // Validate against sub-attributes for sub_attr in definition.sub_attributes() { match sub_attr.name() { "value" => { if sub_attr.attribute_type() != AttributeType::String { return Err(ValidationError::SubAttributeTypeMismatch { attribute: "email.value".to_string(), expected: AttributeType::String, actual: sub_attr.attribute_type(), }); } }, "type" | "display" => { if sub_attr.attribute_type() != AttributeType::String { return Err(ValidationError::SubAttributeTypeMismatch { attribute: format!("email.{}", sub_attr.name()), expected: AttributeType::String, actual: sub_attr.attribute_type(), }); } }, "primary" => { if sub_attr.attribute_type() != AttributeType::Boolean { return Err(ValidationError::SubAttributeTypeMismatch { attribute: "email.primary".to_string(), expected: AttributeType::Boolean, actual: sub_attr.attribute_type(), }); } }, _ => {} // Allow unknown sub-attributes for extensibility } } Ok(()) } } impl SchemaConstructible for Email { fn from_schema_and_value( definition: &AttributeDefinition, value: &Value, ) -> ValidationResult<Self> { if definition.attribute_type() != AttributeType::Complex { return Err(ValidationError::TypeMismatch { expected: AttributeType::Complex, actual: definition.attribute_type(), }); } let obj = value.as_object() .ok_or(ValidationError::InvalidStructure("Expected object for email".to_string()))?; let email_value = obj.get("value") .and_then(|v| v.as_str()) .ok_or(ValidationError::MissingRequiredField("email.value".to_string()))?; let mut email = Email::new(email_value.to_string())?; if let Some(email_type) = obj.get("type").and_then(|v| v.as_str()) { email = email.with_type(email_type.to_string()); } if let Some(primary) = obj.get("primary").and_then(|v| v.as_bool()) { email = email.with_primary(primary); } if let Some(display) = obj.get("display").and_then(|v| v.as_str()) { email = email.with_display(display.to_string()); } Ok(email) } } // Custom value object for business-specific data #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EmployeeId { value: String, department_code: String, hire_year: u16, } impl EmployeeId { pub fn new(value: String) -> ValidationResult<Self> { Self::parse_employee_id(&value) } fn parse_employee_id(id: &str) -> ValidationResult<Self> { // Format: DEPT-YYYY-NNNN (e.g., ENG-2023-0001) let parts: Vec<&str> = id.split('-').collect(); if parts.len() != 3 { return Err(ValidationError::InvalidEmployeeIdFormat(id.to_string())); } let department_code = parts[0].to_string(); let hire_year: u16 = parts[1].parse() .map_err(|_| ValidationError::InvalidEmployeeIdFormat(id.to_string()))?; let sequence: u16 = parts[2].parse() .map_err(|_| ValidationError::InvalidEmployeeIdFormat(id.to_string()))?; // Validate department code if !matches!(department_code.as_str(), "ENG" | "SAL" | "MKT" | "HR" | "FIN") { return Err(ValidationError::InvalidDepartmentCode(department_code)); } // Validate year range let current_year = chrono::Utc::now().year() as u16; if hire_year < 2000 || hire_year > current_year + 1 { return Err(ValidationError::InvalidHireYear(hire_year)); } Ok(Self { value: id.to_string(), department_code, hire_year, }) } pub fn department(&self) -> &str { &self.department_code } pub fn hire_year(&self) -> u16 { self.hire_year } } impl ValueObject for EmployeeId { fn attribute_type(&self) -> AttributeType { AttributeType::String } fn to_json(&self) -> ValidationResult<Value> { Ok(json!(self.value)) } fn validate_against_schema(&self, definition: &AttributeDefinition) -> ValidationResult<()> { if definition.attribute_type() != AttributeType::String { return Err(ValidationError::TypeMismatch { expected: AttributeType::String, actual: definition.attribute_type(), }); } // Additional business rule validation if let Some(pattern) = definition.pattern() { if !pattern.is_match(&self.value) { return Err(ValidationError::PatternMismatch { attribute: "employeeId".to_string(), pattern: pattern.to_string(), value: self.value.clone(), }); } } Ok(()) } } impl SchemaConstructible for EmployeeId { fn from_schema_and_value( _definition: &AttributeDefinition, value: &Value, ) -> ValidationResult<Self> { let id_str = value.as_str() .ok_or(ValidationError::InvalidType { attribute: "employeeId".to_string(), expected: "string".to_string(), actual: value_type_name(value).to_string(), })?; Self::new(id_str.to_string()) } } }
Dynamic Value Objects
#![allow(unused)] fn main() { use serde_json::{Value, Map}; use std::any::{Any, TypeId}; pub struct DynamicValueObject { attribute_name: String, attribute_type: AttributeType, raw_value: Value, validated_value: Option<Box<dyn Any + Send + Sync>>, metadata: HashMap<String, String>, } impl DynamicValueObject { pub fn from_schema_and_value( definition: &AttributeDefinition, value: &Value, ) -> ValidationResult<Self> { let mut dynamic_obj = Self { attribute_name: definition.name().to_string(), attribute_type: definition.attribute_type(), raw_value: value.clone(), validated_value: None, metadata: HashMap::new(), }; // Perform type-specific validation and conversion dynamic_obj.validate_and_convert(definition)?; Ok(dynamic_obj) } fn validate_and_convert(&mut self, definition: &AttributeDefinition) -> ValidationResult<()> { match self.attribute_type { AttributeType::String => { let str_value = self.raw_value.as_str() .ok_or_else(|| ValidationError::InvalidType { attribute: self.attribute_name.clone(), expected: "string".to_string(), actual: value_type_name(&self.raw_value).to_string(), })?; self.validate_string_constraints(str_value, definition)?; self.validated_value = Some(Box::new(str_value.to_string())); }, AttributeType::Integer => { let int_value = self.raw_value.as_i64() .ok_or_else(|| ValidationError::InvalidType { attribute: self.attribute_name.clone(), expected: "integer".to_string(), actual: value_type_name(&self.raw_value).to_string(), })?; self.validate_numeric_constraints(int_value, definition)?; self.validated_value = Some(Box::new(int_value)); }, AttributeType::Complex => { let obj_value = self.raw_value.as_object() .ok_or_else(|| ValidationError::InvalidType { attribute: self.attribute_name.clone(), expected: "object".to_string(), actual: value_type_name(&self.raw_value).to_string(), })?; let validated_complex = self.validate_complex_object(obj_value, definition)?; self.validated_value = Some(Box::new(validated_complex)); }, // Handle other types... _ => { return Err(ValidationError::UnsupportedAttributeType(self.attribute_type)); } } Ok(()) } fn validate_string_constraints( &mut self, value: &str, definition: &AttributeDefinition, ) -> ValidationResult<()> { // Length validation if let Some(max_length) = definition.max_length() { if value.len() > max_length { return Err(ValidationError::StringTooLong { attribute: self.attribute_name.clone(), max_length, actual_length: value.len(), }); } } // Pattern validation if let Some(pattern) = definition.pattern() { if !pattern.is_match(value) { return Err(ValidationError::PatternMismatch { attribute: self.attribute_name.clone(), pattern: pattern.to_string(), value: value.to_string(), }); } } // Enum validation if let Some(canonical_values) = definition.canonical_values() { if !canonical_values.contains(&value.to_string()) { return Err(ValidationError::InvalidEnumValue { attribute: self.attribute_name.clone(), allowed_values: canonical_values.clone(), actual_value: value.to_string(), }); } } Ok(()) } fn validate_numeric_constraints( &mut self, value: i64, definition: &AttributeDefinition, ) -> ValidationResult<()> { if let Some(min_value) = definition.min_value() { if value < min_value { return Err(ValidationError::ValueTooSmall { attribute: self.attribute_name.clone(), min_value, actual_value: value, }); } } if let Some(max_value) = definition.max_value() { if value > max_value { return Err(ValidationError::ValueTooLarge { attribute: self.attribute_name.clone(), max_value, actual_value: value, }); } } Ok(()) } fn validate_complex_object( &mut self, obj: &Map<String, Value>, definition: &AttributeDefinition, ) -> ValidationResult<HashMap<String, DynamicValueObject>> { let mut validated_complex = HashMap::new(); // Validate each sub-attribute for sub_attr in definition.sub_attributes() { if let Some(sub_value) = obj.get(sub_attr.name()) { let validated_sub = DynamicValueObject::from_schema_and_value(sub_attr, sub_value)?; validated_complex.insert(sub_attr.name().to_string(), validated_sub); } else if sub_attr.is_required() { return Err(ValidationError::MissingRequiredAttribute { parent: self.attribute_name.clone(), attribute: sub_attr.name().to_string(), }); } } // Check for unknown attributes for (field_name, _) in obj { if !definition.sub_attributes().iter().any(|attr| attr.name() == field_name) { self.metadata.insert( format!("unknown_field_{}", field_name), "present".to_string(), ); } } Ok(validated_complex) } pub fn get_validated_value<T: 'static>(&self) -> Option<&T> { self.validated_value.as_ref() .and_then(|boxed| boxed.downcast_ref::<T>()) } pub fn to_json(&self) -> ValidationResult<Value> { Ok(self.raw_value.clone()) } pub fn has_metadata(&self, key: &str) -> bool { self.metadata.contains_key(key) } } impl ValueObject for DynamicValueObject { fn attribute_type(&self) -> AttributeType { self.attribute_type } fn to_json(&self) -> ValidationResult<Value> { self.to_json() } fn validate_against_schema(&self, definition: &AttributeDefinition) -> ValidationResult<()> { if self.attribute_type != definition.attribute_type() { return Err(ValidationError::TypeMismatch { expected: definition.attribute_type(), actual: self.attribute_type, }); } // Re-validate with current definition (schema may have changed) let mut temp_obj = Self { attribute_name: self.attribute_name.clone(), attribute_type: self.attribute_type, raw_value: self.raw_value.clone(), validated_value: None, metadata: HashMap::new(), }; temp_obj.validate_and_convert(definition)?; Ok(()) } } }
Schema Extension Patterns
Tenant-Specific Extensions
#![allow(unused)] fn main() { pub struct TenantSchemaManager { base_registry: Arc<ExtendedSchemaRegistry>, tenant_schemas: RwLock<HashMap<String, TenantSchemaSet>>, extension_loader: Box<dyn SchemaLoader>, } #[derive(Clone)] struct TenantSchemaSet { tenant_id: String, extensions: Vec<Schema>, compiled_schemas: HashMap<String, CompositeSchema>, last_updated: chrono::DateTime<chrono::Utc>, } pub trait SchemaLoader: Send + Sync { async fn load_tenant_extensions(&self, tenant_id: &str) -> Result<Vec<Schema>, SchemaLoadError>; async fn watch_schema_changes(&self, callback: Box<dyn Fn(&str, &Schema) + Send + Sync>); } impl TenantSchemaManager { pub fn new( base_registry: Arc<ExtendedSchemaRegistry>, extension_loader: Box<dyn SchemaLoader>, ) -> Self { Self { base_registry, tenant_schemas: RwLock::new(HashMap::new()), extension_loader, } } pub async fn load_tenant_schemas(&self, tenant_id: &str) -> Result<(), SchemaError> { let extensions = self.extension_loader.load_tenant_extensions(tenant_id).await?; let tenant_set = TenantSchemaSet { tenant_id: tenant_id.to_string(), extensions: extensions.clone(), compiled_schemas: HashMap::new(), last_updated: chrono::Utc::now(), }; // Validate and register extensions for extension in &extensions { self.base_registry.register_extension_schema(extension.clone())?; } let mut tenant_schemas = self.tenant_schemas.write().unwrap(); tenant_schemas.insert(tenant_id.to_string(), tenant_set); Ok(()) } pub async fn get_tenant_schema( &self, tenant_id: &str, resource_type: &str, ) -> Result<CompositeSchema, SchemaError> { // Check if tenant schemas are loaded { let tenant_schemas = self.tenant_schemas.read().unwrap(); if !tenant_schemas.contains_key(tenant_id) { drop(tenant_schemas); self.load_tenant_schemas(tenant_id).await?; } } let mut tenant_schemas = self.tenant_schemas.write().unwrap(); let tenant_set = tenant_schemas.get_mut(tenant_id) .ok_or_else(|| SchemaError::TenantNotFound(tenant_id.to_string()))?; // Check if we have a compiled schema cached if let Some(compiled) = tenant_set.compiled_schemas.get(resource_type) { return Ok(compiled.clone()); } // Build composite schema let base_schema = self.base_registry.get_schema(resource_type)?; let mut composite = CompositeSchema::new(); composite.add_schema(base_schema); // Add relevant tenant extensions for extension in &tenant_set.extensions { if extension.applies_to_resource_type(resource_type) { composite.add_extension(extension.clone()); } } let compiled = composite.compile()?; tenant_set.compiled_schemas.insert(resource_type.to_string(), compiled.clone()); Ok(compiled) } pub async fn validate_tenant_resource( &self, tenant_id: &str, resource_type: &str, resource_data: &Value, ) -> ValidationResult<ValidatedResource> { let schema = self.get_tenant_schema(tenant_id, resource_type).await?; let validation_context = ValidationContext { resource_type: resource_type.to_string(), tenant_id: Some(tenant_id.to_string()), schema_registry: self.base_registry.as_ref(), custom_context: HashMap::new(), }; schema.validate(resource_data, &validation_context) } } // Database-backed schema loader pub struct DatabaseSchemaLoader { db_pool: PgPool, schema_cache: RwLock<HashMap<String, CachedTenantSchemas>>, } #[derive(Clone)] struct CachedTenantSchemas { schemas: Vec<Schema>, cached_at: chrono::DateTime<chrono::Utc>, } impl DatabaseSchemaLoader { pub fn new(db_pool: PgPool) -> Self { Self { db_pool, schema_cache: RwLock::new(HashMap::new()), } } } impl SchemaLoader for DatabaseSchemaLoader { async fn load_tenant_extensions(&self, tenant_id: &str) -> Result<Vec<Schema>, SchemaLoadError> { // Check cache first { let cache = self.schema_cache.read().unwrap(); if let Some(cached) = cache.get(tenant_id) { if cached.cached_at.signed_duration_since(chrono::Utc::now()).num_minutes().abs() < 5 { return Ok(cached.schemas.clone()); } } } // Query database for tenant extensions let rows = sqlx::query!( r#" SELECT schema_id, schema_name, schema_definition, resource_types FROM tenant_schema_extensions WHERE tenant_id = $1 AND active = true ORDER BY priority DESC "#, tenant_id ) .fetch_all(&self.db_pool) .await?; let mut schemas = Vec::new(); for row in rows { let schema_def: Value = serde_json::from_str(&row.schema_definition)?; let resource_types: Vec<String> = serde_json::from_str(&row.resource_types)?; let schema = Schema::from_json(schema_def) .with_id(row.schema_id) .with_name(row.schema_name) .with_resource_types(resource_types) .build()?; schemas.push(schema); } // Update cache { let mut cache = self.schema_cache.write().unwrap(); cache.insert(tenant_id.to_string(), CachedTenantSchemas { schemas: schemas.clone(), cached_at: chrono::Utc::now(), }); } Ok(schemas) } async fn watch_schema_changes(&self, callback: Box<dyn Fn(&str, &Schema) + Send + Sync>) { // Implementation would use database change streams or polling // This is a simplified example tokio::spawn(async move { let mut interval = tokio::time::interval(Duration::from_secs(30)); loop { interval.tick().await; // Check for schema changes and call callback // callback(&tenant_id, &changed_schema); } }); } } }
Performance Optimization
Schema Compilation and Caching
#![allow(unused)] fn main() { use std::sync::Arc; use tokio::sync::RwLock as AsyncRwLock; pub struct OptimizedSchemaRegistry { inner: ExtendedSchemaRegistry, compiled_validators: AsyncRwLock<HashMap<String, Arc<CompiledValidator>>>, validation_cache: AsyncRwLock<lru::LruCache<String, ValidationResult<ValidatedResource>>>, compilation_stats: Arc<CompilationStats>, } pub struct CompiledValidator { schema_id: String, validator_fn: Box<dyn Fn(&Value, &ValidationContext) -> ValidationResult<ValidatedResource> + Send + Sync>, attribute_validators: HashMap<String, AttributeValidator>, required_attributes: HashSet<String>, compilation_metadata: CompilationMetadata, } #[derive(Debug)] struct CompilationMetadata { compiled_at: chrono::DateTime<chrono::Utc>, optimization_level: OptimizationLevel, validation_stats: ValidationStats, } #[derive(Debug, Clone)] enum OptimizationLevel { None, Basic, Aggressive, } impl OptimizedSchemaRegistry { pub fn new(optimization_level: OptimizationLevel) -> Result<Self, SchemaError> { Ok(Self { inner: ExtendedSchemaRegistry::new()?, compiled_validators: AsyncRwLock::new(HashMap::new()), validation_cache: AsyncRwLock::new(lru::LruCache::new(1000)), compilation_stats: Arc::new(CompilationStats::new()), }) } pub async fn register_optimized_schema(&self, schema: Schema) -> Result<(), SchemaError> { // Register with inner registry self.inner.register_extension_schema(schema.clone())?; // Compile optimized validator let compiled_validator = self.compile_schema_validator(&schema).await?; let mut validators = self.compiled_validators.write().await; validators.insert(schema.id().to_string(), Arc::new(compiled_validator)); // Clear validation cache for this schema let mut cache = self.validation_cache.write().await; cache.clear(); Ok(()) } async fn compile_schema_validator(&self, schema: &Schema) -> Result<CompiledValidator, SchemaError> { let start_time = std::time::Instant::now(); let mut attribute_validators = HashMap::new(); let mut required_attributes = HashSet::new(); // Pre-compile attribute validators for attribute in schema.attributes() { let attr_validator = self.compile_attribute_validator(attribute).await?; attribute_validators.insert(attribute.name().to_string(), attr_validator); if attribute.is_required() { required_attributes.insert(attribute.name().to_string()); } } // Create optimized validation function let schema_id = schema.id().to_string(); let validator_fn = self.create_validation_function( schema_id.clone(), attribute_validators.clone(), required_attributes.clone(), ); let compilation_time = start_time.elapsed(); self.compilation_stats.record_compilation(schema.id(), compilation_time); Ok(CompiledValidator { schema_id, validator_fn, attribute_validators, required_attributes, compilation_metadata: CompilationMetadata { compiled_at: chrono::Utc::now(), optimization_level: OptimizationLevel::Aggressive, validation_stats: ValidationStats::new(), }, }) } async fn compile_attribute_validator( &self, attribute: &AttributeDefinition, ) -> Result<AttributeValidator, SchemaError> { match attribute.attribute_type() { AttributeType::String => { let mut constraints = Vec::new(); if let Some(max_len) = attribute.max_length() { constraints.push(StringConstraint::MaxLength(max_len)); } if let Some(pattern) = attribute.pattern() { constraints.push(StringConstraint::Pattern(pattern.clone())); } if let Some(canonical_values) = attribute.canonical_values() { constraints.push(StringConstraint::Enum(canonical_values.clone())); } Ok(AttributeValidator::String(StringValidator { constraints })) }, AttributeType::Integer => { let mut constraints = Vec::new(); if let Some(min) = attribute.min_value() { constraints.push(IntegerConstraint::MinValue(min)); } if let Some(max) = attribute.max_value() { constraints.push(IntegerConstraint::MaxValue(max)); } Ok(AttributeValidator::Integer(IntegerValidator { constraints })) }, AttributeType::Complex => { let mut sub_validators = HashMap::new(); for sub_attr in attribute.sub_attributes() { let sub_validator = Box::pin(self.compile_attribute_validator(sub_attr)).await?; sub_validators.insert(sub_attr.name().to_string(), sub_validator); } Ok(AttributeValidator::Complex(ComplexValidator { sub_validators, required_sub_attributes: attribute.sub_attributes() .iter() .filter(|attr| attr.is_required()) .map(|attr| attr.name().to_string()) .collect(), })) }, // Handle other types... _ => Ok(AttributeValidator::Generic), } } fn create_validation_function( &self, schema_id: String, attribute_validators: HashMap<String, AttributeValidator>, required_attributes: HashSet<String>, ) -> Box<dyn Fn(&Value, &ValidationContext) -> ValidationResult<ValidatedResource> + Send + Sync> { Box::new(move |value: &Value, context: &ValidationContext| -> ValidationResult<ValidatedResource> { let start_time = std::time::Instant::now(); let mut validated_resource = ValidatedResource::new(); validated_resource.set_schema_id(schema_id.clone()); // Fast path: validate object structure first let obj = value.as_object() .ok_or_else(|| ValidationError::InvalidStructure("Expected object".to_string()))?; // Check required attributes (optimized with pre-computed set) for required_attr in &required_attributes { if !obj.contains_key(required_attr) { return Err(ValidationError::MissingRequiredAttribute { parent: "root".to_string(), attribute: required_attr.clone(), }); } } // Validate each attribute using compiled validators for (attr_name, attr_value) in obj { if let Some(validator) = attribute_validators.get(attr_name) { let validated_value = validator.validate_fast(attr_value, context)?; validated_resource.add_attribute(attr_name.clone(), validated_value); } else if !is_core_attribute(attr_name) { return Err(ValidationError::UnknownAttribute(attr_name.clone())); } } let validation_time = start_time.elapsed(); validated_resource.set_validation_time(validation_time); Ok(validated_resource) }) } pub async fn validate_with_cache( &self, resource_type: &str, resource_data: &Value, context: &ValidationContext, ) -> ValidationResult<ValidatedResource> { // Create cache key from resource data hash and context let cache_key = self.create_cache_key(resource_type, resource_data, context); // Check validation cache { let mut cache = self.validation_cache.write().await; if let Some(cached_result) = cache.get(&cache_key) { return cached_result.clone(); } } // Perform validation let result = self.validate_optimized(resource_type, resource_data, context).await; // Cache successful validations if result.is_ok() { let mut cache = self.validation_cache.write().await; cache.put(cache_key, result.clone()); } result } async fn validate_optimized( &self, resource_type: &str, resource_data: &Value, context: &ValidationContext, ) -> ValidationResult<ValidatedResource> { let validators = self.compiled_validators.read().await; if let Some(compiled_validator) = validators.get(resource_type) { // Use compiled validator for optimal performance (compiled_validator.validator_fn)(resource_data, context) } else { // Fall back to standard validation self.inner.validate_resource(resource_type, resource_data, context.tenant_id.as_deref()) } } fn create_cache_key( &self, resource_type: &str, resource_data: &Value, context: &ValidationContext, ) -> String { use std::hash::{Hash, Hasher}; use std::collections::hash_map::DefaultHasher; let mut hasher = DefaultHasher::new(); resource_type.hash(&mut hasher); resource_data.to_string().hash(&mut hasher); context.tenant_id.hash(&mut hasher); format!("validation_{}_{}", resource_type, hasher.finish()) } } #[derive(Debug, Clone)] enum AttributeValidator { String(StringValidator), Integer(IntegerValidator), Boolean, DateTime, Complex(ComplexValidator), Reference(ReferenceValidator), Generic, } impl AttributeValidator { fn validate_fast(&self, value: &Value, context: &ValidationContext) -> ValidationResult<ValidatedValue> { match self { AttributeValidator::String(validator) => validator.validate_fast(value, context), AttributeValidator::Integer(validator) => validator.validate_fast(value, context), AttributeValidator::Complex(validator) => validator.validate_fast(value, context), // ... other validators _ => Err(ValidationError::UnsupportedValidationType), } } } }
Best Practices Summary
Schema Design Guidelines
-
Start with SCIM Core Schemas
- Extend rather than replace standard schemas
- Maintain SCIM compliance for interoperability
- Use standard attribute names where possible
-
Design Extensible Schemas
- Use complex attributes for structured data
- Plan for multi-valued attributes early
- Consider tenant-specific extensions
-
Implement Efficient Validation
- Compile schemas for performance
- Cache validation results appropriately
- Use type-safe value objects where beneficial
-
Handle Schema Evolution
- Version your custom schemas
- Plan migration strategies
- Support backward compatibility
-
Monitor Schema Performance
- Track validation times
- Monitor cache hit rates
- Profile schema compilation costs
Related Topics
- Understanding SCIM Schemas - Core schema concepts
- Schema Mechanisms in SCIM Server - Implementation details
- Resource Provider Architecture - How schemas integrate with providers
- Multi-Tenant Architecture Patterns - Tenant-specific schema extensions
Next Steps
Now that you understand schema system architecture:
- Design your schema extensions based on your domain requirements
- Implement custom value objects for complex business data
- Set up tenant-specific extensions for multi-tenant systems
- Optimize schema validation performance for high-throughput scenarios
- Plan schema evolution strategies for long-term maintenance
Storage & Persistence Patterns
This deep dive explores storage and persistence patterns in SCIM Server, covering different storage backends, caching strategies, performance optimization techniques, and patterns for integrating with existing databases and external systems.
Overview
The storage layer in SCIM Server provides the foundation for data persistence while maintaining abstraction from specific storage technologies. This document shows how to implement robust storage patterns that scale from simple in-memory setups to complex distributed systems.
Core Storage Flow:
Resource Operations β Storage Provider β Backend Implementation β
Data Persistence β Caching β Performance Optimization
Storage Architecture Overview
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Storage Provider Trait (Abstract Interface) β
β β
β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββββββββββββββββββ β
β β In-Memory β β Database β β External System β β
β β Storage β β Storage β β Storage β β
β β β β β β β β
β β β’ Development β β β’ PostgreSQL β β β’ REST APIs β β
β β β’ Testing β β β’ MongoDB β β β’ GraphQL β β
β β β’ Prototyping β β β’ Redis β β β’ Message Queues β β
β β β’ Simple apps β β β’ DynamoDB β β β’ Legacy systems β β
β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Storage Enhancement Layers β
β β
β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββββββββββββββββββ β
β β Caching Layer β β Connection β β Monitoring & β β
β β β β Pooling β β Observability β β
β β β’ Redis β β β β β β
β β β’ In-memory β β β’ Database β β β’ Metrics collection β β
β β β’ Multi-level β β pools β β β’ Performance tracking β β
β β β’ Write-through β β β’ Connection β β β’ Error monitoring β β
β β β’ Write-behind β β management β β β’ Distributed tracing β β
β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Advanced Storage Patterns β
β β’ Sharding β’ Replication β’ Event sourcing β’ CQRS β’ Backup/Recovery β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Core Storage Patterns
Pattern 1: Database Storage with Connection Pooling
#![allow(unused)] fn main() { use scim_server::storage::{StorageProvider, StorageKey, StoragePrefix}; use sqlx::{PgPool, Row, Postgres, Transaction}; use serde_json::{Value, json}; use std::collections::HashMap; use uuid::Uuid; pub struct PostgresStorageProvider { pool: PgPool, table_name: String, connection_config: ConnectionConfig, performance_monitor: Arc<PerformanceMonitor>, } #[derive(Clone)] pub struct ConnectionConfig { pub max_connections: u32, pub min_connections: u32, pub acquire_timeout: Duration, pub idle_timeout: Duration, pub max_lifetime: Duration, } impl PostgresStorageProvider { pub async fn new( database_url: &str, table_name: String, config: ConnectionConfig, ) -> Result<Self, StorageError> { let pool = PgPool::connect_with( sqlx::postgres::PgPoolOptions::new() .max_connections(config.max_connections) .min_connections(config.min_connections) .acquire_timeout(config.acquire_timeout) .idle_timeout(config.idle_timeout) .max_lifetime(config.max_lifetime) .parse(database_url)? ).await?; let provider = Self { pool, table_name, connection_config: config, performance_monitor: Arc::new(PerformanceMonitor::new()), }; // Initialize database schema provider.initialize_schema().await?; Ok(provider) } async fn initialize_schema(&self) -> Result<(), StorageError> { let create_table_sql = format!( r#" CREATE TABLE IF NOT EXISTS {} ( id TEXT PRIMARY KEY, resource_type TEXT NOT NULL, tenant_id TEXT, data JSONB NOT NULL, version TEXT NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), deleted_at TIMESTAMP WITH TIME ZONE, INDEX idx_resource_type ON {} (resource_type), INDEX idx_tenant_id ON {} (tenant_id), INDEX idx_tenant_resource ON {} (tenant_id, resource_type), INDEX idx_data_gin ON {} USING GIN (data) ) "#, self.table_name, self.table_name, self.table_name, self.table_name, self.table_name ); sqlx::query(&create_table_sql) .execute(&self.pool) .await?; Ok(()) } async fn with_transaction<F, R>(&self, operation: F) -> Result<R, StorageError> where F: FnOnce(&mut Transaction<Postgres>) -> futures::future::BoxFuture<Result<R, StorageError>>, { let mut tx = self.pool.begin().await?; let result = operation(&mut tx).await; match result { Ok(value) => { tx.commit().await?; Ok(value) } Err(err) => { tx.rollback().await?; Err(err) } } } fn extract_tenant_and_resource_info(&self, key: &StorageKey) -> (Option<String>, String, String) { let key_str = key.as_str(); // Handle tenant-scoped keys (format: "tenant:tenant_id:resource_type:resource_id") if key_str.starts_with("tenant:") { let parts: Vec<&str> = key_str.splitn(4, ':').collect(); if parts.len() == 4 { return ( Some(parts[1].to_string()), parts[2].to_string(), parts[3].to_string(), ); } } // Handle non-tenant keys (format: "resource_type:resource_id") let parts: Vec<&str> = key_str.splitn(2, ':').collect(); if parts.len() == 2 { (None, parts[0].to_string(), parts[1].to_string()) } else { (None, "unknown".to_string(), key_str.to_string()) } } async fn generate_version(&self, data: &Value) -> String { use std::hash::{Hash, Hasher}; use std::collections::hash_map::DefaultHasher; let mut hasher = DefaultHasher::new(); data.to_string().hash(&mut hasher); chrono::Utc::now().timestamp_nanos().hash(&mut hasher); format!("W/\"{}\"", hasher.finish()) } } impl StorageProvider for PostgresStorageProvider { type Error = PostgresStorageError; async fn put( &self, key: StorageKey, mut data: Value, context: &RequestContext, ) -> Result<Value, Self::Error> { let start_time = std::time::Instant::now(); let (tenant_id, resource_type, resource_id) = self.extract_tenant_and_resource_info(&key); let version = self.generate_version(&data).await; // Add metadata to the data data["meta"] = json!({ "version": version, "created": chrono::Utc::now().to_rfc3339(), "lastModified": chrono::Utc::now().to_rfc3339(), "resourceType": resource_type, "location": format!("/scim/v2/{}/{}", resource_type, resource_id) }); data["id"] = json!(resource_id); let result = self.with_transaction(|tx| { Box::pin(async move { // Use UPSERT to handle both create and update let query = format!( r#" INSERT INTO {} (id, resource_type, tenant_id, data, version, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, NOW(), NOW()) ON CONFLICT (id) DO UPDATE SET data = EXCLUDED.data, version = EXCLUDED.version, updated_at = NOW() RETURNING data "#, self.table_name ); let row = sqlx::query(&query) .bind(&resource_id) .bind(&resource_type) .bind(&tenant_id) .bind(&data) .bind(&version) .fetch_one(&mut **tx) .await?; let stored_data: Value = row.get("data"); Ok(stored_data) }) }).await?; let duration = start_time.elapsed(); self.performance_monitor.record_operation("put", duration, true); Ok(result) } async fn get( &self, key: StorageKey, _context: &RequestContext, ) -> Result<Option<Value>, Self::Error> { let start_time = std::time::Instant::now(); let (tenant_id, _resource_type, resource_id) = self.extract_tenant_and_resource_info(&key); let query = format!( "SELECT data FROM {} WHERE id = $1 AND ($2::TEXT IS NULL OR tenant_id = $2) AND deleted_at IS NULL", self.table_name ); let result = sqlx::query(&query) .bind(&resource_id) .bind(&tenant_id) .fetch_optional(&self.pool) .await?; let duration = start_time.elapsed(); self.performance_monitor.record_operation("get", duration, true); Ok(result.map(|row| row.get("data"))) } async fn list( &self, prefix: StoragePrefix, _context: &RequestContext, ) -> Result<Vec<Value>, Self::Error> { let start_time = std::time::Instant::now(); let prefix_str = prefix.as_str(); let (tenant_id, resource_type) = if prefix_str.starts_with("tenant:") { let parts: Vec<&str> = prefix_str.splitn(3, ':').collect(); if parts.len() == 3 { (Some(parts[1].to_string()), parts[2].trim_end_matches(':').to_string()) } else { (None, prefix_str.trim_end_matches(':').to_string()) } } else { (None, prefix_str.trim_end_matches(':').to_string()) }; let query = format!( r#" SELECT data FROM {} WHERE resource_type = $1 AND ($2::TEXT IS NULL OR tenant_id = $2) AND deleted_at IS NULL ORDER BY created_at "#, self.table_name ); let rows = sqlx::query(&query) .bind(&resource_type) .bind(&tenant_id) .fetch_all(&self.pool) .await?; let result: Vec<Value> = rows.into_iter() .map(|row| row.get("data")) .collect(); let duration = start_time.elapsed(); self.performance_monitor.record_operation("list", duration, true); Ok(result) } async fn delete( &self, key: StorageKey, _context: &RequestContext, ) -> Result<bool, Self::Error> { let start_time = std::time::Instant::now(); let (tenant_id, _resource_type, resource_id) = self.extract_tenant_and_resource_info(&key); let result = self.with_transaction(|tx| { Box::pin(async move { // Soft delete by setting deleted_at timestamp let query = format!( "UPDATE {} SET deleted_at = NOW() WHERE id = $1 AND ($2::TEXT IS NULL OR tenant_id = $2) AND deleted_at IS NULL", self.table_name ); let result = sqlx::query(&query) .bind(&resource_id) .bind(&tenant_id) .execute(&mut **tx) .await?; Ok(result.rows_affected() > 0) }) }).await?; let duration = start_time.elapsed(); self.performance_monitor.record_operation("delete", duration, true); Ok(result) } async fn exists( &self, key: StorageKey, _context: &RequestContext, ) -> Result<bool, Self::Error> { let start_time = std::time::Instant::now(); let (tenant_id, _resource_type, resource_id) = self.extract_tenant_and_resource_info(&key); let query = format!( "SELECT 1 FROM {} WHERE id = $1 AND ($2::TEXT IS NULL OR tenant_id = $2) AND deleted_at IS NULL LIMIT 1", self.table_name ); let result = sqlx::query(&query) .bind(&resource_id) .bind(&tenant_id) .fetch_optional(&self.pool) .await?; let duration = start_time.elapsed(); self.performance_monitor.record_operation("exists", duration, true); Ok(result.is_some()) } } }
Pattern 2: Redis-Based Caching Storage
#![allow(unused)] fn main() { use redis::{Client, Commands, Connection, RedisResult}; use serde_json::{Value, json}; pub struct RedisStorageProvider { client: Client, connection_pool: deadpool_redis::Pool, key_prefix: String, default_ttl: Option<usize>, performance_monitor: Arc<PerformanceMonitor>, } impl RedisStorageProvider { pub async fn new( redis_url: &str, key_prefix: String, default_ttl: Option<usize>, pool_size: usize, ) -> Result<Self, RedisStorageError> { let client = Client::open(redis_url)?; let config = deadpool_redis::Config::from_url(redis_url); let pool = config.create_pool(Some(deadpool_redis::Runtime::Tokio1))?; Ok(Self { client, connection_pool: pool, key_prefix, default_ttl, performance_monitor: Arc::new(PerformanceMonitor::new()), }) } fn make_redis_key(&self, key: &StorageKey) -> String { format!("{}:{}", self.key_prefix, key.as_str()) } fn make_index_key(&self, prefix: &StoragePrefix) -> String { format!("{}:index:{}", self.key_prefix, prefix.as_str().trim_end_matches(':')) } async fn get_connection(&self) -> Result<deadpool_redis::Connection, RedisStorageError> { self.connection_pool.get().await .map_err(RedisStorageError::from) } } impl StorageProvider for RedisStorageProvider { type Error = RedisStorageError; async fn put( &self, key: StorageKey, mut data: Value, context: &RequestContext, ) -> Result<Value, Self::Error> { let start_time = std::time::Instant::now(); let redis_key = self.make_redis_key(&key); let mut conn = self.get_connection().await?; // Add metadata data["meta"] = json!({ "version": format!("W/\"{}\"", uuid::Uuid::new_v4()), "created": chrono::Utc::now().to_rfc3339(), "lastModified": chrono::Utc::now().to_rfc3339(), }); let serialized = serde_json::to_string(&data)?; // Store the data if let Some(ttl) = self.default_ttl { redis::cmd("SETEX") .arg(&redis_key) .arg(ttl) .arg(&serialized) .query_async(&mut conn) .await?; } else { redis::cmd("SET") .arg(&redis_key) .arg(&serialized) .query_async(&mut conn) .await?; } // Update indexes for list operations let (tenant_id, resource_type, resource_id) = self.extract_key_parts(&key); if let Some(resource_type) = resource_type { let index_key = if let Some(tenant_id) = tenant_id { format!("{}:index:tenant:{}:{}", self.key_prefix, tenant_id, resource_type) } else { format!("{}:index:{}", self.key_prefix, resource_type) }; redis::cmd("SADD") .arg(&index_key) .arg(&redis_key) .query_async(&mut conn) .await?; // Set TTL on index as well if configured if let Some(ttl) = self.default_ttl { redis::cmd("EXPIRE") .arg(&index_key) .arg(ttl * 2) // Indexes live longer than data .query_async(&mut conn) .await?; } } let duration = start_time.elapsed(); self.performance_monitor.record_operation("put", duration, true); Ok(data) } async fn get( &self, key: StorageKey, _context: &RequestContext, ) -> Result<Option<Value>, Self::Error> { let start_time = std::time::Instant::now(); let redis_key = self.make_redis_key(&key); let mut conn = self.get_connection().await?; let result: Option<String> = redis::cmd("GET") .arg(&redis_key) .query_async(&mut conn) .await?; let duration = start_time.elapsed(); self.performance_monitor.record_operation("get", duration, true); match result { Some(data_str) => { let data: Value = serde_json::from_str(&data_str)?; Ok(Some(data)) }, None => Ok(None), } } async fn list( &self, prefix: StoragePrefix, _context: &RequestContext, ) -> Result<Vec<Value>, Self::Error> { let start_time = std::time::Instant::now(); let index_key = self.make_index_key(&prefix); let mut conn = self.get_connection().await?; // Get all keys from the index let redis_keys: Vec<String> = redis::cmd("SMEMBERS") .arg(&index_key) .query_async(&mut conn) .await?; let mut results = Vec::new(); // Batch get all values if !redis_keys.is_empty() { let values: Vec<Option<String>> = redis::cmd("MGET") .arg(&redis_keys) .query_async(&mut conn) .await?; for value in values.into_iter().flatten() { if let Ok(data) = serde_json::from_str::<Value>(&value) { results.push(data); } } } let duration = start_time.elapsed(); self.performance_monitor.record_operation("list", duration, true); Ok(results) } async fn delete( &self, key: StorageKey, _context: &RequestContext, ) -> Result<bool, Self::Error> { let start_time = std::time::Instant::now(); let redis_key = self.make_redis_key(&key); let mut conn = self.get_connection().await?; // Remove from main storage let deleted: i32 = redis::cmd("DEL") .arg(&redis_key) .query_async(&mut conn) .await?; // Remove from indexes let (tenant_id, resource_type, _) = self.extract_key_parts(&key); if let Some(resource_type) = resource_type { let index_key = if let Some(tenant_id) = tenant_id { format!("{}:index:tenant:{}:{}", self.key_prefix, tenant_id, resource_type) } else { format!("{}:index:{}", self.key_prefix, resource_type) }; redis::cmd("SREM") .arg(&index_key) .arg(&redis_key) .query_async(&mut conn) .await?; } let duration = start_time.elapsed(); self.performance_monitor.record_operation("delete", duration, true); Ok(deleted > 0) } async fn exists( &self, key: StorageKey, _context: &RequestContext, ) -> Result<bool, Self::Error> { let start_time = std::time::Instant::now(); let redis_key = self.make_redis_key(&key); let mut conn = self.get_connection().await?; let exists: bool = redis::cmd("EXISTS") .arg(&redis_key) .query_async(&mut conn) .await?; let duration = start_time.elapsed(); self.performance_monitor.record_operation("exists", duration, true); Ok(exists) } } }
Pattern 3: Multi-Level Caching Storage
#![allow(unused)] fn main() { pub struct MultiLevelCacheStorage<P: StorageProvider> { primary_storage: P, l1_cache: Arc<RwLock<lru::LruCache<String, CacheEntry>>>, l2_cache: Option<Arc<dyn L2Cache>>, cache_config: CacheConfig, performance_monitor: Arc<PerformanceMonitor>, } #[derive(Clone)] struct CacheEntry { data: Value, cached_at: std::time::Instant, ttl: Duration, access_count: AtomicU64, } #[derive(Clone)] pub struct CacheConfig { pub l1_size: usize, pub l1_ttl: Duration, pub l2_ttl: Duration, pub write_through: bool, pub write_behind: bool, pub write_behind_delay: Duration, } pub trait L2Cache: Send + Sync { async fn get(&self, key: &str) -> Result<Option<Value>, CacheError>; async fn put(&self, key: &str, value: &Value, ttl: Duration) -> Result<(), CacheError>; async fn delete(&self, key: &str) -> Result<bool, CacheError>; } impl<P: StorageProvider> MultiLevelCacheStorage<P> { pub fn new(primary_storage: P, config: CacheConfig) -> Self { Self { primary_storage, l1_cache: Arc::new(RwLock::new(lru::LruCache::new(config.l1_size))), l2_cache: None, cache_config: config, performance_monitor: Arc::new(PerformanceMonitor::new()), } } pub fn with_l2_cache(mut self, l2_cache: Arc<dyn L2Cache>) -> Self { self.l2_cache = Some(l2_cache); self } async fn get_from_l1(&self, key: &str) -> Option<Value> { let mut cache = self.l1_cache.write().unwrap(); if let Some(entry) = cache.get_mut(key) { // Check TTL if entry.cached_at.elapsed() < entry.ttl { entry.access_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed); return Some(entry.data.clone()); } else { // Entry expired, remove it cache.pop(key); } } None } fn put_to_l1(&self, key: String, value: Value) { let entry = CacheEntry { data: value, cached_at: std::time::Instant::now(), ttl: self.cache_config.l1_ttl, access_count: AtomicU64::new(1), }; let mut cache = self.l1_cache.write().unwrap(); cache.put(key, entry); } async fn get_from_l2(&self, key: &str) -> Result<Option<Value>, CacheError> { if let Some(ref l2) = self.l2_cache { l2.get(key).await } else { Ok(None) } } async fn put_to_l2(&self, key: &str, value: &Value) -> Result<(), CacheError> { if let Some(ref l2) = self.l2_cache { l2.put(key, value, self.cache_config.l2_ttl).await } else { Ok(()) } } async fn invalidate_cache(&self, key: &str) { // Remove from L1 { let mut cache = self.l1_cache.write().unwrap(); cache.pop(key); } // Remove from L2 if let Some(ref l2) = self.l2_cache { let _ = l2.delete(key).await; } } fn make_cache_key(&self, storage_key: &StorageKey) -> String { format!("cache:{}", storage_key.as_str()) } } impl<P: StorageProvider> StorageProvider for MultiLevelCacheStorage<P> { type Error = MultiLevelCacheError<P::Error>; async fn get( &self, key: StorageKey, context: &RequestContext, ) -> Result<Option<Value>, Self::Error> { let start_time = std::time::Instant::now(); let cache_key = self.make_cache_key(&key); // Try L1 cache first if let Some(value) = self.get_from_l1(&cache_key).await { let duration = start_time.elapsed(); self.performance_monitor.record_cache_hit("l1", duration); return Ok(Some(value)); } // Try L2 cache if let Ok(Some(value)) = self.get_from_l2(&cache_key).await { // Promote to L1 self.put_to_l1(cache_key.clone(), value.clone()); let duration = start_time.elapsed(); self.performance_monitor.record_cache_hit("l2", duration); return Ok(Some(value)); } // Cache miss - fetch from primary storage let result = self.primary_storage.get(key, context).await .map_err(MultiLevelCacheError::StorageError)?; if let Some(ref value) = result { // Cache the result self.put_to_l1(cache_key.clone(), value.clone()); let _ = self.put_to_l2(&cache_key, value).await; } let duration = start_time.elapsed(); self.performance_monitor.record_cache_miss(duration); Ok(result) } async fn put( &self, key: StorageKey, data: Value, context: &RequestContext, ) -> Result<Value, Self::Error> { let start_time = std::time::Instant::now(); let cache_key = self.make_cache_key(&key); if self.cache_config.write_through { // Write-through: write to storage first, then cache let result = self.primary_storage.put(key, data, context).await .map_err(MultiLevelCacheError::StorageError)?; // Update caches with the result self.put_to_l1(cache_key.clone(), result.clone()); let _ = self.put_to_l2(&cache_key, &result).await; let duration = start_time.elapsed(); self.performance_monitor.record_operation("put_write_through", duration, true); Ok(result) } else if self.cache_config.write_behind { // Write-behind: write to cache immediately, schedule storage write self.put_to_l1(cache_key.clone(), data.clone()); let _ = self.put_to_l2(&cache_key, &data).await; // Schedule async write to primary storage let primary = self.primary_storage.clone(); let write_key = key.clone(); let write_data = data.clone(); let write_context = context.clone(); let delay = self.cache_config.write_behind_delay; tokio::spawn(async move { tokio::time::sleep(delay).await; let _ = primary.put(write_key, write_data, &write_context).await; }); let duration = start_time.elapsed(); self.performance_monitor.record_operation("put_write_behind", duration, true); Ok(data) } else { // Direct write-through to primary storage let result = self.primary_storage.put(key, data, context).await .map_err(MultiLevelCacheError::StorageError)?; // Invalidate cache to ensure consistency self.invalidate_cache(&cache_key).await; let duration = start_time.elapsed(); self.performance_monitor.record_operation("put_direct", duration, true); Ok(result) } } async fn delete( &self, key: StorageKey, context: &RequestContext, ) -> Result<bool, Self::Error> { let start_time = std::time::Instant::now(); let cache_key = self.make_cache_key(&key); // Delete from primary storage let result = self.primary_storage.delete(key, context).await .map_err(MultiLevelCacheError::StorageError)?; // Invalidate cache self.invalidate_cache(&cache_key).await; let duration = start_time.elapsed(); self.performance_monitor.record_operation("delete", duration, true); Ok(result) } async fn list( &self, prefix: StoragePrefix, context: &RequestContext, ) -> Result<Vec<Value>, Self::Error> { // List operations typically bypass cache due to complexity // In production, you might cache list results with invalidation strategies self.primary_storage.list(prefix, context).await .map_err(MultiLevelCacheError::StorageError) } async fn exists( &self, key: StorageKey, context: &RequestContext, ) -> Result<bool, Self::Error> { let cache_key = self.make_cache_key(&key); // Check L1 cache first if self.get_from_l1(&cache_key).await.is_some() { return Ok(true); } // Check L2 cache if let Ok(Some(_)) = self.get_from_l2(&cache_key).await { return Ok(true); } // Check primary storage self.primary_storage.exists(key, context).await .map_err(MultiLevelCacheError::StorageError) } } // Redis-based L2 cache implementation pub struct RedisL2Cache { pool: deadpool_redis::Pool, key_prefix: String, } impl RedisL2Cache { pub fn new(redis_url: &str, key_prefix: String) -> Result<Self, RedisError> { let config = deadpool_redis::Config::from_url(redis_url); let pool = config.create_pool(Some(deadpool_redis::Runtime::Tokio1))?; Ok(Self { pool, key_prefix, }) } } impl L2Cache for RedisL2Cache { async fn get(&self, key: &str) -> Result<Option<Value>, CacheError> { let redis_key = format!("{}:l2:{}", self.key_prefix, key); let mut conn = self.pool.get().await?; let result: Option<String> = redis::cmd("GET") .arg(&redis_key) .query_async(&mut conn) .await?; match result { Some(data_str) => Ok(Some(serde_json::from_str(&data_str)?)), None => Ok(None), } } async fn put(&self, key: &str, value: &Value, ttl: Duration) -> Result<(), CacheError> { let redis_key = format!("{}:l2:{}", self.key_prefix, key); let mut conn = self.pool.get().await?; let serialized = serde_json::to_string(value)?; redis::cmd("SETEX") .arg(&redis_key) .arg(ttl.as_secs()) .arg(&serialized) .query_async(&mut conn) .await?; Ok(()) } async fn delete(&self, key: &str) -> Result<bool, CacheError> { let redis_key = format!("{}:l2:{}", self.key_prefix, key); let mut conn = self.pool.get().await?; let deleted: i32 = redis::cmd("DEL") .arg(&redis_key) .query_async(&mut conn) .await?; Ok(deleted > 0) } } }
Advanced Storage Patterns
Pattern 4: Event Sourcing with SCIM
#![allow(unused)] fn main() { use serde::{Deserialize, Serialize}; use chrono::{DateTime, Utc}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ScimEvent { pub event_id: String, pub event_type: ScimEventType, pub aggregate_id: String, pub aggregate_type: String, pub tenant_id: Option<String>, pub event_data: Value, pub metadata: EventMetadata, pub timestamp: DateTime<Utc>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ScimEventType { ResourceCreated, ResourceUpdated, ResourceDeleted, ResourceRestored, SchemaRegistered, SchemaUpdated, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EventMetadata { pub user_id: Option<String>, pub request_id: String, pub client_id: String, pub ip_address: Option<String>, pub user_agent: Option<String>, } pub struct EventSourcedStorageProvider<E: EventStore> { event_store: E, snapshot_store: Box<dyn SnapshotStore>, projector: Arc<ScimProjector>, snapshot_frequency: usize, performance_monitor: Arc<PerformanceMonitor>, } pub trait EventStore: Send + Sync { type Error: std::error::Error + Send + Sync + 'static; async fn append_events( &self, stream_id: &str, events: Vec<ScimEvent>, expected_version: Option<u64>, ) -> Result<u64, Self::Error>; async fn read_events( &self, stream_id: &str, from_version: u64, max_count: Option<usize>, ) -> Result<Vec<ScimEvent>, Self::Error>; async fn read_all_events( &self, from_position: u64, max_count: Option<usize>, ) -> Result<Vec<ScimEvent>, Self::Error>; } pub trait SnapshotStore: Send + Sync { async fn save_snapshot( &self, aggregate_id: &str, version: u64, data: &Value, ) -> Result<(), SnapshotError>; async fn load_snapshot( &self, aggregate_id: &str, ) -> Result<Option<(u64, Value)>, SnapshotError>; } impl<E: EventStore> EventSourcedStorageProvider<E> { pub fn new( event_store: E, snapshot_store: Box<dyn SnapshotStore>, snapshot_frequency: usize, ) -> Self { Self { event_store, snapshot_store, projector: Arc::new(ScimProjector::new()), snapshot_frequency, performance_monitor: Arc::new(PerformanceMonitor::new()), } } async fn load_aggregate( &self, aggregate_id: &str, ) -> Result<Option<ScimAggregate>, EventStoreError> { let start_time = std::time::Instant::now(); // Try to load from snapshot first let (start_version, mut aggregate) = match self.snapshot_store.load_snapshot(aggregate_id).await? { Some((version, data)) => (version + 1, Some(ScimAggregate::from_snapshot(data)?)), None => (0, None), }; // Load events since snapshot let events = self.event_store.read_events( &format!("resource-{}", aggregate_id), start_version, None, ).await?; // Apply events to rebuild current state if !events.is_empty() { let mut current_aggregate = aggregate.unwrap_or_else(|| ScimAggregate::new(aggregate_id.to_string())); for event in events { current_aggregate = self.projector.apply_event(current_aggregate, &event)?; } aggregate = Some(current_aggregate); } let duration = start_time.elapsed(); self.performance_monitor.record_operation("load_aggregate", duration, aggregate.is_some()); Ok(aggregate) } async fn save_events_and_maybe_snapshot( &self, aggregate: &ScimAggregate, events: Vec<ScimEvent>, ) -> Result<(), EventStoreError> { let stream_id = format!("resource-{}", aggregate.id); // Append events let new_version = self.event_store.append_events( &stream_id, events, Some(aggregate.version), ).await?; // Save snapshot if needed if new_version % self.snapshot_frequency as u64 == 0 { self.snapshot_store.save_snapshot( &aggregate.id, new_version, &aggregate.to_snapshot()?, ).await?; } Ok(()) } } impl<E: EventStore> StorageProvider for EventSourcedStorageProvider<E> { type Error = EventStoreError; async fn put( &self, key: StorageKey, data: Value, context: &RequestContext, ) -> Result<Value, Self::Error> { let (tenant_id, resource_type, resource_id) = self.extract_key_parts(&key); // Load existing aggregate let mut aggregate = self.load_aggregate(&resource_id).await? .unwrap_or_else(|| ScimAggregate::new(resource_id.clone())); // Determine event type let event_type = if aggregate.version == 0 { ScimEventType::ResourceCreated } else { ScimEventType::ResourceUpdated }; // Create event let event = ScimEvent { event_id: uuid::Uuid::new_v4().to_string(), event_type, aggregate_id: resource_id.clone(), aggregate_type: resource_type.unwrap_or_else(|| "Unknown".to_string()), tenant_id: tenant_id.clone(), event_data: data.clone(), metadata: EventMetadata { user_id: context.user_id(), request_id: context.request_id.clone(), client_id: context.client_id().unwrap_or_else(|| "unknown".to_string()), ip_address: None, // Would be extracted from HTTP context in real implementation user_agent: None, }, timestamp: Utc::now(), }; // Apply event to aggregate aggregate = self.projector.apply_event(aggregate, &event)?; // Save event and maybe snapshot self.save_events_and_maybe_snapshot(&aggregate, vec![event]).await?; // Return the current state Ok(aggregate.current_data) } async fn get( &self, key: StorageKey, _context: &RequestContext, ) -> Result<Option<Value>, Self::Error> { let (_, _, resource_id) = self.extract_key_parts(&key); let aggregate = self.load_aggregate(&resource_id).await?; Ok(aggregate.map(|a| a.current_data)) } async fn delete( &self, key: StorageKey, context: &RequestContext, ) -> Result<bool, Self::Error> { let (tenant_id, resource_type, resource_id) = self.extract_key_parts(&key); // Load aggregate to check if it exists let aggregate = match self.load_aggregate(&resource_id).await? { Some(agg) if !agg.is_deleted => agg, _ => return Ok(false), // Already deleted or doesn't exist }; // Create deletion event let event = ScimEvent { event_id: uuid::Uuid::new_v4().to_string(), event_type: ScimEventType::ResourceDeleted, aggregate_id: resource_id.clone(), aggregate_type: resource_type.unwrap_or_else(|| "Unknown".to_string()), tenant_id: tenant_id.clone(), event_data: json!({"deleted": true}), metadata: EventMetadata { user_id: context.user_id(), request_id: context.request_id.clone(), client_id: context.client_id().unwrap_or_else(|| "unknown".to_string()), ip_address: None, user_agent: None, }, timestamp: Utc::now(), }; // Apply event and save let updated_aggregate = self.projector.apply_event(aggregate, &event)?; self.save_events_and_maybe_snapshot(&updated_aggregate, vec![event]).await?; Ok(true) } // List and exists operations would need to be implemented with projections // This is simplified for brevity async fn list( &self, _prefix: StoragePrefix, _context: &RequestContext, ) -> Result<Vec<Value>, Self::Error> { // In a real implementation, this would use read-model projections todo!("List operations require read-model projections") } async fn exists( &self, key: StorageKey, context: &RequestContext, ) -> Result<bool, Self::Error> { Ok(self.get(key, context).await?.is_some()) } } #[derive(Debug, Clone)] pub struct ScimAggregate { pub id: String, pub version: u64, pub current_data: Value, pub is_deleted: bool, pub created_at: DateTime<Utc>, pub updated_at: DateTime<Utc>, } impl ScimAggregate { pub fn new(id: String) -> Self { Self { id, version: 0, current_data: json!({}), is_deleted: false, created_at: Utc::now(), updated_at: Utc::now(), } } pub fn from_snapshot(data: Value) -> Result<Self, serde_json::Error> { serde_json::from_value(data) } pub fn to_snapshot(&self) -> Result<Value, serde_json::Error> { serde_json::to_value(self) } } pub struct ScimProjector; impl ScimProjector { pub fn new() -> Self { Self } pub fn apply_event( &self, mut aggregate: ScimAggregate, event: &ScimEvent, ) -> Result<ScimAggregate, ProjectionError> { match event.event_type { ScimEventType::ResourceCreated => { aggregate.current_data = event.event_data.clone(); aggregate.created_at = event.timestamp; aggregate.updated_at = event.timestamp; }, ScimEventType::ResourceUpdated => { // Merge the update data if let (Value::Object(ref mut current), Value::Object(update)) = (&mut aggregate.current_data, &event.event_data) { for (key, value) in update { current.insert(key.clone(), value.clone()); } } aggregate.updated_at = event.timestamp; }, ScimEventType::ResourceDeleted => { aggregate.is_deleted = true; aggregate.updated_at = event.timestamp; }, ScimEventType::ResourceRestored => { aggregate.is_deleted = false; aggregate.updated_at = event.timestamp; }, _ => {} // Other event types handled elsewhere } aggregate.version += 1; Ok(aggregate) } } }
Performance Monitoring and Observability
#![allow(unused)] fn main() { use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; use std::time::{Duration, Instant}; use tracing::{info, warn, error}; #[derive(Debug)] pub struct PerformanceMonitor { metrics: PerformanceMetrics, slow_query_threshold: Duration, error_rate_threshold: f64, } #[derive(Debug, Default)] pub struct PerformanceMetrics { pub total_operations: AtomicU64, pub successful_operations: AtomicU64, pub failed_operations: AtomicU64, pub total_duration: AtomicU64, // in nanoseconds pub cache_hits: AtomicU64, pub cache_misses: AtomicU64, pub connection_pool_size: AtomicUsize, pub active_connections: AtomicUsize, } impl PerformanceMonitor { pub fn new() -> Self { Self { metrics: PerformanceMetrics::default(), slow_query_threshold: Duration::from_millis(100), error_rate_threshold: 0.05, // 5% } } pub fn record_operation(&self, operation: &str, duration: Duration, success: bool) { self.metrics.total_operations.fetch_add(1, Ordering::Relaxed); self.metrics.total_duration.fetch_add(duration.as_nanos() as u64, Ordering::Relaxed); if success { self.metrics.successful_operations.fetch_add(1, Ordering::Relaxed); } else { self.metrics.failed_operations.fetch_add(1, Ordering::Relaxed); } // Log slow operations if duration > self.slow_query_threshold { warn!( operation = operation, duration_ms = duration.as_millis(), "Slow storage operation detected" ); } // Check error rate let total = self.metrics.total_operations.load(Ordering::Relaxed); let failed = self.metrics.failed_operations.load(Ordering::Relaxed); if total > 0 { let error_rate = failed as f64 / total as f64; if error_rate > self.error_rate_threshold { error!( operation = operation, error_rate = error_rate, threshold = self.error_rate_threshold, "High error rate detected" ); } } } pub fn record_cache_hit(&self, level: &str, duration: Duration) { self.metrics.cache_hits.fetch_add(1, Ordering::Relaxed); info!( cache_level = level, duration_ΞΌs = duration.as_micros(), "Cache hit" ); } pub fn record_cache_miss(&self, duration: Duration) { self.metrics.cache_misses.fetch_add(1, Ordering::Relaxed); info!( duration_ms = duration.as_millis(), "Cache miss" ); } pub fn get_metrics_snapshot(&self) -> MetricsSnapshot { let total_ops = self.metrics.total_operations.load(Ordering::Relaxed); let successful_ops = self.metrics.successful_operations.load(Ordering::Relaxed); let failed_ops = self.metrics.failed_operations.load(Ordering::Relaxed); let total_duration_ns = self.metrics.total_duration.load(Ordering::Relaxed); let cache_hits = self.metrics.cache_hits.load(Ordering::Relaxed); let cache_misses = self.metrics.cache_misses.load(Ordering::Relaxed); MetricsSnapshot { total_operations: total_ops, successful_operations: successful_ops, failed_operations: failed_ops, error_rate: if total_ops > 0 { failed_ops as f64 / total_ops as f64 } else { 0.0 }, average_duration: if total_ops > 0 { Duration::from_nanos(total_duration_ns / total_ops) } else { Duration::ZERO }, cache_hit_rate: if (cache_hits + cache_misses) > 0 { cache_hits as f64 / (cache_hits + cache_misses) as f64 } else { 0.0 }, active_connections: self.metrics.active_connections.load(Ordering::Relaxed), pool_size: self.metrics.connection_pool_size.load(Ordering::Relaxed), } } } #[derive(Debug, Clone)] pub struct MetricsSnapshot { pub total_operations: u64, pub successful_operations: u64, pub failed_operations: u64, pub error_rate: f64, pub average_duration: Duration, pub cache_hit_rate: f64, pub active_connections: usize, pub pool_size: usize, } impl MetricsSnapshot { pub fn log_summary(&self) { info!( total_operations = self.total_operations, error_rate = %format!("{:.2}%", self.error_rate * 100.0), avg_duration_ms = self.average_duration.as_millis(), cache_hit_rate = %format!("{:.2}%", self.cache_hit_rate * 100.0), active_connections = self.active_connections, pool_size = self.pool_size, "Storage performance metrics" ); } } }
Best Practices Summary
Storage Selection Guidelines
-
Development and Testing
- Use
InMemoryStorage
for unit tests and local development - Quick to set up, no external dependencies
- Perfect for prototyping and CI/CD pipelines
- Use
-
Production Database Storage
- Use PostgreSQL for ACID compliance and complex queries
- Use MongoDB for document-oriented flexibility
- Use Redis for high-performance caching and session storage
-
Hybrid Approaches
- Multi-level caching for read-heavy workloads
- Event sourcing for audit trails and complex business logic
- CQRS for separating read/write optimization
Performance Optimization
-
Connection Management
- Use connection pooling for database storage
- Configure appropriate pool sizes and timeouts
- Monitor connection usage and adjust as needed
-
Caching Strategies
- Implement L1 (in-memory) cache for hot data
- Use L2 (Redis) cache for shared/distributed caching
- Choose appropriate TTLs based on data volatility
-
Query Optimization
- Index frequently queried fields (tenant_id, resource_type)
- Use batch operations where possible
- Implement pagination for large result sets
-
Monitoring and Alerting
- Track key metrics (latency, error rate, cache hit ratio)
- Set up alerts for performance degradation
- Use distributed tracing for complex request flows
Related Topics
- Resource Provider Architecture - How storage integrates with business logic
- Multi-Tenant Architecture Patterns - Tenant-specific storage isolation
- Storage Providers - Core storage concepts and interfaces
Next Steps
Now that you understand storage and persistence patterns:
- Choose your storage backend based on scalability and consistency requirements
- Implement appropriate caching for your read/write patterns
- Set up monitoring and alerting for production operations
- Consider advanced patterns like event sourcing for audit requirements
- Plan for data migration and backup/recovery strategies
Examples Overview
This section provides practical examples demonstrating the key features and capabilities of the SCIM Server library. Each example is designed to showcase specific functionality with clear, executable code that you can run and modify.
How to Use These Examples
All examples are located in the examples/
directory of the repository. Each example can be run directly:
# Basic examples
cargo run --example basic_usage
cargo run --example multi_tenant_example
# MCP examples (require mcp feature)
cargo run --example mcp_server_example --features mcp
Example Categories
π Core Examples
Learn the fundamental building blocks of SCIM server implementation:
- Basic Usage - Essential CRUD operations with users and groups
- Multi-Tenant Server - Complete tenant isolation and management
- Group Management - Working with groups and member relationships
π§ Advanced Features
Explore sophisticated server capabilities:
- ETag Concurrency Control - Prevent data conflicts with version control
- Operation Handlers - Framework-agnostic request/response handling
- Builder Pattern Configuration - Flexible server setup and configuration
π€ MCP Integration (AI Agents)
See how AI agents can interact with your SCIM server:
- MCP Server - Expose SCIM operations as AI tools
- MCP with ETag Support - Version-aware AI operations
- Simple MCP Demo - Quick AI integration setup
- MCP STDIO Server - Standard I/O protocol server
π Security & Authentication
Implement robust security patterns:
- Compile-Time Auth - Type-safe authentication patterns
- Role-Based Access Control - Advanced permission management
π οΈ Infrastructure & Operations
Production-ready operational patterns:
- Logging Backends - Structured logging with multiple backends
- Logging Configuration - Comprehensive logging setup
- Provider Modes - Different provider implementation patterns
- Automated Capabilities - Dynamic capability discovery
Learning Path
New to SCIM? Start with Basic Usage to understand core concepts.
Building multi-tenant systems? Progress to Multi-Tenant Server for isolation patterns.
Adding AI capabilities? Explore the MCP Integration examples starting with Simple MCP Demo.
Production deployment? Review ETag Concurrency Control and logging examples.
Running Examples
Prerequisites
- Rust 1.75 or later
- Clone the scim-server repository
Basic Examples
cd scim-server
cargo run --example basic_usage
MCP Examples
# Enable MCP feature for AI integration examples
cargo run --example mcp_server_example --features mcp
Development Setup
# Run with logging to see detailed output
RUST_LOG=debug cargo run --example multi_tenant_example
Key Concepts Demonstrated
Each example showcases different aspects of the SCIM Server library:
ScimServer
- Central orchestration componentResourceProvider
- Business logic abstractionStorageProvider
- Data persistence layer- Multi-Tenant Context - Tenant isolation
- Schema System - Validation and extensions
- MCP Integration - AI agent support
Related Documentation
- Getting Started Guide - Step-by-step tutorials
- Architecture Overview - System design principles
- API Reference - Complete API documentation
Contributing Examples
Have an interesting use case or pattern? Examples are welcome! See the contribution guidelines for details on adding new examples.
Basic Usage
This example demonstrates the fundamental operations of a SCIM server using the StandardResourceProvider
with in-memory storage. It's the perfect starting point for understanding core SCIM functionality.
What This Example Demonstrates
- Essential CRUD Operations - Create, read, update, delete, and list users
- SCIM 2.0 Compliance - Proper schema validation and metadata management
- Resource Provider Pattern - Using the standard provider for typical use cases
- Request Context - Tracking operations with unique request identifiers
- Error Handling - Graceful handling of validation and operational errors
Key Features Showcased
Resource Creation and Validation
The example shows how the StandardResourceProvider
automatically validates user data against SCIM 2.0 schemas, ensuring compliance and preventing invalid data from entering your system.
Metadata Management
Watch as the server automatically generates proper SCIM metadata including timestamps, resource versions, and location URLs - all handled transparently by the ResourceProvider
implementation.
Storage Abstraction
See how the InMemoryStorage
backend provides a clean separation between business logic and data persistence, making it easy to swap storage implementations.
Concepts Explored
This example serves as an introduction to several key concepts covered in depth elsewhere:
- Resource Providers - The business logic layer that implements SCIM semantics
- Storage Providers - The data persistence abstraction
- Request Context - Operation tracking and tenant scoping
- Schema Validation - Ensuring SCIM 2.0 compliance
Perfect For Learning
This example is ideal if you're:
- New to SCIM - Understand the basic protocol operations
- Evaluating the library - See core functionality in action
- Building simple systems - Single-tenant identity management
- Understanding the architecture - See how components work together
Running the Example
cargo run --example basic_usage
The example creates several users, demonstrates various query patterns, shows update operations, and illustrates proper error handling - all with clear console output explaining each step.
Next Steps
After exploring basic usage, consider:
- Multi-Tenant Server - Add tenant isolation capabilities
- Group Management - Work with groups and member relationships
- Configuration Guide - Learn advanced server setup
Source Code
View the complete implementation: examples/basic_usage.rs
Multi-Tenant Server
This example demonstrates a complete multi-tenant SCIM server implementation, showcasing how to isolate resources and operations across different customer organizations within a single deployment. It's essential for SaaS providers who need to serve multiple enterprise customers.
What This Example Demonstrates
- Complete Tenant Isolation - Strict separation of data and operations between tenants
- Tenant Resolution - Mapping authentication credentials to tenant contexts
- Resource Permissions - Granular control over tenant capabilities and quotas
- Isolation Levels - Different strategies for tenant data separation
- Realistic Multi-Tenant Patterns - Production-ready tenant management scenarios
Key Features Showcased
Tenant Context Management
See how TenantContext
provides complete tenant identity and permissions, ensuring that every operation is properly scoped to the correct customer organization.
Credential-Based Resolution
The example demonstrates StaticTenantResolver
mapping authentication tokens to specific tenants, showing how to integrate with your existing authentication infrastructure.
Resource Isolation Strategies
Explore different IsolationLevel
options - from strict separation to shared resources with tenant scoping - and understand when to use each approach.
Tenant Permissions
Watch TenantPermissions
in action, controlling resource quotas, operation limits, and feature access on a per-tenant basis.
Concepts Explored
This example brings together multiple advanced concepts:
- Multi-Tenant Architecture - Complete tenant isolation patterns
- Resource Providers - Tenant-aware business logic
- Request Context - Tenant information flow
- Storage Providers - Tenant-scoped data persistence
Perfect For Building
This example is essential if you're:
- Building SaaS Platforms - Multi-customer identity management
- Enterprise Integration - Serving multiple organizations
- Scalable Architecture - Tenant isolation at scale
- Production Systems - Real-world multi-tenancy patterns
Scenario Walkthrough
The example creates multiple tenant scenarios:
- Enterprise Customer A - Full-featured tenant with high limits
- Startup Customer B - Basic tenant with restricted permissions
- Trial Customer C - Limited-time tenant with minimal access
Each tenant operates completely independently, demonstrating true isolation while sharing the same server infrastructure.
Multi-Tenant Operations
Watch how the same operations behave differently across tenants:
- User Creation - Respects per-tenant quotas and permissions
- Resource Queries - Returns only tenant-specific data
- Schema Extensions - Tenant-specific attribute customizations
- Audit Trails - Proper tenant attribution for all operations
Running the Example
cargo run --example multi_tenant_example
The output shows clear tenant separation, permission enforcement, and isolation verification - demonstrating that tenants truly cannot access each other's data.
Production Considerations
This example illustrates production-ready patterns:
- Security Boundaries - Preventing cross-tenant data leakage
- Resource Management - Quota enforcement and capacity planning
- Operational Visibility - Tenant-aware logging and monitoring
- Configuration Management - Per-tenant customization capabilities
Next Steps
After exploring multi-tenant architecture:
- ETag Concurrency Control - Add conflict prevention to multi-tenant operations
- MCP Server - Enable AI agents to work with tenant-scoped operations
- Operation Handlers - Framework-agnostic tenant-aware request handling
Source Code
View the complete implementation: examples/multi_tenant_example.rs
Group Management
This example demonstrates comprehensive group management capabilities in SCIM, including group creation, member management, and the complex relationships between users and groups. It showcases how SCIM handles group membership references and maintains referential integrity.
What This Example Demonstrates
- Group Lifecycle Management - Creating, updating, and deleting groups with proper SCIM semantics
- Member Relationship Handling - Adding and removing users from groups with automatic reference management
- $ref Field Generation - How SCIM automatically creates proper resource references
- Referential Integrity - Maintaining consistency between users and their group memberships
- Complex Group Scenarios - Nested groups, bulk operations, and membership queries
Key Features Showcased
Automatic Reference Management
See how the ScimServer
automatically generates proper SCIM $ref
fields when creating group memberships, ensuring full protocol compliance without manual URL construction.
Group Schema Validation
Watch the StandardResourceProvider
validate group data against the SCIM 2.0 Group schema, ensuring displayName requirements and proper member structure.
Member Relationship Patterns
Explore different approaches to group membership - from simple user references to complex nested group structures with proper SCIM semantics.
Bulk Membership Operations
The example demonstrates efficient patterns for managing large numbers of group memberships while maintaining referential integrity and performance.
Concepts Explored
This example builds on several key architectural concepts:
- Resources - How groups are represented as SCIM resources
- Schema Validation - Group schema enforcement and compliance
- Resource Providers - Group-specific business logic
- SCIM Server - Orchestrating group operations
- Referential Integrity - Understanding client responsibility for data consistency
Perfect For Understanding
This example is ideal if you're:
- Implementing Access Control - Groups as authorization units
- Building Team Management - Organizing users into teams or departments
- Working with Complex Hierarchies - Nested organizational structures
- Ensuring SCIM Compliance - Proper group and membership handling
Group Operation Patterns
The example covers essential group management scenarios:
Basic Group Operations
- Creating groups with displayName and optional metadata
- Updating group properties and descriptions
- Deleting groups and handling member cleanup
Membership Management
- Adding individual users to groups
- Removing members while maintaining references
- Bulk membership operations for efficiency
- Querying group membership and user affiliations
Advanced Scenarios
- Nested group hierarchies and inheritance patterns
- Group-to-group relationships and complex structures
- Membership validation and constraint enforcement
- Cross-reference consistency checking
SCIM Protocol Details
Watch how the library handles SCIM-specific group requirements:
- displayName Attribute - Required field validation and uniqueness
- members Array - Proper structure with value, type, and $ref fields
- Meta Information - Automatic timestamp and version management
- Resource Location - Proper endpoint URL generation
Running the Example
cargo run --example group_example
The output demonstrates group creation, membership addition, reference generation, and complex query patterns - all with detailed explanations of the SCIM protocol behavior.
Real-World Applications
This example shows patterns useful for:
- Enterprise Directory Services - Organizational unit management
- Application Security - Role-based access control
- Team Collaboration - Project and department groupings
- Identity Federation - Cross-system group synchronization
Next Steps
After exploring group management:
- Multi-Tenant Server - Add tenant isolation to group operations
- ETag Concurrency Control - Prevent conflicts in concurrent group updates
- Operation Handlers - Framework-agnostic group API handling
Source Code
View the complete implementation: examples/group_example.rs
ETag Concurrency Control
This example demonstrates the built-in ETag concurrency control features of the SCIM server library, showing how to use conditional operations to prevent lost updates and handle version conflicts in multi-client scenarios.
What This Example Demonstrates
- Version-Based Conflict Prevention - Using ETags to detect and prevent concurrent modification conflicts
- Conditional Operations - HTTP-style conditional requests with If-Match and If-None-Match semantics
- Optimistic Locking - Non-blocking concurrency control for high-performance scenarios
- Conflict Resolution Patterns - Handling version mismatches and update conflicts gracefully
- Production-Ready Patterns - Real-world concurrency scenarios and best practices
Key Features Showcased
Automatic ETag Generation
See how the ResourceProvider
automatically generates version identifiers for every resource, enabling precise conflict detection without manual version management.
Conditional Update Operations
Watch ConditionalOperations
in action, demonstrating how to perform updates only when the expected version matches the current resource state.
Version Conflict Handling
Explore different ConditionalResult
outcomes and learn how to implement proper retry logic and conflict resolution strategies.
HTTP ETag Integration
The example shows how HttpVersion
and RawVersion
types integrate with standard HTTP caching and conditional request mechanisms.
Concepts Explored
This example demonstrates advanced concurrency concepts:
- Concurrency Control - Complete overview of version-based conflict prevention
- Resource Providers - Provider-level concurrency implementation
- Operation Handlers - Framework integration with ETag support
- SCIM Server - Server-level concurrency orchestration
Perfect For Understanding
This example is essential if you're:
- Building Multi-Client Systems - Multiple applications updating the same resources
- Implementing Enterprise Integration - HR systems, identity providers, and applications synchronizing data
- Ensuring Data Consistency - Preventing lost updates in concurrent environments
- Production Deployment - Real-world conflict handling and resolution
Concurrency Scenarios
The example simulates realistic concurrent access patterns:
Simultaneous Updates
Two clients attempt to update the same user simultaneously, demonstrating how ETag validation prevents the "last writer wins" problem and preserves both sets of changes.
Retry Logic Implementation
Watch proper retry patterns when version conflicts occur, including exponential backoff and conflict resolution strategies.
Bulk Operation Safety
See how concurrency control applies to bulk operations, ensuring consistency across multiple resource updates.
Mixed Operation Types
The example shows how different operation types (create, read, update, delete) interact with version control mechanisms.
Version Management
Understand the complete version lifecycle:
- Version Generation - How ETags are computed from resource content
- Version Validation - Comparing expected vs. actual versions
- Version Evolution - How versions change with resource updates
- Version Exposure - Making versions available to HTTP clients
Running the Example
cargo run --example etag_concurrency_example
The output shows detailed version tracking, conflict detection, successful conditional operations, and failed operations with proper error handling - all demonstrating production-ready concurrency patterns.
Production Benefits
This example illustrates critical production capabilities:
- Data Integrity - Preventing corruption from concurrent modifications
- Performance - Non-blocking optimistic concurrency vs. expensive locking
- Scalability - Handling multiple clients without serialization bottlenecks
- Reliability - Predictable conflict detection and resolution
Integration Patterns
See how ETag concurrency integrates with:
- HTTP Frameworks - Standard If-Match/If-None-Match header handling
- Multi-Tenant Systems - Per-tenant version management
- AI Agent Operations - Version-aware automated operations
- Bulk APIs - Consistent versioning across multiple resources
Next Steps
After exploring concurrency control:
- Operation Handlers - Framework integration with conditional operations
- MCP with ETag Support - Version-aware AI agent operations
- Multi-Tenant Server - Tenant-scoped concurrency control
Source Code
View the complete implementation: examples/etag_concurrency_example.rs
Operation Handlers
This example demonstrates the framework-agnostic operation handler layer that bridges transport protocols and the SCIM server core. It shows how to build structured request/response handling with built-in concurrency control and comprehensive error management.
What This Example Demonstrates
- Framework-Agnostic Integration - Working with any transport layer (HTTP, MCP, CLI, custom protocols)
- Structured Request/Response Handling - Consistent patterns across all operation types
- Built-in ETag Support - Automatic version control and concurrency management
- Comprehensive Error Translation - Converting internal errors to structured responses
- Request Tracing - Built-in request ID correlation and operational logging
- Multi-Tenant Request Handling - Seamless tenant context propagation
Key Features Showcased
Operation Handler Abstraction
See how ScimOperationHandler
provides a clean abstraction layer between HTTP frameworks and SCIM business logic, enabling consistent behavior across different integration patterns.
Structured Request Processing
Watch ScimOperationRequest
standardize request handling with built-in validation, parameter extraction, and context management - regardless of the underlying transport.
Consistent Response Formatting
Explore how ScimOperationResponse
ensures uniform response structure with proper HTTP status codes, headers, and JSON formatting across all operations.
Operational Metadata Management
The example shows OperationMetadata
handling version control, request tracing, and performance metrics automatically.
Concepts Explored
This example demonstrates the bridge between transport and business logic:
- Operation Handlers - Complete framework abstraction patterns
- SCIM Server - Core business logic integration
- Concurrency Control - Version-aware operation handling
- Multi-Tenant Architecture - Tenant-aware request processing
Perfect For Building
This example is essential if you're:
- Building REST APIs - HTTP framework integration with any web server
- Creating Custom Protocols - Non-HTTP transport layer implementation
- Implementing Middleware - Request/response processing pipelines
- Testing SCIM Operations - Framework-independent testing harnesses
Integration Patterns
The example covers multiple integration scenarios:
HTTP Framework Integration
See how operation handlers work with popular Rust web frameworks:
- Axum - Clean async handler integration
- Actix-web - Actor-based request processing
- Warp - Filter-based routing compatibility
- Rocket - Type-safe request handling
Custom Protocol Support
Explore how the same operation handlers can work with:
- gRPC Services - Protocol buffer integration
- WebSocket Connections - Real-time operation handling
- Command-Line Tools - CLI-based SCIM operations
- Message Queues - Asynchronous operation processing
Testing and Development
The framework-agnostic design enables:
- Unit Testing - Testing business logic without HTTP setup
- Integration Testing - Protocol-independent test suites
- Development Tools - CLI utilities and debugging tools
Request Processing Pipeline
Watch the complete request lifecycle:
- Request Structuring - Converting transport-specific requests to standard format
- Validation - Parameter checking and constraint enforcement
- Context Extraction - Tenant and authentication information processing
- Operation Dispatch - Routing to appropriate business logic handlers
- Response Formatting - Converting results to transport-appropriate format
Error Handling Excellence
The example demonstrates sophisticated error management:
- Error Translation - Converting internal errors to appropriate HTTP status codes
- Structured Responses - Consistent error format across all operations
- Context Preservation - Maintaining request context through error scenarios
- Logging Integration - Comprehensive error tracking and debugging support
Running the Example
cargo run --example operation_handler_example
The output shows complete request/response cycles with detailed logging, error scenarios, and performance metrics - demonstrating production-ready operation handling.
Production Benefits
This example illustrates critical production capabilities:
- Transport Flexibility - Easy migration between different protocols and frameworks
- Consistent Behavior - Same business logic regardless of integration method
- Operational Visibility - Built-in logging, metrics, and tracing
- Error Resilience - Graceful handling of edge cases and failures
Advanced Features
Explore sophisticated operation handler capabilities:
- Conditional Operations - Built-in ETag support for concurrency control
- Bulk Operation Support - Efficient handling of multiple resources
- Schema Validation - Automatic request/response validation
- Performance Optimization - Minimal overhead and maximum throughput
Next Steps
After exploring operation handlers:
- ETag Concurrency Control - Add version-aware operations
- Multi-Tenant Server - Tenant-aware request processing
- MCP Server - AI agent protocol integration
Source Code
View the complete implementation: examples/operation_handler_example.rs
Related Documentation
- Operation Handlers Concepts - Architectural overview and design patterns
- Configuration Guide - Integrating handlers with server setup
- Operation Handler API Reference - Complete API documentation
Builder Pattern Configuration
This example demonstrates the flexible configuration capabilities of the SCIM server using the builder pattern. It shows how to construct servers with different deployment patterns, tenant strategies, and feature configurations through a fluent, type-safe API.
What This Example Demonstrates
- Fluent Configuration API - Chain configuration methods for readable server setup
- Deployment Pattern Flexibility - Single-tenant, multi-tenant, and hybrid configurations
- URL Generation Strategies - Different approaches to endpoint and reference URL creation
- Feature Toggle Management - Enabling and configuring optional capabilities
- Environment-Specific Setup - Development, staging, and production configurations
- Configuration Validation - Compile-time and runtime validation of server settings
Key Features Showcased
Flexible Server Construction
See how ScimServerBuilder
enables readable, maintainable server configuration through method chaining and type-safe parameter handling.
Tenant Strategy Configuration
Explore different TenantStrategy
options and understand when to use subdomain-based, path-based, or single-tenant URL patterns.
Base URL Management
Watch how proper base URL configuration affects $ref
field generation, resource location URLs, and integration with different deployment environments.
Configuration Composition
The example demonstrates how to compose complex configurations from simpler building blocks, enabling reusable configuration patterns across different environments.
Concepts Explored
This example showcases configuration and deployment patterns:
- SCIM Server - Builder pattern implementation and usage
- Multi-Tenant Architecture - Tenant strategy selection and configuration
- Architecture Overview - How configuration affects system behavior
Perfect For Understanding
This example is essential if you're:
- Configuring Production Deployments - Environment-specific server setup
- Building Flexible Systems - Runtime configuration and feature toggles
- Managing Multiple Environments - Development, testing, and production configurations
- Creating Deployment Templates - Reusable configuration patterns
Configuration Scenarios
The example covers multiple deployment patterns:
Development Configuration
Simple, localhost-based setup with minimal security and maximum debugging visibility:
- In-memory storage for quick iteration
- Detailed logging and error reporting
- Single-tenant mode for simplicity
- Development-friendly base URLs
Production Multi-Tenant Setup
Enterprise-ready configuration with proper tenant isolation:
- Database-backed storage with connection pooling
- Path-based tenant strategy for clean URLs
- Production base URLs with HTTPS
- Enhanced security and audit logging
Hybrid Cloud Deployment
Sophisticated configuration for cloud-native deployments:
- Subdomain-based tenant isolation
- Environment variable integration
- Feature flags and capability toggles
- Observability and monitoring integration
Builder Method Categories
Explore the different types of configuration available:
Core Configuration
- Base URL Setup - Foundation for all URL generation
- SCIM Version - Protocol version specification
- Server Metadata - Identity and capability information
Multi-Tenancy Configuration
- Tenant Strategy - URL pattern and isolation approach
- Default Permissions - Baseline tenant capabilities
- Isolation Levels - Data separation strategies
Feature Configuration
- Optional Capabilities - MCP integration, bulk operations, filtering
- Performance Tuning - Connection pools, caching, timeouts
- Security Settings - Authentication requirements, CORS policies
Running the Example
cargo run --example builder_example
The output shows different server configurations being built and validated, demonstrating how the builder pattern creates properly configured servers for various deployment scenarios.
Configuration Best Practices
The example illustrates production-ready configuration patterns:
- Environment Separation - Different configurations for different environments
- Validation Strategy - Early validation of configuration parameters
- Default Management - Sensible defaults with explicit overrides
- Documentation - Self-documenting configuration through method names
Type Safety Benefits
See how the builder pattern provides compile-time guarantees:
- Required Parameters - Compiler ensures essential configuration is provided
- Valid Combinations - Type system prevents invalid configuration states
- Method Chaining - Fluent API with proper return types
- Error Prevention - Many configuration errors caught at compile time
Configuration Reusability
Learn patterns for creating reusable configurations:
- Configuration Templates - Base configurations for common scenarios
- Environment Abstraction - Parameterized configurations for different deployments
- Feature Composition - Mixing and matching capabilities as needed
- Validation Helpers - Shared validation logic across configurations
Next Steps
After exploring builder pattern configuration:
- Multi-Tenant Server - See multi-tenant configurations in action
- Operation Handlers - Framework integration with configured servers
- Basic Usage - Simple configurations for getting started
Source Code
View the complete implementation: examples/builder_example.rs
Related Documentation
- Configuration Guide - Comprehensive server setup documentation
- ScimServerBuilder API Reference - Complete builder API
- Multi-Tenant Architecture - Tenant strategy details and patterns
MCP Server
This example demonstrates how to create and run a SCIM server that exposes its functionality as MCP (Model Context Protocol) tools for AI agents. The MCP integration transforms SCIM operations into a structured tool interface that AI systems can discover, understand, and execute.
What This Example Demonstrates
- AI-Native SCIM Interface - Complete SCIM operations exposed as discoverable AI tools
- Tool Schema Generation - Automatic JSON Schema creation for AI agent understanding
- Multi-Tenant AI Support - Tenant-aware operations for enterprise AI deployment
- Error-Resilient AI Workflows - Structured error responses enabling AI decision making
- Schema Introspection - Dynamic discovery of SCIM capabilities and resource types
- Version-Aware AI Operations - Built-in concurrency control for AI-driven updates
Key Features Showcased
AI Tool Discovery
Watch how ScimMcpServer
automatically exposes SCIM operations as structured tools that AI agents can discover and understand without manual configuration.
Structured Tool Execution
See how complex SCIM operations are transformed into simple, parameterized tools that AI agents can execute with natural language input, complete with validation and error handling.
Schema-Driven AI Understanding
The example demonstrates how AI agents can introspect SCIM schemas to understand resource structures, attribute types, and validation rules - enabling intelligent data manipulation.
Enterprise AI Integration
Explore how McpServerInfo
provides comprehensive server capabilities to AI agents, enabling sophisticated identity management workflows.
Concepts Explored
This example bridges AI and identity management through several key concepts:
- MCP Integration - Complete AI agent support architecture
- Operation Handlers - Framework-agnostic operation abstraction
- SCIM Server - Core protocol implementation
- Schema Discovery - Dynamic capability advertisement
Perfect For Building
This example is essential if you're:
- Building AI-Powered Identity Systems - Automated user provisioning and management
- Creating Conversational HR Tools - Natural language identity operations
- Implementing Smart Workflows - AI-driven identity lifecycle management
- Enterprise AI Integration - Connecting AI agents to identity infrastructure
AI Agent Capabilities
The MCP server exposes comprehensive identity management tools:
User Management Tools
- Create User - Provision new user accounts with validation
- Get User - Retrieve user information by ID or username
- Update User - Modify user attributes with conflict detection
- Delete User - Deactivate or remove user accounts
- List Users - Query and filter user populations
- Search Users - Find users by specific attributes
Group Management Tools
- Create Group - Establish new groups with member management
- Manage Members - Add and remove group members
- Group Queries - Search and filter group collections
Schema Discovery Tools
- List Schemas - Discover available resource types and attributes
- Get Server Info - Understand server capabilities and configuration
- Introspect Resources - Examine resource structure and validation rules
AI Workflow Examples
The example demonstrates several AI agent interaction patterns:
Conversational User Creation
AI agents can create users from natural language descriptions, automatically mapping human-readable requests to proper SCIM resource structures.
Intelligent Error Recovery
When operations fail, the structured error responses help AI agents understand what went wrong and how to correct the issue.
Multi-Step Workflows
Complex identity operations can be broken down into multiple tool calls, with the AI agent orchestrating the sequence based on business logic.
Schema-Aware Operations
AI agents can inspect schemas before operations, ensuring they provide appropriate data types and required fields.
Running the Example
cargo run --example mcp_server_example --features mcp
The server starts listening on standard input/output for MCP protocol messages, ready to receive tool discovery and execution requests from AI agents.
Integration with AI Systems
This example works with various AI agent frameworks:
- Claude Desktop - Direct MCP protocol integration
- Custom AI Agents - JSON-RPC 2.0 protocol support
- Workflow Automation - Programmatic AI agent integration
- Enterprise AI Platforms - Structured tool interface compatibility
Production Considerations
The example illustrates enterprise-ready AI integration patterns:
- Security Boundaries - Tenant isolation for AI operations
- Audit Trails - Comprehensive logging of AI-driven changes
- Rate Limiting - Controlled AI agent access patterns
- Error Handling - Graceful failure modes for AI workflows
Multi-Tenant AI Operations
See how AI agents can work with tenant-scoped operations, enabling:
- Customer-Specific AI - Agents that operate within tenant boundaries
- Isolated AI Workflows - Preventing cross-tenant data access
- Tenant-Aware Automation - Context-sensitive AI operations
Next Steps
After exploring MCP integration:
- MCP with ETag Support - Add version control to AI operations
- Simple MCP Demo - Quick integration patterns
- MCP STDIO Server - Standard I/O protocol implementation
Source Code
View the complete implementation: examples/mcp_server_example.rs
Related Documentation
- Setting Up Your MCP Server - Step-by-step MCP setup guide
- MCP Integration Concepts - Architectural overview and patterns
- MCP API Reference - Complete MCP integration documentation
MCP with ETag Support
This example demonstrates how to combine MCP (Model Context Protocol) integration with ETag-based concurrency control, enabling AI agents to perform version-aware identity management operations. It shows how AI systems can handle concurrent access scenarios and prevent data conflicts through proper version management.
What This Example Demonstrates
- Version-Aware AI Operations - AI agents that understand and work with resource versions
- Conflict-Resilient AI Workflows - Automatic handling of version conflicts in AI-driven updates
- Optimistic Concurrency for AI - Non-blocking AI operations with conflict detection
- Intelligent Retry Logic - AI agents that can recover from version conflicts gracefully
- Production-Safe AI Integration - Preventing AI-induced data corruption in concurrent environments
- Multi-Client AI Scenarios - Multiple AI agents working safely with shared resources
Key Features Showcased
AI-Aware Version Management
See how ScimMcpServer
exposes version information to AI agents, enabling them to make informed decisions about when and how to update resources.
Conditional AI Operations
Watch AI agents use version parameters in their tool calls, leveraging the same ConditionalOperations
that power HTTP-based concurrency control.
Structured Conflict Responses
Explore how version conflicts are communicated to AI agents through ScimToolResult
, providing enough context for intelligent conflict resolution.
AI Retry Patterns
The example demonstrates how AI agents can implement exponential backoff and intelligent retry logic when encountering version conflicts, preventing infinite retry loops.
Concepts Explored
This example combines advanced AI and concurrency concepts:
- MCP Integration - AI agent support with advanced features
- Concurrency Control - Version-based conflict prevention
- Operation Handlers - Framework-agnostic version handling
- SCIM Server - Server-level concurrency orchestration
Perfect For Building
This example is essential if you're:
- Building Production AI Systems - AI agents that work safely in concurrent environments
- Implementing Automated Workflows - AI-driven processes that must handle conflicts gracefully
- Creating Resilient AI Agents - Systems that can recover from operational conflicts
- Enterprise AI Integration - AI agents working with shared enterprise resources
AI Agent Scenarios
The example covers sophisticated AI interaction patterns:
Collaborative AI Agents
Multiple AI agents working on the same resources simultaneously, with automatic conflict detection and resolution when their operations overlap.
Long-Running AI Workflows
AI agents that perform multi-step operations over time, using version checking to ensure their assumptions about resource state remain valid.
AI-Human Collaboration
Scenarios where AI agents and human administrators work on the same resources, with version control preventing conflicts between automated and manual changes.
Batch AI Operations
AI agents performing bulk updates with version validation, ensuring consistency across multiple related resource changes.
Version-Aware Tool Operations
The MCP server exposes enhanced tools with version support:
User Management with Versions
- Get User with Version - Retrieve users with current version information
- Update User with Version Check - Conditional updates that respect current versions
- Create User with Conflict Detection - Prevent duplicate creation with version validation
Group Operations with Concurrency Control
- Modify Group Membership - Version-aware member addition and removal
- Update Group Properties - Conditional group updates with conflict detection
- Bulk Group Operations - Multiple group changes with consistent versioning
Schema Operations
- Version-Aware Schema Discovery - Understanding current schema versions
- Schema Extension Validation - Ensuring extensions don't conflict with current state
Running the Example
cargo run --example mcp_etag_example --features mcp
The server demonstrates version-aware AI operations with simulated concurrent access scenarios, showing how AI agents handle conflicts and maintain data consistency.
AI Conflict Resolution Strategies
The example illustrates different approaches AI agents can take when encountering version conflicts:
Immediate Retry
Simple retry logic for transient conflicts, with exponential backoff to prevent system overload.
Conflict Analysis
AI agents that examine conflict details and make intelligent decisions about how to proceed based on the nature of the changes.
User Consultation
AI workflows that escalate version conflicts to human decision-makers when automatic resolution isn't appropriate.
Alternative Strategy Selection
AI agents that choose different approaches when their preferred operation conflicts with concurrent changes.
Production Benefits
This example demonstrates critical production AI capabilities:
- Data Integrity - Preventing AI-induced data corruption through version validation
- System Stability - Avoiding cascade failures from AI retry storms
- Operational Reliability - Predictable AI behavior in concurrent scenarios
- Audit Compliance - Version tracking for all AI-initiated changes
Multi-Tenant Version Management
See how version control works in multi-tenant AI scenarios:
- Tenant-Scoped Versions - Version management within tenant boundaries
- Cross-Tenant Conflict Prevention - Ensuring AI agents respect tenant isolation
- Per-Tenant AI Policies - Different conflict resolution strategies for different customers
Integration with AI Frameworks
The example works with various AI agent systems:
- Autonomous Agents - Self-directing AI systems with conflict awareness
- Workflow Orchestrators - Multi-step AI processes with version checkpoints
- Decision Support Systems - AI assistants that help humans navigate conflicts
- Automated Operations - AI-driven identity lifecycle management
Advanced Features
Explore sophisticated version-aware AI capabilities:
- Predictive Conflict Avoidance - AI agents that anticipate and avoid conflicts
- Collaborative Decision Making - Multiple AI agents negotiating resource changes
- Version History Analysis - AI systems that learn from past conflict patterns
- Adaptive Retry Strategies - AI agents that adjust behavior based on conflict frequency
Next Steps
After exploring MCP with ETag support:
- ETag Concurrency Control - Understanding the underlying concurrency mechanisms
- MCP Server - Full-featured MCP integration without version focus
- Multi-Tenant Server - Tenant-aware version management
Source Code
View the complete implementation: examples/mcp_etag_example.rs
Related Documentation
- Concurrency Control Concepts - Complete concurrency control overview
- MCP Integration Guide - AI agent architecture and patterns
- MCP API Reference - Complete MCP integration documentation
Simple MCP Demo
This example provides the quickest way to get started with MCP (Model Context Protocol) integration, demonstrating how to expose basic SCIM operations to AI agents with minimal setup. It's perfect for understanding MCP concepts and testing AI agent interactions.
What This Example Demonstrates
- Minimal MCP Setup - Get AI agents working with SCIM in under 50 lines of code
- Basic Tool Exposure - Essential user management operations as AI tools
- Simple Protocol Integration - Standard I/O based MCP communication
- Quick Testing Patterns - Immediate feedback and validation for AI interactions
- Foundation Building - Starting point for more sophisticated AI integrations
Key Features Showcased
Streamlined Integration
See how ScimMcpServer
can be set up with minimal configuration, focusing on core functionality rather than advanced features.
Essential Tool Set
The example exposes a focused set of AI tools covering the most common identity management operations:
- User creation and retrieval
- Basic user queries
- Schema discovery for AI understanding
Zero-Configuration AI Support
Watch how AI agents can immediately start working with your SCIM server without complex setup, authentication, or protocol negotiation.
Interactive Testing
The demo is designed for immediate interaction, allowing you to test AI agent communication patterns and understand the request/response flow.
Concepts Explored
This example introduces fundamental MCP concepts:
- MCP Integration - AI agent support architecture basics
- SCIM Server - Core server functionality
- Basic Usage Patterns - Underlying SCIM operations
Perfect For Getting Started
This example is ideal if you're:
- New to MCP - Understanding AI agent integration concepts
- Rapid Prototyping - Quick setup for testing AI workflows
- Proof of Concept - Demonstrating AI-driven identity management
- Learning Integration - Understanding how SCIM and AI agents work together
Tool Capabilities
The simple demo exposes core identity management tools:
User Management
- Create User - Provision new accounts with basic validation
- Get User - Retrieve user information by username or ID
- List Users - Browse available user accounts
System Discovery
- Server Info - Basic server capabilities and configuration
- Schema Info - Available resource types for AI understanding
AI Interaction Flow
The example demonstrates a typical AI agent workflow:
- Tool Discovery - AI agent requests available tools
- Schema Understanding - Agent learns about user attributes and validation
- Operation Execution - Agent performs identity management tasks
- Result Processing - Agent receives structured responses for decision making
Running the Example
cargo run --example simple_mcp_demo --features mcp
The server starts in interactive mode, ready to receive MCP protocol messages and demonstrate AI agent communication patterns.
Testing with AI Agents
Once running, you can test with various AI systems:
Manual Protocol Testing
Send JSON-RPC messages directly to understand the protocol:
echo '{"jsonrpc":"2.0","method":"tools/list","id":1}' | cargo run --example simple_mcp_demo --features mcp
AI Agent Integration
Connect with MCP-compatible AI agents to see natural language identity management in action.
Key Differences from Full MCP Server
This simplified demo differs from the full MCP server example:
- Reduced Tool Set - Focus on essential operations only
- Minimal Configuration - Default settings for quick startup
- No Multi-Tenancy - Single-tenant operation for simplicity
- Basic Error Handling - Simple error responses without complex recovery
Extending the Demo
Natural extensions to explore:
- Additional Tools - Add group management or advanced user operations
- Authentication - Integrate with your authentication system
- Multi-Tenancy - Add tenant context for enterprise scenarios
- Custom Schemas - Extend with organization-specific attributes
Running the Example
The demo starts immediately and provides clear output showing:
- Available tools and their schemas
- Example AI agent interactions
- Request/response patterns
- Error handling demonstrations
Next Steps
After exploring the simple demo:
- MCP Server - Full-featured MCP integration
- MCP with ETag Support - Add version control for AI operations
- Basic Usage - Understanding underlying SCIM operations
Source Code
View the complete implementation: examples/simple_mcp_demo.rs
Related Documentation
- Setting Up Your MCP Server - Step-by-step MCP setup guide
- MCP Integration Concepts - Architectural overview
- MCP API Reference - Complete MCP documentation
MCP STDIO Server
π Placeholder Content
This example is currently a placeholder and will be expanded with detailed implementation examples in future releases.
This section will cover:
- Setting up an MCP server with STDIO transport
- Configuring STDIO-based communication
- Best practices for STDIO MCP integration
- Error handling and debugging techniques
Examples and detailed implementation guides coming soon.
Compile-Time Auth
π Placeholder Content
This example is currently a placeholder and will be expanded with detailed implementation examples in future releases.
This section will cover:
- Type-safe authentication at compile time
- Zero-runtime-cost authentication patterns
- Phantom types for authorization levels
- Compile-time permission checking
- Integration with SCIM operation handlers
- Authentication trait implementations
Examples and detailed implementation guides coming soon.
Role-Based Access Control
π Placeholder Content
This example is currently a placeholder and will be expanded with detailed implementation examples in future releases.
This section will cover:
- Implementing role-based access control in SCIM operations
- Defining roles and permissions at compile time
- User role assignment and validation
- Fine-grained resource access control
- Integration with authentication systems
- Multi-tenant RBAC patterns
- Permission inheritance and hierarchies
Examples and detailed implementation guides coming soon.
Logging Backends
π Placeholder Content
This example is currently a placeholder and will be expanded with detailed implementation examples in future releases.
This section will cover:
- Configuring different logging backends (tracing, env_logger, etc.)
- Structured logging for SCIM operations
- Log level configuration and filtering
- Custom log formatters and outputs
- Integration with observability systems
- Performance considerations for logging
- Multi-tenant logging strategies
Examples and detailed implementation guides coming soon.
Logging Configuration
This example demonstrates comprehensive logging setup for SCIM servers, showing how to configure structured logging, multiple output formats, and operational visibility for production deployments. It covers everything from basic console logging to sophisticated structured logging with multiple backends.
What This Example Demonstrates
- Structured Logging Setup - JSON and key-value formatted log output for machine processing
- Multiple Log Levels - Fine-grained control over logging verbosity and filtering
- Request Tracing - Correlation of log entries across complex operations
- Performance Logging - Operation timing and performance metrics
- Error Context Preservation - Detailed error information for debugging and monitoring
- Production-Ready Patterns - Log management strategies for enterprise deployments
Key Features Showcased
Comprehensive Log Configuration
See how to set up logging that captures all aspects of SCIM server operations, from request processing to storage operations, with appropriate detail levels for different deployment environments.
Request Context Integration
Watch how RequestContext
flows through all operations, enabling request correlation and distributed tracing across system boundaries.
Structured Data Logging
The example demonstrates logging complex SCIM data structures in formats that support automated processing, alerting, and analysis by log management systems.
Performance Monitoring
Explore how to capture operation timing, resource usage, and throughput metrics through logging, enabling performance analysis without dedicated monitoring infrastructure.
Concepts Explored
This example integrates logging throughout the SCIM architecture:
- SCIM Server - Server-level operational logging
- Resource Providers - Business logic operation logging
- Storage Providers - Data persistence operation logging
- Multi-Tenant Architecture - Tenant-aware logging patterns
Perfect For Understanding
This example is essential if you're:
- Building Production Systems - Comprehensive operational visibility requirements
- Implementing Monitoring - Log-based observability and alerting strategies
- Debugging Complex Issues - Detailed logging for troubleshooting and root cause analysis
- Managing Enterprise Deployments - Audit trails and compliance logging
Logging Categories
The example covers different types of logging needs:
Request/Response Logging
- Complete request and response capture for audit trails
- Parameter sanitization for security-sensitive data
- Response time and status code tracking
- Error condition documentation
Business Logic Logging
- SCIM operation execution with context
- Validation failures and constraint violations
- Resource lifecycle events (creation, updates, deletions)
- Schema validation and extension processing
System Operations Logging
- Storage backend operations and performance
- Connection pool usage and database interactions
- Cache operations and efficiency metrics
- Background task execution and scheduling
Security and Audit Logging
- Authentication and authorization events
- Tenant boundary enforcement
- Data access patterns and privacy compliance
- Security policy violations and responses
Log Format Options
Explore different logging formats for various use cases:
Development Logging
- Human-readable console output with color coding
- Detailed stack traces and debug information
- Interactive logging with immediate feedback
- Local file rotation and management
Production Structured Logging
- JSON format for log aggregation systems
- Key-value pairs for efficient querying
- Standardized field names and formats
- Integration with monitoring and alerting systems
Compliance and Audit Logging
- Immutable log entries with integrity verification
- Standardized audit event formats
- Long-term retention and archival strategies
- Privacy-aware logging with data sanitization
Running the Example
# Basic logging setup
RUST_LOG=info cargo run --example logging_example
# Detailed debug logging
RUST_LOG=debug cargo run --example logging_example
# Structured JSON logging
RUST_LOG=info SCIM_LOG_FORMAT=json cargo run --example logging_example
The output demonstrates different logging levels, formats, and integration patterns with clear examples of request correlation and structured data capture.
Log Management Integration
The example shows integration with popular log management tools:
Log Aggregation
- ELK Stack - Elasticsearch, Logstash, and Kibana integration
- Fluentd/Fluent Bit - Log forwarding and processing
- Splunk - Enterprise log management and analysis
- DataDog/New Relic - Cloud-based logging and monitoring
Observability Platforms
- Jaeger - Distributed tracing integration
- Prometheus - Metrics extraction from logs
- Grafana - Log-based dashboard and alerting
- OpenTelemetry - Standardized observability data
Configuration Patterns
Learn flexible logging configuration approaches:
Environment-Based Configuration
- Development vs. production logging levels
- Feature-specific logging toggles
- Performance vs. verbosity trade-offs
- Security-sensitive data handling
Runtime Log Control
- Dynamic log level adjustment without restarts
- Feature-specific logging enable/disable
- Performance-sensitive logging optimization
- Emergency debugging activation
Production Considerations
The example illustrates enterprise logging requirements:
- Performance Impact - Minimizing logging overhead in high-throughput scenarios
- Storage Management - Log rotation, compression, and archival strategies
- Security - Protecting sensitive data in log files
- Compliance - Meeting regulatory requirements for audit logging
Next Steps
After exploring logging configuration:
- Multi-Tenant Server - Tenant-aware logging patterns
- ETag Concurrency Control - Version conflict logging
- Operation Handlers - Request/response logging integration
Source Code
View the complete implementation: examples/logging_example.rs
Related Documentation
- Configuration Guide - Server configuration including logging setup
- Production Deployment - Production-ready server configuration
- Logging Backends Example - Multiple logging backend implementation
Provider Modes
π Placeholder Content
This example is currently a placeholder and will be expanded with detailed implementation examples in future releases.
This section will cover:
- Different provider operation modes (read-only, read-write, etc.)
- Configuring provider capabilities and limitations
- Mode-specific behavior and validation
- Dynamic mode switching at runtime
- Performance optimizations per mode
- Error handling for unsupported operations
- Integration with storage backends
- Multi-tenant provider mode configuration
Examples and detailed implementation guides coming soon.
Automated Capabilities
π Placeholder Content
This example is currently a placeholder and will be expanded with detailed implementation examples in future releases.
This section will cover:
- Automatically detecting and advertising SCIM server capabilities
- Dynamic capability discovery based on provider configuration
- ServiceProviderConfig endpoint implementation
- Feature detection and capability negotiation
- Runtime capability validation and enforcement
- Custom capability extensions and metadata
- Performance optimizations for capability queries
- Multi-tenant capability management
Examples and detailed implementation guides coming soon.