scim_server/
provider_capabilities.rs

1//! Automated Provider Capability Discovery System
2//!
3//! This module provides automatic discovery of SCIM provider capabilities by introspecting
4//! the current server configuration, registered resource types, schemas, and provider
5//! implementation. This eliminates manual capability configuration and ensures that
6//! the ServiceProviderConfig always accurately reflects the actual server capabilities.
7//!
8//! # Key Features
9//!
10//! * **Automatic Discovery**: Capabilities are derived from registered components
11//! * **SCIM Compliance**: Generates RFC 7644 compliant ServiceProviderConfig
12//! * **Type Safety**: Leverages Rust's type system for capability constraints
13//! * **Real-time Updates**: Capabilities reflect current server state
14//! * **Mandatory ETag Support**: All providers automatically support conditional operations
15//!
16//! # Discovery Sources
17//!
18//! * **Schemas**: From SchemaRegistry - determines supported resource types
19//! * **Operations**: From registered resource handlers - determines CRUD capabilities
20//! * **Provider Type**: From ResourceProvider implementation - determines advanced features
21//! * **Attribute Metadata**: From schema definitions - determines filtering capabilities
22//! * **ETag Versioning**: Always enabled - conditional operations are mandatory for all providers
23
24use 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/// Comprehensive provider capability information automatically discovered
33/// from the current server configuration and registered components.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct ProviderCapabilities {
36    /// SCIM operations supported per resource type
37    pub supported_operations: HashMap<String, Vec<ScimOperation>>,
38
39    /// All schemas currently registered and available
40    pub supported_schemas: Vec<String>,
41
42    /// Resource types that can be managed
43    pub supported_resource_types: Vec<String>,
44
45    /// Bulk operation capabilities
46    pub bulk_capabilities: BulkCapabilities,
47
48    /// Filtering and query capabilities
49    pub filter_capabilities: FilterCapabilities,
50
51    /// Pagination support information
52    pub pagination_capabilities: PaginationCapabilities,
53
54    /// Authentication schemes available
55    pub authentication_capabilities: AuthenticationCapabilities,
56
57    /// Provider-specific extended capabilities
58    pub extended_capabilities: ExtendedCapabilities,
59}
60
61/// Bulk operation support information discovered from provider implementation
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct BulkCapabilities {
64    /// Whether bulk operations are supported at all
65    pub supported: bool,
66
67    /// Maximum number of operations in a single bulk request
68    pub max_operations: Option<usize>,
69
70    /// Maximum payload size for bulk requests in bytes
71    pub max_payload_size: Option<usize>,
72
73    /// Whether bulk operations support failOnErrors
74    pub fail_on_errors_supported: bool,
75}
76
77/// Filtering capabilities discovered from schema attribute definitions
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct FilterCapabilities {
80    /// Whether filtering is supported
81    pub supported: bool,
82
83    /// Maximum number of results that can be returned
84    pub max_results: Option<usize>,
85
86    /// Attributes that support filtering (derived from schema)
87    pub filterable_attributes: HashMap<String, Vec<String>>, // resource_type -> [attribute_names]
88
89    /// Supported filter operators
90    pub supported_operators: Vec<FilterOperator>,
91
92    /// Whether complex filters with AND/OR are supported
93    pub complex_filters_supported: bool,
94}
95
96/// Pagination support capabilities
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct PaginationCapabilities {
99    /// Whether pagination is supported
100    pub supported: bool,
101
102    /// Default page size
103    pub default_page_size: Option<usize>,
104
105    /// Maximum page size allowed
106    pub max_page_size: Option<usize>,
107
108    /// Whether cursor-based pagination is supported
109    pub cursor_based_supported: bool,
110}
111
112/// Authentication capabilities (typically configured rather than discovered)
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct AuthenticationCapabilities {
115    /// Supported authentication schemes
116    pub schemes: Vec<AuthenticationScheme>,
117
118    /// Whether multi-factor authentication is supported
119    pub mfa_supported: bool,
120
121    /// Whether token refresh is supported
122    pub token_refresh_supported: bool,
123}
124
125/// Extended capabilities specific to the provider implementation
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct ExtendedCapabilities {
128    /// Whether ETag versioning is supported (always true - conditional operations are mandatory)
129    pub etag_supported: bool,
130
131    /// Whether PATCH operations are supported
132    pub patch_supported: bool,
133
134    /// Whether password change operations are supported
135    pub change_password_supported: bool,
136
137    /// Whether sorting is supported
138    pub sort_supported: bool,
139
140    /// Custom provider-specific capabilities
141    pub custom_capabilities: HashMap<String, serde_json::Value>,
142}
143
144/// SCIM filter operators that can be supported
145#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
146pub enum FilterOperator {
147    /// Equal comparison
148    #[serde(rename = "eq")]
149    Equal,
150
151    /// Not equal comparison
152    #[serde(rename = "ne")]
153    NotEqual,
154
155    /// Contains operation for strings
156    #[serde(rename = "co")]
157    Contains,
158
159    /// Starts with operation for strings
160    #[serde(rename = "sw")]
161    StartsWith,
162
163    /// Ends with operation for strings
164    #[serde(rename = "ew")]
165    EndsWith,
166
167    /// Present (attribute exists)
168    #[serde(rename = "pr")]
169    Present,
170
171    /// Greater than
172    #[serde(rename = "gt")]
173    GreaterThan,
174
175    /// Greater than or equal
176    #[serde(rename = "ge")]
177    GreaterThanOrEqual,
178
179    /// Less than
180    #[serde(rename = "lt")]
181    LessThan,
182
183    /// Less than or equal
184    #[serde(rename = "le")]
185    LessThanOrEqual,
186}
187
188/// Trait for providers that support capability introspection
189pub trait CapabilityIntrospectable {
190    /// Get provider-specific capability information that cannot be auto-discovered
191    fn get_provider_specific_capabilities(&self) -> ExtendedCapabilities {
192        ExtendedCapabilities::default()
193    }
194
195    /// Get bulk operation limits from the provider
196    fn get_bulk_limits(&self) -> Option<BulkCapabilities> {
197        None
198    }
199
200    /// Get pagination limits from the provider
201    fn get_pagination_limits(&self) -> Option<PaginationCapabilities> {
202        None
203    }
204
205    /// Get authentication capabilities (usually configured)
206    fn get_authentication_capabilities(&self) -> Option<AuthenticationCapabilities> {
207        None
208    }
209}
210
211/// Automatic capability discovery engine that introspects server configuration
212pub struct CapabilityDiscovery;
213
214impl CapabilityDiscovery {
215    /// Discover all provider capabilities from the current server state
216    ///
217    /// This method introspects the registered resource types, schemas, and provider
218    /// implementation to automatically determine what capabilities are supported.
219    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        // Discover supported schemas from registry
229        let supported_schemas = Self::discover_schemas(schema_registry);
230
231        // Discover resource types from registered handlers
232        let supported_resource_types = Self::discover_resource_types(resource_handlers);
233
234        // Copy operation support directly from registration
235        let supported_operations_map = supported_operations.clone();
236
237        // Discover filtering capabilities from schema attributes
238        let filter_capabilities =
239            Self::discover_filter_capabilities(schema_registry, resource_handlers)?;
240
241        // Use default capabilities for basic providers
242        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        // Ensure ETag support is always enabled (conditional operations are mandatory)
248        extended_capabilities.etag_supported = true;
249
250        // Detect patch support from registered operations
251        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    /// Discover capabilities with provider introspection
268    ///
269    /// This version works with providers that implement CapabilityIntrospectable
270    /// to get provider-specific capability information.
271    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        // Discover supported schemas from registry
281        let supported_schemas = Self::discover_schemas(schema_registry);
282
283        // Discover resource types from registered handlers
284        let supported_resource_types = Self::discover_resource_types(resource_handlers);
285
286        // Copy operation support directly from registration
287        let supported_operations_map = supported_operations.clone();
288
289        // Discover filtering capabilities from schema attributes
290        let filter_capabilities =
291            Self::discover_filter_capabilities(schema_registry, resource_handlers)?;
292
293        // Get provider-specific capabilities
294        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    /// Discover all registered schemas
321    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    /// Discover registered resource types
330    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    /// Discover filtering capabilities from schema attribute definitions
337    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 each resource type, discover which attributes can be filtered
344        for (resource_type, handler) in resource_handlers {
345            // Get schema for this resource type
346            if let Some(schema) = schema_registry.get_schema(&handler.schema.id) {
347                // Recursively collect all filterable attributes including sub-attributes
348                let attrs = Self::collect_filterable_attributes(&schema.attributes, "");
349                filterable_attributes.insert(resource_type.clone(), attrs);
350            }
351        }
352
353        // Determine supported operators based on attribute types
354        let supported_operators = Self::determine_supported_operators(schema_registry);
355
356        Ok(FilterCapabilities {
357            supported: !filterable_attributes.is_empty(),
358            max_results: Some(200), // Default SCIM recommendation
359            filterable_attributes,
360            supported_operators,
361            complex_filters_supported: true, // Most implementations support AND/OR
362        })
363    }
364
365    /// Determine if an attribute can be used in filters
366    fn is_attribute_filterable(attr: &AttributeDefinition) -> bool {
367        // Most simple attributes are filterable
368        // Complex attributes and some special cases may not be
369        match attr.data_type {
370            crate::schema::AttributeType::Complex => false, // Complex attributes typically not directly filterable
371            _ => true, // String, boolean, integer, decimal, dateTime, binary, reference are filterable
372        }
373    }
374
375    /// Recursively collect filterable attributes from a schema
376    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            // Recursively check sub-attributes
394            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    /// Determine which filter operators are supported based on schema attribute types
406    fn determine_supported_operators(schema_registry: &SchemaRegistry) -> Vec<FilterOperator> {
407        let mut operators = HashSet::new();
408
409        // Basic operators always supported
410        operators.insert(FilterOperator::Equal);
411        operators.insert(FilterOperator::NotEqual);
412        operators.insert(FilterOperator::Present);
413
414        // Check if we have string attributes (enables string operations)
415        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        // Check if we have numeric/date attributes (enables comparison operations)
422        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    /// Check if any registered schemas have string attributes
433    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    /// Check if any registered schemas have comparable attributes (numeric, date)
448    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    /// Default bulk capabilities for providers that don't specify them
467    fn default_bulk_capabilities() -> BulkCapabilities {
468        BulkCapabilities {
469            supported: false, // Conservative default
470            max_operations: None,
471            max_payload_size: None,
472            fail_on_errors_supported: false,
473        }
474    }
475
476    /// Default pagination capabilities
477    fn default_pagination_capabilities() -> PaginationCapabilities {
478        PaginationCapabilities {
479            supported: true, // Most providers support basic pagination
480            default_page_size: Some(20),
481            max_page_size: Some(200),
482            cursor_based_supported: false, // Conservative default
483        }
484    }
485
486    /// Default authentication capabilities
487    fn default_authentication_capabilities() -> AuthenticationCapabilities {
488        AuthenticationCapabilities {
489            schemes: vec![], // Must be explicitly configured
490            mfa_supported: false,
491            token_refresh_supported: false,
492        }
493    }
494
495    /// Generate RFC 7644 compliant ServiceProviderConfig from discovered capabilities
496    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, // Always true - conditional operations are mandatory
571            patch_supported: false,
572            change_password_supported: false,
573            sort_supported: false,
574            custom_capabilities: HashMap::new(),
575        }
576    }
577}
578
579// Default implementation can be provided via a blanket impl, but users can override
580// by implementing the trait directly on their provider types
581
582#[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(&registry);
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(&registry));
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(&registry));
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(&registry);
642
643        log::debug!("Discovered filter operators: {:?}", operators);
644
645        // Should have basic operators
646        assert!(operators.contains(&FilterOperator::Equal));
647        assert!(operators.contains(&FilterOperator::Present));
648
649        // Should have string operators since User schema has string attributes
650        assert!(operators.contains(&FilterOperator::Contains));
651        assert!(operators.contains(&FilterOperator::StartsWith));
652
653        // Should have comparison operators since User schema has dateTime attributes (in sub-attributes)
654        assert!(operators.contains(&FilterOperator::GreaterThan));
655        assert!(operators.contains(&FilterOperator::LessThan));
656    }
657}