scim_server/multi_tenant/
scim_config.rs

1//! SCIM-specific tenant configuration for multi-tenant SCIM operations.
2//!
3//! This module provides SCIM-focused configuration management for multi-tenant
4//! deployments. Unlike general-purpose configuration systems, this focuses
5//! exclusively on SCIM protocol requirements and multi-tenant orchestration.
6//!
7//! # Design Principles
8//!
9//! * **SCIM Protocol Focus**: Only configuration related to SCIM 2.0 specification
10//! * **Tenant Isolation**: Configuration for SCIM-level tenant separation
11//! * **Client Management**: SCIM client connection and authentication settings
12//! * **Protocol Compliance**: Settings that affect SCIM protocol behavior
13//!
14//! # Scope Boundaries
15//!
16//! ## ✅ In Scope (SCIM-Specific Configuration)
17//! - SCIM endpoint configuration per tenant
18//! - SCIM client authentication and connection settings
19//! - SCIM protocol rate limiting and throttling
20//! - SCIM schema extensions and customizations
21//! - SCIM operation audit trails
22//! - SCIM filtering and search configuration
23//!
24//! ## ❌ Out of Scope (General Application Configuration)
25//! - UI branding and theming
26//! - General performance tuning
27//! - Business logic configuration
28//! - General session management
29//! - Infrastructure encryption settings
30//! - General compliance frameworks
31//!
32//! # Example Usage
33//!
34//! ```rust
35//! use scim_server::multi_tenant::{ScimTenantConfiguration, ScimEndpointConfig};
36//! use std::time::Duration;
37//!
38//! // Create SCIM-specific tenant configuration
39//! let config = ScimTenantConfiguration::builder("tenant-a".to_string())
40//!     .with_endpoint_path("/scim/v2")
41//!     .with_scim_rate_limit(100, Duration::from_secs(60))
42//!     .with_scim_client("client-1", "api_key_123")
43//!     .enable_scim_audit_log()
44//!     .build()
45//!     .expect("Valid SCIM configuration");
46//! ```
47
48use chrono::{DateTime, Utc};
49use serde::{Deserialize, Serialize};
50use serde_json::Value;
51use std::collections::HashMap;
52use std::time::Duration;
53use thiserror::Error;
54
55/// Errors specific to SCIM configuration management.
56#[derive(Debug, Error)]
57pub enum ScimConfigurationError {
58    /// SCIM configuration validation failed
59    #[error("SCIM configuration validation failed: {message}")]
60    ValidationError { message: String },
61    /// SCIM configuration not found for tenant
62    #[error("SCIM configuration not found for tenant: {tenant_id}")]
63    NotFound { tenant_id: String },
64    /// SCIM client configuration conflict
65    #[error("SCIM client configuration conflict: {message}")]
66    ClientConflict { message: String },
67    /// Invalid SCIM endpoint configuration
68    #[error("Invalid SCIM endpoint configuration: {message}")]
69    InvalidEndpoint { message: String },
70    /// SCIM schema extension error
71    #[error("SCIM schema extension error: {message}")]
72    SchemaExtensionError { message: String },
73}
74
75/// Complete SCIM-specific configuration for a tenant.
76///
77/// This configuration encompasses all SCIM protocol-related settings
78/// for a tenant, including endpoint configuration, client connections,
79/// schema customizations, and protocol-specific operational settings.
80#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
81pub struct ScimTenantConfiguration {
82    /// Unique identifier for the tenant
83    pub tenant_id: String,
84    /// When this configuration was created
85    pub created_at: DateTime<Utc>,
86    /// When this configuration was last modified
87    pub last_modified: DateTime<Utc>,
88    /// Configuration version for optimistic locking
89    pub version: u64,
90    /// SCIM endpoint configuration
91    pub endpoint: ScimEndpointConfig,
92    /// SCIM client connection configurations
93    pub clients: Vec<ScimClientConfig>,
94    /// SCIM protocol-specific rate limiting
95    pub rate_limits: ScimRateLimits,
96    /// SCIM schema extensions and customizations
97    pub schema_config: ScimSchemaConfig,
98    /// SCIM operation audit settings
99    pub audit_config: ScimAuditConfig,
100    /// SCIM filtering and search configuration
101    pub search_config: ScimSearchConfig,
102}
103
104impl ScimTenantConfiguration {
105    /// Create a new builder for SCIM tenant configuration.
106    pub fn builder(tenant_id: String) -> ScimTenantConfigurationBuilder {
107        ScimTenantConfigurationBuilder::new(tenant_id)
108    }
109
110    /// Get SCIM client configuration by client ID.
111    pub fn get_client_config(&self, client_id: &str) -> Option<&ScimClientConfig> {
112        self.clients.iter().find(|c| c.client_id == client_id)
113    }
114
115    /// Check if a SCIM operation is rate limited for this tenant.
116    pub fn is_rate_limited(&self, operation: &str, current_count: u32) -> bool {
117        match operation {
118            "create" => self.rate_limits.check_create_limit(current_count),
119            "read" => self.rate_limits.check_read_limit(current_count),
120            "update" => self.rate_limits.check_update_limit(current_count),
121            "delete" => self.rate_limits.check_delete_limit(current_count),
122            "list" => self.rate_limits.check_list_limit(current_count),
123            "search" => self.rate_limits.check_search_limit(current_count),
124            _ => false,
125        }
126    }
127
128    /// Check if a SCIM schema extension is enabled for this tenant.
129    pub fn has_schema_extension(&self, extension_uri: &str) -> bool {
130        self.schema_config
131            .extensions
132            .iter()
133            .any(|ext| ext.uri == extension_uri && ext.enabled)
134    }
135}
136
137/// SCIM endpoint configuration for a tenant.
138#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
139pub struct ScimEndpointConfig {
140    /// Base path for SCIM endpoints (e.g., "/scim/v2")
141    pub base_path: String,
142    /// Whether to include tenant ID in the path
143    pub include_tenant_in_path: bool,
144    /// Custom path pattern if include_tenant_in_path is true
145    pub tenant_path_pattern: Option<String>,
146    /// Maximum request payload size for SCIM operations
147    pub max_payload_size: usize,
148    /// SCIM protocol version (typically "2.0")
149    pub scim_version: String,
150    /// Supported SCIM authentication schemes
151    pub supported_auth_schemes: Vec<ScimAuthScheme>,
152}
153
154impl Default for ScimEndpointConfig {
155    fn default() -> Self {
156        Self {
157            base_path: "/scim/v2".to_string(),
158            include_tenant_in_path: false,
159            tenant_path_pattern: None,
160            max_payload_size: 1024 * 1024, // 1MB
161            scim_version: "2.0".to_string(),
162            supported_auth_schemes: vec![ScimAuthScheme::Bearer, ScimAuthScheme::ApiKey],
163        }
164    }
165}
166
167/// SCIM authentication schemes supported by the endpoint.
168#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
169pub enum ScimAuthScheme {
170    /// Bearer token authentication
171    Bearer,
172    /// API key authentication
173    ApiKey,
174    /// HTTP Basic authentication
175    Basic,
176    /// OAuth 2.0 authentication
177    OAuth2,
178    /// Custom authentication scheme
179    Custom(String),
180}
181
182/// SCIM client connection configuration.
183#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
184pub struct ScimClientConfig {
185    /// Unique identifier for this SCIM client
186    pub client_id: String,
187    /// Human-readable name for the client
188    pub client_name: String,
189    /// Authentication credentials for this client
190    pub auth_config: ScimClientAuth,
191    /// Client-specific rate limits (overrides tenant defaults)
192    pub rate_limits: Option<ScimRateLimits>,
193    /// SCIM operations this client is allowed to perform
194    pub allowed_operations: Vec<ScimOperation>,
195    /// Resource types this client can access
196    pub allowed_resource_types: Vec<String>,
197    /// Whether audit logging is enabled for this client
198    pub audit_enabled: bool,
199    /// Client-specific configuration metadata
200    pub metadata: HashMap<String, Value>,
201}
202
203/// SCIM client authentication configuration.
204#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
205pub struct ScimClientAuth {
206    /// Authentication scheme for this client
207    pub scheme: ScimAuthScheme,
208    /// Authentication credentials (hashed/encrypted)
209    pub credentials: HashMap<String, String>,
210    /// Token expiration settings
211    pub token_expiration: Option<Duration>,
212    /// Whether to validate client IP restrictions
213    pub ip_restrictions: Vec<String>,
214}
215
216/// SCIM operations that can be performed.
217#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
218pub enum ScimOperation {
219    /// Create new resources
220    Create,
221    /// Read existing resources
222    Read,
223    /// Update existing resources
224    Update,
225    /// Patch existing resources with partial updates
226    Patch,
227    /// Delete resources
228    Delete,
229    /// List resources with pagination
230    List,
231    /// Search resources with filtering
232    Search,
233    /// Bulk operations
234    Bulk,
235    /// Schema discovery
236    Schema,
237}
238
239/// SCIM protocol-specific rate limiting configuration.
240#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
241pub struct ScimRateLimits {
242    /// Rate limit for SCIM create operations
243    pub create_operations: Option<RateLimit>,
244    /// Rate limit for SCIM read operations
245    pub read_operations: Option<RateLimit>,
246    /// Rate limit for SCIM update operations
247    pub update_operations: Option<RateLimit>,
248    /// Rate limit for SCIM delete operations
249    pub delete_operations: Option<RateLimit>,
250    /// Rate limit for SCIM list operations
251    pub list_operations: Option<RateLimit>,
252    /// Rate limit for SCIM search operations
253    pub search_operations: Option<RateLimit>,
254    /// Rate limit for SCIM bulk operations
255    pub bulk_operations: Option<RateLimit>,
256    /// Global rate limit across all SCIM operations
257    pub global_limit: Option<RateLimit>,
258}
259
260impl ScimRateLimits {
261    pub fn check_create_limit(&self, current_count: u32) -> bool {
262        self.create_operations
263            .as_ref()
264            .map_or(false, |limit| current_count >= limit.max_requests)
265    }
266
267    pub fn check_read_limit(&self, current_count: u32) -> bool {
268        self.read_operations
269            .as_ref()
270            .map_or(false, |limit| current_count >= limit.max_requests)
271    }
272
273    pub fn check_update_limit(&self, current_count: u32) -> bool {
274        self.update_operations
275            .as_ref()
276            .map_or(false, |limit| current_count >= limit.max_requests)
277    }
278
279    pub fn check_delete_limit(&self, current_count: u32) -> bool {
280        self.delete_operations
281            .as_ref()
282            .map_or(false, |limit| current_count >= limit.max_requests)
283    }
284
285    pub fn check_list_limit(&self, current_count: u32) -> bool {
286        self.list_operations
287            .as_ref()
288            .map_or(false, |limit| current_count >= limit.max_requests)
289    }
290
291    pub fn check_search_limit(&self, current_count: u32) -> bool {
292        self.search_operations
293            .as_ref()
294            .map_or(false, |limit| current_count >= limit.max_requests)
295    }
296}
297
298impl Default for ScimRateLimits {
299    fn default() -> Self {
300        Self {
301            create_operations: Some(RateLimit::new(100, Duration::from_secs(60))),
302            read_operations: Some(RateLimit::new(1000, Duration::from_secs(60))),
303            update_operations: Some(RateLimit::new(100, Duration::from_secs(60))),
304            delete_operations: Some(RateLimit::new(50, Duration::from_secs(60))),
305            list_operations: Some(RateLimit::new(200, Duration::from_secs(60))),
306            search_operations: Some(RateLimit::new(100, Duration::from_secs(60))),
307            bulk_operations: Some(RateLimit::new(10, Duration::from_secs(60))),
308            global_limit: Some(RateLimit::new(2000, Duration::from_secs(60))),
309        }
310    }
311}
312
313/// Rate limiting configuration for specific operations.
314#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
315pub struct RateLimit {
316    /// Maximum number of requests allowed
317    pub max_requests: u32,
318    /// Time window for the rate limit
319    #[serde(with = "duration_serde")]
320    pub window: Duration,
321    /// Burst allowance for short-term spikes
322    pub burst_allowance: Option<u32>,
323}
324
325impl RateLimit {
326    pub fn new(max_requests: u32, window: Duration) -> Self {
327        Self {
328            max_requests,
329            window,
330            burst_allowance: None,
331        }
332    }
333
334    pub fn with_burst(mut self, burst: u32) -> Self {
335        self.burst_allowance = Some(burst);
336        self
337    }
338}
339
340/// SCIM schema customization configuration.
341#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
342pub struct ScimSchemaConfig {
343    /// SCIM schema extensions enabled for this tenant
344    pub extensions: Vec<ScimSchemaExtension>,
345    /// Custom attributes added to standard SCIM schemas
346    pub custom_attributes: HashMap<String, ScimCustomAttribute>,
347    /// Standard SCIM attributes disabled for this tenant
348    pub disabled_attributes: Vec<String>,
349    /// Additional required attributes for this tenant
350    pub additional_required: Vec<String>,
351}
352
353impl Default for ScimSchemaConfig {
354    fn default() -> Self {
355        Self {
356            extensions: Vec::new(),
357            custom_attributes: HashMap::new(),
358            disabled_attributes: Vec::new(),
359            additional_required: Vec::new(),
360        }
361    }
362}
363
364/// SCIM schema extension configuration.
365#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
366pub struct ScimSchemaExtension {
367    /// SCIM extension URI (e.g., "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User")
368    pub uri: String,
369    /// Whether this extension is enabled for the tenant
370    pub enabled: bool,
371    /// Whether this extension is required for resources
372    pub required: bool,
373    /// Custom attributes defined in this extension
374    pub attributes: HashMap<String, ScimCustomAttribute>,
375}
376
377/// Custom SCIM attribute definition.
378#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
379pub struct ScimCustomAttribute {
380    /// Attribute name
381    pub name: String,
382    /// SCIM attribute type (string, boolean, decimal, integer, dateTime, reference, complex)
383    pub attribute_type: String,
384    /// Whether the attribute supports multiple values
385    pub multi_valued: bool,
386    /// Whether the attribute is required
387    pub required: bool,
388    /// Whether the attribute is case-sensitive
389    pub case_exact: bool,
390    /// Mutability of the attribute (readOnly, readWrite, immutable, writeOnly)
391    pub mutability: String,
392    /// When the attribute is returned (always, never, default, request)
393    pub returned: String,
394    /// Uniqueness constraint (none, server, global)
395    pub uniqueness: String,
396    /// Description of the attribute
397    pub description: Option<String>,
398    /// Canonical values for the attribute
399    pub canonical_values: Option<Vec<String>>,
400}
401
402/// SCIM operation audit configuration.
403#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
404pub struct ScimAuditConfig {
405    /// Whether SCIM operation auditing is enabled
406    pub enabled: bool,
407    /// SCIM operations to audit
408    pub audited_operations: Vec<ScimOperation>,
409    /// Whether to include request/response payloads in audit logs
410    pub include_payloads: bool,
411    /// Whether to include sensitive attributes in audit logs
412    pub include_sensitive_data: bool,
413    /// How long to retain SCIM audit logs
414    #[serde(with = "duration_serde")]
415    pub retention_period: Duration,
416    /// Additional metadata to include in audit logs
417    pub additional_metadata: HashMap<String, String>,
418}
419
420impl Default for ScimAuditConfig {
421    fn default() -> Self {
422        Self {
423            enabled: true,
424            audited_operations: vec![
425                ScimOperation::Create,
426                ScimOperation::Update,
427                ScimOperation::Delete,
428            ],
429            include_payloads: false,
430            include_sensitive_data: false,
431            retention_period: Duration::from_secs(90 * 24 * 60 * 60), // 90 days
432            additional_metadata: HashMap::new(),
433        }
434    }
435}
436
437/// SCIM search and filtering configuration.
438#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
439pub struct ScimSearchConfig {
440    /// Maximum number of resources returned in a single search
441    pub max_results: u32,
442    /// Default number of resources returned if not specified
443    pub default_count: u32,
444    /// Maximum allowed depth for complex filter expressions
445    pub max_filter_depth: u32,
446    /// Attributes that support filtering
447    pub filterable_attributes: Vec<String>,
448    /// Attributes that support sorting
449    pub sortable_attributes: Vec<String>,
450    /// Whether case-insensitive filtering is supported
451    pub case_insensitive_filtering: bool,
452    /// Custom search operators supported
453    pub custom_operators: Vec<String>,
454}
455
456impl Default for ScimSearchConfig {
457    fn default() -> Self {
458        Self {
459            max_results: 200,
460            default_count: 20,
461            max_filter_depth: 10,
462            filterable_attributes: vec![
463                "userName".to_string(),
464                "displayName".to_string(),
465                "emails.value".to_string(),
466                "active".to_string(),
467                "meta.created".to_string(),
468                "meta.lastModified".to_string(),
469            ],
470            sortable_attributes: vec![
471                "userName".to_string(),
472                "displayName".to_string(),
473                "meta.created".to_string(),
474                "meta.lastModified".to_string(),
475            ],
476            case_insensitive_filtering: true,
477            custom_operators: Vec::new(),
478        }
479    }
480}
481
482/// Builder for creating SCIM tenant configurations.
483pub struct ScimTenantConfigurationBuilder {
484    tenant_id: String,
485    endpoint: Option<ScimEndpointConfig>,
486    clients: Vec<ScimClientConfig>,
487    rate_limits: Option<ScimRateLimits>,
488    schema_config: Option<ScimSchemaConfig>,
489    audit_config: Option<ScimAuditConfig>,
490    search_config: Option<ScimSearchConfig>,
491}
492
493impl ScimTenantConfigurationBuilder {
494    pub fn new(tenant_id: String) -> Self {
495        Self {
496            tenant_id,
497            endpoint: None,
498            clients: Vec::new(),
499            rate_limits: None,
500            schema_config: None,
501            audit_config: None,
502            search_config: None,
503        }
504    }
505
506    pub fn with_endpoint_path(mut self, path: &str) -> Self {
507        let mut endpoint = self.endpoint.unwrap_or_default();
508        endpoint.base_path = path.to_string();
509        self.endpoint = Some(endpoint);
510        self
511    }
512
513    pub fn with_scim_rate_limit(mut self, max_requests: u32, window: Duration) -> Self {
514        let rate_limit = RateLimit::new(max_requests, window);
515        let mut rate_limits = self.rate_limits.unwrap_or_default();
516
517        // Set the global limit and all operation-specific limits to the same value
518        rate_limits.global_limit = Some(rate_limit.clone());
519        rate_limits.create_operations = Some(rate_limit.clone());
520        rate_limits.read_operations = Some(rate_limit.clone());
521        rate_limits.update_operations = Some(rate_limit.clone());
522        rate_limits.delete_operations = Some(rate_limit.clone());
523        rate_limits.list_operations = Some(rate_limit.clone());
524        rate_limits.search_operations = Some(rate_limit.clone());
525        rate_limits.bulk_operations = Some(rate_limit);
526
527        self.rate_limits = Some(rate_limits);
528        self
529    }
530
531    pub fn with_scim_client(mut self, client_id: &str, api_key: &str) -> Self {
532        let mut credentials = HashMap::new();
533        credentials.insert("api_key".to_string(), api_key.to_string());
534
535        let client = ScimClientConfig {
536            client_id: client_id.to_string(),
537            client_name: client_id.to_string(),
538            auth_config: ScimClientAuth {
539                scheme: ScimAuthScheme::ApiKey,
540                credentials,
541                token_expiration: None,
542                ip_restrictions: Vec::new(),
543            },
544            rate_limits: None,
545            allowed_operations: vec![
546                ScimOperation::Create,
547                ScimOperation::Read,
548                ScimOperation::Update,
549                ScimOperation::Delete,
550                ScimOperation::List,
551                ScimOperation::Search,
552            ],
553            allowed_resource_types: vec!["User".to_string(), "Group".to_string()],
554            audit_enabled: true,
555            metadata: HashMap::new(),
556        };
557
558        self.clients.push(client);
559        self
560    }
561
562    pub fn enable_scim_audit_log(mut self) -> Self {
563        let mut audit_config = self.audit_config.unwrap_or_default();
564        audit_config.enabled = true;
565        self.audit_config = Some(audit_config);
566        self
567    }
568
569    pub fn with_schema_extension(mut self, uri: &str, required: bool) -> Self {
570        let mut schema_config = self.schema_config.unwrap_or_default();
571        schema_config.extensions.push(ScimSchemaExtension {
572            uri: uri.to_string(),
573            enabled: true,
574            required,
575            attributes: HashMap::new(),
576        });
577        self.schema_config = Some(schema_config);
578        self
579    }
580
581    pub fn build(self) -> Result<ScimTenantConfiguration, ScimConfigurationError> {
582        let now = Utc::now();
583
584        Ok(ScimTenantConfiguration {
585            tenant_id: self.tenant_id,
586            created_at: now,
587            last_modified: now,
588            version: 1,
589            endpoint: self.endpoint.unwrap_or_default(),
590            clients: self.clients,
591            rate_limits: self.rate_limits.unwrap_or_default(),
592            schema_config: self.schema_config.unwrap_or_default(),
593            audit_config: self.audit_config.unwrap_or_default(),
594            search_config: self.search_config.unwrap_or_default(),
595        })
596    }
597}
598
599// Custom serialization for Duration fields
600mod duration_serde {
601    use serde::{Deserialize, Deserializer, Serializer};
602    use std::time::Duration;
603
604    pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
605    where
606        S: Serializer,
607    {
608        serializer.serialize_u64(duration.as_secs())
609    }
610
611    pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
612    where
613        D: Deserializer<'de>,
614    {
615        let secs = u64::deserialize(deserializer)?;
616        Ok(Duration::from_secs(secs))
617    }
618}
619
620#[cfg(test)]
621mod tests {
622    use super::*;
623
624    #[test]
625    fn test_scim_tenant_configuration_builder() {
626        let config = ScimTenantConfiguration::builder("test-tenant".to_string())
627            .with_endpoint_path("/scim/v2")
628            .with_scim_rate_limit(100, Duration::from_secs(60))
629            .with_scim_client("client-1", "api_key_123")
630            .enable_scim_audit_log()
631            .with_schema_extension(
632                "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User",
633                false,
634            )
635            .build()
636            .expect("Valid SCIM configuration");
637
638        assert_eq!(config.tenant_id, "test-tenant");
639        assert_eq!(config.endpoint.base_path, "/scim/v2");
640        assert_eq!(config.clients.len(), 1);
641        assert_eq!(config.clients[0].client_id, "client-1");
642        assert!(config.audit_config.enabled);
643        assert_eq!(config.schema_config.extensions.len(), 1);
644    }
645
646    #[test]
647    fn test_rate_limit_checking() {
648        let rate_limits = ScimRateLimits::default();
649
650        // Test with default create limit (100 per minute)
651        assert!(!rate_limits.check_create_limit(50));
652        assert!(rate_limits.check_create_limit(100));
653        assert!(rate_limits.check_create_limit(150));
654    }
655
656    #[test]
657    fn test_client_config_lookup() {
658        let config = ScimTenantConfiguration::builder("test-tenant".to_string())
659            .with_scim_client("client-1", "api_key_123")
660            .with_scim_client("client-2", "api_key_456")
661            .build()
662            .expect("Valid configuration");
663
664        assert!(config.get_client_config("client-1").is_some());
665        assert!(config.get_client_config("client-2").is_some());
666        assert!(config.get_client_config("client-3").is_none());
667    }
668
669    #[test]
670    fn test_schema_extension_checking() {
671        let config = ScimTenantConfiguration::builder("test-tenant".to_string())
672            .with_schema_extension(
673                "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User",
674                true,
675            )
676            .build()
677            .expect("Valid configuration");
678
679        assert!(
680            config
681                .has_schema_extension("urn:ietf:params:scim:schemas:extension:enterprise:2.0:User")
682        );
683        assert!(!config.has_schema_extension("urn:example:custom:extension"));
684    }
685
686    #[test]
687    fn test_default_configurations() {
688        let endpoint = ScimEndpointConfig::default();
689        assert_eq!(endpoint.base_path, "/scim/v2");
690        assert_eq!(endpoint.scim_version, "2.0");
691
692        let rate_limits = ScimRateLimits::default();
693        assert!(rate_limits.create_operations.is_some());
694        assert!(rate_limits.global_limit.is_some());
695
696        let audit_config = ScimAuditConfig::default();
697        assert!(audit_config.enabled);
698        assert_eq!(audit_config.audited_operations.len(), 3);
699    }
700}