1use super::{MigrationConfig, MigrationError, MigrationOperation, MigrationPlan, ValidationType};
7use std::collections::{HashMap, HashSet};
8
9pub async fn validate_migration_plan(
11 plan: &MigrationPlan,
12 _config: &MigrationConfig,
13) -> Result<Vec<String>, MigrationError> {
14 let mut warnings = Vec::new();
15
16 validate_phase_dependencies(plan, &mut warnings)?;
18
19 validate_role_mappings(plan, &mut warnings)?;
21
22 validate_permission_mappings(plan, &mut warnings)?;
24
25 validate_user_migrations(plan, &mut warnings)?;
27
28 validate_backup_operations(plan, &mut warnings)?;
30
31 validate_rollback_plan(plan, &mut warnings)?;
33
34 validate_validation_steps(plan, &mut warnings)?;
36
37 check_migration_pitfalls(plan, &mut warnings)?;
39
40 Ok(warnings)
41}
42
43fn validate_phase_dependencies(
45 plan: &MigrationPlan,
46 warnings: &mut Vec<String>,
47) -> Result<(), MigrationError> {
48 let mut phase_ids: HashSet<String> = HashSet::new();
49 let mut phase_order_map: HashMap<String, u32> = HashMap::new();
50
51 for phase in &plan.phases {
53 if phase_ids.contains(&phase.id) {
54 return Err(MigrationError::ValidationError(format!(
55 "Duplicate phase ID found: {}",
56 phase.id
57 )));
58 }
59 phase_ids.insert(phase.id.clone());
60 phase_order_map.insert(phase.id.clone(), phase.order);
61 }
62
63 if has_circular_dependencies(&plan.phases) {
65 return Err(MigrationError::ValidationError(
66 "Circular dependencies detected in migration phases".to_string(),
67 ));
68 }
69
70 for phase in &plan.phases {
72 for dependency in &phase.dependencies {
73 if !phase_ids.contains(dependency) {
74 return Err(MigrationError::ValidationError(format!(
75 "Phase '{}' depends on non-existent phase '{}'",
76 phase.id, dependency
77 )));
78 }
79
80 if let Some(&dep_order) = phase_order_map.get(dependency)
82 && dep_order >= phase.order
83 {
84 return Err(MigrationError::ValidationError(format!(
85 "Phase '{}' (order {}) depends on phase '{}' (order {}), but dependency should have lower order",
86 phase.id, phase.order, dependency, dep_order
87 )));
88 }
89 }
90 }
91
92 let phases_with_deps: HashSet<_> = plan.phases.iter().flat_map(|p| &p.dependencies).collect();
96
97 for phase in &plan.phases {
98 if phase.dependencies.is_empty() && phase.order > 1 {
99 warnings.push(format!(
100 "Phase '{}' has no dependencies but is not the first phase",
101 phase.id
102 ));
103 }
104 if !phases_with_deps.contains(&phase.id) && phase.order < plan.phases.len() as u32 {
105 warnings.push(format!(
106 "Phase '{}' is not a dependency of any other phase",
107 phase.id
108 ));
109 }
110 }
111
112 Ok(())
113}
114
115fn has_circular_dependencies(phases: &[super::MigrationPhase]) -> bool {
117 let mut graph: HashMap<String, Vec<String>> = HashMap::new();
118
119 for phase in phases {
120 graph.insert(phase.id.clone(), phase.dependencies.clone());
121 }
122
123 for phase_id in graph.keys() {
124 if has_cycle_from_node(phase_id, &graph, &mut HashSet::new(), &mut HashSet::new()) {
125 return true;
126 }
127 }
128
129 false
130}
131
132fn has_cycle_from_node(
134 node: &str,
135 graph: &HashMap<String, Vec<String>>,
136 visiting: &mut HashSet<String>,
137 visited: &mut HashSet<String>,
138) -> bool {
139 if visiting.contains(node) {
140 return true; }
142
143 if visited.contains(node) {
144 return false; }
146
147 visiting.insert(node.to_string());
148
149 if let Some(dependencies) = graph.get(node) {
150 for dep in dependencies {
151 if has_cycle_from_node(dep, graph, visiting, visited) {
152 return true;
153 }
154 }
155 }
156
157 visiting.remove(node);
158 visited.insert(node.to_string());
159 false
160}
161
162fn validate_role_mappings(
164 plan: &MigrationPlan,
165 warnings: &mut Vec<String>,
166) -> Result<(), MigrationError> {
167 let legacy_roles: HashSet<_> = plan.source_analysis.roles.iter().map(|r| &r.id).collect();
168
169 let mapped_roles: HashSet<_> = plan.role_mappings.keys().collect();
170
171 for legacy_role in &legacy_roles {
173 if !mapped_roles.contains(legacy_role) {
174 warnings.push(format!(
175 "Legacy role '{}' is not mapped to a new role",
176 legacy_role
177 ));
178 }
179 }
180
181 let mut new_role_ids: HashMap<&String, Vec<&String>> = HashMap::new();
183 for (legacy_id, new_id) in &plan.role_mappings {
184 new_role_ids.entry(new_id).or_default().push(legacy_id);
185 }
186
187 for (new_id, legacy_ids) in new_role_ids {
188 if legacy_ids.len() > 1 {
189 warnings.push(format!(
190 "New role '{}' is mapped from multiple legacy roles: {:?}",
191 new_id, legacy_ids
192 ));
193 }
194 }
195
196 let role_creation_ops: HashSet<_> = plan
198 .phases
199 .iter()
200 .flat_map(|p| &p.operations)
201 .filter_map(|op| match op {
202 MigrationOperation::CreateRole { role_id, .. } => Some(role_id),
203 _ => None,
204 })
205 .collect();
206
207 for new_role_id in plan.role_mappings.values() {
208 if !role_creation_ops.contains(new_role_id) {
209 warnings.push(format!(
210 "Role '{}' is mapped but not created in any phase",
211 new_role_id
212 ));
213 }
214 }
215
216 Ok(())
217}
218
219fn validate_permission_mappings(
221 plan: &MigrationPlan,
222 warnings: &mut Vec<String>,
223) -> Result<(), MigrationError> {
224 let legacy_permissions: HashSet<_> = plan
225 .source_analysis
226 .permissions
227 .iter()
228 .map(|p| &p.id)
229 .collect();
230
231 let mapped_permissions: HashSet<_> = plan.permission_mappings.keys().collect();
232
233 for legacy_permission in &legacy_permissions {
235 if !mapped_permissions.contains(legacy_permission) {
236 warnings.push(format!(
237 "Legacy permission '{}' is not mapped",
238 legacy_permission
239 ));
240 }
241 }
242
243 let permission_creation_ops: HashSet<_> = plan
245 .phases
246 .iter()
247 .flat_map(|p| &p.operations)
248 .filter_map(|op| match op {
249 MigrationOperation::CreatePermission { permission_id, .. } => Some(permission_id),
250 _ => None,
251 })
252 .collect();
253
254 for new_permission_id in plan.permission_mappings.values() {
255 if !permission_creation_ops.contains(new_permission_id) {
256 warnings.push(format!(
257 "Permission '{}' is mapped but not created in any phase",
258 new_permission_id
259 ));
260 }
261 }
262
263 Ok(())
264}
265
266fn validate_user_migrations(
268 plan: &MigrationPlan,
269 warnings: &mut Vec<String>,
270) -> Result<(), MigrationError> {
271 let legacy_users: HashSet<_> = plan
272 .source_analysis
273 .user_assignments
274 .iter()
275 .map(|u| &u.user_id)
276 .collect();
277
278 let migrated_users: HashSet<_> = plan.user_migrations.iter().map(|m| &m.user_id).collect();
279
280 for legacy_user in &legacy_users {
282 if !migrated_users.contains(legacy_user) {
283 warnings.push(format!(
284 "User '{}' has legacy assignments but no migration plan",
285 legacy_user
286 ));
287 }
288 }
289
290 let user_assignment_ops: HashSet<_> = plan
292 .phases
293 .iter()
294 .flat_map(|p| &p.operations)
295 .filter_map(|op| match op {
296 MigrationOperation::AssignUserRole { user_id, .. } => Some(user_id),
297 _ => None,
298 })
299 .collect();
300
301 for user_migration in &plan.user_migrations {
302 if user_migration.user_id != "TEMPLATE"
303 && !user_assignment_ops.contains(&user_migration.user_id)
304 {
305 warnings.push(format!(
306 "User '{}' has migration plan but no assignment operations",
307 user_migration.user_id
308 ));
309 }
310 }
311
312 for user_migration in &plan.user_migrations {
314 for legacy_role in &user_migration.legacy_roles {
315 if !plan.role_mappings.contains_key(legacy_role)
316 && legacy_role != "REQUIRES_MANUAL_MAPPING"
317 {
318 warnings.push(format!(
319 "User migration for '{}' references unmapped legacy role '{}'",
320 user_migration.user_id, legacy_role
321 ));
322 }
323 }
324 }
325
326 Ok(())
327}
328
329fn validate_backup_operations(
331 plan: &MigrationPlan,
332 warnings: &mut Vec<String>,
333) -> Result<(), MigrationError> {
334 let backup_ops: Vec<_> = plan
335 .phases
336 .iter()
337 .flat_map(|p| &p.operations)
338 .filter_map(|op| match op {
339 MigrationOperation::Backup {
340 backup_location,
341 backup_type,
342 } => Some((backup_location, backup_type)),
343 _ => None,
344 })
345 .collect();
346
347 if backup_ops.is_empty() {
348 return Err(MigrationError::ValidationError(
349 "No backup operations found in migration plan".to_string(),
350 ));
351 }
352
353 if let Some(first_phase) = plan.phases.first() {
355 let has_backup = first_phase
356 .operations
357 .iter()
358 .any(|op| matches!(op, MigrationOperation::Backup { .. }));
359
360 if !has_backup {
361 warnings.push("First phase does not include a backup operation".to_string());
362 }
363 }
364
365 let mut backup_locations = HashSet::new();
367 for (location, _) in backup_ops {
368 if !backup_locations.insert(location) {
369 warnings.push(format!("Duplicate backup location: {:?}", location));
370 }
371 }
372
373 Ok(())
374}
375
376fn validate_rollback_plan(
378 plan: &MigrationPlan,
379 warnings: &mut Vec<String>,
380) -> Result<(), MigrationError> {
381 if plan.rollback_plan.phases.is_empty() {
382 return Err(MigrationError::ValidationError(
383 "Rollback plan has no phases".to_string(),
384 ));
385 }
386
387 if plan.rollback_plan.backup_locations.is_empty() {
388 warnings.push("Rollback plan has no backup locations specified".to_string());
389 }
390
391 let mut last_order = 0;
393 for phase in &plan.rollback_plan.phases {
394 if phase.order <= last_order && last_order > 0 {
395 warnings.push(format!("Rollback phase '{}' has incorrect order", phase.id));
396 }
397 last_order = phase.order;
398 }
399
400 if plan.rollback_plan.recovery_time_objective > chrono::Duration::hours(24) {
402 warnings.push("Recovery Time Objective exceeds 24 hours".to_string());
403 }
404
405 Ok(())
406}
407
408fn validate_validation_steps(
410 plan: &MigrationPlan,
411 warnings: &mut Vec<String>,
412) -> Result<(), MigrationError> {
413 let required_validations = vec![
415 ValidationType::HierarchyIntegrity,
416 ValidationType::PermissionConsistency,
417 ValidationType::UserAssignmentValidity,
418 ];
419
420 let post_validation_types: HashSet<_> = plan
421 .post_validation_steps
422 .iter()
423 .map(|step| &step.validation_type)
424 .collect();
425
426 for required_validation in required_validations {
427 if !post_validation_types.contains(&required_validation) {
428 warnings.push(format!(
429 "Missing required post-migration validation: {:?}",
430 required_validation
431 ));
432 }
433 }
434
435 let mut validation_ids = HashSet::new();
437 for step in &plan.pre_validation_steps {
438 if !validation_ids.insert(&step.id) {
439 return Err(MigrationError::ValidationError(format!(
440 "Duplicate pre-validation step ID: {}",
441 step.id
442 )));
443 }
444 }
445
446 validation_ids.clear();
447 for step in &plan.post_validation_steps {
448 if !validation_ids.insert(&step.id) {
449 return Err(MigrationError::ValidationError(format!(
450 "Duplicate post-validation step ID: {}",
451 step.id
452 )));
453 }
454 }
455
456 Ok(())
457}
458
459fn check_migration_pitfalls(
461 plan: &MigrationPlan,
462 warnings: &mut Vec<String>,
463) -> Result<(), MigrationError> {
464 check_privilege_escalation_risks(plan, warnings);
466
467 check_orphaned_permissions_handling(plan, warnings);
469
470 check_circular_dependency_handling(plan, warnings);
472
473 check_duration_estimates(plan, warnings);
475
476 check_error_handling(plan, warnings);
478
479 Ok(())
480}
481
482fn check_privilege_escalation_risks(plan: &MigrationPlan, warnings: &mut Vec<String>) {
484 for user_migration in &plan.user_migrations {
486 if user_migration.legacy_permissions.len() < user_migration.new_roles.len() * 5 {
487 warnings.push(format!(
489 "User '{}' may have privilege escalation - verify role assignments",
490 user_migration.user_id
491 ));
492 }
493 }
494
495 let has_privilege_check = plan.post_validation_steps.iter().any(|step| {
497 matches!(
498 step.validation_type,
499 ValidationType::PrivilegeEscalationCheck
500 )
501 });
502
503 if !has_privilege_check {
504 warnings
505 .push("No privilege escalation check found in post-migration validation".to_string());
506 }
507}
508
509fn check_orphaned_permissions_handling(plan: &MigrationPlan, warnings: &mut Vec<String>) {
511 if !plan.source_analysis.orphaned_permissions.is_empty() {
512 let orphaned_handled = plan.phases.iter().any(|phase| {
513 phase.operations.iter().any(|op| match op {
514 MigrationOperation::CreatePermission { permission_id, .. } => plan
515 .source_analysis
516 .orphaned_permissions
517 .contains(permission_id),
518 _ => false,
519 })
520 });
521
522 if !orphaned_handled {
523 warnings.push(format!("Found {} orphaned permissions in source system, but no handling strategy in migration plan",
524 plan.source_analysis.orphaned_permissions.len()));
525 }
526 }
527}
528
529fn check_circular_dependency_handling(plan: &MigrationPlan, warnings: &mut Vec<String>) {
531 if !plan.source_analysis.circular_dependencies.is_empty() {
532 warnings.push(format!(
533 "Source system has {} circular dependencies - ensure migration plan addresses these",
534 plan.source_analysis.circular_dependencies.len()
535 ));
536 }
537}
538
539fn check_duration_estimates(plan: &MigrationPlan, warnings: &mut Vec<String>) {
541 let total_operations = plan
542 .phases
543 .iter()
544 .map(|phase| phase.operations.len())
545 .sum::<usize>();
546
547 let avg_time_per_operation =
548 plan.estimated_duration.num_minutes() as f64 / total_operations as f64;
549
550 if avg_time_per_operation < 1.0 {
551 warnings.push(
552 "Duration estimates may be too optimistic - less than 1 minute per operation"
553 .to_string(),
554 );
555 } else if avg_time_per_operation > 30.0 {
556 warnings.push(
557 "Duration estimates may be too conservative - more than 30 minutes per operation"
558 .to_string(),
559 );
560 }
561
562 if let Some(downtime) = plan.downtime_required
564 && downtime > chrono::Duration::hours(8)
565 {
566 warnings.push("Required downtime exceeds 8 hours - consider gradual migration".to_string());
567 }
568}
569
570fn check_error_handling(plan: &MigrationPlan, warnings: &mut Vec<String>) {
572 let has_validation_ops = plan.phases.iter().any(|phase| {
573 phase
574 .operations
575 .iter()
576 .any(|op| matches!(op, MigrationOperation::ValidateIntegrity { .. }))
577 });
578
579 if !has_validation_ops {
580 warnings.push("No validation operations found in migration phases".to_string());
581 }
582
583 for phase in &plan.phases {
585 if phase.rollback_operations.is_empty() {
586 warnings.push(format!(
587 "Phase '{}' has no rollback operations defined",
588 phase.id
589 ));
590 }
591 }
592}
593
594#[cfg(test)]
595mod tests {
596 use super::*;
597 use crate::migration::{
598 BackupType, LegacySystemAnalysis, LegacySystemType, MigrationOperation, MigrationPhase,
599 MigrationStrategy,
600 };
601
602 fn create_test_plan() -> MigrationPlan {
603 MigrationPlan {
604 id: "test_plan".to_string(),
605 source_analysis: LegacySystemAnalysis {
606 system_type: LegacySystemType::BasicRbac,
607 role_count: 1,
608 permission_count: 1,
609 user_assignment_count: 1,
610 roles: vec![],
611 permissions: vec![],
612 user_assignments: vec![],
613 hierarchy_depth: 0,
614 duplicates_found: false,
615 orphaned_permissions: vec![],
616 circular_dependencies: vec![],
617 custom_attributes: std::collections::HashSet::new(),
618 complexity_score: 3,
619 recommended_strategy: MigrationStrategy::DirectMapping,
620 },
621 strategy: MigrationStrategy::DirectMapping,
622 phases: vec![
623 MigrationPhase {
624 id: "phase1".to_string(),
625 name: "Phase 1".to_string(),
626 description: "First phase".to_string(),
627 order: 1,
628 operations: vec![MigrationOperation::Backup {
629 backup_location: std::path::PathBuf::from("./backup"),
630 backup_type: BackupType::Full,
631 }],
632 dependencies: vec![],
633 estimated_duration: chrono::Duration::minutes(30),
634 rollback_operations: vec![],
635 },
636 MigrationPhase {
637 id: "phase2".to_string(),
638 name: "Phase 2".to_string(),
639 description: "Second phase".to_string(),
640 order: 2,
641 operations: vec![],
642 dependencies: vec!["phase1".to_string()],
643 estimated_duration: chrono::Duration::minutes(30),
644 rollback_operations: vec![],
645 },
646 ],
647 role_mappings: HashMap::new(),
648 permission_mappings: HashMap::new(),
649 user_migrations: vec![],
650 pre_validation_steps: vec![],
651 post_validation_steps: vec![],
652 rollback_plan: super::super::RollbackPlan {
653 phases: vec![
654 super::super::RollbackPhase {
655 id: "rollback_phase1".to_string(),
656 name: "Rollback Phase 1".to_string(),
657 order: 1,
658 operations: vec![],
659 },
660 super::super::RollbackPhase {
661 id: "rollback_phase2".to_string(),
662 name: "Rollback Phase 2".to_string(),
663 order: 2,
664 operations: vec![],
665 },
666 ],
667 backup_locations: vec![std::path::PathBuf::from("./backup")],
668 recovery_time_objective: chrono::Duration::hours(2),
669 manual_steps: vec![],
670 },
671 estimated_duration: chrono::Duration::hours(1),
672 risk_level: super::super::RiskLevel::Low,
673 downtime_required: None,
674 }
675 }
676
677 #[tokio::test]
678 async fn test_validate_phase_dependencies() {
679 let plan = create_test_plan();
680 let config = MigrationConfig::default();
681
682 let warnings = validate_migration_plan(&plan, &config).await.unwrap();
683
684 assert!(!warnings.is_empty());
686 }
687
688 #[tokio::test]
689 async fn test_circular_dependency_detection() {
690 let mut plan = create_test_plan();
691
692 plan.phases.push(super::super::MigrationPhase {
695 id: "phase0".to_string(),
696 name: "Phase 0".to_string(),
697 description: "Zero phase".to_string(),
698 order: 0,
699 operations: vec![],
700 dependencies: vec!["phase2".to_string()], estimated_duration: chrono::Duration::minutes(30),
702 rollback_operations: vec![],
703 });
704
705 plan.phases[0].dependencies = vec!["phase0".to_string()];
707
708 let config = MigrationConfig::default();
709 let result = validate_migration_plan(&plan, &config).await;
710
711 assert!(result.is_err());
712 assert!(
713 result
714 .unwrap_err()
715 .to_string()
716 .contains("Circular dependencies")
717 );
718 }
719
720 #[test]
721 fn test_has_circular_dependencies() {
722 let phases = vec![
723 super::super::MigrationPhase {
724 id: "a".to_string(),
725 name: "A".to_string(),
726 description: "".to_string(),
727 order: 1,
728 operations: vec![],
729 dependencies: vec!["b".to_string()],
730 estimated_duration: chrono::Duration::minutes(1),
731 rollback_operations: vec![],
732 },
733 super::super::MigrationPhase {
734 id: "b".to_string(),
735 name: "B".to_string(),
736 description: "".to_string(),
737 order: 2,
738 operations: vec![],
739 dependencies: vec!["a".to_string()],
740 estimated_duration: chrono::Duration::minutes(1),
741 rollback_operations: vec![],
742 },
743 ];
744
745 assert!(has_circular_dependencies(&phases));
746 }
747}
748
749