Advanced Validation
This guide covers complex validation scenarios including external system integration, conditional validation, and sophisticated business logic that requires asynchronous operations or external dependencies.
External System Integration
HR System Validation
Validate users against external HR systems to ensure data consistency:
#![allow(unused)] fn main() { use scim_server::validation::{CustomValidator, ValidationContext, ValidationError}; use reqwest::Client; use serde_json::json; use std::time::Duration; use async_trait::async_trait; pub struct ExternalValidationService { http_client: Client, hr_system_url: String, compliance_service_url: String, api_key: String, } impl ExternalValidationService { pub fn new(hr_system_url: String, compliance_service_url: String, api_key: String) -> Self { Self { http_client: Client::new(), hr_system_url, compliance_service_url, api_key, } } } #[async_trait] impl CustomValidator for ExternalValidationService { async fn validate_user( &self, user: &User, context: &ValidationContext, ) -> Result<(), ValidationError> { // Validate against HR system self.validate_against_hr_system(user).await?; // Validate compliance requirements self.validate_compliance_requirements(user, context).await?; // Validate security clearance if present if let Some(security_clearance) = self.extract_security_clearance(user) { self.validate_security_clearance(&security_clearance, user).await?; } Ok(()) } async fn validate_group( &self, group: &Group, context: &ValidationContext, ) -> Result<(), ValidationError> { // Validate group against organizational structure self.validate_group_structure(group, context).await?; Ok(()) } } impl ExternalValidationService { async fn validate_against_hr_system(&self, user: &User) -> Result<(), ValidationError> { if let Some(employee_number) = self.extract_employee_number(user) { let response = self.http_client .get(&format!("{}/employees/{}", self.hr_system_url, employee_number)) .header("Authorization", format!("Bearer {}", self.api_key)) .timeout(Duration::from_secs(5)) .send() .await .map_err(|e| ValidationError::new( "HR_SYSTEM_ERROR", &format!("Failed to validate employee: {}", e), ))?; if response.status() == 404 { return Err(ValidationError::new( "EMPLOYEE_NOT_FOUND", "Employee not found in HR system", ).with_field("enterpriseUser:employeeNumber")); } if !response.status().is_success() { return Err(ValidationError::new( "HR_SYSTEM_ERROR", &format!("HR system returned status: {}", response.status()), )); } let hr_employee: HrEmployee = response.json().await .map_err(|e| ValidationError::new( "HR_SYSTEM_ERROR", &format!("Failed to parse HR response: {}", e), ))?; // Validate employee status if hr_employee.status != "ACTIVE" { return Err(ValidationError::new( "EMPLOYEE_INACTIVE", &format!("Employee status in HR system is: {}", hr_employee.status), ).with_field("active")); } // Validate department consistency if let Some(department) = self.extract_department(user) { if hr_employee.department != department { return Err(ValidationError::new( "DEPARTMENT_MISMATCH", "Department does not match HR system", ).with_field("enterpriseUser:department")); } } // Validate manager hierarchy if let Some(manager_id) = self.extract_manager_id(user) { if let Some(hr_manager_id) = hr_employee.manager_id { if manager_id != hr_manager_id { return Err(ValidationError::new( "MANAGER_MISMATCH", "Manager does not match HR system", ).with_field("enterpriseUser:manager")); } } } } Ok(()) } async fn validate_compliance_requirements( &self, user: &User, context: &ValidationContext, ) -> Result<(), ValidationError> { let compliance_request = json!({ "user_data": { "name": user.name, "emails": user.emails, "phone_numbers": user.phone_numbers, "addresses": user.addresses, "country": self.extract_country(user), }, "tenant_id": context.tenant_id, "operation": context.operation, }); let response = self.http_client .post(&format!("{}/validate", self.compliance_service_url)) .header("Authorization", format!("Bearer {}", self.api_key)) .json(&compliance_request) .timeout(Duration::from_secs(10)) .send() .await .map_err(|e| ValidationError::new( "COMPLIANCE_SYSTEM_ERROR", &format!("Failed to validate compliance: {}", e), ))?; if !response.status().is_success() { return Err(ValidationError::new( "COMPLIANCE_SYSTEM_ERROR", &format!("Compliance service returned status: {}", response.status()), )); } let compliance_result: ComplianceValidationResult = response.json().await .map_err(|e| ValidationError::new( "COMPLIANCE_SYSTEM_ERROR", &format!("Failed to parse compliance response: {}", e), ))?; if !compliance_result.is_compliant { let violations = compliance_result.violations.join(", "); return Err(ValidationError::new( "COMPLIANCE_VIOLATION", &format!("Compliance violations: {}", violations), )); } Ok(()) } async fn validate_security_clearance( &self, security_clearance: &str, user: &User, ) -> Result<(), ValidationError> { // Validate security clearance levels let valid_clearances = ["PUBLIC", "CONFIDENTIAL", "SECRET", "TOP_SECRET"]; if !valid_clearances.contains(&security_clearance) { return Err(ValidationError::new( "INVALID_SECURITY_CLEARANCE", &format!("Invalid security clearance level: {}", security_clearance), ).with_field("enterpriseUser:securityClearance")); } // Validate clearance requirements based on department if let Some(department) = self.extract_department(user) { match department.as_str() { "Defense" | "Intelligence" => { if security_clearance == "PUBLIC" { return Err(ValidationError::new( "INSUFFICIENT_CLEARANCE", "Department requires minimum CONFIDENTIAL clearance", ).with_field("enterpriseUser:securityClearance")); } } "Research" => { if !["CONFIDENTIAL", "SECRET", "TOP_SECRET"].contains(&security_clearance) { return Err(ValidationError::new( "INSUFFICIENT_CLEARANCE", "Research department requires minimum CONFIDENTIAL clearance", ).with_field("enterpriseUser:securityClearance")); } } _ => {} // No special requirements } } Ok(()) } async fn validate_group_structure( &self, group: &Group, context: &ValidationContext, ) -> Result<(), ValidationError> { // Validate against organizational chart let org_response = self.http_client .get(&format!("{}/organizational-chart/{}", self.hr_system_url, context.tenant_id)) .header("Authorization", format!("Bearer {}", self.api_key)) .send() .await .map_err(|e| ValidationError::new( "ORG_CHART_ERROR", &format!("Failed to fetch organizational chart: {}", e), ))?; let org_chart: OrganizationalChart = org_response.json().await .map_err(|e| ValidationError::new( "ORG_CHART_ERROR", &format!("Failed to parse org chart: {}", e), ))?; // Validate group exists in org chart if !org_chart.groups.iter().any(|g| g.name == group.display_name) { return Err(ValidationError::new( "GROUP_NOT_IN_ORG_CHART", "Group does not exist in organizational chart", ).with_field("displayName")); } Ok(()) } // Helper methods for extracting user attributes fn extract_employee_number(&self, user: &User) -> Option<String> { user.extension_attributes .as_ref() .and_then(|attrs| attrs.get("employeeNumber")) .and_then(|v| v.as_str()) .map(|s| s.to_string()) } fn extract_department(&self, user: &User) -> Option<String> { user.extension_attributes .as_ref() .and_then(|attrs| attrs.get("department")) .and_then(|v| v.as_str()) .map(|s| s.to_string()) } fn extract_manager_id(&self, user: &User) -> Option<String> { user.extension_attributes .as_ref() .and_then(|attrs| attrs.get("manager")) .and_then(|v| v.get("value")) .and_then(|v| v.as_str()) .map(|s| s.to_string()) } fn extract_security_clearance(&self, user: &User) -> Option<String> { user.extension_attributes .as_ref() .and_then(|attrs| attrs.get("securityClearance")) .and_then(|v| v.as_str()) .map(|s| s.to_string()) } fn extract_country(&self, user: &User) -> Option<String> { user.addresses .as_ref() .and_then(|addrs| addrs.first()) .map(|addr| addr.country.clone()) .unwrap_or_default() } } // Supporting types #[derive(serde::Deserialize)] struct HrEmployee { employee_id: String, status: String, department: String, manager_id: Option<String>, hire_date: String, termination_date: Option<String>, } #[derive(serde::Deserialize)] struct ComplianceValidationResult { is_compliant: bool, violations: Vec<String>, severity: String, } #[derive(serde::Deserialize)] struct OrganizationalChart { groups: Vec<OrgGroup>, } #[derive(serde::Deserialize)] struct OrgGroup { name: String, parent: Option<String>, level: u32, } }
Conditional Validation
Implement validation rules that apply only under specific conditions:
#![allow(unused)] fn main() { use scim_server::validation::{CustomValidator, ValidationContext, ValidationError}; use std::collections::HashMap; pub struct ConditionalValidator { rules: Vec<ConditionalRule>, } pub struct ConditionalRule { pub name: String, pub condition: fn(&User, &ValidationContext) -> bool, pub validator: fn(&User, &ValidationContext) -> Result<(), ValidationError>, } impl ConditionalValidator { pub fn new() -> Self { let mut rules = Vec::new(); // Rule: Contractors must have end date rules.push(ConditionalRule { name: "contractor_end_date".to_string(), condition: |user, _| { user.extension_attributes .as_ref() .and_then(|attrs| attrs.get("userType")) .and_then(|v| v.as_str()) == Some("Contractor") }, validator: |user, _| { let has_end_date = user.extension_attributes .as_ref() .and_then(|attrs| attrs.get("employmentEndDate")) .is_some(); if !has_end_date { return Err(ValidationError::new( "MISSING_END_DATE", "Contractors must have an employment end date", ).with_field("enterpriseUser:employmentEndDate")); } Ok(()) }, }); // Rule: VIP users require additional security rules.push(ConditionalRule { name: "vip_security_requirements".to_string(), condition: |user, _| { user.extension_attributes .as_ref() .and_then(|attrs| attrs.get("vipStatus")) .and_then(|v| v.as_bool()) .unwrap_or(false) }, validator: |user, _| { // Check for required security attributes let security_attrs = ["securityClearance", "backgroundCheckDate", "securityTraining"]; for attr in &security_attrs { if !user.extension_attributes .as_ref() .map(|attrs| attrs.contains_key(*attr)) .unwrap_or(false) { return Err(ValidationError::new( "MISSING_VIP_SECURITY_ATTR", &format!("VIP users must have {} attribute", attr), ).with_field(&format!("enterpriseUser:{}", attr))); } } Ok(()) }, }); // Rule: Remote workers require specific equipment rules.push(ConditionalRule { name: "remote_worker_equipment".to_string(), condition: |user, _| { user.extension_attributes .as_ref() .and_then(|attrs| attrs.get("workLocation")) .and_then(|v| v.as_str()) == Some("Remote") }, validator: |user, _| { let required_equipment = ["laptop", "vpnAccess", "phoneStipend"]; for equipment in &required_equipment { if !user.extension_attributes .as_ref() .and_then(|attrs| attrs.get("equipment")) .and_then(|v| v.as_array()) .map(|arr| arr.iter().any(|item| item.as_str().map(|s| s == *equipment).unwrap_or(false) )) .unwrap_or(false) { return Err(ValidationError::new( "MISSING_REMOTE_EQUIPMENT", &format!("Remote workers must have {} assigned", equipment), ).with_field("enterpriseUser:equipment")); } } Ok(()) }, }); Self { rules } } } #[async_trait] impl CustomValidator for ConditionalValidator { async fn validate_user( &self, user: &User, context: &ValidationContext, ) -> Result<(), ValidationError> { for rule in &self.rules { if (rule.condition)(user, context) { (rule.validator)(user, context)?; } } Ok(()) } async fn validate_group( &self, _group: &Group, _context: &ValidationContext, ) -> Result<(), ValidationError> { // Groups don't typically need conditional validation Ok(()) } } }
Async Workflow Integration
Integrate with approval workflows and external processes:
#![allow(unused)] fn main() { use scim_server::validation::{CustomValidator, ValidationContext, ValidationError}; use tokio::time::{sleep, Duration}; pub struct WorkflowValidator { workflow_client: WorkflowClient, approval_timeout: Duration, } impl WorkflowValidator { pub fn new(workflow_url: String, api_key: String) -> Self { Self { workflow_client: WorkflowClient::new(workflow_url, api_key), approval_timeout: Duration::from_secs(30), } } } #[async_trait] impl CustomValidator for WorkflowValidator { async fn validate_user( &self, user: &User, context: &ValidationContext, ) -> Result<(), ValidationError> { match context.operation { Operation::Create => { // Check if user creation requires approval if self.requires_approval(user, context).await? { self.validate_approval_exists(user, context).await?; } } Operation::Update => { // Check for sensitive attribute changes if self.has_sensitive_changes(user, context).await? { self.validate_change_approval(user, context).await?; } } _ => {} } Ok(()) } async fn validate_group( &self, group: &Group, context: &ValidationContext, ) -> Result<(), ValidationError> { // Group creation/modification might require approval for certain types if self.is_privileged_group(group) { self.validate_group_approval(group, context).await?; } Ok(()) } } impl WorkflowValidator { async fn requires_approval(&self, user: &User, context: &ValidationContext) -> Result<bool, ValidationError> { // External users always require approval if self.is_external_user(user) { return Ok(true); } // High-privilege roles require approval if let Some(roles) = &user.roles { let privileged_roles = ["Admin", "Security", "HR"]; if roles.iter().any(|role| privileged_roles.contains(&role.value.as_str())) { return Ok(true); } } // Users with high security clearance require approval if let Some(clearance) = user.extension_attributes .as_ref() .and_then(|attrs| attrs.get("securityClearance")) .and_then(|v| v.as_str()) { if ["SECRET", "TOP_SECRET"].contains(&clearance) { return Ok(true); } } Ok(false) } async fn validate_approval_exists( &self, user: &User, context: &ValidationContext, ) -> Result<(), ValidationError> { let approval_request = ApprovalRequest { request_type: "user_creation".to_string(), tenant_id: context.tenant_id.clone(), requester: context.authenticated_user.clone().unwrap_or_default(), subject: user.username.clone().unwrap_or_default(), details: serde_json::to_value(user).unwrap_or_default(), }; let approval_status = self.workflow_client .check_approval_status(&approval_request) .await .map_err(|e| ValidationError::new( "WORKFLOW_ERROR", &format!("Failed to check approval status: {}", e), ))?; match approval_status.status.as_str() { "approved" => Ok(()), "pending" => { // Wait for approval with timeout self.wait_for_approval(&approval_request).await } "rejected" => { Err(ValidationError::new( "APPROVAL_REJECTED", &format!("User creation was rejected: {}", approval_status.reason.unwrap_or_default()), )) } _ => { // Create new approval request self.workflow_client .create_approval_request(&approval_request) .await .map_err(|e| ValidationError::new( "WORKFLOW_ERROR", &format!("Failed to create approval request: {}", e), ))?; Err(ValidationError::new( "APPROVAL_PENDING", "User creation requires approval. Request has been submitted.", )) } } } async fn wait_for_approval(&self, request: &ApprovalRequest) -> Result<(), ValidationError> { let mut attempts = 0; let max_attempts = (self.approval_timeout.as_secs() / 5) as usize; // Check every 5 seconds while attempts < max_attempts { sleep(Duration::from_secs(5)).await; let status = self.workflow_client .check_approval_status(request) .await .map_err(|e| ValidationError::new( "WORKFLOW_ERROR", &format!("Failed to check approval status: {}", e), ))?; match status.status.as_str() { "approved" => return Ok(()), "rejected" => return Err(ValidationError::new( "APPROVAL_REJECTED", &format!("Request was rejected: {}", status.reason.unwrap_or_default()), )), "pending" => { attempts += 1; continue; } _ => return Err(ValidationError::new( "WORKFLOW_ERROR", "Unexpected approval status", )), } } Err(ValidationError::new( "APPROVAL_TIMEOUT", "Approval request timed out", )) } async fn has_sensitive_changes(&self, user: &User, context: &ValidationContext) -> Result<bool, ValidationError> { // This would typically compare with the existing user record // For brevity, we'll assume sensitive attributes are being checked let sensitive_attributes = [ "roles", "permissions", "securityClearance", "department", "manager", "salary" ]; // In a real implementation, you would fetch the existing user // and compare the attributes to detect changes Ok(true) // Simplified for example } async fn validate_change_approval( &self, user: &User, context: &ValidationContext, ) -> Result<(), ValidationError> { // Similar to validate_approval_exists but for user changes let approval_request = ApprovalRequest { request_type: "user_modification".to_string(), tenant_id: context.tenant_id.clone(), requester: context.authenticated_user.clone().unwrap_or_default(), subject: user.username.clone().unwrap_or_default(), details: serde_json::to_value(user).unwrap_or_default(), }; // Check for existing approval or create new request self.validate_approval_exists(user, context).await } async fn validate_group_approval( &self, group: &Group, context: &ValidationContext, ) -> Result<(), ValidationError> { let approval_request = ApprovalRequest { request_type: "privileged_group_creation".to_string(), tenant_id: context.tenant_id.clone(), requester: context.authenticated_user.clone().unwrap_or_default(), subject: group.display_name.clone(), details: serde_json::to_value(group).unwrap_or_default(), }; self.validate_approval_exists_for_group(group, &approval_request).await } async fn validate_approval_exists_for_group( &self, group: &Group, approval_request: &ApprovalRequest, ) -> Result<(), ValidationError> { // Similar logic to user approval validation let approval_status = self.workflow_client .check_approval_status(approval_request) .await .map_err(|e| ValidationError::new( "WORKFLOW_ERROR", &format!("Failed to check group approval status: {}", e), ))?; match approval_status.status.as_str() { "approved" => Ok(()), "pending" => self.wait_for_approval(approval_request).await, "rejected" => Err(ValidationError::new( "GROUP_APPROVAL_REJECTED", &format!("Group creation was rejected: {}", approval_status.reason.unwrap_or_default()), )), _ => { self.workflow_client .create_approval_request(approval_request) .await .map_err(|e| ValidationError::new( "WORKFLOW_ERROR", &format!("Failed to create group approval request: {}", e), ))?; Err(ValidationError::new( "GROUP_APPROVAL_PENDING", "Privileged group creation requires approval. Request has been submitted.", )) } } } fn is_external_user(&self, user: &User) -> bool { if let Some(emails) = &user.emails { return emails.iter().any(|email| { !email.value.ends_with("@company.com") && !email.value.ends_with("@subsidiary.com") }); } false } fn is_privileged_group(&self, group: &Group) -> bool { let privileged_patterns = ["admin", "security", "hr", "finance", "executive"]; privileged_patterns.iter().any(|pattern| { group.display_name.to_lowercase().contains(pattern) }) } } // Supporting types and client struct WorkflowClient { base_url: String, api_key: String, client: reqwest::Client, } impl WorkflowClient { fn new(base_url: String, api_key: String) -> Self { Self { base_url, api_key, client: reqwest::Client::new(), } } async fn check_approval_status(&self, request: &ApprovalRequest) -> Result<ApprovalStatus, Box<dyn std::error::Error>> { let response = self.client .get(&format!("{}/approvals/{}", self.base_url, request.get_id())) .header("Authorization", format!("Bearer {}", self.api_key)) .send() .await?; Ok(response.json().await?) } async fn create_approval_request(&self, request: &ApprovalRequest) -> Result<ApprovalStatus, Box<dyn std::error::Error>> { let response = self.client .post(&format!("{}/approvals", self.base_url)) .header("Authorization", format!("Bearer {}", self.api_key)) .json(request) .send() .await?; Ok(response.json().await?) } } #[derive(serde::Serialize, serde::Deserialize)] struct ApprovalRequest { request_type: String, tenant_id: String, requester: String, subject: String, details: serde_json::Value, } impl ApprovalRequest { fn get_id(&self) -> String { // Generate ID based on request content format!("{}_{}_{}_{}", self.request_type, self.tenant_id, self.requester, self.subject ) } } #[derive(serde::Deserialize)] struct ApprovalStatus { status: String, reason: Option<String>, approved_by: Option<String>, approved_at: Option<String>, } }
Testing Advanced Validators
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; use tokio_test; use wiremock::{MockServer, Mock, ResponseTemplate}; #[tokio::test] async fn test_external_validation_service() { // Setup mock HR system let mock_server = MockServer::start().await; Mock::given(wiremock::matchers::method("GET")) .and(wiremock::matchers::path("/employees/EMP123456")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "employee_id": "EMP123456", "status": "ACTIVE", "department": "Engineering", "manager_id": "MGR789" }))) .mount(&mock_server) .await; let validator = ExternalValidationService::new( mock_server.uri(), "http://compliance.test".to_string(), "test-api-key".to_string(), ); let mut user = User::default(); user.extension_attributes = Some(serde_json::json!({ "employeeNumber": "EMP123456", "department": "Engineering" }).as_object().unwrap().clone()); let context = ValidationContext::default(); let result = validator.validate_user(&user, &context).await; assert!(result.is_ok()); } #[tokio::test] async fn test_conditional_validation() { let validator = ConditionalValidator::new(); // Test contractor without end date let mut contractor = User::default(); contractor.extension_attributes = Some(serde_json::json!({ "userType": "Contractor" }).as_object().unwrap().clone()); let context = ValidationContext::default(); let result = validator.validate_user(&contractor, &context).await; assert!(result.is_err()); assert_eq!(result.unwrap_err().code, "MISSING_END_DATE"); // Test contractor with end date contractor.extension_attributes = Some(serde_json::json!({ "userType": "Contractor", "employmentEndDate": "2024-12-31" }).as_object().unwrap().clone()); let result = validator.validate_user(&contractor, &context).await; assert!(result.is_ok()); } } }
Usage Examples
use scim_server::ScimServerBuilder; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let server = ScimServerBuilder::new() .with_provider(my_provider) .add_validator(ExternalValidationService::new( "https://hr.company.com/api".to_string(), "https://compliance.company.com/api".to_string(), std::env::var("API_KEY")?, )) .add_validator(ConditionalValidator::new()) .add_validator(WorkflowValidator::new( "https://workflow.company.com/api".to_string(), std::env::var("WORKFLOW_API_KEY")?, )) .build(); server.run().await?; Ok(()) }
Next Steps
- [Field-