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(
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 pub fn with_id_prefix(mut self, prefix: impl Into<String>) -> Self {
85 self.id_prefix = prefix.into();
86 self
87 }
88
89 pub fn with_hierarchy(mut self) -> Self {
91 self.preserve_hierarchy = true;
92 self
93 }
94
95 pub fn without_hierarchy(mut self) -> Self {
97 self.preserve_hierarchy = false;
98 self
99 }
100
101 pub fn with_deduplication(mut self) -> Self {
103 self.merge_duplicate_permissions = true;
104 self
105 }
106
107 pub fn without_deduplication(mut self) -> Self {
109 self.merge_duplicate_permissions = false;
110 self
111 }
112
113 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 if self.merge_duplicate_permissions {
121 permissions.sort();
122 permissions.dedup();
123 }
124
125 let parent_role_id = if self.preserve_hierarchy && !legacy_role.parent_roles.is_empty() {
127 Some(format!(
129 "{}{}",
130 self.id_prefix, &legacy_role.parent_roles[0]
131 ))
132 } else {
133 None
134 };
135
136 let mut metadata = legacy_role.metadata.clone();
138
139 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 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 for role in legacy_roles {
172 role_map.insert(role.id.clone(), role);
173 }
174
175 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 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 #[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 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
250pub 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 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 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 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 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 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 fn normalize_resource(&self, resource: &str) -> String {
337 resource
339 .to_lowercase()
340 .replace("-", "_")
341 .replace(" ", "_")
342 .replace("/", "_")
343 }
344}
345
346pub 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 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 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 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 return Ok(None); };
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 for (key, value) in &legacy_assignment.attributes {
403 metadata.insert(key.clone(), value.clone());
404 }
405
406 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 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#[derive(Default)]
445pub struct LegacySystemConverter {
446 role_converter: RoleConverter,
447 permission_converter: PermissionConverter,
448 user_assignment_converter: UserAssignmentConverter,
449}
450
451impl LegacySystemConverter {
452 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 pub fn convert_system(
467 &self,
468 legacy_roles: &[LegacyRole],
469 legacy_permissions: &[LegacyPermission],
470 legacy_assignments: &[LegacyUserAssignment],
471 ) -> Result<ConvertedSystem, MigrationError> {
472 let converted_roles = self.role_converter.convert_roles(legacy_roles)?;
474
475 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 let converted_permissions = self
484 .permission_converter
485 .convert_permissions(legacy_permissions)?;
486
487 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 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#[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#[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()], 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); 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"); assert_eq!(converted.resource, "user_data"); 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}