1use crate::error::ScimError;
25use crate::providers::ResourceProvider;
26use crate::resource::ScimOperation;
27use crate::schema::{AttributeDefinition, SchemaRegistry};
28use crate::schema_discovery::{AuthenticationScheme, ServiceProviderConfig};
29use serde::{Deserialize, Serialize};
30use std::collections::{HashMap, HashSet};
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct ProviderCapabilities {
36 pub supported_operations: HashMap<String, Vec<ScimOperation>>,
38
39 pub supported_schemas: Vec<String>,
41
42 pub supported_resource_types: Vec<String>,
44
45 pub bulk_capabilities: BulkCapabilities,
47
48 pub filter_capabilities: FilterCapabilities,
50
51 pub pagination_capabilities: PaginationCapabilities,
53
54 pub authentication_capabilities: AuthenticationCapabilities,
56
57 pub extended_capabilities: ExtendedCapabilities,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct BulkCapabilities {
64 pub supported: bool,
66
67 pub max_operations: Option<usize>,
69
70 pub max_payload_size: Option<usize>,
72
73 pub fail_on_errors_supported: bool,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct FilterCapabilities {
80 pub supported: bool,
82
83 pub max_results: Option<usize>,
85
86 pub filterable_attributes: HashMap<String, Vec<String>>, pub supported_operators: Vec<FilterOperator>,
91
92 pub complex_filters_supported: bool,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct PaginationCapabilities {
99 pub supported: bool,
101
102 pub default_page_size: Option<usize>,
104
105 pub max_page_size: Option<usize>,
107
108 pub cursor_based_supported: bool,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct AuthenticationCapabilities {
115 pub schemes: Vec<AuthenticationScheme>,
117
118 pub mfa_supported: bool,
120
121 pub token_refresh_supported: bool,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct ExtendedCapabilities {
128 pub etag_supported: bool,
130
131 pub patch_supported: bool,
133
134 pub change_password_supported: bool,
136
137 pub sort_supported: bool,
139
140 pub custom_capabilities: HashMap<String, serde_json::Value>,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
146pub enum FilterOperator {
147 #[serde(rename = "eq")]
149 Equal,
150
151 #[serde(rename = "ne")]
153 NotEqual,
154
155 #[serde(rename = "co")]
157 Contains,
158
159 #[serde(rename = "sw")]
161 StartsWith,
162
163 #[serde(rename = "ew")]
165 EndsWith,
166
167 #[serde(rename = "pr")]
169 Present,
170
171 #[serde(rename = "gt")]
173 GreaterThan,
174
175 #[serde(rename = "ge")]
177 GreaterThanOrEqual,
178
179 #[serde(rename = "lt")]
181 LessThan,
182
183 #[serde(rename = "le")]
185 LessThanOrEqual,
186}
187
188pub trait CapabilityIntrospectable {
190 fn get_provider_specific_capabilities(&self) -> ExtendedCapabilities {
192 ExtendedCapabilities::default()
193 }
194
195 fn get_bulk_limits(&self) -> Option<BulkCapabilities> {
197 None
198 }
199
200 fn get_pagination_limits(&self) -> Option<PaginationCapabilities> {
202 None
203 }
204
205 fn get_authentication_capabilities(&self) -> Option<AuthenticationCapabilities> {
207 None
208 }
209}
210
211pub struct CapabilityDiscovery;
213
214impl CapabilityDiscovery {
215 pub fn discover_capabilities<P>(
220 schema_registry: &SchemaRegistry,
221 resource_handlers: &HashMap<String, std::sync::Arc<crate::resource::ResourceHandler>>,
222 supported_operations: &HashMap<String, Vec<ScimOperation>>,
223 _provider: &P,
224 ) -> Result<ProviderCapabilities, ScimError>
225 where
226 P: ResourceProvider,
227 {
228 let supported_schemas = Self::discover_schemas(schema_registry);
230
231 let supported_resource_types = Self::discover_resource_types(resource_handlers);
233
234 let supported_operations_map = supported_operations.clone();
236
237 let filter_capabilities =
239 Self::discover_filter_capabilities(schema_registry, resource_handlers)?;
240
241 let bulk_capabilities = Self::default_bulk_capabilities();
243 let pagination_capabilities = Self::default_pagination_capabilities();
244 let authentication_capabilities = Self::default_authentication_capabilities();
245 let mut extended_capabilities = ExtendedCapabilities::default();
246
247 extended_capabilities.etag_supported = true;
249
250 extended_capabilities.patch_supported = supported_operations
252 .values()
253 .any(|ops| ops.contains(&ScimOperation::Patch));
254
255 Ok(ProviderCapabilities {
256 supported_operations: supported_operations_map,
257 supported_schemas,
258 supported_resource_types,
259 bulk_capabilities,
260 filter_capabilities,
261 pagination_capabilities,
262 authentication_capabilities,
263 extended_capabilities,
264 })
265 }
266
267 pub fn discover_capabilities_with_introspection<P>(
272 schema_registry: &SchemaRegistry,
273 resource_handlers: &HashMap<String, std::sync::Arc<crate::resource::ResourceHandler>>,
274 supported_operations: &HashMap<String, Vec<ScimOperation>>,
275 provider: &P,
276 ) -> Result<ProviderCapabilities, ScimError>
277 where
278 P: ResourceProvider + CapabilityIntrospectable,
279 {
280 let supported_schemas = Self::discover_schemas(schema_registry);
282
283 let supported_resource_types = Self::discover_resource_types(resource_handlers);
285
286 let supported_operations_map = supported_operations.clone();
288
289 let filter_capabilities =
291 Self::discover_filter_capabilities(schema_registry, resource_handlers)?;
292
293 let bulk_capabilities = provider
295 .get_bulk_limits()
296 .unwrap_or_else(|| Self::default_bulk_capabilities());
297
298 let pagination_capabilities = provider
299 .get_pagination_limits()
300 .unwrap_or_else(|| Self::default_pagination_capabilities());
301
302 let authentication_capabilities = provider
303 .get_authentication_capabilities()
304 .unwrap_or_else(|| Self::default_authentication_capabilities());
305
306 let extended_capabilities = provider.get_provider_specific_capabilities();
307
308 Ok(ProviderCapabilities {
309 supported_operations: supported_operations_map,
310 supported_schemas,
311 supported_resource_types,
312 bulk_capabilities,
313 filter_capabilities,
314 pagination_capabilities,
315 authentication_capabilities,
316 extended_capabilities,
317 })
318 }
319
320 fn discover_schemas(schema_registry: &SchemaRegistry) -> Vec<String> {
322 schema_registry
323 .get_schemas()
324 .iter()
325 .map(|schema| schema.id.clone())
326 .collect()
327 }
328
329 fn discover_resource_types(
331 resource_handlers: &HashMap<String, std::sync::Arc<crate::resource::ResourceHandler>>,
332 ) -> Vec<String> {
333 resource_handlers.keys().cloned().collect()
334 }
335
336 fn discover_filter_capabilities(
338 schema_registry: &SchemaRegistry,
339 resource_handlers: &HashMap<String, std::sync::Arc<crate::resource::ResourceHandler>>,
340 ) -> Result<FilterCapabilities, ScimError> {
341 let mut filterable_attributes = HashMap::new();
342
343 for (resource_type, handler) in resource_handlers {
345 if let Some(schema) = schema_registry.get_schema(&handler.schema.id) {
347 let attrs = Self::collect_filterable_attributes(&schema.attributes, "");
349 filterable_attributes.insert(resource_type.clone(), attrs);
350 }
351 }
352
353 let supported_operators = Self::determine_supported_operators(schema_registry);
355
356 Ok(FilterCapabilities {
357 supported: !filterable_attributes.is_empty(),
358 max_results: Some(200), filterable_attributes,
360 supported_operators,
361 complex_filters_supported: true, })
363 }
364
365 fn is_attribute_filterable(attr: &AttributeDefinition) -> bool {
367 match attr.data_type {
370 crate::schema::AttributeType::Complex => false, _ => true, }
373 }
374
375 fn collect_filterable_attributes(
377 attributes: &[AttributeDefinition],
378 prefix: &str,
379 ) -> Vec<String> {
380 let mut filterable = Vec::new();
381
382 for attr in attributes {
383 let attr_name = if prefix.is_empty() {
384 attr.name.clone()
385 } else {
386 format!("{}.{}", prefix, attr.name)
387 };
388
389 if Self::is_attribute_filterable(attr) {
390 filterable.push(attr_name.clone());
391 }
392
393 if !attr.sub_attributes.is_empty() {
395 filterable.extend(Self::collect_filterable_attributes(
396 &attr.sub_attributes,
397 &attr_name,
398 ));
399 }
400 }
401
402 filterable
403 }
404
405 fn determine_supported_operators(schema_registry: &SchemaRegistry) -> Vec<FilterOperator> {
407 let mut operators = HashSet::new();
408
409 operators.insert(FilterOperator::Equal);
411 operators.insert(FilterOperator::NotEqual);
412 operators.insert(FilterOperator::Present);
413
414 if Self::has_string_attributes(schema_registry) {
416 operators.insert(FilterOperator::Contains);
417 operators.insert(FilterOperator::StartsWith);
418 operators.insert(FilterOperator::EndsWith);
419 }
420
421 if Self::has_comparable_attributes(schema_registry) {
423 operators.insert(FilterOperator::GreaterThan);
424 operators.insert(FilterOperator::GreaterThanOrEqual);
425 operators.insert(FilterOperator::LessThan);
426 operators.insert(FilterOperator::LessThanOrEqual);
427 }
428
429 operators.into_iter().collect()
430 }
431
432 fn has_string_attributes(schema_registry: &SchemaRegistry) -> bool {
434 fn has_string_in_attributes(attributes: &[AttributeDefinition]) -> bool {
435 attributes.iter().any(|attr| {
436 matches!(attr.data_type, crate::schema::AttributeType::String)
437 || has_string_in_attributes(&attr.sub_attributes)
438 })
439 }
440
441 schema_registry
442 .get_schemas()
443 .iter()
444 .any(|schema| has_string_in_attributes(&schema.attributes))
445 }
446
447 fn has_comparable_attributes(schema_registry: &SchemaRegistry) -> bool {
449 fn has_comparable_in_attributes(attributes: &[AttributeDefinition]) -> bool {
450 attributes.iter().any(|attr| {
451 matches!(
452 attr.data_type,
453 crate::schema::AttributeType::Integer
454 | crate::schema::AttributeType::Decimal
455 | crate::schema::AttributeType::DateTime
456 ) || has_comparable_in_attributes(&attr.sub_attributes)
457 })
458 }
459
460 schema_registry
461 .get_schemas()
462 .iter()
463 .any(|schema| has_comparable_in_attributes(&schema.attributes))
464 }
465
466 fn default_bulk_capabilities() -> BulkCapabilities {
468 BulkCapabilities {
469 supported: false, max_operations: None,
471 max_payload_size: None,
472 fail_on_errors_supported: false,
473 }
474 }
475
476 fn default_pagination_capabilities() -> PaginationCapabilities {
478 PaginationCapabilities {
479 supported: true, default_page_size: Some(20),
481 max_page_size: Some(200),
482 cursor_based_supported: false, }
484 }
485
486 fn default_authentication_capabilities() -> AuthenticationCapabilities {
488 AuthenticationCapabilities {
489 schemes: vec![], mfa_supported: false,
491 token_refresh_supported: false,
492 }
493 }
494
495 pub fn generate_service_provider_config(
497 capabilities: &ProviderCapabilities,
498 ) -> ServiceProviderConfig {
499 ServiceProviderConfig {
500 patch_supported: capabilities.extended_capabilities.patch_supported,
501 bulk_supported: capabilities.bulk_capabilities.supported,
502 filter_supported: capabilities.filter_capabilities.supported,
503 change_password_supported: capabilities.extended_capabilities.change_password_supported,
504 sort_supported: capabilities.extended_capabilities.sort_supported,
505 etag_supported: capabilities.extended_capabilities.etag_supported,
506 authentication_schemes: capabilities.authentication_capabilities.schemes.clone(),
507 bulk_max_operations: capabilities
508 .bulk_capabilities
509 .max_operations
510 .map(|n| n as u32),
511 bulk_max_payload_size: capabilities
512 .bulk_capabilities
513 .max_payload_size
514 .map(|n| n as u64),
515 filter_max_results: capabilities
516 .filter_capabilities
517 .max_results
518 .map(|n| n as u32),
519 }
520 }
521}
522
523impl Default for BulkCapabilities {
524 fn default() -> Self {
525 Self {
526 supported: false,
527 max_operations: None,
528 max_payload_size: None,
529 fail_on_errors_supported: false,
530 }
531 }
532}
533
534impl Default for FilterCapabilities {
535 fn default() -> Self {
536 Self {
537 supported: false,
538 max_results: Some(200),
539 filterable_attributes: HashMap::new(),
540 supported_operators: vec![FilterOperator::Equal, FilterOperator::Present],
541 complex_filters_supported: false,
542 }
543 }
544}
545
546impl Default for PaginationCapabilities {
547 fn default() -> Self {
548 Self {
549 supported: true,
550 default_page_size: Some(20),
551 max_page_size: Some(200),
552 cursor_based_supported: false,
553 }
554 }
555}
556
557impl Default for AuthenticationCapabilities {
558 fn default() -> Self {
559 Self {
560 schemes: vec![],
561 mfa_supported: false,
562 token_refresh_supported: false,
563 }
564 }
565}
566
567impl Default for ExtendedCapabilities {
568 fn default() -> Self {
569 Self {
570 etag_supported: true, patch_supported: false,
572 change_password_supported: false,
573 sort_supported: false,
574 custom_capabilities: HashMap::new(),
575 }
576 }
577}
578
579#[cfg(test)]
583mod tests {
584 use super::*;
585 use crate::schema::SchemaRegistry;
586 use std::collections::HashMap;
587
588 #[test]
589 fn test_discover_schemas() {
590 let registry = SchemaRegistry::new().expect("Failed to create schema registry");
591 let schemas = CapabilityDiscovery::discover_schemas(®istry);
592
593 assert!(!schemas.is_empty());
594 assert!(schemas.contains(&"urn:ietf:params:scim:schemas:core:2.0:User".to_string()));
595 }
596
597 #[test]
598 fn test_has_string_attributes() {
599 let registry = SchemaRegistry::new().expect("Failed to create schema registry");
600 assert!(CapabilityDiscovery::has_string_attributes(®istry));
601 }
602
603 #[test]
604 fn test_has_comparable_attributes() {
605 let registry = SchemaRegistry::new().expect("Failed to create schema registry");
606 assert!(CapabilityDiscovery::has_comparable_attributes(®istry));
607 }
608
609 #[test]
610 fn test_service_provider_config_generation() {
611 let capabilities = ProviderCapabilities {
612 supported_operations: HashMap::new(),
613 supported_schemas: vec!["urn:ietf:params:scim:schemas:core:2.0:User".to_string()],
614 supported_resource_types: vec!["User".to_string()],
615 bulk_capabilities: BulkCapabilities {
616 supported: true,
617 max_operations: Some(100),
618 max_payload_size: Some(1024 * 1024),
619 fail_on_errors_supported: true,
620 },
621 filter_capabilities: FilterCapabilities::default(),
622 pagination_capabilities: PaginationCapabilities::default(),
623 authentication_capabilities: AuthenticationCapabilities::default(),
624 extended_capabilities: ExtendedCapabilities {
625 patch_supported: true,
626 ..Default::default()
627 },
628 };
629
630 let config = CapabilityDiscovery::generate_service_provider_config(&capabilities);
631
632 assert!(config.bulk_supported);
633 assert!(config.patch_supported);
634 assert_eq!(config.bulk_max_operations, Some(100));
635 assert_eq!(config.bulk_max_payload_size, Some(1024 * 1024));
636 }
637
638 #[test]
639 fn test_filter_operators() {
640 let registry = SchemaRegistry::new().expect("Failed to create schema registry");
641 let operators = CapabilityDiscovery::determine_supported_operators(®istry);
642
643 log::debug!("Discovered filter operators: {:?}", operators);
644
645 assert!(operators.contains(&FilterOperator::Equal));
647 assert!(operators.contains(&FilterOperator::Present));
648
649 assert!(operators.contains(&FilterOperator::Contains));
651 assert!(operators.contains(&FilterOperator::StartsWith));
652
653 assert!(operators.contains(&FilterOperator::GreaterThan));
655 assert!(operators.contains(&FilterOperator::LessThan));
656 }
657}