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 Ok(ProviderCapabilities {
250 supported_operations: supported_operations_map,
251 supported_schemas,
252 supported_resource_types,
253 bulk_capabilities,
254 filter_capabilities,
255 pagination_capabilities,
256 authentication_capabilities,
257 extended_capabilities,
258 })
259 }
260
261 pub fn discover_capabilities_with_introspection<P>(
266 schema_registry: &SchemaRegistry,
267 resource_handlers: &HashMap<String, std::sync::Arc<crate::resource::ResourceHandler>>,
268 supported_operations: &HashMap<String, Vec<ScimOperation>>,
269 provider: &P,
270 ) -> Result<ProviderCapabilities, ScimError>
271 where
272 P: ResourceProvider + CapabilityIntrospectable,
273 {
274 let supported_schemas = Self::discover_schemas(schema_registry);
276
277 let supported_resource_types = Self::discover_resource_types(resource_handlers);
279
280 let supported_operations_map = supported_operations.clone();
282
283 let filter_capabilities =
285 Self::discover_filter_capabilities(schema_registry, resource_handlers)?;
286
287 let bulk_capabilities = provider
289 .get_bulk_limits()
290 .unwrap_or_else(|| Self::default_bulk_capabilities());
291
292 let pagination_capabilities = provider
293 .get_pagination_limits()
294 .unwrap_or_else(|| Self::default_pagination_capabilities());
295
296 let authentication_capabilities = provider
297 .get_authentication_capabilities()
298 .unwrap_or_else(|| Self::default_authentication_capabilities());
299
300 let extended_capabilities = provider.get_provider_specific_capabilities();
301
302 Ok(ProviderCapabilities {
303 supported_operations: supported_operations_map,
304 supported_schemas,
305 supported_resource_types,
306 bulk_capabilities,
307 filter_capabilities,
308 pagination_capabilities,
309 authentication_capabilities,
310 extended_capabilities,
311 })
312 }
313
314 fn discover_schemas(schema_registry: &SchemaRegistry) -> Vec<String> {
316 schema_registry
317 .get_schemas()
318 .iter()
319 .map(|schema| schema.id.clone())
320 .collect()
321 }
322
323 fn discover_resource_types(
325 resource_handlers: &HashMap<String, std::sync::Arc<crate::resource::ResourceHandler>>,
326 ) -> Vec<String> {
327 resource_handlers.keys().cloned().collect()
328 }
329
330 fn discover_filter_capabilities(
332 schema_registry: &SchemaRegistry,
333 resource_handlers: &HashMap<String, std::sync::Arc<crate::resource::ResourceHandler>>,
334 ) -> Result<FilterCapabilities, ScimError> {
335 let mut filterable_attributes = HashMap::new();
336
337 for (resource_type, handler) in resource_handlers {
339 if let Some(schema) = schema_registry.get_schema(&handler.schema.id) {
341 let attrs = Self::collect_filterable_attributes(&schema.attributes, "");
343 filterable_attributes.insert(resource_type.clone(), attrs);
344 }
345 }
346
347 let supported_operators = Self::determine_supported_operators(schema_registry);
349
350 Ok(FilterCapabilities {
351 supported: !filterable_attributes.is_empty(),
352 max_results: Some(200), filterable_attributes,
354 supported_operators,
355 complex_filters_supported: true, })
357 }
358
359 fn is_attribute_filterable(attr: &AttributeDefinition) -> bool {
361 match attr.data_type {
364 crate::schema::AttributeType::Complex => false, _ => true, }
367 }
368
369 fn collect_filterable_attributes(
371 attributes: &[AttributeDefinition],
372 prefix: &str,
373 ) -> Vec<String> {
374 let mut filterable = Vec::new();
375
376 for attr in attributes {
377 let attr_name = if prefix.is_empty() {
378 attr.name.clone()
379 } else {
380 format!("{}.{}", prefix, attr.name)
381 };
382
383 if Self::is_attribute_filterable(attr) {
384 filterable.push(attr_name.clone());
385 }
386
387 if !attr.sub_attributes.is_empty() {
389 filterable.extend(Self::collect_filterable_attributes(
390 &attr.sub_attributes,
391 &attr_name,
392 ));
393 }
394 }
395
396 filterable
397 }
398
399 fn determine_supported_operators(schema_registry: &SchemaRegistry) -> Vec<FilterOperator> {
401 let mut operators = HashSet::new();
402
403 operators.insert(FilterOperator::Equal);
405 operators.insert(FilterOperator::NotEqual);
406 operators.insert(FilterOperator::Present);
407
408 if Self::has_string_attributes(schema_registry) {
410 operators.insert(FilterOperator::Contains);
411 operators.insert(FilterOperator::StartsWith);
412 operators.insert(FilterOperator::EndsWith);
413 }
414
415 if Self::has_comparable_attributes(schema_registry) {
417 operators.insert(FilterOperator::GreaterThan);
418 operators.insert(FilterOperator::GreaterThanOrEqual);
419 operators.insert(FilterOperator::LessThan);
420 operators.insert(FilterOperator::LessThanOrEqual);
421 }
422
423 operators.into_iter().collect()
424 }
425
426 fn has_string_attributes(schema_registry: &SchemaRegistry) -> bool {
428 fn has_string_in_attributes(attributes: &[AttributeDefinition]) -> bool {
429 attributes.iter().any(|attr| {
430 matches!(attr.data_type, crate::schema::AttributeType::String)
431 || has_string_in_attributes(&attr.sub_attributes)
432 })
433 }
434
435 schema_registry
436 .get_schemas()
437 .iter()
438 .any(|schema| has_string_in_attributes(&schema.attributes))
439 }
440
441 fn has_comparable_attributes(schema_registry: &SchemaRegistry) -> bool {
443 fn has_comparable_in_attributes(attributes: &[AttributeDefinition]) -> bool {
444 attributes.iter().any(|attr| {
445 matches!(
446 attr.data_type,
447 crate::schema::AttributeType::Integer
448 | crate::schema::AttributeType::Decimal
449 | crate::schema::AttributeType::DateTime
450 ) || has_comparable_in_attributes(&attr.sub_attributes)
451 })
452 }
453
454 schema_registry
455 .get_schemas()
456 .iter()
457 .any(|schema| has_comparable_in_attributes(&schema.attributes))
458 }
459
460 fn default_bulk_capabilities() -> BulkCapabilities {
462 BulkCapabilities {
463 supported: false, max_operations: None,
465 max_payload_size: None,
466 fail_on_errors_supported: false,
467 }
468 }
469
470 fn default_pagination_capabilities() -> PaginationCapabilities {
472 PaginationCapabilities {
473 supported: true, default_page_size: Some(20),
475 max_page_size: Some(200),
476 cursor_based_supported: false, }
478 }
479
480 fn default_authentication_capabilities() -> AuthenticationCapabilities {
482 AuthenticationCapabilities {
483 schemes: vec![], mfa_supported: false,
485 token_refresh_supported: false,
486 }
487 }
488
489 pub fn generate_service_provider_config(
491 capabilities: &ProviderCapabilities,
492 ) -> ServiceProviderConfig {
493 ServiceProviderConfig {
494 patch_supported: capabilities.extended_capabilities.patch_supported,
495 bulk_supported: capabilities.bulk_capabilities.supported,
496 filter_supported: capabilities.filter_capabilities.supported,
497 change_password_supported: capabilities.extended_capabilities.change_password_supported,
498 sort_supported: capabilities.extended_capabilities.sort_supported,
499 etag_supported: capabilities.extended_capabilities.etag_supported,
500 authentication_schemes: capabilities.authentication_capabilities.schemes.clone(),
501 bulk_max_operations: capabilities
502 .bulk_capabilities
503 .max_operations
504 .map(|n| n as u32),
505 bulk_max_payload_size: capabilities
506 .bulk_capabilities
507 .max_payload_size
508 .map(|n| n as u64),
509 filter_max_results: capabilities
510 .filter_capabilities
511 .max_results
512 .map(|n| n as u32),
513 }
514 }
515}
516
517impl Default for BulkCapabilities {
518 fn default() -> Self {
519 Self {
520 supported: false,
521 max_operations: None,
522 max_payload_size: None,
523 fail_on_errors_supported: false,
524 }
525 }
526}
527
528impl Default for FilterCapabilities {
529 fn default() -> Self {
530 Self {
531 supported: false,
532 max_results: Some(200),
533 filterable_attributes: HashMap::new(),
534 supported_operators: vec![FilterOperator::Equal, FilterOperator::Present],
535 complex_filters_supported: false,
536 }
537 }
538}
539
540impl Default for PaginationCapabilities {
541 fn default() -> Self {
542 Self {
543 supported: true,
544 default_page_size: Some(20),
545 max_page_size: Some(200),
546 cursor_based_supported: false,
547 }
548 }
549}
550
551impl Default for AuthenticationCapabilities {
552 fn default() -> Self {
553 Self {
554 schemes: vec![],
555 mfa_supported: false,
556 token_refresh_supported: false,
557 }
558 }
559}
560
561impl Default for ExtendedCapabilities {
562 fn default() -> Self {
563 Self {
564 etag_supported: true, patch_supported: false,
566 change_password_supported: false,
567 sort_supported: false,
568 custom_capabilities: HashMap::new(),
569 }
570 }
571}
572
573#[cfg(test)]
577mod tests {
578 use super::*;
579 use crate::schema::SchemaRegistry;
580 use std::collections::HashMap;
581
582 #[test]
583 fn test_discover_schemas() {
584 let registry = SchemaRegistry::new().expect("Failed to create schema registry");
585 let schemas = CapabilityDiscovery::discover_schemas(®istry);
586
587 assert!(!schemas.is_empty());
588 assert!(schemas.contains(&"urn:ietf:params:scim:schemas:core:2.0:User".to_string()));
589 }
590
591 #[test]
592 fn test_has_string_attributes() {
593 let registry = SchemaRegistry::new().expect("Failed to create schema registry");
594 assert!(CapabilityDiscovery::has_string_attributes(®istry));
595 }
596
597 #[test]
598 fn test_has_comparable_attributes() {
599 let registry = SchemaRegistry::new().expect("Failed to create schema registry");
600 assert!(CapabilityDiscovery::has_comparable_attributes(®istry));
601 }
602
603 #[test]
604 fn test_service_provider_config_generation() {
605 let capabilities = ProviderCapabilities {
606 supported_operations: HashMap::new(),
607 supported_schemas: vec!["urn:ietf:params:scim:schemas:core:2.0:User".to_string()],
608 supported_resource_types: vec!["User".to_string()],
609 bulk_capabilities: BulkCapabilities {
610 supported: true,
611 max_operations: Some(100),
612 max_payload_size: Some(1024 * 1024),
613 fail_on_errors_supported: true,
614 },
615 filter_capabilities: FilterCapabilities::default(),
616 pagination_capabilities: PaginationCapabilities::default(),
617 authentication_capabilities: AuthenticationCapabilities::default(),
618 extended_capabilities: ExtendedCapabilities {
619 patch_supported: true,
620 ..Default::default()
621 },
622 };
623
624 let config = CapabilityDiscovery::generate_service_provider_config(&capabilities);
625
626 assert!(config.bulk_supported);
627 assert!(config.patch_supported);
628 assert_eq!(config.bulk_max_operations, Some(100));
629 assert_eq!(config.bulk_max_payload_size, Some(1024 * 1024));
630 }
631
632 #[test]
633 fn test_filter_operators() {
634 let registry = SchemaRegistry::new().expect("Failed to create schema registry");
635 let operators = CapabilityDiscovery::determine_supported_operators(®istry);
636
637 log::debug!("Discovered filter operators: {:?}", operators);
638
639 assert!(operators.contains(&FilterOperator::Equal));
641 assert!(operators.contains(&FilterOperator::Present));
642
643 assert!(operators.contains(&FilterOperator::Contains));
645 assert!(operators.contains(&FilterOperator::StartsWith));
646
647 assert!(operators.contains(&FilterOperator::GreaterThan));
649 assert!(operators.contains(&FilterOperator::LessThan));
650 }
651}