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        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    /// Discover capabilities with provider introspection
262    ///
263    /// This version works with providers that implement CapabilityIntrospectable
264    /// to get provider-specific capability information.
265    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        // Discover supported schemas from registry
275        let supported_schemas = Self::discover_schemas(schema_registry);
276
277        // Discover resource types from registered handlers
278        let supported_resource_types = Self::discover_resource_types(resource_handlers);
279
280        // Copy operation support directly from registration
281        let supported_operations_map = supported_operations.clone();
282
283        // Discover filtering capabilities from schema attributes
284        let filter_capabilities =
285            Self::discover_filter_capabilities(schema_registry, resource_handlers)?;
286
287        // Get provider-specific capabilities
288        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    /// Discover all registered schemas
315    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    /// Discover registered resource types
324    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    /// Discover filtering capabilities from schema attribute definitions
331    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 each resource type, discover which attributes can be filtered
338        for (resource_type, handler) in resource_handlers {
339            // Get schema for this resource type
340            if let Some(schema) = schema_registry.get_schema(&handler.schema.id) {
341                // Recursively collect all filterable attributes including sub-attributes
342                let attrs = Self::collect_filterable_attributes(&schema.attributes, "");
343                filterable_attributes.insert(resource_type.clone(), attrs);
344            }
345        }
346
347        // Determine supported operators based on attribute types
348        let supported_operators = Self::determine_supported_operators(schema_registry);
349
350        Ok(FilterCapabilities {
351            supported: !filterable_attributes.is_empty(),
352            max_results: Some(200), // Default SCIM recommendation
353            filterable_attributes,
354            supported_operators,
355            complex_filters_supported: true, // Most implementations support AND/OR
356        })
357    }
358
359    /// Determine if an attribute can be used in filters
360    fn is_attribute_filterable(attr: &AttributeDefinition) -> bool {
361        // Most simple attributes are filterable
362        // Complex attributes and some special cases may not be
363        match attr.data_type {
364            crate::schema::AttributeType::Complex => false, // Complex attributes typically not directly filterable
365            _ => true, // String, boolean, integer, decimal, dateTime, binary, reference are filterable
366        }
367    }
368
369    /// Recursively collect filterable attributes from a schema
370    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            // Recursively check sub-attributes
388            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    /// Determine which filter operators are supported based on schema attribute types
400    fn determine_supported_operators(schema_registry: &SchemaRegistry) -> Vec<FilterOperator> {
401        let mut operators = HashSet::new();
402
403        // Basic operators always supported
404        operators.insert(FilterOperator::Equal);
405        operators.insert(FilterOperator::NotEqual);
406        operators.insert(FilterOperator::Present);
407
408        // Check if we have string attributes (enables string operations)
409        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        // Check if we have numeric/date attributes (enables comparison operations)
416        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    /// Check if any registered schemas have string attributes
427    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    /// Check if any registered schemas have comparable attributes (numeric, date)
442    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    /// Default bulk capabilities for providers that don't specify them
461    fn default_bulk_capabilities() -> BulkCapabilities {
462        BulkCapabilities {
463            supported: false, // Conservative default
464            max_operations: None,
465            max_payload_size: None,
466            fail_on_errors_supported: false,
467        }
468    }
469
470    /// Default pagination capabilities
471    fn default_pagination_capabilities() -> PaginationCapabilities {
472        PaginationCapabilities {
473            supported: true, // Most providers support basic pagination
474            default_page_size: Some(20),
475            max_page_size: Some(200),
476            cursor_based_supported: false, // Conservative default
477        }
478    }
479
480    /// Default authentication capabilities
481    fn default_authentication_capabilities() -> AuthenticationCapabilities {
482        AuthenticationCapabilities {
483            schemes: vec![], // Must be explicitly configured
484            mfa_supported: false,
485            token_refresh_supported: false,
486        }
487    }
488
489    /// Generate RFC 7644 compliant ServiceProviderConfig from discovered capabilities
490    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, // Always true - conditional operations are mandatory
565            patch_supported: false,
566            change_password_supported: false,
567            sort_supported: false,
568            custom_capabilities: HashMap::new(),
569        }
570    }
571}
572
573// Default implementation can be provided via a blanket impl, but users can override
574// by implementing the trait directly on their provider types
575
576#[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(&registry);
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(&registry));
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(&registry));
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(&registry);
636
637        log::debug!("Discovered filter operators: {:?}", operators);
638
639        // Should have basic operators
640        assert!(operators.contains(&FilterOperator::Equal));
641        assert!(operators.contains(&FilterOperator::Present));
642
643        // Should have string operators since User schema has string attributes
644        assert!(operators.contains(&FilterOperator::Contains));
645        assert!(operators.contains(&FilterOperator::StartsWith));
646
647        // Should have comparison operators since User schema has dateTime attributes (in sub-attributes)
648        assert!(operators.contains(&FilterOperator::GreaterThan));
649        assert!(operators.contains(&FilterOperator::LessThan));
650    }
651}