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