1use chrono::{DateTime, Utc};
49use serde::{Deserialize, Serialize};
50use serde_json::Value;
51use std::collections::HashMap;
52use std::time::Duration;
53use thiserror::Error;
54
55#[derive(Debug, Error)]
57pub enum ScimConfigurationError {
58 #[error("SCIM configuration validation failed: {message}")]
60 ValidationError { message: String },
61 #[error("SCIM configuration not found for tenant: {tenant_id}")]
63 NotFound { tenant_id: String },
64 #[error("SCIM client configuration conflict: {message}")]
66 ClientConflict { message: String },
67 #[error("Invalid SCIM endpoint configuration: {message}")]
69 InvalidEndpoint { message: String },
70 #[error("SCIM schema extension error: {message}")]
72 SchemaExtensionError { message: String },
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
81pub struct ScimTenantConfiguration {
82 pub tenant_id: String,
84 pub created_at: DateTime<Utc>,
86 pub last_modified: DateTime<Utc>,
88 pub version: u64,
90 pub endpoint: ScimEndpointConfig,
92 pub clients: Vec<ScimClientConfig>,
94 pub rate_limits: ScimRateLimits,
96 pub schema_config: ScimSchemaConfig,
98 pub audit_config: ScimAuditConfig,
100 pub search_config: ScimSearchConfig,
102}
103
104impl ScimTenantConfiguration {
105 pub fn builder(tenant_id: String) -> ScimTenantConfigurationBuilder {
107 ScimTenantConfigurationBuilder::new(tenant_id)
108 }
109
110 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 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 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
139pub struct ScimEndpointConfig {
140 pub base_path: String,
142 pub include_tenant_in_path: bool,
144 pub tenant_path_pattern: Option<String>,
146 pub max_payload_size: usize,
148 pub scim_version: String,
150 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, scim_version: "2.0".to_string(),
162 supported_auth_schemes: vec![ScimAuthScheme::Bearer, ScimAuthScheme::ApiKey],
163 }
164 }
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
169pub enum ScimAuthScheme {
170 Bearer,
172 ApiKey,
174 Basic,
176 OAuth2,
178 Custom(String),
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
184pub struct ScimClientConfig {
185 pub client_id: String,
187 pub client_name: String,
189 pub auth_config: ScimClientAuth,
191 pub rate_limits: Option<ScimRateLimits>,
193 pub allowed_operations: Vec<ScimOperation>,
195 pub allowed_resource_types: Vec<String>,
197 pub audit_enabled: bool,
199 pub metadata: HashMap<String, Value>,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
205pub struct ScimClientAuth {
206 pub scheme: ScimAuthScheme,
208 pub credentials: HashMap<String, String>,
210 pub token_expiration: Option<Duration>,
212 pub ip_restrictions: Vec<String>,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
218pub enum ScimOperation {
219 Create,
221 Read,
223 Update,
225 Delete,
227 List,
229 Search,
231 Bulk,
233 Schema,
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
239pub struct ScimRateLimits {
240 pub create_operations: Option<RateLimit>,
242 pub read_operations: Option<RateLimit>,
244 pub update_operations: Option<RateLimit>,
246 pub delete_operations: Option<RateLimit>,
248 pub list_operations: Option<RateLimit>,
250 pub search_operations: Option<RateLimit>,
252 pub bulk_operations: Option<RateLimit>,
254 pub global_limit: Option<RateLimit>,
256}
257
258impl ScimRateLimits {
259 pub fn check_create_limit(&self, current_count: u32) -> bool {
260 self.create_operations
261 .as_ref()
262 .map_or(false, |limit| current_count >= limit.max_requests)
263 }
264
265 pub fn check_read_limit(&self, current_count: u32) -> bool {
266 self.read_operations
267 .as_ref()
268 .map_or(false, |limit| current_count >= limit.max_requests)
269 }
270
271 pub fn check_update_limit(&self, current_count: u32) -> bool {
272 self.update_operations
273 .as_ref()
274 .map_or(false, |limit| current_count >= limit.max_requests)
275 }
276
277 pub fn check_delete_limit(&self, current_count: u32) -> bool {
278 self.delete_operations
279 .as_ref()
280 .map_or(false, |limit| current_count >= limit.max_requests)
281 }
282
283 pub fn check_list_limit(&self, current_count: u32) -> bool {
284 self.list_operations
285 .as_ref()
286 .map_or(false, |limit| current_count >= limit.max_requests)
287 }
288
289 pub fn check_search_limit(&self, current_count: u32) -> bool {
290 self.search_operations
291 .as_ref()
292 .map_or(false, |limit| current_count >= limit.max_requests)
293 }
294}
295
296impl Default for ScimRateLimits {
297 fn default() -> Self {
298 Self {
299 create_operations: Some(RateLimit::new(100, Duration::from_secs(60))),
300 read_operations: Some(RateLimit::new(1000, Duration::from_secs(60))),
301 update_operations: Some(RateLimit::new(100, Duration::from_secs(60))),
302 delete_operations: Some(RateLimit::new(50, Duration::from_secs(60))),
303 list_operations: Some(RateLimit::new(200, Duration::from_secs(60))),
304 search_operations: Some(RateLimit::new(100, Duration::from_secs(60))),
305 bulk_operations: Some(RateLimit::new(10, Duration::from_secs(60))),
306 global_limit: Some(RateLimit::new(2000, Duration::from_secs(60))),
307 }
308 }
309}
310
311#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
313pub struct RateLimit {
314 pub max_requests: u32,
316 #[serde(with = "duration_serde")]
318 pub window: Duration,
319 pub burst_allowance: Option<u32>,
321}
322
323impl RateLimit {
324 pub fn new(max_requests: u32, window: Duration) -> Self {
325 Self {
326 max_requests,
327 window,
328 burst_allowance: None,
329 }
330 }
331
332 pub fn with_burst(mut self, burst: u32) -> Self {
333 self.burst_allowance = Some(burst);
334 self
335 }
336}
337
338#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
340pub struct ScimSchemaConfig {
341 pub extensions: Vec<ScimSchemaExtension>,
343 pub custom_attributes: HashMap<String, ScimCustomAttribute>,
345 pub disabled_attributes: Vec<String>,
347 pub additional_required: Vec<String>,
349}
350
351impl Default for ScimSchemaConfig {
352 fn default() -> Self {
353 Self {
354 extensions: Vec::new(),
355 custom_attributes: HashMap::new(),
356 disabled_attributes: Vec::new(),
357 additional_required: Vec::new(),
358 }
359 }
360}
361
362#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
364pub struct ScimSchemaExtension {
365 pub uri: String,
367 pub enabled: bool,
369 pub required: bool,
371 pub attributes: HashMap<String, ScimCustomAttribute>,
373}
374
375#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
377pub struct ScimCustomAttribute {
378 pub name: String,
380 pub attribute_type: String,
382 pub multi_valued: bool,
384 pub required: bool,
386 pub case_exact: bool,
388 pub mutability: String,
390 pub returned: String,
392 pub uniqueness: String,
394 pub description: Option<String>,
396 pub canonical_values: Option<Vec<String>>,
398}
399
400#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
402pub struct ScimAuditConfig {
403 pub enabled: bool,
405 pub audited_operations: Vec<ScimOperation>,
407 pub include_payloads: bool,
409 pub include_sensitive_data: bool,
411 #[serde(with = "duration_serde")]
413 pub retention_period: Duration,
414 pub additional_metadata: HashMap<String, String>,
416}
417
418impl Default for ScimAuditConfig {
419 fn default() -> Self {
420 Self {
421 enabled: true,
422 audited_operations: vec![
423 ScimOperation::Create,
424 ScimOperation::Update,
425 ScimOperation::Delete,
426 ],
427 include_payloads: false,
428 include_sensitive_data: false,
429 retention_period: Duration::from_secs(90 * 24 * 60 * 60), additional_metadata: HashMap::new(),
431 }
432 }
433}
434
435#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
437pub struct ScimSearchConfig {
438 pub max_results: u32,
440 pub default_count: u32,
442 pub max_filter_depth: u32,
444 pub filterable_attributes: Vec<String>,
446 pub sortable_attributes: Vec<String>,
448 pub case_insensitive_filtering: bool,
450 pub custom_operators: Vec<String>,
452}
453
454impl Default for ScimSearchConfig {
455 fn default() -> Self {
456 Self {
457 max_results: 200,
458 default_count: 20,
459 max_filter_depth: 10,
460 filterable_attributes: vec![
461 "userName".to_string(),
462 "displayName".to_string(),
463 "emails.value".to_string(),
464 "active".to_string(),
465 "meta.created".to_string(),
466 "meta.lastModified".to_string(),
467 ],
468 sortable_attributes: vec![
469 "userName".to_string(),
470 "displayName".to_string(),
471 "meta.created".to_string(),
472 "meta.lastModified".to_string(),
473 ],
474 case_insensitive_filtering: true,
475 custom_operators: Vec::new(),
476 }
477 }
478}
479
480pub struct ScimTenantConfigurationBuilder {
482 tenant_id: String,
483 endpoint: Option<ScimEndpointConfig>,
484 clients: Vec<ScimClientConfig>,
485 rate_limits: Option<ScimRateLimits>,
486 schema_config: Option<ScimSchemaConfig>,
487 audit_config: Option<ScimAuditConfig>,
488 search_config: Option<ScimSearchConfig>,
489}
490
491impl ScimTenantConfigurationBuilder {
492 pub fn new(tenant_id: String) -> Self {
493 Self {
494 tenant_id,
495 endpoint: None,
496 clients: Vec::new(),
497 rate_limits: None,
498 schema_config: None,
499 audit_config: None,
500 search_config: None,
501 }
502 }
503
504 pub fn with_endpoint_path(mut self, path: &str) -> Self {
505 let mut endpoint = self.endpoint.unwrap_or_default();
506 endpoint.base_path = path.to_string();
507 self.endpoint = Some(endpoint);
508 self
509 }
510
511 pub fn with_scim_rate_limit(mut self, max_requests: u32, window: Duration) -> Self {
512 let rate_limit = RateLimit::new(max_requests, window);
513 let mut rate_limits = self.rate_limits.unwrap_or_default();
514
515 rate_limits.global_limit = Some(rate_limit.clone());
517 rate_limits.create_operations = Some(rate_limit.clone());
518 rate_limits.read_operations = Some(rate_limit.clone());
519 rate_limits.update_operations = Some(rate_limit.clone());
520 rate_limits.delete_operations = Some(rate_limit.clone());
521 rate_limits.list_operations = Some(rate_limit.clone());
522 rate_limits.search_operations = Some(rate_limit.clone());
523 rate_limits.bulk_operations = Some(rate_limit);
524
525 self.rate_limits = Some(rate_limits);
526 self
527 }
528
529 pub fn with_scim_client(mut self, client_id: &str, api_key: &str) -> Self {
530 let mut credentials = HashMap::new();
531 credentials.insert("api_key".to_string(), api_key.to_string());
532
533 let client = ScimClientConfig {
534 client_id: client_id.to_string(),
535 client_name: client_id.to_string(),
536 auth_config: ScimClientAuth {
537 scheme: ScimAuthScheme::ApiKey,
538 credentials,
539 token_expiration: None,
540 ip_restrictions: Vec::new(),
541 },
542 rate_limits: None,
543 allowed_operations: vec![
544 ScimOperation::Create,
545 ScimOperation::Read,
546 ScimOperation::Update,
547 ScimOperation::Delete,
548 ScimOperation::List,
549 ScimOperation::Search,
550 ],
551 allowed_resource_types: vec!["User".to_string(), "Group".to_string()],
552 audit_enabled: true,
553 metadata: HashMap::new(),
554 };
555
556 self.clients.push(client);
557 self
558 }
559
560 pub fn enable_scim_audit_log(mut self) -> Self {
561 let mut audit_config = self.audit_config.unwrap_or_default();
562 audit_config.enabled = true;
563 self.audit_config = Some(audit_config);
564 self
565 }
566
567 pub fn with_schema_extension(mut self, uri: &str, required: bool) -> Self {
568 let mut schema_config = self.schema_config.unwrap_or_default();
569 schema_config.extensions.push(ScimSchemaExtension {
570 uri: uri.to_string(),
571 enabled: true,
572 required,
573 attributes: HashMap::new(),
574 });
575 self.schema_config = Some(schema_config);
576 self
577 }
578
579 pub fn build(self) -> Result<ScimTenantConfiguration, ScimConfigurationError> {
580 let now = Utc::now();
581
582 Ok(ScimTenantConfiguration {
583 tenant_id: self.tenant_id,
584 created_at: now,
585 last_modified: now,
586 version: 1,
587 endpoint: self.endpoint.unwrap_or_default(),
588 clients: self.clients,
589 rate_limits: self.rate_limits.unwrap_or_default(),
590 schema_config: self.schema_config.unwrap_or_default(),
591 audit_config: self.audit_config.unwrap_or_default(),
592 search_config: self.search_config.unwrap_or_default(),
593 })
594 }
595}
596
597mod duration_serde {
599 use serde::{Deserialize, Deserializer, Serializer};
600 use std::time::Duration;
601
602 pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
603 where
604 S: Serializer,
605 {
606 serializer.serialize_u64(duration.as_secs())
607 }
608
609 pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
610 where
611 D: Deserializer<'de>,
612 {
613 let secs = u64::deserialize(deserializer)?;
614 Ok(Duration::from_secs(secs))
615 }
616}
617
618#[cfg(test)]
619mod tests {
620 use super::*;
621
622 #[test]
623 fn test_scim_tenant_configuration_builder() {
624 let config = ScimTenantConfiguration::builder("test-tenant".to_string())
625 .with_endpoint_path("/scim/v2")
626 .with_scim_rate_limit(100, Duration::from_secs(60))
627 .with_scim_client("client-1", "api_key_123")
628 .enable_scim_audit_log()
629 .with_schema_extension(
630 "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User",
631 false,
632 )
633 .build()
634 .expect("Valid SCIM configuration");
635
636 assert_eq!(config.tenant_id, "test-tenant");
637 assert_eq!(config.endpoint.base_path, "/scim/v2");
638 assert_eq!(config.clients.len(), 1);
639 assert_eq!(config.clients[0].client_id, "client-1");
640 assert!(config.audit_config.enabled);
641 assert_eq!(config.schema_config.extensions.len(), 1);
642 }
643
644 #[test]
645 fn test_rate_limit_checking() {
646 let rate_limits = ScimRateLimits::default();
647
648 assert!(!rate_limits.check_create_limit(50));
650 assert!(rate_limits.check_create_limit(100));
651 assert!(rate_limits.check_create_limit(150));
652 }
653
654 #[test]
655 fn test_client_config_lookup() {
656 let config = ScimTenantConfiguration::builder("test-tenant".to_string())
657 .with_scim_client("client-1", "api_key_123")
658 .with_scim_client("client-2", "api_key_456")
659 .build()
660 .expect("Valid configuration");
661
662 assert!(config.get_client_config("client-1").is_some());
663 assert!(config.get_client_config("client-2").is_some());
664 assert!(config.get_client_config("client-3").is_none());
665 }
666
667 #[test]
668 fn test_schema_extension_checking() {
669 let config = ScimTenantConfiguration::builder("test-tenant".to_string())
670 .with_schema_extension(
671 "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User",
672 true,
673 )
674 .build()
675 .expect("Valid configuration");
676
677 assert!(
678 config
679 .has_schema_extension("urn:ietf:params:scim:schemas:extension:enterprise:2.0:User")
680 );
681 assert!(!config.has_schema_extension("urn:example:custom:extension"));
682 }
683
684 #[test]
685 fn test_default_configurations() {
686 let endpoint = ScimEndpointConfig::default();
687 assert_eq!(endpoint.base_path, "/scim/v2");
688 assert_eq!(endpoint.scim_version, "2.0");
689
690 let rate_limits = ScimRateLimits::default();
691 assert!(rate_limits.create_operations.is_some());
692 assert!(rate_limits.global_limit.is_some());
693
694 let audit_config = ScimAuditConfig::default();
695 assert!(audit_config.enabled);
696 assert_eq!(audit_config.audited_operations.len(), 3);
697 }
698}