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 Patch,
227 Delete,
229 List,
231 Search,
233 Bulk,
235 Schema,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
241pub struct ScimRateLimits {
242 pub create_operations: Option<RateLimit>,
244 pub read_operations: Option<RateLimit>,
246 pub update_operations: Option<RateLimit>,
248 pub delete_operations: Option<RateLimit>,
250 pub list_operations: Option<RateLimit>,
252 pub search_operations: Option<RateLimit>,
254 pub bulk_operations: Option<RateLimit>,
256 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
315pub struct RateLimit {
316 pub max_requests: u32,
318 #[serde(with = "duration_serde")]
320 pub window: Duration,
321 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
342pub struct ScimSchemaConfig {
343 pub extensions: Vec<ScimSchemaExtension>,
345 pub custom_attributes: HashMap<String, ScimCustomAttribute>,
347 pub disabled_attributes: Vec<String>,
349 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
366pub struct ScimSchemaExtension {
367 pub uri: String,
369 pub enabled: bool,
371 pub required: bool,
373 pub attributes: HashMap<String, ScimCustomAttribute>,
375}
376
377#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
379pub struct ScimCustomAttribute {
380 pub name: String,
382 pub attribute_type: String,
384 pub multi_valued: bool,
386 pub required: bool,
388 pub case_exact: bool,
390 pub mutability: String,
392 pub returned: String,
394 pub uniqueness: String,
396 pub description: Option<String>,
398 pub canonical_values: Option<Vec<String>>,
400}
401
402#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
404pub struct ScimAuditConfig {
405 pub enabled: bool,
407 pub audited_operations: Vec<ScimOperation>,
409 pub include_payloads: bool,
411 pub include_sensitive_data: bool,
413 #[serde(with = "duration_serde")]
415 pub retention_period: Duration,
416 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), additional_metadata: HashMap::new(),
433 }
434 }
435}
436
437#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
439pub struct ScimSearchConfig {
440 pub max_results: u32,
442 pub default_count: u32,
444 pub max_filter_depth: u32,
446 pub filterable_attributes: Vec<String>,
448 pub sortable_attributes: Vec<String>,
450 pub case_insensitive_filtering: bool,
452 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
482pub 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 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
599mod 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 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}