use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use thiserror::Error;
use tokio::fs;
pub mod analyzers;
pub mod converters;
pub mod executors;
pub mod planners;
pub mod validators;
#[derive(Error, Debug)]
pub enum MigrationError {
#[error("Legacy system analysis failed: {0}")]
AnalysisError(String),
#[error("Migration plan generation failed: {0}")]
PlanningError(String),
#[error("Migration execution failed: {0}")]
ExecutionError(String),
#[error("Migration validation failed: {0}")]
ValidationError(String),
#[error("Rollback operation failed: {0}")]
RollbackError(String),
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
#[error("Serialization error: {0}")]
SerializationError(#[from] serde_json::Error),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum LegacySystemType {
PermissionBased,
BasicRbac,
Abac,
Custom(String),
Hybrid(Vec<LegacySystemType>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LegacyRole {
pub id: String,
pub name: String,
pub description: Option<String>,
pub permissions: Vec<String>,
pub parent_roles: Vec<String>,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LegacyUserAssignment {
pub user_id: String,
pub role_id: Option<String>,
pub permissions: Vec<String>,
pub attributes: HashMap<String, String>,
pub expiration: Option<chrono::DateTime<chrono::Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LegacyPermission {
pub id: String,
pub action: String,
pub resource: String,
pub conditions: HashMap<String, String>,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LegacySystemAnalysis {
pub system_type: LegacySystemType,
pub role_count: usize,
pub permission_count: usize,
pub user_assignment_count: usize,
pub roles: Vec<LegacyRole>,
pub permissions: Vec<LegacyPermission>,
pub user_assignments: Vec<LegacyUserAssignment>,
pub hierarchy_depth: usize,
pub duplicates_found: bool,
pub orphaned_permissions: Vec<String>,
pub circular_dependencies: Vec<Vec<String>>,
pub custom_attributes: HashSet<String>,
pub complexity_score: u8,
pub recommended_strategy: MigrationStrategy,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum MigrationStrategy {
DirectMapping,
GradualMigration,
Rebuild,
Custom(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MigrationPlan {
pub id: String,
pub source_analysis: LegacySystemAnalysis,
pub strategy: MigrationStrategy,
pub phases: Vec<MigrationPhase>,
pub role_mappings: HashMap<String, String>,
pub permission_mappings: HashMap<String, String>,
pub user_migrations: Vec<UserMigration>,
pub pre_validation_steps: Vec<ValidationStep>,
pub post_validation_steps: Vec<ValidationStep>,
pub rollback_plan: RollbackPlan,
pub estimated_duration: chrono::Duration,
pub risk_level: RiskLevel,
pub downtime_required: Option<chrono::Duration>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MigrationPhase {
pub id: String,
pub name: String,
pub description: String,
pub order: u32,
pub operations: Vec<MigrationOperation>,
pub dependencies: Vec<String>,
pub estimated_duration: chrono::Duration,
pub rollback_operations: Vec<MigrationOperation>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MigrationOperation {
CreateRole {
role_id: String,
name: String,
description: Option<String>,
permissions: Vec<String>,
parent_role: Option<String>,
},
AssignUserRole {
user_id: String,
role_id: String,
expiration: Option<chrono::DateTime<chrono::Utc>>,
},
CreatePermission {
permission_id: String,
action: String,
resource: String,
conditions: HashMap<String, String>,
},
MigrateCustomAttribute {
attribute_name: String,
conversion_logic: String,
},
ValidateIntegrity {
validation_type: String,
parameters: HashMap<String, String>,
},
Backup {
backup_location: PathBuf,
backup_type: BackupType,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserMigration {
pub user_id: String,
pub legacy_roles: Vec<String>,
pub legacy_permissions: Vec<String>,
pub new_roles: Vec<String>,
pub migration_notes: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationStep {
pub id: String,
pub name: String,
pub description: String,
pub validation_type: ValidationType,
pub parameters: HashMap<String, String>,
pub required: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum ValidationType {
HierarchyIntegrity,
PermissionConsistency,
UserAssignmentValidity,
PrivilegeEscalationCheck,
Custom(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RollbackPlan {
pub phases: Vec<RollbackPhase>,
pub backup_locations: Vec<PathBuf>,
pub recovery_time_objective: chrono::Duration,
pub manual_steps: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RollbackPhase {
pub id: String,
pub name: String,
pub operations: Vec<MigrationOperation>,
pub order: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub enum RiskLevel {
Low,
Medium,
High,
Critical,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum BackupType {
Full,
Incremental,
ConfigOnly,
DataOnly,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MigrationResult {
pub plan_id: String,
pub status: MigrationStatus,
pub started_at: chrono::DateTime<chrono::Utc>,
pub completed_at: Option<chrono::DateTime<chrono::Utc>>,
pub phases_completed: Vec<String>,
pub current_phase: Option<String>,
pub errors: Vec<String>,
pub warnings: Vec<String>,
pub metrics: MigrationMetrics,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum MigrationStatus {
Planned,
InProgress,
Completed,
Failed,
RolledBack,
Paused,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MigrationMetrics {
pub roles_migrated: usize,
pub permissions_migrated: usize,
pub users_migrated: usize,
pub errors_encountered: usize,
pub warnings_generated: usize,
pub validation_failures: usize,
pub rollback_count: usize,
}
pub struct MigrationManager {
config: MigrationConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MigrationConfig {
pub working_directory: PathBuf,
pub backup_directory: PathBuf,
pub max_concurrent_operations: usize,
pub operation_timeout: chrono::Duration,
pub dry_run: bool,
pub verbose: bool,
}
impl Default for MigrationConfig {
fn default() -> Self {
Self {
working_directory: PathBuf::from("./migration"),
backup_directory: PathBuf::from("./migration/backups"),
max_concurrent_operations: 4,
operation_timeout: chrono::Duration::minutes(30),
dry_run: false,
verbose: false,
}
}
}
impl MigrationConfig {
pub fn dry_run() -> Self {
Self {
dry_run: true,
verbose: true,
..Default::default()
}
}
pub fn conservative() -> Self {
Self {
max_concurrent_operations: 1,
operation_timeout: chrono::Duration::minutes(120),
verbose: true,
..Default::default()
}
}
pub fn aggressive() -> Self {
Self {
max_concurrent_operations: 16,
operation_timeout: chrono::Duration::minutes(60),
..Default::default()
}
}
pub fn builder() -> MigrationConfigBuilder {
MigrationConfigBuilder {
config: MigrationConfig::default(),
}
}
}
#[derive(Debug, Clone)]
pub struct MigrationConfigBuilder {
config: MigrationConfig,
}
impl MigrationConfigBuilder {
pub fn working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
self.config.working_directory = dir.into();
self
}
pub fn backup_directory(mut self, dir: impl Into<PathBuf>) -> Self {
self.config.backup_directory = dir.into();
self
}
pub fn max_concurrent(mut self, count: usize) -> Self {
self.config.max_concurrent_operations = count;
self
}
pub fn timeout(mut self, timeout: chrono::Duration) -> Self {
self.config.operation_timeout = timeout;
self
}
pub fn dry_run(mut self, enabled: bool) -> Self {
self.config.dry_run = enabled;
self
}
pub fn verbose(mut self, enabled: bool) -> Self {
self.config.verbose = enabled;
self
}
pub fn build(self) -> MigrationConfig {
self.config
}
}
impl MigrationManager {
pub fn new(config: MigrationConfig) -> Result<Self, MigrationError> {
std::fs::create_dir_all(&config.working_directory)?;
std::fs::create_dir_all(&config.backup_directory)?;
Ok(Self { config })
}
pub async fn analyze_legacy_system<P: AsRef<std::path::Path>>(
&self,
config_path: P,
) -> Result<LegacySystemAnalysis, MigrationError> {
analyzers::analyze_legacy_system(config_path, &self.config).await
}
pub async fn generate_migration_plan(
&self,
analysis: &LegacySystemAnalysis,
strategy: Option<MigrationStrategy>,
) -> Result<MigrationPlan, MigrationError> {
planners::generate_migration_plan(analysis, strategy, &self.config).await
}
pub async fn validate_migration_plan(
&self,
plan: &MigrationPlan,
) -> Result<Vec<String>, MigrationError> {
validators::validate_migration_plan(plan, &self.config).await
}
pub async fn execute_migration(
&self,
plan: &MigrationPlan,
) -> Result<MigrationResult, MigrationError> {
executors::execute_migration_plan(plan, &self.config).await
}
pub async fn get_migration_status(
&self,
plan_id: &str,
) -> Result<Option<MigrationResult>, MigrationError> {
let status_file = self
.config
.working_directory
.join(format!("{}_status.json", plan_id));
if !status_file.exists() {
return Ok(None);
}
let content = fs::read_to_string(status_file).await?;
let result: MigrationResult = serde_json::from_str(&content)?;
Ok(Some(result))
}
pub async fn rollback_migration(
&self,
plan: &MigrationPlan,
) -> Result<MigrationResult, MigrationError> {
executors::rollback_migration(plan, &self.config).await
}
pub async fn list_migration_plans(&self) -> Result<Vec<String>, MigrationError> {
let mut plans = Vec::new();
let mut entries = fs::read_dir(&self.config.working_directory).await?;
while let Some(entry) = entries.next_entry().await? {
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "json")
&& let Some(file_name) = path.file_stem()
&& let Some(name) = file_name.to_str()
&& name.ends_with("_plan")
{
plans.push(name.trim_end_matches("_plan").to_string());
}
}
Ok(plans)
}
pub async fn save_migration_plan(
&self,
plan: &MigrationPlan,
) -> Result<PathBuf, MigrationError> {
let plan_file = self
.config
.working_directory
.join(format!("{}_plan.json", plan.id));
let content = serde_json::to_string_pretty(plan)?;
fs::write(&plan_file, content).await?;
Ok(plan_file)
}
pub async fn load_migration_plan(
&self,
plan_id: &str,
) -> Result<MigrationPlan, MigrationError> {
let plan_file = self
.config
.working_directory
.join(format!("{}_plan.json", plan_id));
let content = fs::read_to_string(plan_file).await?;
let plan: MigrationPlan = serde_json::from_str(&content)?;
Ok(plan)
}
pub async fn generate_migration_report(
&self,
result: &MigrationResult,
) -> Result<String, MigrationError> {
let mut report = String::new();
report.push_str("# Migration Report\n\n");
report.push_str(&format!("**Plan ID**: {}\n", result.plan_id));
report.push_str(&format!("**Status**: {:?}\n", result.status));
report.push_str(&format!("**Started**: {}\n", result.started_at));
if let Some(completed) = result.completed_at {
report.push_str(&format!("**Completed**: {}\n", completed));
let duration = completed - result.started_at;
report.push_str(&format!(
"**Duration**: {} minutes\n",
duration.num_minutes()
));
}
report.push_str("\n## Metrics\n\n");
report.push_str(&format!(
"- Roles migrated: {}\n",
result.metrics.roles_migrated
));
report.push_str(&format!(
"- Permissions migrated: {}\n",
result.metrics.permissions_migrated
));
report.push_str(&format!(
"- Users migrated: {}\n",
result.metrics.users_migrated
));
report.push_str(&format!(
"- Errors: {}\n",
result.metrics.errors_encountered
));
report.push_str(&format!(
"- Warnings: {}\n",
result.metrics.warnings_generated
));
if !result.errors.is_empty() {
report.push_str("\n## Errors\n\n");
for error in &result.errors {
report.push_str(&format!("- {}\n", error));
}
}
if !result.warnings.is_empty() {
report.push_str("\n## Warnings\n\n");
for warning in &result.warnings {
report.push_str(&format!("- {}\n", warning));
}
}
Ok(report)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_migration_manager_creation() {
let config = MigrationConfig::default();
let manager = MigrationManager::new(config);
assert!(manager.is_ok());
}
#[test]
fn test_legacy_system_type_serialization() {
let system_type = LegacySystemType::BasicRbac;
let serialized = serde_json::to_string(&system_type).unwrap();
let deserialized: LegacySystemType = serde_json::from_str(&serialized).unwrap();
assert_eq!(system_type, deserialized);
}
#[test]
fn test_migration_strategy_serialization() {
let strategy = MigrationStrategy::GradualMigration;
let serialized = serde_json::to_string(&strategy).unwrap();
let deserialized: MigrationStrategy = serde_json::from_str(&serialized).unwrap();
assert_eq!(strategy, deserialized);
}
#[test]
fn test_risk_level_ordering() {
assert!(RiskLevel::Low < RiskLevel::Medium);
assert!(RiskLevel::Medium < RiskLevel::High);
assert!(RiskLevel::High < RiskLevel::Critical);
}
#[test]
fn test_migration_config_dry_run_preset() {
let config = MigrationConfig::dry_run();
assert!(config.dry_run);
assert!(config.verbose);
}
#[test]
fn test_migration_config_conservative_preset() {
let config = MigrationConfig::conservative();
assert_eq!(config.max_concurrent_operations, 1);
assert!(config.verbose);
assert!(!config.dry_run);
}
#[test]
fn test_migration_config_aggressive_preset() {
let config = MigrationConfig::aggressive();
assert_eq!(config.max_concurrent_operations, 16);
}
#[test]
fn test_migration_config_builder() {
let config = MigrationConfig::builder()
.working_directory("/tmp/migration")
.dry_run(true)
.max_concurrent(8)
.verbose(true)
.build();
assert_eq!(config.working_directory, PathBuf::from("/tmp/migration"));
assert!(config.dry_run);
assert_eq!(config.max_concurrent_operations, 8);
assert!(config.verbose);
}
#[tokio::test]
async fn test_migration_manager_with_dry_run_preset() {
let config = MigrationConfig::dry_run();
let manager = MigrationManager::new(config);
assert!(manager.is_ok());
}
}