Skip to main content

auth_framework/migration/
converters.rs

1//! Migration converters for transforming legacy data structures
2//!
3//! This module provides converters to transform legacy authorization
4//! data into role-system v1.0 compatible formats.
5
6use super::{LegacyPermission, LegacyRole, LegacyUserAssignment, MigrationError};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Converted role data compatible with role-system v1.0
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ConvertedRole {
13    pub id: String,
14    pub name: String,
15    pub description: Option<String>,
16    pub permissions: Vec<String>,
17    pub parent_role_id: Option<String>,
18    pub metadata: HashMap<String, String>,
19    pub created_at: chrono::DateTime<chrono::Utc>,
20    pub updated_at: chrono::DateTime<chrono::Utc>,
21}
22
23/// Converted permission data
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct ConvertedPermission {
26    pub id: String,
27    pub action: String,
28    pub resource: String,
29    pub conditions: HashMap<String, String>,
30    pub created_at: chrono::DateTime<chrono::Utc>,
31}
32
33/// Converted user assignment data
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct ConvertedUserAssignment {
36    pub user_id: String,
37    pub role_id: String,
38    pub assigned_at: chrono::DateTime<chrono::Utc>,
39    pub assigned_by: Option<String>,
40    pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
41    pub metadata: HashMap<String, String>,
42}
43
44/// Role converter for transforming legacy roles
45pub struct RoleConverter {
46    id_prefix: String,
47    preserve_hierarchy: bool,
48    merge_duplicate_permissions: bool,
49}
50
51impl Default for RoleConverter {
52    fn default() -> Self {
53        Self {
54            id_prefix: "migrated_".to_string(),
55            preserve_hierarchy: true,
56            merge_duplicate_permissions: true,
57        }
58    }
59}
60
61impl RoleConverter {
62    /// Create new role converter with custom settings.
63    ///
64    /// For a more readable alternative, prefer the builder API:
65    ///
66    /// ```rust,ignore
67    /// RoleConverter::default()
68    ///     .with_id_prefix("imported_")
69    ///     .without_hierarchy()
70    /// ```
71    pub fn new(
72        id_prefix: String,
73        preserve_hierarchy: bool,
74        merge_duplicate_permissions: bool,
75    ) -> Self {
76        Self {
77            id_prefix,
78            preserve_hierarchy,
79            merge_duplicate_permissions,
80        }
81    }
82
83    /// Set the ID prefix for converted roles (default: `"migrated_"`).
84    pub fn with_id_prefix(mut self, prefix: impl Into<String>) -> Self {
85        self.id_prefix = prefix.into();
86        self
87    }
88
89    /// Preserve the role hierarchy from the legacy system (default: `true`).
90    pub fn with_hierarchy(mut self) -> Self {
91        self.preserve_hierarchy = true;
92        self
93    }
94
95    /// Discard the role hierarchy from the legacy system.
96    pub fn without_hierarchy(mut self) -> Self {
97        self.preserve_hierarchy = false;
98        self
99    }
100
101    /// Merge duplicate permissions during conversion (default: `true`).
102    pub fn with_deduplication(mut self) -> Self {
103        self.merge_duplicate_permissions = true;
104        self
105    }
106
107    /// Keep duplicate permissions as-is during conversion.
108    pub fn without_deduplication(mut self) -> Self {
109        self.merge_duplicate_permissions = false;
110        self
111    }
112
113    /// Convert legacy role to role-system v1.0 format
114    pub fn convert_role(&self, legacy_role: &LegacyRole) -> Result<ConvertedRole, MigrationError> {
115        let now = chrono::Utc::now();
116
117        let mut permissions = legacy_role.permissions.clone();
118
119        // Remove duplicates if enabled
120        if self.merge_duplicate_permissions {
121            permissions.sort();
122            permissions.dedup();
123        }
124
125        // Handle parent role (role-system v1.0 supports single parent)
126        let parent_role_id = if self.preserve_hierarchy && !legacy_role.parent_roles.is_empty() {
127            // Take the first parent role if multiple exist
128            Some(format!(
129                "{}{}",
130                self.id_prefix, &legacy_role.parent_roles[0]
131            ))
132        } else {
133            None
134        };
135
136        // Convert metadata
137        let mut metadata = legacy_role.metadata.clone();
138
139        // Add migration metadata
140        metadata.insert("migration_source".to_string(), "legacy_system".to_string());
141        metadata.insert("original_id".to_string(), legacy_role.id.clone());
142
143        if legacy_role.parent_roles.len() > 1 {
144            metadata.insert(
145                "original_parent_roles".to_string(),
146                legacy_role.parent_roles.join(","),
147            );
148        }
149
150        Ok(ConvertedRole {
151            id: format!("{}{}", self.id_prefix, legacy_role.id),
152            name: legacy_role.name.clone(),
153            description: legacy_role.description.clone(),
154            permissions,
155            parent_role_id,
156            metadata,
157            created_at: now,
158            updated_at: now,
159        })
160    }
161
162    /// Convert multiple legacy roles with dependency resolution
163    pub fn convert_roles(
164        &self,
165        legacy_roles: &[LegacyRole],
166    ) -> Result<Vec<ConvertedRole>, MigrationError> {
167        let mut converted_roles = Vec::new();
168        let mut role_map: HashMap<String, &LegacyRole> = HashMap::new();
169
170        // Build role map for dependency lookup
171        for role in legacy_roles {
172            role_map.insert(role.id.clone(), role);
173        }
174
175        // Convert roles in dependency order (parents first)
176        let ordered_roles = self.order_roles_by_dependencies(legacy_roles)?;
177
178        for role in ordered_roles {
179            let converted = self.convert_role(role)?;
180            converted_roles.push(converted);
181        }
182
183        Ok(converted_roles)
184    }
185
186    /// Order roles by dependencies (parents before children)
187    fn order_roles_by_dependencies<'a>(
188        &self,
189        roles: &'a [LegacyRole],
190    ) -> Result<Vec<&'a LegacyRole>, MigrationError> {
191        let mut ordered = Vec::new();
192        let mut visited = std::collections::HashSet::new();
193        let mut visiting = std::collections::HashSet::new();
194        let role_map: HashMap<String, &LegacyRole> =
195            roles.iter().map(|role| (role.id.clone(), role)).collect();
196
197        for role in roles {
198            if !visited.contains(&role.id) {
199                self.visit_role_dependencies(
200                    role,
201                    &role_map,
202                    &mut ordered,
203                    &mut visited,
204                    &mut visiting,
205                )?;
206            }
207        }
208
209        Ok(ordered)
210    }
211
212    /// Visit role dependencies recursively
213    #[allow(clippy::only_used_in_recursion)]
214    fn visit_role_dependencies<'a>(
215        &self,
216        role: &'a LegacyRole,
217        role_map: &HashMap<String, &'a LegacyRole>,
218        ordered: &mut Vec<&'a LegacyRole>,
219        visited: &mut std::collections::HashSet<String>,
220        visiting: &mut std::collections::HashSet<String>,
221    ) -> Result<(), MigrationError> {
222        if visiting.contains(&role.id) {
223            return Err(MigrationError::AnalysisError(format!(
224                "Circular dependency detected involving role '{}'",
225                role.id
226            )));
227        }
228
229        if visited.contains(&role.id) {
230            return Ok(());
231        }
232
233        visiting.insert(role.id.clone());
234
235        // Visit all parent roles first
236        for parent_id in &role.parent_roles {
237            if let Some(parent_role) = role_map.get(parent_id) {
238                self.visit_role_dependencies(parent_role, role_map, ordered, visited, visiting)?;
239            }
240        }
241
242        visiting.remove(&role.id);
243        visited.insert(role.id.clone());
244        ordered.push(role);
245
246        Ok(())
247    }
248}
249
250/// Permission converter for transforming legacy permissions
251pub struct PermissionConverter {
252    id_prefix: String,
253    normalize_actions: bool,
254    normalize_resources: bool,
255}
256
257impl Default for PermissionConverter {
258    fn default() -> Self {
259        Self {
260            id_prefix: "migrated_".to_string(),
261            normalize_actions: true,
262            normalize_resources: true,
263        }
264    }
265}
266
267impl PermissionConverter {
268    /// Create new permission converter
269    pub fn new(id_prefix: String, normalize_actions: bool, normalize_resources: bool) -> Self {
270        Self {
271            id_prefix,
272            normalize_actions,
273            normalize_resources,
274        }
275    }
276
277    /// Convert legacy permission to role-system v1.0 format
278    pub fn convert_permission(
279        &self,
280        legacy_permission: &LegacyPermission,
281    ) -> Result<ConvertedPermission, MigrationError> {
282        let now = chrono::Utc::now();
283
284        let action = if self.normalize_actions {
285            self.normalize_action(&legacy_permission.action)
286        } else {
287            legacy_permission.action.clone()
288        };
289
290        let resource = if self.normalize_resources {
291            self.normalize_resource(&legacy_permission.resource)
292        } else {
293            legacy_permission.resource.clone()
294        };
295
296        let mut conditions = legacy_permission.conditions.clone();
297
298        // Add migration metadata to conditions
299        conditions.insert("migration_source".to_string(), "legacy_system".to_string());
300        conditions.insert("original_id".to_string(), legacy_permission.id.clone());
301
302        Ok(ConvertedPermission {
303            id: format!("{}{}", self.id_prefix, legacy_permission.id),
304            action,
305            resource,
306            conditions,
307            created_at: now,
308        })
309    }
310
311    /// Convert multiple permissions
312    pub fn convert_permissions(
313        &self,
314        legacy_permissions: &[LegacyPermission],
315    ) -> Result<Vec<ConvertedPermission>, MigrationError> {
316        legacy_permissions
317            .iter()
318            .map(|perm| self.convert_permission(perm))
319            .collect()
320    }
321
322    /// Normalize action names to standard format
323    fn normalize_action(&self, action: &str) -> String {
324        match action.to_lowercase().as_str() {
325            "read" | "view" | "get" | "list" => "read".to_string(),
326            "write" | "create" | "post" | "add" => "create".to_string(),
327            "update" | "put" | "patch" | "modify" | "edit" => "update".to_string(),
328            "delete" | "remove" | "destroy" => "delete".to_string(),
329            "execute" | "run" | "invoke" => "execute".to_string(),
330            "admin" | "manage" | "administrate" => "manage".to_string(),
331            _ => action.to_string(),
332        }
333    }
334
335    /// Normalize resource names to standard format
336    fn normalize_resource(&self, resource: &str) -> String {
337        // Convert to lowercase and replace common separators
338        resource
339            .to_lowercase()
340            .replace("-", "_")
341            .replace(" ", "_")
342            .replace("/", "_")
343    }
344}
345
346/// User assignment converter
347pub struct UserAssignmentConverter {
348    default_assigned_by: Option<String>,
349    preserve_expiration: bool,
350}
351
352impl Default for UserAssignmentConverter {
353    fn default() -> Self {
354        Self {
355            default_assigned_by: Some("migration_system".to_string()),
356            preserve_expiration: true,
357        }
358    }
359}
360
361impl UserAssignmentConverter {
362    /// Create new user assignment converter
363    pub fn new(default_assigned_by: Option<String>, preserve_expiration: bool) -> Self {
364        Self {
365            default_assigned_by,
366            preserve_expiration,
367        }
368    }
369
370    /// Convert legacy user assignment
371    pub fn convert_user_assignment(
372        &self,
373        legacy_assignment: &LegacyUserAssignment,
374        role_mappings: &HashMap<String, String>,
375    ) -> Result<Option<ConvertedUserAssignment>, MigrationError> {
376        let now = chrono::Utc::now();
377
378        // Get the mapped role ID
379        let role_id = if let Some(legacy_role_id) = &legacy_assignment.role_id {
380            if let Some(new_role_id) = role_mappings.get(legacy_role_id) {
381                new_role_id.clone()
382            } else {
383                return Err(MigrationError::AnalysisError(format!(
384                    "No role mapping found for legacy role '{}'",
385                    legacy_role_id
386                )));
387            }
388        } else {
389            // Handle permission-only assignments by creating a temporary role
390            return Ok(None); // Skip for now, could be handled with dynamic role creation
391        };
392
393        let expires_at = if self.preserve_expiration {
394            legacy_assignment.expiration
395        } else {
396            None
397        };
398
399        let mut metadata = HashMap::new();
400
401        // Convert attributes to metadata
402        for (key, value) in &legacy_assignment.attributes {
403            metadata.insert(key.clone(), value.clone());
404        }
405
406        // Add migration metadata
407        metadata.insert("migration_source".to_string(), "legacy_system".to_string());
408        metadata.insert(
409            "original_permissions".to_string(),
410            legacy_assignment.permissions.join(","),
411        );
412
413        Ok(Some(ConvertedUserAssignment {
414            user_id: legacy_assignment.user_id.clone(),
415            role_id,
416            assigned_at: now,
417            assigned_by: self.default_assigned_by.clone(),
418            expires_at,
419            metadata,
420        }))
421    }
422
423    /// Convert multiple user assignments
424    pub fn convert_user_assignments(
425        &self,
426        legacy_assignments: &[LegacyUserAssignment],
427        role_mappings: &HashMap<String, String>,
428    ) -> Result<Vec<ConvertedUserAssignment>, MigrationError> {
429        let mut converted = Vec::new();
430
431        for assignment in legacy_assignments {
432            if let Some(converted_assignment) =
433                self.convert_user_assignment(assignment, role_mappings)?
434            {
435                converted.push(converted_assignment);
436            }
437        }
438
439        Ok(converted)
440    }
441}
442
443/// Comprehensive converter that handles all data types
444#[derive(Default)]
445pub struct LegacySystemConverter {
446    role_converter: RoleConverter,
447    permission_converter: PermissionConverter,
448    user_assignment_converter: UserAssignmentConverter,
449}
450
451impl LegacySystemConverter {
452    /// Create new system converter with custom components
453    pub fn new(
454        role_converter: RoleConverter,
455        permission_converter: PermissionConverter,
456        user_assignment_converter: UserAssignmentConverter,
457    ) -> Self {
458        Self {
459            role_converter,
460            permission_converter,
461            user_assignment_converter,
462        }
463    }
464
465    /// Convert entire legacy system
466    pub fn convert_system(
467        &self,
468        legacy_roles: &[LegacyRole],
469        legacy_permissions: &[LegacyPermission],
470        legacy_assignments: &[LegacyUserAssignment],
471    ) -> Result<ConvertedSystem, MigrationError> {
472        // Convert roles first (needed for user assignment mapping)
473        let converted_roles = self.role_converter.convert_roles(legacy_roles)?;
474
475        // Build role mapping
476        let role_mappings: HashMap<String, String> = legacy_roles
477            .iter()
478            .zip(&converted_roles)
479            .map(|(legacy, converted)| (legacy.id.clone(), converted.id.clone()))
480            .collect();
481
482        // Convert permissions
483        let converted_permissions = self
484            .permission_converter
485            .convert_permissions(legacy_permissions)?;
486
487        // Convert user assignments
488        let converted_assignments = self
489            .user_assignment_converter
490            .convert_user_assignments(legacy_assignments, &role_mappings)?;
491
492        Ok(ConvertedSystem {
493            roles: converted_roles,
494            permissions: converted_permissions,
495            user_assignments: converted_assignments,
496            role_mappings,
497            conversion_metadata: self.generate_conversion_metadata(
498                legacy_roles,
499                legacy_permissions,
500                legacy_assignments,
501            ),
502        })
503    }
504
505    /// Generate metadata about the conversion process
506    fn generate_conversion_metadata(
507        &self,
508        legacy_roles: &[LegacyRole],
509        legacy_permissions: &[LegacyPermission],
510        legacy_assignments: &[LegacyUserAssignment],
511    ) -> ConversionMetadata {
512        ConversionMetadata {
513            converted_at: chrono::Utc::now(),
514            legacy_role_count: legacy_roles.len(),
515            legacy_permission_count: legacy_permissions.len(),
516            legacy_assignment_count: legacy_assignments.len(),
517            conversion_summary: format!(
518                "Converted {} roles, {} permissions, and {} user assignments",
519                legacy_roles.len(),
520                legacy_permissions.len(),
521                legacy_assignments.len()
522            ),
523        }
524    }
525}
526
527/// Complete converted system
528#[derive(Debug, Clone, Serialize, Deserialize)]
529pub struct ConvertedSystem {
530    pub roles: Vec<ConvertedRole>,
531    pub permissions: Vec<ConvertedPermission>,
532    pub user_assignments: Vec<ConvertedUserAssignment>,
533    pub role_mappings: HashMap<String, String>,
534    pub conversion_metadata: ConversionMetadata,
535}
536
537/// Metadata about the conversion process
538#[derive(Debug, Clone, Serialize, Deserialize)]
539pub struct ConversionMetadata {
540    pub converted_at: chrono::DateTime<chrono::Utc>,
541    pub legacy_role_count: usize,
542    pub legacy_permission_count: usize,
543    pub legacy_assignment_count: usize,
544    pub conversion_summary: String,
545}
546
547#[cfg(test)]
548mod tests {
549    use super::*;
550
551    fn create_test_role() -> LegacyRole {
552        LegacyRole {
553            id: "admin".to_string(),
554            name: "Administrator".to_string(),
555            description: Some("Admin role".to_string()),
556            permissions: vec!["read".to_string(), "write".to_string(), "read".to_string()], // Duplicate
557            parent_roles: vec!["super_admin".to_string()],
558            metadata: {
559                let mut map = HashMap::new();
560                map.insert("priority".to_string(), "high".to_string());
561                map
562            },
563        }
564    }
565
566    #[test]
567    fn test_role_converter() {
568        let converter = RoleConverter::default();
569        let legacy_role = create_test_role();
570
571        let converted = converter.convert_role(&legacy_role).unwrap();
572
573        assert_eq!(converted.id, "migrated_admin");
574        assert_eq!(converted.name, "Administrator");
575        assert_eq!(converted.permissions.len(), 2); // Duplicates removed
576        assert_eq!(
577            converted.parent_role_id,
578            Some("migrated_super_admin".to_string())
579        );
580        assert!(converted.metadata.contains_key("migration_source"));
581    }
582
583    #[test]
584    fn test_permission_converter() {
585        let converter = PermissionConverter::default();
586        let legacy_permission = LegacyPermission {
587            id: "read_data".to_string(),
588            action: "VIEW".to_string(),
589            resource: "User-Data".to_string(),
590            conditions: HashMap::new(),
591            metadata: HashMap::new(),
592        };
593
594        let converted = converter.convert_permission(&legacy_permission).unwrap();
595
596        assert_eq!(converted.id, "migrated_read_data");
597        assert_eq!(converted.action, "read"); // Normalized
598        assert_eq!(converted.resource, "user_data"); // Normalized
599        assert!(converted.conditions.contains_key("migration_source"));
600    }
601
602    #[test]
603    fn test_user_assignment_converter() {
604        let converter = UserAssignmentConverter::default();
605        let legacy_assignment = LegacyUserAssignment {
606            user_id: "user123".to_string(),
607            role_id: Some("admin".to_string()),
608            permissions: vec!["read".to_string()],
609            attributes: {
610                let mut map = HashMap::new();
611                map.insert("department".to_string(), "IT".to_string());
612                map
613            },
614            expiration: None,
615        };
616
617        let mut role_mappings = HashMap::new();
618        role_mappings.insert("admin".to_string(), "migrated_admin".to_string());
619
620        let converted = converter
621            .convert_user_assignment(&legacy_assignment, &role_mappings)
622            .unwrap()
623            .unwrap();
624
625        assert_eq!(converted.user_id, "user123");
626        assert_eq!(converted.role_id, "migrated_admin");
627        assert!(converted.metadata.contains_key("department"));
628        assert!(converted.metadata.contains_key("migration_source"));
629    }
630
631    #[test]
632    fn test_system_converter() {
633        let converter = LegacySystemConverter::default();
634
635        let legacy_roles = vec![create_test_role()];
636        let legacy_permissions = vec![];
637        let legacy_assignments = vec![];
638
639        let converted_system = converter
640            .convert_system(&legacy_roles, &legacy_permissions, &legacy_assignments)
641            .unwrap();
642
643        assert_eq!(converted_system.roles.len(), 1);
644        assert_eq!(converted_system.permissions.len(), 0);
645        assert_eq!(converted_system.user_assignments.len(), 0);
646        assert_eq!(converted_system.role_mappings.len(), 1);
647    }
648}