use crate::providers::ResourceProvider;
pub trait ScimValidator: ResourceProvider {
fn is_valid_scim_path(&self, path: &str) -> bool {
if path.is_empty() {
return false;
}
let actual_path = if path.contains(':') && path.contains("urn:ietf:params:scim:schemas:") {
if let Some(colon_pos) = path.rfind(':') {
&path[colon_pos + 1..]
} else {
path
}
} else {
path
};
let clean_path = if actual_path.contains('[') {
if let Some(bracket_pos) = actual_path.find('[') {
&actual_path[..bracket_pos]
} else {
actual_path
}
} else {
actual_path
};
if clean_path.contains('.') {
self.is_valid_complex_path(clean_path)
} else {
self.is_valid_simple_path(clean_path)
}
}
fn is_valid_complex_path(&self, path: &str) -> bool {
let parts: Vec<&str> = path.split('.').collect();
if parts.len() < 2 {
return false;
}
parts.iter().all(|part| {
!part.is_empty()
&& part.chars().all(|c| c.is_alphanumeric() || c == '_')
&& part.chars().next().map_or(false, |c| c.is_alphabetic())
})
}
fn is_valid_simple_path(&self, attribute: &str) -> bool {
let user_attributes = [
"id",
"externalId",
"userName",
"password",
"displayName",
"nickName",
"profileUrl",
"title",
"userType",
"preferredLanguage",
"locale",
"timezone",
"active",
"name",
"emails",
"phoneNumbers",
"ims",
"photos",
"addresses",
"groups",
"entitlements",
"roles",
"x509Certificates",
"meta",
];
let group_attributes = ["id", "externalId", "displayName", "members", "meta"];
let meta_attributes = [
"resourceType",
"created",
"lastModified",
"location",
"version",
];
let complex_sub_attributes = [
"formatted",
"familyName",
"givenName",
"middleName",
"honorificPrefix",
"honorificSuffix",
"value",
"display",
"type",
"primary",
"operation",
"$ref",
"streetAddress",
"locality",
"region",
"postalCode",
"country",
"employeeNumber",
"costCenter",
"organization",
"division",
"department",
"manager",
];
user_attributes.contains(&attribute) ||
group_attributes.contains(&attribute) ||
meta_attributes.contains(&attribute) ||
complex_sub_attributes.contains(&attribute) ||
(attribute.chars().next().map_or(false, |c| c.is_alphabetic()) &&
attribute.chars().all(|c| c.is_alphanumeric() || c == '_'))
}
fn is_multivalued_attribute(&self, attribute_name: &str) -> bool {
matches!(
attribute_name,
"emails"
| "phoneNumbers"
| "ims"
| "photos"
| "addresses"
| "groups"
| "entitlements"
| "roles"
| "x509Certificates"
| "members" )
}
fn is_readonly_attribute(&self, path: &str) -> bool {
match path.to_lowercase().as_str() {
"id" => true,
"meta" => true,
"meta.resourcetype" => true,
"meta.created" => true,
"meta.location" => true,
path if path.starts_with("meta.")
&& (path.ends_with(".resourcetype")
|| path.ends_with(".created")
|| path.ends_with(".location")) =>
{
true
}
"groups.display" => true,
"groups.$ref" => true,
_ => false,
}
}
fn is_valid_username(&self, username: &str) -> bool {
if username.is_empty() || username.len() > 256 {
return false;
}
if username.trim().is_empty() || username.chars().any(|c| c.is_control()) {
return false;
}
username.chars().all(|c| {
c.is_alphanumeric() || c == '.' || c == '_' || c == '-' || c == '@' || c == '+'
})
}
fn is_valid_external_id(&self, external_id: &str) -> bool {
if external_id.is_empty() || external_id.len() > 512 {
return false;
}
!external_id.trim().is_empty() && !external_id.chars().any(|c| c.is_control())
}
fn is_valid_schema_uri(&self, schema_uri: &str) -> bool {
if schema_uri.is_empty() {
return false;
}
schema_uri.starts_with("urn:") &&
schema_uri.contains("scim:schemas") &&
schema_uri.len() <= 512 &&
!schema_uri.chars().any(|c| c.is_control())
}
fn extract_base_attribute<'a>(&self, path: &'a str) -> &'a str {
let clean_path = if path.contains(':') && path.contains("urn:ietf:params:scim:schemas:") {
if let Some(colon_pos) = path.rfind(':') {
&path[colon_pos + 1..]
} else {
path
}
} else {
path
};
let without_filter = if let Some(bracket_pos) = clean_path.find('[') {
&clean_path[..bracket_pos]
} else {
clean_path
};
if let Some(dot_pos) = without_filter.find('.') {
&without_filter[..dot_pos]
} else {
without_filter
}
}
}
impl<T: ResourceProvider> ScimValidator for T {}