1use super::{LegacyPermission, LegacyRole, LegacyUserAssignment, MigrationError};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[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#[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#[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
44pub 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 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 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 if self.merge_duplicate_permissions {
83 permissions.sort();
84 permissions.dedup();
85 }
86
87 let parent_role_id = if self.preserve_hierarchy && !legacy_role.parent_roles.is_empty() {
89 Some(format!(
91 "{}{}",
92 self.id_prefix, &legacy_role.parent_roles[0]
93 ))
94 } else {
95 None
96 };
97
98 let mut metadata = legacy_role.metadata.clone();
100
101 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 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 for role in legacy_roles {
134 role_map.insert(role.id.clone(), role);
135 }
136
137 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 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 #[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 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
212pub 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 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 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 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 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 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 fn normalize_resource(&self, resource: &str) -> String {
299 resource
301 .to_lowercase()
302 .replace("-", "_")
303 .replace(" ", "_")
304 .replace("/", "_")
305 }
306}
307
308pub 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 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 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 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 return Ok(None); };
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 for (key, value) in &legacy_assignment.attributes {
365 metadata.insert(key.clone(), value.clone());
366 }
367
368 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 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#[derive(Default)]
407pub struct LegacySystemConverter {
408 role_converter: RoleConverter,
409 permission_converter: PermissionConverter,
410 user_assignment_converter: UserAssignmentConverter,
411}
412
413impl LegacySystemConverter {
414 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 pub fn convert_system(
429 &self,
430 legacy_roles: &[LegacyRole],
431 legacy_permissions: &[LegacyPermission],
432 legacy_assignments: &[LegacyUserAssignment],
433 ) -> Result<ConvertedSystem, MigrationError> {
434 let converted_roles = self.role_converter.convert_roles(legacy_roles)?;
436
437 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 let converted_permissions = self
446 .permission_converter
447 .convert_permissions(legacy_permissions)?;
448
449 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 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#[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#[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()], 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); 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"); assert_eq!(converted.resource, "user_data"); 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