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