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 Server | With 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:
- Getting Started: Quick setup and basic usage
- Core Concepts: Understanding the fundamental ideas
- Tutorials: Step-by-step guides for common scenarios
- How-To Guides: Solutions for specific problems
- Advanced Topics: Deep dives into complex scenarios
- Reference: Technical specifications and details
Learning Path
New to SCIM? Start with 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
- Examples: Check the examples directory for working code
- API Documentation: See docs.rs for detailed API reference
- Issues: Report bugs or ask questions on GitHub Issues
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
Option 1: Using Cargo (Recommended)
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:
| Feature | Description | Default |
|---|---|---|
mcp | Model Context Protocol for AI integration (includes async-trait and rust-mcp-sdk) | β |
async-trait | Async trait support (included with mcp) | β |
rust-mcp-sdk | MCP 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:
- Create Your First Server - Build a basic SCIM server
- 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(()) }