use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::time::Duration;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ScimConfigurationError {
#[error("SCIM configuration validation failed: {message}")]
ValidationError { message: String },
#[error("SCIM configuration not found for tenant: {tenant_id}")]
NotFound { tenant_id: String },
#[error("SCIM client configuration conflict: {message}")]
ClientConflict { message: String },
#[error("Invalid SCIM endpoint configuration: {message}")]
InvalidEndpoint { message: String },
#[error("SCIM schema extension error: {message}")]
SchemaExtensionError { message: String },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ScimTenantConfiguration {
pub tenant_id: String,
pub created_at: DateTime<Utc>,
pub last_modified: DateTime<Utc>,
pub version: u64,
pub endpoint: ScimEndpointConfig,
pub clients: Vec<ScimClientConfig>,
pub rate_limits: ScimRateLimits,
pub schema_config: ScimSchemaConfig,
pub audit_config: ScimAuditConfig,
pub search_config: ScimSearchConfig,
}
impl ScimTenantConfiguration {
pub fn builder(tenant_id: String) -> ScimTenantConfigurationBuilder {
ScimTenantConfigurationBuilder::new(tenant_id)
}
pub fn get_client_config(&self, client_id: &str) -> Option<&ScimClientConfig> {
self.clients.iter().find(|c| c.client_id == client_id)
}
pub fn is_rate_limited(&self, operation: &str, current_count: u32) -> bool {
match operation {
"create" => self.rate_limits.check_create_limit(current_count),
"read" => self.rate_limits.check_read_limit(current_count),
"update" => self.rate_limits.check_update_limit(current_count),
"delete" => self.rate_limits.check_delete_limit(current_count),
"list" => self.rate_limits.check_list_limit(current_count),
"search" => self.rate_limits.check_search_limit(current_count),
_ => false,
}
}
pub fn has_schema_extension(&self, extension_uri: &str) -> bool {
self.schema_config
.extensions
.iter()
.any(|ext| ext.uri == extension_uri && ext.enabled)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ScimEndpointConfig {
pub base_path: String,
pub include_tenant_in_path: bool,
pub tenant_path_pattern: Option<String>,
pub max_payload_size: usize,
pub scim_version: String,
pub supported_auth_schemes: Vec<ScimAuthScheme>,
}
impl Default for ScimEndpointConfig {
fn default() -> Self {
Self {
base_path: "/scim/v2".to_string(),
include_tenant_in_path: false,
tenant_path_pattern: None,
max_payload_size: 1024 * 1024, scim_version: "2.0".to_string(),
supported_auth_schemes: vec![ScimAuthScheme::Bearer, ScimAuthScheme::ApiKey],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ScimAuthScheme {
Bearer,
ApiKey,
Basic,
OAuth2,
Custom(String),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ScimClientConfig {
pub client_id: String,
pub client_name: String,
pub auth_config: ScimClientAuth,
pub rate_limits: Option<ScimRateLimits>,
pub allowed_operations: Vec<ScimOperation>,
pub allowed_resource_types: Vec<String>,
pub audit_enabled: bool,
pub metadata: HashMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ScimClientAuth {
pub scheme: ScimAuthScheme,
pub credentials: HashMap<String, String>,
pub token_expiration: Option<Duration>,
pub ip_restrictions: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ScimOperation {
Create,
Read,
Update,
Patch,
Delete,
List,
Search,
Bulk,
Schema,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ScimRateLimits {
pub create_operations: Option<RateLimit>,
pub read_operations: Option<RateLimit>,
pub update_operations: Option<RateLimit>,
pub delete_operations: Option<RateLimit>,
pub list_operations: Option<RateLimit>,
pub search_operations: Option<RateLimit>,
pub bulk_operations: Option<RateLimit>,
pub global_limit: Option<RateLimit>,
}
impl ScimRateLimits {
pub fn check_create_limit(&self, current_count: u32) -> bool {
self.create_operations
.as_ref()
.map_or(false, |limit| current_count >= limit.max_requests)
}
pub fn check_read_limit(&self, current_count: u32) -> bool {
self.read_operations
.as_ref()
.map_or(false, |limit| current_count >= limit.max_requests)
}
pub fn check_update_limit(&self, current_count: u32) -> bool {
self.update_operations
.as_ref()
.map_or(false, |limit| current_count >= limit.max_requests)
}
pub fn check_delete_limit(&self, current_count: u32) -> bool {
self.delete_operations
.as_ref()
.map_or(false, |limit| current_count >= limit.max_requests)
}
pub fn check_list_limit(&self, current_count: u32) -> bool {
self.list_operations
.as_ref()
.map_or(false, |limit| current_count >= limit.max_requests)
}
pub fn check_search_limit(&self, current_count: u32) -> bool {
self.search_operations
.as_ref()
.map_or(false, |limit| current_count >= limit.max_requests)
}
}
impl Default for ScimRateLimits {
fn default() -> Self {
Self {
create_operations: Some(RateLimit::new(100, Duration::from_secs(60))),
read_operations: Some(RateLimit::new(1000, Duration::from_secs(60))),
update_operations: Some(RateLimit::new(100, Duration::from_secs(60))),
delete_operations: Some(RateLimit::new(50, Duration::from_secs(60))),
list_operations: Some(RateLimit::new(200, Duration::from_secs(60))),
search_operations: Some(RateLimit::new(100, Duration::from_secs(60))),
bulk_operations: Some(RateLimit::new(10, Duration::from_secs(60))),
global_limit: Some(RateLimit::new(2000, Duration::from_secs(60))),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct RateLimit {
pub max_requests: u32,
#[serde(with = "duration_serde")]
pub window: Duration,
pub burst_allowance: Option<u32>,
}
impl RateLimit {
pub fn new(max_requests: u32, window: Duration) -> Self {
Self {
max_requests,
window,
burst_allowance: None,
}
}
pub fn with_burst(mut self, burst: u32) -> Self {
self.burst_allowance = Some(burst);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ScimSchemaConfig {
pub extensions: Vec<ScimSchemaExtension>,
pub custom_attributes: HashMap<String, ScimCustomAttribute>,
pub disabled_attributes: Vec<String>,
pub additional_required: Vec<String>,
}
impl Default for ScimSchemaConfig {
fn default() -> Self {
Self {
extensions: Vec::new(),
custom_attributes: HashMap::new(),
disabled_attributes: Vec::new(),
additional_required: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ScimSchemaExtension {
pub uri: String,
pub enabled: bool,
pub required: bool,
pub attributes: HashMap<String, ScimCustomAttribute>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ScimCustomAttribute {
pub name: String,
pub attribute_type: String,
pub multi_valued: bool,
pub required: bool,
pub case_exact: bool,
pub mutability: String,
pub returned: String,
pub uniqueness: String,
pub description: Option<String>,
pub canonical_values: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ScimAuditConfig {
pub enabled: bool,
pub audited_operations: Vec<ScimOperation>,
pub include_payloads: bool,
pub include_sensitive_data: bool,
#[serde(with = "duration_serde")]
pub retention_period: Duration,
pub additional_metadata: HashMap<String, String>,
}
impl Default for ScimAuditConfig {
fn default() -> Self {
Self {
enabled: true,
audited_operations: vec![
ScimOperation::Create,
ScimOperation::Update,
ScimOperation::Delete,
],
include_payloads: false,
include_sensitive_data: false,
retention_period: Duration::from_secs(90 * 24 * 60 * 60), additional_metadata: HashMap::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ScimSearchConfig {
pub max_results: u32,
pub default_count: u32,
pub max_filter_depth: u32,
pub filterable_attributes: Vec<String>,
pub sortable_attributes: Vec<String>,
pub case_insensitive_filtering: bool,
pub custom_operators: Vec<String>,
}
impl Default for ScimSearchConfig {
fn default() -> Self {
Self {
max_results: 200,
default_count: 20,
max_filter_depth: 10,
filterable_attributes: vec![
"userName".to_string(),
"displayName".to_string(),
"emails.value".to_string(),
"active".to_string(),
"meta.created".to_string(),
"meta.lastModified".to_string(),
],
sortable_attributes: vec![
"userName".to_string(),
"displayName".to_string(),
"meta.created".to_string(),
"meta.lastModified".to_string(),
],
case_insensitive_filtering: true,
custom_operators: Vec::new(),
}
}
}
pub struct ScimTenantConfigurationBuilder {
tenant_id: String,
endpoint: Option<ScimEndpointConfig>,
clients: Vec<ScimClientConfig>,
rate_limits: Option<ScimRateLimits>,
schema_config: Option<ScimSchemaConfig>,
audit_config: Option<ScimAuditConfig>,
search_config: Option<ScimSearchConfig>,
}
impl ScimTenantConfigurationBuilder {
pub fn new(tenant_id: String) -> Self {
Self {
tenant_id,
endpoint: None,
clients: Vec::new(),
rate_limits: None,
schema_config: None,
audit_config: None,
search_config: None,
}
}
pub fn with_endpoint_path(mut self, path: &str) -> Self {
let mut endpoint = self.endpoint.unwrap_or_default();
endpoint.base_path = path.to_string();
self.endpoint = Some(endpoint);
self
}
pub fn with_scim_rate_limit(mut self, max_requests: u32, window: Duration) -> Self {
let rate_limit = RateLimit::new(max_requests, window);
let mut rate_limits = self.rate_limits.unwrap_or_default();
rate_limits.global_limit = Some(rate_limit.clone());
rate_limits.create_operations = Some(rate_limit.clone());
rate_limits.read_operations = Some(rate_limit.clone());
rate_limits.update_operations = Some(rate_limit.clone());
rate_limits.delete_operations = Some(rate_limit.clone());
rate_limits.list_operations = Some(rate_limit.clone());
rate_limits.search_operations = Some(rate_limit.clone());
rate_limits.bulk_operations = Some(rate_limit);
self.rate_limits = Some(rate_limits);
self
}
pub fn with_scim_client(mut self, client_id: &str, api_key: &str) -> Self {
let mut credentials = HashMap::new();
credentials.insert("api_key".to_string(), api_key.to_string());
let client = ScimClientConfig {
client_id: client_id.to_string(),
client_name: client_id.to_string(),
auth_config: ScimClientAuth {
scheme: ScimAuthScheme::ApiKey,
credentials,
token_expiration: None,
ip_restrictions: Vec::new(),
},
rate_limits: None,
allowed_operations: vec![
ScimOperation::Create,
ScimOperation::Read,
ScimOperation::Update,
ScimOperation::Delete,
ScimOperation::List,
ScimOperation::Search,
],
allowed_resource_types: vec!["User".to_string(), "Group".to_string()],
audit_enabled: true,
metadata: HashMap::new(),
};
self.clients.push(client);
self
}
pub fn enable_scim_audit_log(mut self) -> Self {
let mut audit_config = self.audit_config.unwrap_or_default();
audit_config.enabled = true;
self.audit_config = Some(audit_config);
self
}
pub fn with_schema_extension(mut self, uri: &str, required: bool) -> Self {
let mut schema_config = self.schema_config.unwrap_or_default();
schema_config.extensions.push(ScimSchemaExtension {
uri: uri.to_string(),
enabled: true,
required,
attributes: HashMap::new(),
});
self.schema_config = Some(schema_config);
self
}
pub fn build(self) -> Result<ScimTenantConfiguration, ScimConfigurationError> {
let now = Utc::now();
Ok(ScimTenantConfiguration {
tenant_id: self.tenant_id,
created_at: now,
last_modified: now,
version: 1,
endpoint: self.endpoint.unwrap_or_default(),
clients: self.clients,
rate_limits: self.rate_limits.unwrap_or_default(),
schema_config: self.schema_config.unwrap_or_default(),
audit_config: self.audit_config.unwrap_or_default(),
search_config: self.search_config.unwrap_or_default(),
})
}
}
mod duration_serde {
use serde::{Deserialize, Deserializer, Serializer};
use std::time::Duration;
pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u64(duration.as_secs())
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
D: Deserializer<'de>,
{
let secs = u64::deserialize(deserializer)?;
Ok(Duration::from_secs(secs))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scim_tenant_configuration_builder() {
let config = ScimTenantConfiguration::builder("test-tenant".to_string())
.with_endpoint_path("/scim/v2")
.with_scim_rate_limit(100, Duration::from_secs(60))
.with_scim_client("client-1", "api_key_123")
.enable_scim_audit_log()
.with_schema_extension(
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User",
false,
)
.build()
.expect("Valid SCIM configuration");
assert_eq!(config.tenant_id, "test-tenant");
assert_eq!(config.endpoint.base_path, "/scim/v2");
assert_eq!(config.clients.len(), 1);
assert_eq!(config.clients[0].client_id, "client-1");
assert!(config.audit_config.enabled);
assert_eq!(config.schema_config.extensions.len(), 1);
}
#[test]
fn test_rate_limit_checking() {
let rate_limits = ScimRateLimits::default();
assert!(!rate_limits.check_create_limit(50));
assert!(rate_limits.check_create_limit(100));
assert!(rate_limits.check_create_limit(150));
}
#[test]
fn test_client_config_lookup() {
let config = ScimTenantConfiguration::builder("test-tenant".to_string())
.with_scim_client("client-1", "api_key_123")
.with_scim_client("client-2", "api_key_456")
.build()
.expect("Valid configuration");
assert!(config.get_client_config("client-1").is_some());
assert!(config.get_client_config("client-2").is_some());
assert!(config.get_client_config("client-3").is_none());
}
#[test]
fn test_schema_extension_checking() {
let config = ScimTenantConfiguration::builder("test-tenant".to_string())
.with_schema_extension(
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User",
true,
)
.build()
.expect("Valid configuration");
assert!(
config
.has_schema_extension("urn:ietf:params:scim:schemas:extension:enterprise:2.0:User")
);
assert!(!config.has_schema_extension("urn:example:custom:extension"));
}
#[test]
fn test_default_configurations() {
let endpoint = ScimEndpointConfig::default();
assert_eq!(endpoint.base_path, "/scim/v2");
assert_eq!(endpoint.scim_version, "2.0");
let rate_limits = ScimRateLimits::default();
assert!(rate_limits.create_operations.is_some());
assert!(rate_limits.global_limit.is_some());
let audit_config = ScimAuditConfig::default();
assert!(audit_config.enabled);
assert_eq!(audit_config.audited_operations.len(), 3);
}
}