scim_server/resource/value_objects/
composite_validation.rs1use super::value_object_trait::{CompositeValidator, ValueObject};
15use super::{EmailAddress, Name, ResourceId, UserName};
16use crate::error::{ValidationError, ValidationResult};
17use std::collections::HashSet;
18
19#[derive(Debug)]
24pub struct UniquePrimaryValidator;
25
26impl UniquePrimaryValidator {
27 pub fn new() -> Self {
28 Self
29 }
30
31 fn has_primary_values(&self, obj: &dyn ValueObject) -> bool {
33 let attr_name = obj.attribute_name();
36 matches!(
37 attr_name,
38 "emails" | "phoneNumbers" | "addresses" | "members"
39 )
40 }
41
42 fn validate_primary_uniqueness(&self, obj: &dyn ValueObject) -> ValidationResult<()> {
44 if self.has_primary_values(obj) {
48 Ok(())
51 } else {
52 Ok(())
53 }
54 }
55}
56
57impl CompositeValidator for UniquePrimaryValidator {
58 fn validate_composite(&self, objects: &[Box<dyn ValueObject>]) -> ValidationResult<()> {
59 for obj in objects {
60 self.validate_primary_uniqueness(obj.as_ref())?;
61 }
62 Ok(())
63 }
64
65 fn dependent_attributes(&self) -> Vec<String> {
66 vec![
67 "emails".to_string(),
68 "phoneNumbers".to_string(),
69 "addresses".to_string(),
70 "members".to_string(),
71 ]
72 }
73
74 fn applies_to(&self, attribute_names: &[String]) -> bool {
75 let dependent = self.dependent_attributes();
76 attribute_names.iter().any(|name| dependent.contains(name))
77 }
78}
79
80#[derive(Debug)]
85pub struct UserNameUniquenessValidator {
86 case_insensitive: bool,
88 reserved_names: HashSet<String>,
90}
91
92impl UserNameUniquenessValidator {
93 pub fn new(case_insensitive: bool) -> Self {
94 let mut reserved_names = HashSet::new();
95 reserved_names.insert("admin".to_string());
96 reserved_names.insert("root".to_string());
97 reserved_names.insert("system".to_string());
98 reserved_names.insert("api".to_string());
99 reserved_names.insert("null".to_string());
100 reserved_names.insert("undefined".to_string());
101
102 Self {
103 case_insensitive,
104 reserved_names,
105 }
106 }
107
108 pub fn with_reserved_names(mut self, names: Vec<String>) -> Self {
109 for name in names {
110 self.reserved_names.insert(if self.case_insensitive {
111 name.to_lowercase()
112 } else {
113 name
114 });
115 }
116 self
117 }
118
119 fn validate_username(&self, username: &UserName) -> ValidationResult<()> {
120 let username_str = username.as_str();
121 let check_name = if self.case_insensitive {
122 username_str.to_lowercase()
123 } else {
124 username_str.to_string()
125 };
126
127 if self.reserved_names.contains(&check_name) {
128 return Err(ValidationError::ReservedUsername(username_str.to_string()));
129 }
130
131 if username_str.len() < 3 {
133 return Err(ValidationError::UsernameTooShort(username_str.to_string()));
134 }
135
136 if username_str.len() > 64 {
137 return Err(ValidationError::UsernameTooLong(username_str.to_string()));
138 }
139
140 if username_str.contains("..")
142 || username_str.starts_with('.')
143 || username_str.ends_with('.')
144 {
145 return Err(ValidationError::InvalidUsernameFormat(
146 username_str.to_string(),
147 ));
148 }
149
150 Ok(())
151 }
152}
153
154impl CompositeValidator for UserNameUniquenessValidator {
155 fn validate_composite(&self, objects: &[Box<dyn ValueObject>]) -> ValidationResult<()> {
156 for obj in objects {
157 if obj.attribute_name() == "userName" {
158 if let Some(username) = obj.as_any().downcast_ref::<UserName>() {
159 self.validate_username(username)?;
160 }
161 }
162 }
163 Ok(())
164 }
165
166 fn dependent_attributes(&self) -> Vec<String> {
167 vec!["userName".to_string()]
168 }
169
170 fn applies_to(&self, attribute_names: &[String]) -> bool {
171 attribute_names.contains(&"userName".to_string())
172 }
173}
174
175#[derive(Debug)]
180pub struct EmailConsistencyValidator {
181 allowed_domains: Option<Vec<String>>,
183 require_work_email: bool,
185}
186
187impl EmailConsistencyValidator {
188 pub fn new() -> Self {
189 Self {
190 allowed_domains: None,
191 require_work_email: false,
192 }
193 }
194
195 pub fn with_allowed_domains(mut self, domains: Vec<String>) -> Self {
196 self.allowed_domains = Some(domains);
197 self
198 }
199
200 pub fn with_work_email_requirement(mut self, required: bool) -> Self {
201 self.require_work_email = required;
202 self
203 }
204
205 fn validate_email_domain(&self, email: &EmailAddress) -> ValidationResult<()> {
206 if let Some(ref allowed_domains) = self.allowed_domains {
207 let email_str = email.value();
208 if let Some(domain) = email_str.split('@').nth(1) {
209 if !allowed_domains.iter().any(|d| domain.ends_with(d)) {
210 return Err(ValidationError::InvalidEmailDomain {
211 email: email_str.to_string(),
212 allowed_domains: allowed_domains.clone(),
213 });
214 }
215 }
216 }
217 Ok(())
218 }
219
220 fn has_work_email(&self, objects: &[Box<dyn ValueObject>]) -> bool {
221 for obj in objects {
222 if obj.attribute_name() == "emails" {
223 return true; }
227 }
228 false
229 }
230}
231
232impl CompositeValidator for EmailConsistencyValidator {
233 fn validate_composite(&self, objects: &[Box<dyn ValueObject>]) -> ValidationResult<()> {
234 for obj in objects {
236 if let Some(email) = obj.as_any().downcast_ref::<EmailAddress>() {
237 self.validate_email_domain(email)?;
238 }
239 }
240
241 if self.require_work_email && !self.has_work_email(objects) {
243 return Err(ValidationError::WorkEmailRequired);
244 }
245
246 Ok(())
247 }
248
249 fn dependent_attributes(&self) -> Vec<String> {
250 vec!["emails".to_string()]
251 }
252
253 fn applies_to(&self, attribute_names: &[String]) -> bool {
254 attribute_names.contains(&"emails".to_string())
255 }
256}
257
258#[derive(Debug)]
263pub struct IdentityConsistencyValidator {
264 require_external_id: bool,
266 validate_id_format: bool,
268}
269
270impl IdentityConsistencyValidator {
271 pub fn new() -> Self {
272 Self {
273 require_external_id: false,
274 validate_id_format: true,
275 }
276 }
277
278 pub fn with_external_id_requirement(mut self, required: bool) -> Self {
279 self.require_external_id = required;
280 self
281 }
282
283 pub fn with_id_format_validation(mut self, enabled: bool) -> Self {
284 self.validate_id_format = enabled;
285 self
286 }
287
288 fn find_attribute<'a, T: 'static>(&self, objects: &'a [Box<dyn ValueObject>]) -> Option<&'a T> {
289 for obj in objects {
290 if let Some(typed_obj) = obj.as_any().downcast_ref::<T>() {
291 return Some(typed_obj);
292 }
293 }
294 None
295 }
296
297 fn validate_id_format_consistency(
298 &self,
299 objects: &[Box<dyn ValueObject>],
300 ) -> ValidationResult<()> {
301 if !self.validate_id_format {
302 return Ok(());
303 }
304
305 if let Some(resource_id) = self.find_attribute::<ResourceId>(objects) {
306 let id_str = resource_id.as_str();
307
308 if id_str.contains('-') && id_str.len() == 36 {
310 if id_str.chars().filter(|&c| c == '-').count() != 4 {
312 return Err(ValidationError::InvalidIdFormat {
313 id: id_str.to_string(),
314 });
315 }
316 }
317 }
318
319 Ok(())
320 }
321}
322
323impl CompositeValidator for IdentityConsistencyValidator {
324 fn validate_composite(&self, objects: &[Box<dyn ValueObject>]) -> ValidationResult<()> {
325 if self.require_external_id {
327 let has_external_id = objects
328 .iter()
329 .any(|obj| obj.attribute_name() == "externalId");
330 if !has_external_id {
331 return Err(ValidationError::ExternalIdRequired);
332 }
333 }
334
335 self.validate_id_format_consistency(objects)?;
337
338 Ok(())
339 }
340
341 fn dependent_attributes(&self) -> Vec<String> {
342 vec![
343 "id".to_string(),
344 "userName".to_string(),
345 "externalId".to_string(),
346 ]
347 }
348
349 fn applies_to(&self, attribute_names: &[String]) -> bool {
350 let dependent = self.dependent_attributes();
351 attribute_names.iter().any(|name| dependent.contains(name))
352 }
353}
354
355#[derive(Debug)]
360pub struct NameConsistencyValidator {
361 validate_formatted_name: bool,
363 require_name_component: bool,
365}
366
367impl NameConsistencyValidator {
368 pub fn new() -> Self {
369 Self {
370 validate_formatted_name: true,
371 require_name_component: true,
372 }
373 }
374
375 pub fn with_formatted_name_validation(mut self, enabled: bool) -> Self {
376 self.validate_formatted_name = enabled;
377 self
378 }
379
380 pub fn with_name_component_requirement(mut self, required: bool) -> Self {
381 self.require_name_component = required;
382 self
383 }
384
385 fn validate_name_object(&self, name: &Name) -> ValidationResult<()> {
386 if self.require_name_component {
387 if name.given_name().is_none()
388 && name.family_name().is_none()
389 && name.formatted().is_none()
390 {
391 return Err(ValidationError::NameComponentRequired);
392 }
393 }
394
395 if self.validate_formatted_name {
396 if let Some(formatted) = name.formatted() {
398 if formatted.trim().is_empty() {
399 return Err(ValidationError::EmptyFormattedName);
400 }
401 }
402 }
403
404 Ok(())
405 }
406}
407
408impl CompositeValidator for NameConsistencyValidator {
409 fn validate_composite(&self, objects: &[Box<dyn ValueObject>]) -> ValidationResult<()> {
410 for obj in objects {
411 if obj.attribute_name() == "name" {
412 if let Some(name) = obj.as_any().downcast_ref::<Name>() {
413 self.validate_name_object(name)?;
414 }
415 }
416 }
417 Ok(())
418 }
419
420 fn dependent_attributes(&self) -> Vec<String> {
421 vec!["name".to_string()]
422 }
423
424 fn applies_to(&self, attribute_names: &[String]) -> bool {
425 attribute_names.contains(&"name".to_string())
426 }
427}
428
429pub struct CompositeValidatorChain {
434 validators: Vec<Box<dyn CompositeValidator>>,
435}
436
437impl CompositeValidatorChain {
438 pub fn new() -> Self {
439 Self {
440 validators: Vec::new(),
441 }
442 }
443
444 pub fn add_validator(mut self, validator: Box<dyn CompositeValidator>) -> Self {
445 self.validators.push(validator);
446 self
447 }
448
449 pub fn with_default_validators() -> Self {
450 Self::new()
451 .add_validator(Box::new(UniquePrimaryValidator::new()))
452 .add_validator(Box::new(UserNameUniquenessValidator::new(true)))
453 .add_validator(Box::new(EmailConsistencyValidator::new()))
454 .add_validator(Box::new(IdentityConsistencyValidator::new()))
455 .add_validator(Box::new(NameConsistencyValidator::new()))
456 }
457}
458
459impl CompositeValidator for CompositeValidatorChain {
460 fn validate_composite(&self, objects: &[Box<dyn ValueObject>]) -> ValidationResult<()> {
461 for validator in &self.validators {
462 validator.validate_composite(objects)?;
463 }
464 Ok(())
465 }
466
467 fn dependent_attributes(&self) -> Vec<String> {
468 let mut all_deps = Vec::new();
469 for validator in &self.validators {
470 all_deps.extend(validator.dependent_attributes());
471 }
472 all_deps.sort();
473 all_deps.dedup();
474 all_deps
475 }
476
477 fn applies_to(&self, attribute_names: &[String]) -> bool {
478 self.validators
479 .iter()
480 .any(|v| v.applies_to(attribute_names))
481 }
482}
483
484impl Default for CompositeValidatorChain {
485 fn default() -> Self {
486 Self::with_default_validators()
487 }
488}
489
490#[cfg(test)]
491mod tests {
492 use super::*;
493 use crate::resource::value_objects::UserName;
494
495 fn create_test_objects() -> Vec<Box<dyn ValueObject>> {
496 vec![
497 Box::new(ResourceId::new("test-id".to_string()).unwrap()),
498 Box::new(UserName::new("testuser".to_string()).unwrap()),
499 Box::new(EmailAddress::new("test@example.com".to_string(), None, None, None).unwrap()),
500 ]
501 }
502
503 #[test]
504 fn test_unique_primary_validator() {
505 let validator = UniquePrimaryValidator::new();
506 let objects = create_test_objects();
507
508 assert!(validator.validate_composite(&objects).is_ok());
509 assert!(validator.applies_to(&["emails".to_string()]));
510 assert!(!validator.applies_to(&["id".to_string()]));
511 }
512
513 #[test]
514 fn test_username_uniqueness_validator() {
515 let validator = UserNameUniquenessValidator::new(true);
516 let objects = create_test_objects();
517
518 assert!(validator.validate_composite(&objects).is_ok());
519
520 let reserved_objects =
522 vec![Box::new(UserName::new("admin".to_string()).unwrap()) as Box<dyn ValueObject>];
523 assert!(validator.validate_composite(&reserved_objects).is_err());
524 }
525
526 #[test]
527 fn test_email_consistency_validator() {
528 let validator =
529 EmailConsistencyValidator::new().with_allowed_domains(vec!["example.com".to_string()]);
530
531 let objects = create_test_objects();
532 assert!(validator.validate_composite(&objects).is_ok());
533
534 let invalid_objects = vec![Box::new(
536 EmailAddress::new("test@invalid.com".to_string(), None, None, None).unwrap(),
537 ) as Box<dyn ValueObject>];
538 assert!(validator.validate_composite(&invalid_objects).is_err());
539 }
540
541 #[test]
542 fn test_identity_consistency_validator() {
543 let validator = IdentityConsistencyValidator::new().with_external_id_requirement(true);
544
545 let objects = create_test_objects();
546 assert!(validator.validate_composite(&objects).is_err());
548
549 }
554
555 #[test]
556 fn test_composite_validator_chain() {
557 let chain = CompositeValidatorChain::with_default_validators();
558 let objects = create_test_objects();
559
560 let _result = chain.validate_composite(&objects);
562
563 assert!(!chain.dependent_attributes().is_empty());
565 assert!(chain.applies_to(&["userName".to_string()]));
566 }
567
568 #[test]
569 fn test_username_length_validation() {
570 let validator = UserNameUniquenessValidator::new(false);
571
572 let short_objects =
574 vec![Box::new(UserName::new("ab".to_string()).unwrap()) as Box<dyn ValueObject>];
575 assert!(validator.validate_composite(&short_objects).is_err());
576
577 let valid_objects =
579 vec![Box::new(UserName::new("validuser".to_string()).unwrap()) as Box<dyn ValueObject>];
580 assert!(validator.validate_composite(&valid_objects).is_ok());
581 }
582
583 #[test]
584 fn test_name_consistency_validator() {
585 let validator = NameConsistencyValidator::new();
586
587 let objects = vec![]; assert!(validator.validate_composite(&objects).is_ok());
592 assert!(validator.applies_to(&["name".to_string()]));
593 }
594}