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

SCIM Server Guide

Welcome to the comprehensive guide for the SCIM Server library! This guide will take you from initial setup to advanced usage patterns, helping you build robust identity provisioning systems with Rust.

What is SCIM Server?

SCIM Server is a comprehensive Rust library that implements the SCIM 2.0 (System for Cross-domain Identity Management) protocol. It provides a type-safe, high-performance foundation for building identity provisioning and management systems.

Why SCIM?

SCIM is the industry standard for automating user provisioning between identity providers and applications. Instead of building custom APIs for user management, SCIM provides:

  • Standardized Operations: Consistent CRUD operations across all systems
  • Rich Filtering: Powerful query capabilities for finding users and groups
  • Bulk Operations: Efficient handling of large-scale provisioning tasks
  • Schema Validation: Automatic validation against well-defined schemas
  • Interoperability: Works with existing identity providers and applications

Why This Library?

The SCIM Server library takes SCIM implementation to the next level with:

  • πŸ›‘οΈ Type Safety: Leverage Rust's type system to prevent runtime errors
  • 🏒 Multi-Tenancy: Built-in support for multiple organizations
  • ⚑ Performance: Async-first design with minimal overhead
  • πŸ”Œ Flexibility: Framework-agnostic with pluggable storage
  • πŸ€– AI Integration: Built-in Model Context Protocol support
  • πŸ”„ Concurrency Control: ETag-based optimistic locking

Architecture Overview

The SCIM Server follows a clean three-layer architecture:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   HTTP Layer    β”‚    β”‚   SCIM Server    β”‚    β”‚   Storage       β”‚
β”‚                 β”‚    β”‚                  β”‚    β”‚                 β”‚
β”‚  β€’ Axum         │───▢│  β€’ Validation    │───▢│  β€’ In-Memory    β”‚
β”‚  β€’ Warp         β”‚    β”‚  β€’ Operations    β”‚    β”‚  β€’ Database     β”‚
β”‚  β€’ Actix        β”‚    β”‚  β€’ Multi-tenant  β”‚    β”‚  β€’ Custom       β”‚
β”‚  β€’ Custom       β”‚    β”‚  β€’ Type Safety   β”‚    β”‚                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

HTTP Layer: Your choice of web framework handles HTTP requests and responses.

SCIM Server: The core library handles SCIM protocol logic, validation, multi-tenancy, and type safety.

Storage: Pluggable storage providers handle data persistence, from simple in-memory stores to enterprise databases.

Value Proposition

Instead of building provisioning logic into every application, the SCIM Server centralizes complexity:

Without SCIM ServerWith SCIM Server
❌ Custom validation in each appβœ… Centralized validation engine
❌ Manual concurrency controlβœ… Automatic ETag versioning
❌ Manual schema managementβœ… Dynamic schema registry
❌ Ad-hoc API endpointsβœ… Standardized SCIM protocol
❌ Build multi-tenancy from scratchβœ… Built-in tenant isolation

Result: Your applications focus on business logic while SCIM Server handles all provisioning complexity.

Who Should Use This Guide?

This guide is designed for:

  • Rust Developers building identity-aware applications
  • System Architects designing multi-tenant SaaS platforms
  • DevOps Engineers automating user provisioning workflows
  • AI Engineers integrating identity management with AI tools
  • Security Engineers implementing enterprise identity solutions

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 SCIM Protocol Overview to understand the standard.

Ready to code? Jump to Your First SCIM Server for hands-on experience.

Building production systems? Read through Core Concepts and Advanced Topics.

Solving specific problems? Use the How-To Guides section.

What You'll Learn

By the end of this guide, you'll be able to:

  • Set up and configure SCIM Server for your use case
  • Implement multi-tenant identity provisioning systems
  • Handle complex scenarios like custom resources and authentication
  • Integrate with web frameworks and AI tools
  • Deploy production-ready SCIM services
  • Troubleshoot common issues and optimize performance

Getting Help

Let's get started! πŸš€

Installation

This guide covers installing and setting up the SCIM Server library in your Rust project.

Prerequisites

Before you begin, ensure you have:

  • Rust 1.75 or later - Install Rust
  • Cargo - Comes with Rust installation
  • Basic familiarity with Rust and async programming

You can verify your Rust installation:

rustc --version
cargo --version

Adding SCIM Server to Your Project

Add SCIM Server to your Cargo.toml dependencies:

[dependencies]
scim-server = "0.3.7"
tokio = { version = "1.0", features = ["full"] }
serde_json = "1.0"

⚠️ Version Pinning: Use flexible versioning (0.3.7) to get patch fixes automatically. For exact version control, use =0.3.7. See Version Strategy for details.

Option 2: Using Cargo Add Command

cargo add scim-server@0.3.7
cargo add tokio --features full
cargo add serde_json

Feature Flags

SCIM Server provides several optional features to reduce compile time and binary size:

[dependencies]
scim-server = { version = "0.3.7", features = ["mcp"] }

Available features:

FeatureDescriptionDefault
mcpModel Context Protocol for AI integration (includes async-trait and rust-mcp-sdk)❌
async-traitAsync trait support (included with mcp)❌
rust-mcp-sdkMCP SDK dependency (included with mcp)❌

Note: Only the mcp feature is currently available. This enables AI integration capabilities through the Model Context Protocol.

Development Dependencies

For development and testing, you may want additional dependencies:

[dev-dependencies]
tokio-test = "0.4"
serde_json = "1.0"
uuid = { version = "1.0", features = ["v4"] }

Verification

Create a simple test to verify your installation:

#![allow(unused)]
fn main() {
use scim_server::{
    StandardResourceProvider,
    InMemoryStorage,
    RequestContext,
    ResourceProvider,  // Required trait for create_resource method
};
use serde_json::json;

#[tokio::test]
async fn test_installation() {
    let storage = InMemoryStorage::new();
    let provider = StandardResourceProvider::new(storage);
    let context = RequestContext::new("test".to_string());
    
    // Test creating a simple resource
    let user_data = json!({
        "userName": "test.user",
        "emails": [{"value": "test@example.com", "primary": true}]
    });
    
    let user = provider.create_resource("User", user_data, &context).await.unwrap();
    
    // If this compiles and runs, installation is successful!
    println!("SCIM Server installed successfully!");
    println!("Created test user: {}", user.get_username().unwrap_or("unknown"));
}
}

Run the test:

cargo test test_installation

Next Steps

Now that you have SCIM Server installed, you're ready to:

  1. Create Your First Server - Build a basic SCIM server
  2. Learn Basic Operations - Understand CRUD operations

Your First SCIM Server

This tutorial walks you through creating your first SCIM server from scratch. By the end, you'll have a working SCIM server that can manage users and groups with full CRUD operations.

What We'll Build

We'll create a simple SCIM server that:

  • Manages users and groups
  • Supports basic CRUD operations
  • Uses in-memory storage for simplicity
  • Includes proper error handling
  • Demonstrates multi-tenant capabilities

Step 1: Project Setup

First, create a new Rust project:

cargo new my-scim-server
cd my-scim-server

Add the required dependencies to your Cargo.toml:

[dependencies]
scim-server = "0.3.7"
tokio = { version = "1.0", features = ["full"] }
serde_json = "1.0"
anyhow = "1.0"

Step 2: Basic Server Setup

Create your first SCIM server in src/main.rs:

use scim_server::{
    ScimServer,
    StandardResourceProvider,
    InMemoryStorage,
    RequestContext, ResourceProvider,
    resource_handlers::{create_user_resource_handler, create_group_resource_handler},
    SchemaRegistry,
    ScimOperation,
};
use serde_json::json;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("πŸš€ Starting SCIM Server...");
    
    // Create storage backend
    let storage = InMemoryStorage::new();
    
    // Create resource provider with storage
    let provider = StandardResourceProvider::new(storage);
    
    // Create SCIM server with provider
    let mut server = ScimServer::new(provider)?;
    
    // Create schema registry (needed for both user and group schemas)
    let schema_registry = SchemaRegistry::new()?;
    
    // Register User resource type
    let user_schema = schema_registry.get_user_schema();
    let user_handler = create_user_resource_handler(user_schema.clone());
    server.register_resource_type(
        "User",
        user_handler,
        vec![
            ScimOperation::Create,
            ScimOperation::Read,
            ScimOperation::Update,
            ScimOperation::Delete,
            ScimOperation::List,
            ScimOperation::Search,
        ],
    )?;
    
    // Register Group resource type
    let group_schema = schema_registry.get_group_schema();
    let group_handler = create_group_resource_handler(group_schema.clone());
    server.register_resource_type(
        "Group",
        group_handler,
        vec![
            ScimOperation::Create,
            ScimOperation::Read,
            ScimOperation::Update,
            ScimOperation::Delete,
            ScimOperation::List,
            ScimOperation::Search,
        ],
    )?;
    
    println!("βœ… SCIM Server initialized successfully!");
    
    // We'll add operations here in the next steps
    
    Ok(())
}

Run this to verify everything works:

cargo run

You should see:

πŸš€ Starting SCIM Server...
βœ… SCIM Server initialized successfully!

Step 3: Creating Your First User

Now let's create a user. Add this after the server initialization:

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // ... previous setup code ...
    
    println!("βœ… SCIM Server initialized successfully!");
    
    // Create a request context for our operations
    let context = RequestContext::with_generated_id();
    
    println!("\nπŸ“ Creating a user...");
    
    // Define user data
    let user_data = json!({
        "userName": "alice@example.com",
        "name": {
            "formatted": "Alice Smith",
            "familyName": "Smith",
            "givenName": "Alice"
        },
        "displayName": "Alice Smith",
        "emails": [
            {
                "value": "alice@example.com",
                "type": "work",
                "primary": true
            }
        ],
        "phoneNumbers": [
            {
                "value": "+1-555-123-4567",
                "type": "work"
            }
        ],
        "active": true
    });
    
    // Create the user
    let user = server.create_resource("User", user_data, &context).await?;
    
    println!("βœ… Created user: {} (ID: {})", 
             user.get_username().unwrap_or("unknown"),
             user.get_id().unwrap_or("unknown"));
    
    Ok(())
}

Run this:

cargo run

You should see:

πŸš€ Starting SCIM Server...
βœ… SCIM Server initialized successfully!

πŸ“ Creating a user...
βœ… Created user: alice@example.com (ID: user_abc123)

Step 4: Reading and Updating Users

Let's add operations to read and update users:

#![allow(unused)]
fn main() {
// ... after creating the user ...

println!("\nπŸ“– Reading the user...");

// Get the user by ID
let user_id = user.get_id().unwrap();
let retrieved_user = server.get_resource("User", user_id, &context).await?;

match retrieved_user {
    Some(user) => {
        println!("βœ… Retrieved user: {} ({})", 
                 user.get_username().unwrap_or("unknown"),
                 user.get_name().map(|n| n.formatted.unwrap_or_default()).unwrap_or("unknown".to_string()));
    }
    None => {
        println!("❌ User not found");
    }
}

println!("\nπŸ“ Updating the user...");

// Update user data
let updated_data = json!({
    "id": user_id,
    "userName": "alice@example.com",
    "name": {
        "formatted": "Alice Johnson",
        "familyName": "Johnson",  // Changed last name
        "givenName": "Alice"
    },
    "displayName": "Alice Johnson",
    "emails": [
        {
            "value": "alice@example.com",
            "type": "work",
            "primary": true
        },
        {
            "value": "alice.johnson@personal.com",  // Added personal email
            "type": "home",
            "primary": false
        }
    ],
    "active": true
});

// Update the user
let updated_user = server.update_resource("User", user_id, updated_data, &context).await?;

println!("βœ… Updated user: {} ({})", 
         updated_user.get_username().unwrap_or("unknown"),
         updated_user.get_name().map(|n| n.formatted.unwrap_or_default()).unwrap_or("unknown".to_string()));
}

Step 5: Working with Groups

Let's create a group and manage membership:

#![allow(unused)]
fn main() {
// ... after updating the user ...

println!("\nπŸ‘₯ Creating a group...");

let group_data = json!({
    "displayName": "Engineering Team",
    "members": [
        {
            "value": user_id,
            "display": "Alice Johnson",
            "type": "User"
        }
    ]
});

// Create the group
let group = server.create_resource("Group", group_data, &context).await?;

println!("βœ… Created group: {} (ID: {})", 
         group.get_name().map(|n| n.formatted.unwrap_or_default()).unwrap_or("unknown".to_string()),
         group.get_id().unwrap_or("unknown"));
}

Step 6: Listing Resources

Add functionality to list users and groups:

#![allow(unused)]
fn main() {
// ... after creating the group ...

println!("\nπŸ“‹ Listing all users...");

// List all users
let users = server.list_resources("User", None, &context).await?;
println!("βœ… Found {} users:", users.len());
for user in &users {
    println!("  - {} ({})", 
             user.get_username().unwrap_or("unknown"),
             user.get_name().map(|n| n.formatted.unwrap_or_default()).unwrap_or("unknown".to_string()));
}

println!("\nπŸ“‹ Listing all groups...");

// List all groups
let groups = server.list_resources("Group", None, &context).await?;
println!("βœ… Found {} groups:", groups.len());
for group in &groups {
    println!("  - {} (ID: {})", 
             group.get_name().map(|n| n.formatted.unwrap_or_default()).unwrap_or("unknown".to_string()),
             group.get_id().unwrap_or("unknown"));
}
}

Step 7: Search Operations

Add search functionality:

#![allow(unused)]
fn main() {
// ... after listing resources ...

println!("\nπŸ” Searching for users...");

// Search for user by username
let found_user = server.find_resource_by_attribute(
    "User",
    "userName",
    &json!("alice@example.com"),
    &context,
).await?;

match found_user {
    Some(user) => {
        println!("βœ… Found user by username: {} ({})", 
                 user.get_username().unwrap_or("unknown"),
                 user.get_name().map(|n| n.formatted.unwrap_or_default()).unwrap_or("unknown".to_string()));
    }
    None => {
        println!("❌ User not found");
    }
}
}

Step 8: Multi-Tenant Operations

Finally, let's demonstrate multi-tenant capabilities:

#![allow(unused)]
fn main() {
use scim_server::resource::TenantContext;

// ... after search operations ...

println!("\n🏒 Multi-tenant operations...");

// Create tenant-specific context
let tenant_context = TenantContext::new(
    "company-123".to_string(),
    "app-456".to_string(),
);
let tenant_request_context = RequestContext::with_tenant_generated_id(tenant_context);

// Create user in specific tenant
let tenant_user_data = json!({
    "userName": "bob@company123.com",
    "name": {
        "formatted": "Bob Wilson",
        "familyName": "Wilson",
        "givenName": "Bob"
    },
    "displayName": "Bob Wilson",
    "active": true
});

let tenant_user = server.create_resource("User", tenant_user_data, &tenant_request_context).await?;

println!("βœ… Created user in tenant: {} (ID: {})", 
         tenant_user.get_username().unwrap_or("unknown"),
         tenant_user.get_id().unwrap_or("unknown"));

// Show tenant isolation
let default_users = server.list_resources("User", None, &context).await?;
let tenant_users = server.list_resources("User", None, &tenant_request_context).await?;

println!("πŸ“Š Default tenant has {} users", default_users.len());
println!("πŸ“Š Company-123 tenant has {} users", tenant_users.len());
println!("βœ… Tenant isolation working correctly!");
}

Complete Example

Here's the complete working example:

use scim_server::{
    ScimServer,
    StandardResourceProvider,
    InMemoryStorage,
    RequestContext, TenantContext, ResourceProvider,
    resource_handlers::{create_user_resource_handler, create_group_resource_handler},
    SchemaRegistry,
    ScimOperation,
};
use serde_json::json;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("πŸš€ Starting SCIM Server...");
    
    // Create storage backend
    let storage = InMemoryStorage::new();
    
    // Create resource provider with storage
    let provider = StandardResourceProvider::new(storage);
    
    // Create SCIM server with provider
    let mut server = ScimServer::new(provider)?;
    
    // Create schema registry (needed for both user and group schemas)
    let schema_registry = SchemaRegistry::new()?;
    
    // Register User resource type
    let user_schema = schema_registry.get_user_schema();
    let user_handler = create_user_resource_handler(user_schema.clone());
    server.register_resource_type(
        "User",
        user_handler,
        vec![
            ScimOperation::Create,
            ScimOperation::Read,
            ScimOperation::Update,
            ScimOperation::Delete,
            ScimOperation::List,
            ScimOperation::Search,
        ],
    )?;
    
    // Register Group resource type
    let group_schema = schema_registry.get_group_schema();
    let group_handler = create_group_resource_handler(group_schema.clone());
    server.register_resource_type(
        "Group",
        group_handler,
        vec![
            ScimOperation::Create,
            ScimOperation::Read,
            ScimOperation::Update,
            ScimOperation::Delete,
            ScimOperation::List,
            ScimOperation::Search,
        ],
    )?;
    
    println!("βœ… SCIM Server initialized successfully!");
    
    // Create request context
    let context = RequestContext::with_generated_id();
    
    // Create a user
    let user_data = json!({
        "userName": "alice@example.com",
        "name": {
            "formatted": "Alice Smith",
            "familyName": "Smith",
            "givenName": "Alice"
        },
        "displayName": "Alice Smith",
        "emails": [
            {
                "value": "alice@example.com",
                "type": "work",
                "primary": true
            }
        ],
        "active": true
    });
    
    let user = server.create_resource("User", user_data, &context).await?;
    let user_id = user.get_id().unwrap();
    
    println!("βœ… Created user: {} (ID: {})", 
             user.get_username().unwrap_or("unknown"), user_id);
    
    // Create a group
    let group_data = json!({
        "displayName": "Engineering Team",
        "members": [
            {
                "value": user_id,
                "display": "Alice Smith",
                "type": "User"
            }
        ]
    });
    
    let group = server.create_resource("Group", group_data, &context).await?;
    
    println!("βœ… Created group: {} (ID: {})", 
             group.get_name().map(|n| n.formatted.unwrap_or_default()).unwrap_or("unknown".to_string()),
             group.get_id().unwrap_or("unknown"));
    
    // List resources
    let users = server.list_resources("User", None, &context).await?;
    let groups = server.list_resources("Group", None, &context).await?;
    
    println!("πŸ“Š Total: {} users, {} groups", users.len(), groups.len());
    
    println!("πŸŽ‰ SCIM Server example completed successfully!");
    
    Ok(())
}