Keyboard shortcuts

Press ← or β†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 ScratchUsing 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

Extension Points

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:

  1. Getting Started: Quick setup and basic usage
  2. Core Concepts: Understanding the fundamental ideas
  3. Tutorials: Step-by-step guides for common scenarios
  4. How-To Guides: Solutions for specific problems
  5. Advanced Topics: Deep dives into complex scenarios
  6. 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:

Getting Help

Let's get started! πŸš€

Installation

This guide will get you up and running with the SCIM server library in under 5 minutes.

Prerequisites

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:

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

Complete Examples

See the examples directory for full working implementations:

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 compliance
  • StandardResourceProvider - Storage abstraction layer
  • InMemoryStorage - Simple storage backend for development
  • RequestContext - 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 testing
  • SqliteStorage::new() - For file-based persistence (requires sqlite 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:

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:

  1. Get schema from server's built-in schema registry
  2. Create resource handler from schema
  3. 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 security
  • IsolationLevel::Standard - Normal isolation with some resource sharing
  • IsolationLevel::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:

For complete working examples, see the examples directory in the repository.

API Reference

For detailed API documentation, see:

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 user
  • scim_get_user - Retrieve user by ID
  • scim_update_user - Update existing user
  • scim_delete_user - Delete user by ID
  • scim_list_users - List all users with pagination
  • scim_search_users - Search users by attribute
  • scim_user_exists - Check if user exists

System Information

  • scim_get_schemas - Get all SCIM schemas
  • scim_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:

  1. Initialize - Establish connection and capabilities
  2. Discover Tools - Get list of available SCIM operations
  3. Get Schemas - Understand your data model structure
  4. Execute Operations - Create, read, update, delete resources
  5. 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

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:

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 include RequestContext
  • 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:

  1. Extract SCIM request details
  2. Create RequestContext with tenant info
  3. Call appropriate ScimServer operations
  4. Format responses per SCIM specification

AI Agent Integration

Model Context Protocol (MCP) components:

  1. Expose SCIM operations as discoverable tools
  2. Structured schemas for AI understanding
  3. Error handling designed for AI decision making
  4. Multi-tenant aware tool descriptions

Custom Client Integration

Direct component usage:

  1. Implement ResourceProvider for your data model
  2. Choose appropriate StorageProvider
  3. Configure schema extensions as needed
  4. 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

  1. ScimOperationHandler: Main dispatcher for all SCIM operations
  2. ScimOperationRequest: Structured request wrapper with validation
  3. ScimOperationResponse: Consistent response format with metadata
  4. OperationMetadata: Version control, tracing, and operational data
  5. 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

  1. HTTP Framework Integration: Building REST APIs that expose SCIM endpoints
  2. Protocol Integration: Adapting SCIM to custom protocols (MCP, GraphQL, gRPC)
  3. CLI Tools: Building command-line identity management utilities
  4. Batch Processing: Implementing bulk identity operations
  5. Testing Frameworks: Creating test harnesses that need structured SCIM operations

Implementation Strategies

ScenarioApproachComplexityBenefits
REST APIDirect handler integrationLowFramework independence, built-in ETag
MCP ProtocolTool handler delegationMediumStructured AI interactions
CLI ToolsCommand handler wrapperLowConsistent CLI behavior
Batch ProcessingAsync handler coordinationMediumConcurrency control, error handling
Custom ProtocolsProtocol adapter layerHighProtocol flexibility, SCIM compliance

Comparison with Direct SCIM Server Usage

ApproachAbstractionError HandlingVersion ControlComplexity
Operation Handlersβœ… Highβœ… Structuredβœ… Built-inLow
Direct SCIM Server⚠️ Medium⚠️ Manual⚠️ ManualMedium
Custom Integration❌ Low❌ Ad-hoc❌ CustomHigh

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. Add features = ["mcp"] to your Cargo.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

  1. ScimMcpServer: Main MCP server wrapper exposing SCIM operations as AI tools
  2. Tool Schemas: JSON Schema definitions for AI agent tool discovery
  3. Tool Handlers: Execution logic for each exposed SCIM operation
  4. Protocol Layer: MCP JSON-RPC 2.0 protocol implementation
  5. 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

  1. AI-Powered HR Systems: Conversational employee lifecycle management
  2. DevOps Automation: AI-driven environment and user provisioning
  3. Compliance Monitoring: Automated identity governance and audit
  4. Customer Support: AI-powered identity troubleshooting and resolution
  5. Security Response: Automated incident response and threat mitigation

Implementation Strategies

ScenarioAI Agent TypeComplexityBenefits
HR AssistantConversational AI (Claude, GPT)LowNatural language HR operations
DevOps AutomationWorkflow AI (custom agents)MediumAutomated provisioning at scale
Compliance MonitorAnalytics AI (specialized)MediumContinuous governance monitoring
Security ResponseResponse AI (real-time)HighInstant threat mitigation
Customer SupportSupport AI (chat-based)Low24/7 identity issue resolution

Comparison with Traditional Integration Approaches

ApproachAI AccessibilityDiscoveryValidationAutomation
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

  1. ScimServer Struct: The main server instance with pluggable providers
  2. ScimServerBuilder: Fluent configuration API for server setup
  3. Resource Registration: Runtime registration of resource types and operations
  4. Schema Management: Automatic schema validation and discovery
  5. Operation Router: Dynamic dispatch to appropriate handlers
  6. 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:

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

  1. HTTP Server Implementation: Building REST APIs that expose SCIM endpoints
  2. Application Integration: Embedding SCIM capabilities into existing applications
  3. Identity Bridges: Creating adapters between different identity systems
  4. Testing Frameworks: Building test harnesses for SCIM compliance
  5. Custom Protocols: Implementing SCIM over non-HTTP transports
  6. MCP Integration: Exposing SCIM operations to AI agents

Implementation Strategies

ScenarioApproachComplexity
Simple REST APIUse with HTTP frameworkLow
Multi-tenant SaaSBuilder with tenant strategyMedium
Custom ResourcesRuntime registrationMedium
Protocol BridgeCustom resource providerHigh
Embedded IdentityDirect server integrationMedium

Comparison with Alternative Approaches

ApproachFlexibilityCompliancePerformanceComplexity
SCIM Serverβœ… Very Highβœ… Completeβœ… HighMedium
Hard-coded Resources❌ Low⚠️ Partialβœ… Very HighLow
Generic REST Frameworkβœ… High❌ Manualβœ… HighHigh
Identity Provider SDK⚠️ Mediumβœ… High⚠️ MediumLow

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:

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

  1. Custom SCIM Servers: Building domain-specific identity management
  2. Data Transformation: Converting between identity formats
  3. Validation Services: Ensuring SCIM data integrity
  4. 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

ApproachType SafetyFlexibilityPerformanceComplexity
Hybrid Designβœ… High (core)βœ… High (extensions)βœ… HighMedium
Full Value Objectsβœ… Very High❌ Lowβœ… Very HighHigh
Pure JSON❌ Noneβœ… Very High⚠️ MediumLow
Schema-Only⚠️ Runtimeβœ… High⚠️ MediumMedium

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

  1. ResourceProvider Trait: Unified interface for all SCIM operations
  2. StandardResourceProvider: Production-ready implementation
  3. Helper Traits: Composable functionality for custom providers
  4. 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

  1. Complex Business Rules: Domain-specific validation beyond SCIM
  2. External System Integration: Real-time sync with HR systems, directories
  3. Compliance Requirements: Audit logging, data residency, encryption
  4. Performance Optimization: Caching, batching, specialized queries
  5. Legacy System Integration: Adapting existing identity stores

Implementation Strategies

RequirementApproachComplexity
Simple ExtensionsDelegate to StandardLow
Custom ValidationOverride Specific MethodsMedium
External IntegrationMiddleware PatternMedium
Full Custom LogicImplement from TraitHigh

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

ScaleRecommended StorageReasoning
< 1K resourcesInMemoryStorageMaximum performance, simple setup
1K - 100K resourcesSqliteStorageBalanced performance, persistence
100K+ resourcesCustom (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:

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 names
  • emails: Multi-valued array of email addresses
  • active: Boolean indicating account status
  • groups: 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 schemas
  • GET /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:

  1. HTTP Method Validation: Operation-specific constraints
  2. Syntax Validation: JSON structure and basic type checking
  3. Schema Validation: Compliance with schema definitions
  4. 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

TypeDescriptionValidation Rules
stringText dataLength limits, case sensitivity, uniqueness
booleanTrue/false valuesMust be valid JSON boolean
decimalNumeric dataPrecision and scale constraints
integerWhole numbersRange validation
dateTimeISO 8601 timestampsFormat and timezone validation
binaryBase64-encoded dataEncoding validation
referenceResource referencesReferential integrity checks
complexNested objectsSub-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:

  1. Getting Started: Begin with the First SCIM Server tutorial
  2. Implementation: Explore How-To Guides for specific scenarios
  3. Advanced Usage: Review Advanced Topics for complex deployments
  4. 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

  1. TenantContext: Complete tenant identity and permissions
  2. RequestContext: Tenant-aware request handling with automatic scoping
  3. TenantResolver: Authentication credential to tenant mapping
  4. TenantStrategy: Flexible URL generation patterns
  5. Multi-Tenant Provider: Storage-level tenant isolation helpers
  6. 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

  1. Software-as-a-Service (SaaS): Multiple customers sharing infrastructure
  2. Enterprise Divisions: Large organizations with multiple business units
  3. Development Environments: Separate dev/staging/production environments
  4. Geographic Regions: Compliance-driven data residency requirements
  5. White-Label Solutions: Customer-specific branding and configuration

Implementation Strategies

ScenarioStrategyComplexityIsolation
SaaS Multi-CustomerSubdomainMediumHigh
Enterprise DivisionsPath-BasedLowMedium
Environment SeparationPath-BasedLowHigh
Geographic RegionsSeparate DeploymentsHighVery High
White-LabelSingle Tenant per DomainMediumVery High

Comparison with Alternative Approaches

ApproachIsolationScalabilityComplexityCompliance
Multi-Tenant Architectureβœ… Completeβœ… HighMediumβœ… Excellent
Separate Deploymentsβœ… Perfect⚠️ LimitedHighβœ… Excellent
Database Schemas⚠️ Goodβœ… HighLow⚠️ Good
Application Logic Only❌ Poorβœ… HighLow❌ 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:

  1. Start Single-Tenant: Begin with TenantStrategy::SingleTenant
  2. Add Default Tenant: Migrate existing data to "default" tenant context
  3. Enable Multi-Tenancy: Switch to TenantStrategy::PathBased or TenantStrategy::Subdomain
  4. 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 to Subdomain 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's members 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:

  1. SCIM Clients (IdPs) maintain authoritative control over referential integrity
  2. The server validates protocol compliance, not business logic consistency
  3. Cross-resource relationship enforcement is the client's responsibility
  4. 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

ApproachProtocol ComplianceOperational ComplexityClient FlexibilityScalability
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 use lastName or last_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 prefer userName 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:

CategoryTypical VariationsImplementation ImpactMitigation Strategy
Attribute SchemaCustom attributes, naming inconsistencies, nesting differencesRequires mapping logic, interoperability riskSchema transformation layers, attribute dictionaries
User IdentificationexternalId vs other identifiers, duplication handlingIdentity conflicts, HTTP 409 errorsFlexible identifier resolution, conflict detection
Group ManagementMembership updates, deletion prerequisites, role handlingGroup fragmentation, manual cleanup requiredProvider-specific group handlers, state validation
Lifecycle StatusDeactivation methods, reactivation support, status semanticsSecurity gaps, access control inconsistenciesUnified status mapping, audit trail normalization
Request ProcessingBulk limits, rate limiting, timeout behaviorPerformance bottlenecks, missed operationsAdaptive batching, provider-aware retry logic
Onboarding LogicAttribute mapping, authentication, configurationTime-consuming setup, error-prone integrationConfiguration 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:

  1. Gradual Adoption: Existing custom logic can be gradually replaced with built-in provider profiles
  2. Override Capability: Built-in profiles can be customized or overridden for specific use cases
  3. Fallback Support: Custom implementations remain fully supported alongside built-in profiles
  4. 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());
}
}

Next Steps

Now that you understand how requests flow through the system:

  1. Implement your HTTP integration layer using the patterns shown above
  2. Set up tenant resolution if building a multi-tenant system
  3. Add proper error handling and observability for production use
  4. 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

Next Steps

Now that you understand multi-tenant architecture patterns:

  1. Choose your tenant strategy (subdomain, path-based, or header-based)
  2. Implement tenant resolution for your authentication system
  3. Configure storage isolation based on your security requirements
  4. Set up monitoring and health checks for production deployment
  5. 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

  1. Start with StandardResourceProvider for rapid development and standard compliance
  2. Move to custom ResourceProvider when you need specialized business logic
  3. Use hybrid approaches for different resource types with different requirements
  4. Consider caching layers for performance-critical applications
  5. Implement comprehensive error handling with proper error mapping

Performance Considerations

  1. Use connection pooling for database providers
  2. Implement caching for frequently accessed resources
  3. Optimize database queries with proper indexing and query patterns
  4. Consider async operations for external API integrations
  5. Monitor resource provider performance with metrics and tracing

Security and Multi-Tenancy

  1. Always validate tenant boundaries in custom providers
  2. Apply tenant scoping at the storage level
  3. Implement proper permission checking before resource operations
  4. Use secure credential handling for external API integrations
  5. Audit all resource operations for compliance requirements

Next Steps

Now that you understand Resource Provider architecture:

  1. Evaluate your requirements using the decision matrix
  2. Choose your provider strategy (Standard, Custom, or Hybrid)
  3. Implement your data integration layer following the patterns
  4. Add proper error handling and logging for production readiness
  5. 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

  1. Layer Security Appropriately

    • Authentication verifies identity
    • Authorization controls access
    • Audit logging tracks all operations
  2. 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
  3. Implement Defense in Depth

    • Multiple authentication factors
    • Authorization at multiple layers
    • Rate limiting and DDoS protection
    • Input validation and sanitization
  4. Use Compile-Time Safety When Possible

    • Type-safe permission systems
    • Phantom types for state tracking
    • Zero-cost abstractions
  5. Monitor and Audit

    • Log all authentication attempts
    • Track authorization decisions
    • Monitor for unusual patterns
    • Implement alerting for security events

Next Steps

Now that you understand authentication and authorization strategies:

  1. Choose your authentication strategy based on your integration requirements
  2. Implement appropriate authorization (permissions, RBAC, or ABAC)
  3. Set up proper middleware integration for your web framework
  4. Add comprehensive audit logging for security monitoring
  5. 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

  1. Start with SCIM Core Schemas

    • Extend rather than replace standard schemas
    • Maintain SCIM compliance for interoperability
    • Use standard attribute names where possible
  2. Design Extensible Schemas

    • Use complex attributes for structured data
    • Plan for multi-valued attributes early
    • Consider tenant-specific extensions
  3. Implement Efficient Validation

    • Compile schemas for performance
    • Cache validation results appropriately
    • Use type-safe value objects where beneficial
  4. Handle Schema Evolution

    • Version your custom schemas
    • Plan migration strategies
    • Support backward compatibility
  5. Monitor Schema Performance

    • Track validation times
    • Monitor cache hit rates
    • Profile schema compilation costs

Next Steps

Now that you understand schema system architecture:

  1. Design your schema extensions based on your domain requirements
  2. Implement custom value objects for complex business data
  3. Set up tenant-specific extensions for multi-tenant systems
  4. Optimize schema validation performance for high-throughput scenarios
  5. 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

  1. 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
  2. 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
  3. 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

  1. Connection Management

    • Use connection pooling for database storage
    • Configure appropriate pool sizes and timeouts
    • Monitor connection usage and adjust as needed
  2. 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
  3. Query Optimization

    • Index frequently queried fields (tenant_id, resource_type)
    • Use batch operations where possible
    • Implement pagination for large result sets
  4. 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

Next Steps

Now that you understand storage and persistence patterns:

  1. Choose your storage backend based on scalability and consistency requirements
  2. Implement appropriate caching for your read/write patterns
  3. Set up monitoring and alerting for production operations
  4. Consider advanced patterns like event sourcing for audit requirements
  5. 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:

πŸ”§ Advanced Features

Explore sophisticated server capabilities:

πŸ€– MCP Integration (AI Agents)

See how AI agents can interact with your SCIM server:

πŸ” Security & Authentication

Implement robust security patterns:

πŸ› οΈ Infrastructure & Operations

Production-ready operational patterns:

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

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:

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:

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:

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:

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:

  1. Enterprise Customer A - Full-featured tenant with high limits
  2. Startup Customer B - Basic tenant with restricted permissions
  3. 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:

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:

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:

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:

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:

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:

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:

  1. Request Structuring - Converting transport-specific requests to standard format
  2. Validation - Parameter checking and constraint enforcement
  3. Context Extraction - Tenant and authentication information processing
  4. Operation Dispatch - Routing to appropriate business logic handlers
  5. 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:

Source Code

View the complete implementation: examples/operation_handler_example.rs

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:

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:

Source Code

View the complete implementation: examples/builder_example.rs

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:

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:

Source Code

View the complete implementation: examples/mcp_server_example.rs

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:

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:

Source Code

View the complete implementation: examples/mcp_etag_example.rs

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:

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:

  1. Tool Discovery - AI agent requests available tools
  2. Schema Understanding - Agent learns about user attributes and validation
  3. Operation Execution - Agent performs identity management tasks
  4. 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:

Source Code

View the complete implementation: examples/simple_mcp_demo.rs

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:

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:

Source Code

View the complete implementation: examples/logging_example.rs

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.