1use crate::error::ScimError;
25use crate::resource::{ResourceProvider, ScimOperation};
26use crate::schema::{AttributeDefinition, SchemaRegistry};
27use crate::schema_discovery::{AuthenticationScheme, ServiceProviderConfig};
28use serde::{Deserialize, Serialize};
29use std::collections::{HashMap, HashSet};
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ProviderCapabilities {
35 pub supported_operations: HashMap<String, Vec<ScimOperation>>,
37
38 pub supported_schemas: Vec<String>,
40
41 pub supported_resource_types: Vec<String>,
43
44 pub bulk_capabilities: BulkCapabilities,
46
47 pub filter_capabilities: FilterCapabilities,
49
50 pub pagination_capabilities: PaginationCapabilities,
52
53 pub authentication_capabilities: AuthenticationCapabilities,
55
56 pub extended_capabilities: ExtendedCapabilities,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct BulkCapabilities {
63 pub supported: bool,
65
66 pub max_operations: Option<usize>,
68
69 pub max_payload_size: Option<usize>,
71
72 pub fail_on_errors_supported: bool,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct FilterCapabilities {
79 pub supported: bool,
81
82 pub max_results: Option<usize>,
84
85 pub filterable_attributes: HashMap<String, Vec<String>>, pub supported_operators: Vec<FilterOperator>,
90
91 pub complex_filters_supported: bool,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct PaginationCapabilities {
98 pub supported: bool,
100
101 pub default_page_size: Option<usize>,
103
104 pub max_page_size: Option<usize>,
106
107 pub cursor_based_supported: bool,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct AuthenticationCapabilities {
114 pub schemes: Vec<AuthenticationScheme>,
116
117 pub mfa_supported: bool,
119
120 pub token_refresh_supported: bool,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct ExtendedCapabilities {
127 pub etag_supported: bool,
129
130 pub patch_supported: bool,
132
133 pub change_password_supported: bool,
135
136 pub sort_supported: bool,
138
139 pub custom_capabilities: HashMap<String, serde_json::Value>,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
145pub enum FilterOperator {
146 #[serde(rename = "eq")]
148 Equal,
149
150 #[serde(rename = "ne")]
152 NotEqual,
153
154 #[serde(rename = "co")]
156 Contains,
157
158 #[serde(rename = "sw")]
160 StartsWith,
161
162 #[serde(rename = "ew")]
164 EndsWith,
165
166 #[serde(rename = "pr")]
168 Present,
169
170 #[serde(rename = "gt")]
172 GreaterThan,
173
174 #[serde(rename = "ge")]
176 GreaterThanOrEqual,
177
178 #[serde(rename = "lt")]
180 LessThan,
181
182 #[serde(rename = "le")]
184 LessThanOrEqual,
185}
186
187pub trait CapabilityIntrospectable {
189 fn get_provider_specific_capabilities(&self) -> ExtendedCapabilities {
191 ExtendedCapabilities::default()
192 }
193
194 fn get_bulk_limits(&self) -> Option<BulkCapabilities> {
196 None
197 }
198
199 fn get_pagination_limits(&self) -> Option<PaginationCapabilities> {
201 None
202 }
203
204 fn get_authentication_capabilities(&self) -> Option<AuthenticationCapabilities> {
206 None
207 }
208}
209
210pub struct CapabilityDiscovery;
212
213impl CapabilityDiscovery {
214 pub fn discover_capabilities<P>(
219 schema_registry: &SchemaRegistry,
220 resource_handlers: &HashMap<String, std::sync::Arc<crate::resource::ResourceHandler>>,
221 supported_operations: &HashMap<String, Vec<ScimOperation>>,
222 _provider: &P,
223 ) -> Result<ProviderCapabilities, ScimError>
224 where
225 P: ResourceProvider,
226 {
227 let supported_schemas = Self::discover_schemas(schema_registry);
229
230 let supported_resource_types = Self::discover_resource_types(resource_handlers);
232
233 let supported_operations_map = supported_operations.clone();
235
236 let filter_capabilities =
238 Self::discover_filter_capabilities(schema_registry, resource_handlers)?;
239
240 let bulk_capabilities = Self::default_bulk_capabilities();
242 let pagination_capabilities = Self::default_pagination_capabilities();
243 let authentication_capabilities = Self::default_authentication_capabilities();
244 let mut extended_capabilities = ExtendedCapabilities::default();
245
246 extended_capabilities.etag_supported = true;
248
249 extended_capabilities.patch_supported = supported_operations
251 .values()
252 .any(|ops| ops.contains(&ScimOperation::Patch));
253
254 Ok(ProviderCapabilities {
255 supported_operations: supported_operations_map,
256 supported_schemas,
257 supported_resource_types,
258 bulk_capabilities,
259 filter_capabilities,
260 pagination_capabilities,
261 authentication_capabilities,
262 extended_capabilities,
263 })
264 }
265
266 pub fn discover_capabilities_with_introspection<P>(
271 schema_registry: &SchemaRegistry,
272 resource_handlers: &HashMap<String, std::sync::Arc<crate::resource::ResourceHandler>>,
273 supported_operations: &HashMap<String, Vec<ScimOperation>>,
274 provider: &P,
275 ) -> Result<ProviderCapabilities, ScimError>
276 where
277 P: ResourceProvider + CapabilityIntrospectable,
278 {
279 let supported_schemas = Self::discover_schemas(schema_registry);
281
282 let supported_resource_types = Self::discover_resource_types(resource_handlers);
284
285 let supported_operations_map = supported_operations.clone();
287
288 let filter_capabilities =
290 Self::discover_filter_capabilities(schema_registry, resource_handlers)?;
291
292 let bulk_capabilities = provider
294 .get_bulk_limits()
295 .unwrap_or_else(|| Self::default_bulk_capabilities());
296
297 let pagination_capabilities = provider
298 .get_pagination_limits()
299 .unwrap_or_else(|| Self::default_pagination_capabilities());
300
301 let authentication_capabilities = provider
302 .get_authentication_capabilities()
303 .unwrap_or_else(|| Self::default_authentication_capabilities());
304
305 let extended_capabilities = provider.get_provider_specific_capabilities();
306
307 Ok(ProviderCapabilities {
308 supported_operations: supported_operations_map,
309 supported_schemas,
310 supported_resource_types,
311 bulk_capabilities,
312 filter_capabilities,
313 pagination_capabilities,
314 authentication_capabilities,
315 extended_capabilities,
316 })
317 }
318
319 fn discover_schemas(schema_registry: &SchemaRegistry) -> Vec<String> {
321 schema_registry
322 .get_schemas()
323 .iter()
324 .map(|schema| schema.id.clone())
325 .collect()
326 }
327
328 fn discover_resource_types(
330 resource_handlers: &HashMap<String, std::sync::Arc<crate::resource::ResourceHandler>>,
331 ) -> Vec<String> {
332 resource_handlers.keys().cloned().collect()
333 }
334
335 fn discover_filter_capabilities(
337 schema_registry: &SchemaRegistry,
338 resource_handlers: &HashMap<String, std::sync::Arc<crate::resource::ResourceHandler>>,
339 ) -> Result<FilterCapabilities, ScimError> {
340 let mut filterable_attributes = HashMap::new();
341
342 for (resource_type, handler) in resource_handlers {
344 if let Some(schema) = schema_registry.get_schema(&handler.schema.id) {
346 let attrs = Self::collect_filterable_attributes(&schema.attributes, "");
348 filterable_attributes.insert(resource_type.clone(), attrs);
349 }
350 }
351
352 let supported_operators = Self::determine_supported_operators(schema_registry);
354
355 Ok(FilterCapabilities {
356 supported: !filterable_attributes.is_empty(),
357 max_results: Some(200), filterable_attributes,
359 supported_operators,
360 complex_filters_supported: true, })
362 }
363
364 fn is_attribute_filterable(attr: &AttributeDefinition) -> bool {
366 match attr.data_type {
369 crate::schema::AttributeType::Complex => false, _ => true, }
372 }
373
374 fn collect_filterable_attributes(
376 attributes: &[AttributeDefinition],
377 prefix: &str,
378 ) -> Vec<String> {
379 let mut filterable = Vec::new();
380
381 for attr in attributes {
382 let attr_name = if prefix.is_empty() {
383 attr.name.clone()
384 } else {
385 format!("{}.{}", prefix, attr.name)
386 };
387
388 if Self::is_attribute_filterable(attr) {
389 filterable.push(attr_name.clone());
390 }
391
392 if !attr.sub_attributes.is_empty() {
394 filterable.extend(Self::collect_filterable_attributes(
395 &attr.sub_attributes,
396 &attr_name,
397 ));
398 }
399 }
400
401 filterable
402 }
403
404 fn determine_supported_operators(schema_registry: &SchemaRegistry) -> Vec<FilterOperator> {
406 let mut operators = HashSet::new();
407
408 operators.insert(FilterOperator::Equal);
410 operators.insert(FilterOperator::NotEqual);
411 operators.insert(FilterOperator::Present);
412
413 if Self::has_string_attributes(schema_registry) {
415 operators.insert(FilterOperator::Contains);
416 operators.insert(FilterOperator::StartsWith);
417 operators.insert(FilterOperator::EndsWith);
418 }
419
420 if Self::has_comparable_attributes(schema_registry) {
422 operators.insert(FilterOperator::GreaterThan);
423 operators.insert(FilterOperator::GreaterThanOrEqual);
424 operators.insert(FilterOperator::LessThan);
425 operators.insert(FilterOperator::LessThanOrEqual);
426 }
427
428 operators.into_iter().collect()
429 }
430
431 fn has_string_attributes(schema_registry: &SchemaRegistry) -> bool {
433 fn has_string_in_attributes(attributes: &[AttributeDefinition]) -> bool {
434 attributes.iter().any(|attr| {
435 matches!(attr.data_type, crate::schema::AttributeType::String)
436 || has_string_in_attributes(&attr.sub_attributes)
437 })
438 }
439
440 schema_registry
441 .get_schemas()
442 .iter()
443 .any(|schema| has_string_in_attributes(&schema.attributes))
444 }
445
446 fn has_comparable_attributes(schema_registry: &SchemaRegistry) -> bool {
448 fn has_comparable_in_attributes(attributes: &[AttributeDefinition]) -> bool {
449 attributes.iter().any(|attr| {
450 matches!(
451 attr.data_type,
452 crate::schema::AttributeType::Integer
453 | crate::schema::AttributeType::Decimal
454 | crate::schema::AttributeType::DateTime
455 ) || has_comparable_in_attributes(&attr.sub_attributes)
456 })
457 }
458
459 schema_registry
460 .get_schemas()
461 .iter()
462 .any(|schema| has_comparable_in_attributes(&schema.attributes))
463 }
464
465 fn default_bulk_capabilities() -> BulkCapabilities {
467 BulkCapabilities {
468 supported: false, max_operations: None,
470 max_payload_size: None,
471 fail_on_errors_supported: false,
472 }
473 }
474
475 fn default_pagination_capabilities() -> PaginationCapabilities {
477 PaginationCapabilities {
478 supported: true, default_page_size: Some(20),
480 max_page_size: Some(200),
481 cursor_based_supported: false, }
483 }
484
485 fn default_authentication_capabilities() -> AuthenticationCapabilities {
487 AuthenticationCapabilities {
488 schemes: vec![], mfa_supported: false,
490 token_refresh_supported: false,
491 }
492 }
493
494 pub fn generate_service_provider_config(
496 capabilities: &ProviderCapabilities,
497 ) -> ServiceProviderConfig {
498 ServiceProviderConfig {
499 patch_supported: capabilities.extended_capabilities.patch_supported,
500 bulk_supported: capabilities.bulk_capabilities.supported,
501 filter_supported: capabilities.filter_capabilities.supported,
502 change_password_supported: capabilities.extended_capabilities.change_password_supported,
503 sort_supported: capabilities.extended_capabilities.sort_supported,
504 etag_supported: capabilities.extended_capabilities.etag_supported,
505 authentication_schemes: capabilities.authentication_capabilities.schemes.clone(),
506 bulk_max_operations: capabilities
507 .bulk_capabilities
508 .max_operations
509 .map(|n| n as u32),
510 bulk_max_payload_size: capabilities
511 .bulk_capabilities
512 .max_payload_size
513 .map(|n| n as u64),
514 filter_max_results: capabilities
515 .filter_capabilities
516 .max_results
517 .map(|n| n as u32),
518 }
519 }
520}
521
522impl Default for BulkCapabilities {
523 fn default() -> Self {
524 Self {
525 supported: false,
526 max_operations: None,
527 max_payload_size: None,
528 fail_on_errors_supported: false,
529 }
530 }
531}
532
533impl Default for FilterCapabilities {
534 fn default() -> Self {
535 Self {
536 supported: false,
537 max_results: Some(200),
538 filterable_attributes: HashMap::new(),
539 supported_operators: vec![FilterOperator::Equal, FilterOperator::Present],
540 complex_filters_supported: false,
541 }
542 }
543}
544
545impl Default for PaginationCapabilities {
546 fn default() -> Self {
547 Self {
548 supported: true,
549 default_page_size: Some(20),
550 max_page_size: Some(200),
551 cursor_based_supported: false,
552 }
553 }
554}
555
556impl Default for AuthenticationCapabilities {
557 fn default() -> Self {
558 Self {
559 schemes: vec![],
560 mfa_supported: false,
561 token_refresh_supported: false,
562 }
563 }
564}
565
566impl Default for ExtendedCapabilities {
567 fn default() -> Self {
568 Self {
569 etag_supported: true, patch_supported: false,
571 change_password_supported: false,
572 sort_supported: false,
573 custom_capabilities: HashMap::new(),
574 }
575 }
576}
577
578#[cfg(test)]
582mod tests {
583 use super::*;
584 use crate::schema::SchemaRegistry;
585 use std::collections::HashMap;
586
587 #[test]
588 fn test_discover_schemas() {
589 let registry = SchemaRegistry::new().expect("Failed to create schema registry");
590 let schemas = CapabilityDiscovery::discover_schemas(®istry);
591
592 assert!(!schemas.is_empty());
593 assert!(schemas.contains(&"urn:ietf:params:scim:schemas:core:2.0:User".to_string()));
594 }
595
596 #[test]
597 fn test_has_string_attributes() {
598 let registry = SchemaRegistry::new().expect("Failed to create schema registry");
599 assert!(CapabilityDiscovery::has_string_attributes(®istry));
600 }
601
602 #[test]
603 fn test_has_comparable_attributes() {
604 let registry = SchemaRegistry::new().expect("Failed to create schema registry");
605 assert!(CapabilityDiscovery::has_comparable_attributes(®istry));
606 }
607
608 #[test]
609 fn test_service_provider_config_generation() {
610 let capabilities = ProviderCapabilities {
611 supported_operations: HashMap::new(),
612 supported_schemas: vec!["urn:ietf:params:scim:schemas:core:2.0:User".to_string()],
613 supported_resource_types: vec!["User".to_string()],
614 bulk_capabilities: BulkCapabilities {
615 supported: true,
616 max_operations: Some(100),
617 max_payload_size: Some(1024 * 1024),
618 fail_on_errors_supported: true,
619 },
620 filter_capabilities: FilterCapabilities::default(),
621 pagination_capabilities: PaginationCapabilities::default(),
622 authentication_capabilities: AuthenticationCapabilities::default(),
623 extended_capabilities: ExtendedCapabilities {
624 patch_supported: true,
625 ..Default::default()
626 },
627 };
628
629 let config = CapabilityDiscovery::generate_service_provider_config(&capabilities);
630
631 assert!(config.bulk_supported);
632 assert!(config.patch_supported);
633 assert_eq!(config.bulk_max_operations, Some(100));
634 assert_eq!(config.bulk_max_payload_size, Some(1024 * 1024));
635 }
636
637 #[test]
638 fn test_filter_operators() {
639 let registry = SchemaRegistry::new().expect("Failed to create schema registry");
640 let operators = CapabilityDiscovery::determine_supported_operators(®istry);
641
642 log::debug!("Discovered filter operators: {:?}", operators);
643
644 assert!(operators.contains(&FilterOperator::Equal));
646 assert!(operators.contains(&FilterOperator::Present));
647
648 assert!(operators.contains(&FilterOperator::Contains));
650 assert!(operators.contains(&FilterOperator::StartsWith));
651
652 assert!(operators.contains(&FilterOperator::GreaterThan));
654 assert!(operators.contains(&FilterOperator::LessThan));
655 }
656}