auth_framework/migration/
analyzers.rs

1//! Legacy system analyzers for migration planning
2//!
3//! This module provides analyzers to detect and analyze various
4//! types of legacy authorization systems.
5
6use super::{
7    LegacyPermission, LegacyRole, LegacySystemAnalysis, LegacySystemType, LegacyUserAssignment,
8    MigrationConfig, MigrationError, MigrationStrategy,
9};
10use serde_json::Value;
11use std::collections::{HashMap, HashSet};
12use std::path::Path;
13use tokio::fs;
14
15/// Analyze legacy authorization system from configuration
16pub async fn analyze_legacy_system<P: AsRef<Path>>(
17    config_path: P,
18    _config: &MigrationConfig,
19) -> Result<LegacySystemAnalysis, MigrationError> {
20    let config_content = fs::read_to_string(config_path).await?;
21
22    // Try to parse as JSON first
23    if let Ok(json_value) = serde_json::from_str::<Value>(&config_content) {
24        return analyze_json_config(&json_value).await;
25    }
26
27    // Try to parse as TOML
28    if let Ok(toml_value) = toml::from_str::<Value>(&config_content) {
29        return analyze_toml_config(&toml_value).await;
30    }
31
32    // Try to parse as YAML
33    if let Ok(yaml_value) = serde_yaml::from_str::<Value>(&config_content) {
34        return analyze_yaml_config(&yaml_value).await;
35    }
36
37    Err(MigrationError::AnalysisError(
38        "Unable to parse configuration file format".to_string(),
39    ))
40}
41
42/// Analyze JSON-based authorization configuration
43async fn analyze_json_config(config: &Value) -> Result<LegacySystemAnalysis, MigrationError> {
44    let mut analysis = LegacySystemAnalysis {
45        system_type: LegacySystemType::Custom("JSON-based".to_string()),
46        role_count: 0,
47        permission_count: 0,
48        user_assignment_count: 0,
49        roles: Vec::new(),
50        permissions: Vec::new(),
51        user_assignments: Vec::new(),
52        hierarchy_depth: 0,
53        duplicates_found: false,
54        orphaned_permissions: Vec::new(),
55        circular_dependencies: Vec::new(),
56        custom_attributes: HashSet::new(),
57        complexity_score: 1,
58        recommended_strategy: MigrationStrategy::DirectMapping,
59    };
60
61    // Detect system type based on structure
62    analysis.system_type = detect_json_system_type(config);
63
64    // Extract roles
65    if let Some(roles_section) = config.get("roles") {
66        analysis.roles = extract_json_roles(roles_section)?;
67        analysis.role_count = analysis.roles.len();
68    }
69
70    // Extract permissions
71    if let Some(permissions_section) = config.get("permissions") {
72        analysis.permissions = extract_json_permissions(permissions_section)?;
73        analysis.permission_count = analysis.permissions.len();
74    }
75
76    // Extract user assignments
77    if let Some(users_section) = config.get("users") {
78        analysis.user_assignments = extract_json_user_assignments(users_section)?;
79        analysis.user_assignment_count = analysis.user_assignments.len();
80    }
81
82    // Analyze complexity and generate recommendations
83    analyze_complexity_and_recommend_strategy(&mut analysis);
84
85    Ok(analysis)
86}
87
88/// Detect system type from JSON structure
89fn detect_json_system_type(config: &Value) -> LegacySystemType {
90    let has_roles = config.get("roles").is_some();
91    let has_permissions = config.get("permissions").is_some();
92    let has_attributes = config.get("attributes").is_some();
93    let has_policies = config.get("policies").is_some();
94
95    match (has_roles, has_permissions, has_attributes, has_policies) {
96        (true, true, false, false) => LegacySystemType::BasicRbac,
97        (false, true, false, false) => LegacySystemType::PermissionBased,
98        (true, true, true, _) => LegacySystemType::Abac,
99        _ => LegacySystemType::Custom("JSON-based".to_string()),
100    }
101}
102
103/// Extract roles from JSON configuration
104fn extract_json_roles(roles_section: &Value) -> Result<Vec<LegacyRole>, MigrationError> {
105    let mut roles = Vec::new();
106
107    match roles_section {
108        Value::Object(roles_map) => {
109            for (role_id, role_data) in roles_map {
110                let role = parse_json_role(role_id, role_data)?;
111                roles.push(role);
112            }
113        }
114        Value::Array(roles_array) => {
115            for role_data in roles_array {
116                if let Some(role_id) = role_data.get("id").and_then(|v| v.as_str()) {
117                    let role = parse_json_role(role_id, role_data)?;
118                    roles.push(role);
119                }
120            }
121        }
122        _ => {
123            return Err(MigrationError::AnalysisError(
124                "Invalid roles format".to_string(),
125            ));
126        }
127    }
128
129    Ok(roles)
130}
131
132/// Parse individual role from JSON
133fn parse_json_role(role_id: &str, role_data: &Value) -> Result<LegacyRole, MigrationError> {
134    let name = role_data
135        .get("name")
136        .and_then(|v| v.as_str())
137        .unwrap_or(role_id)
138        .to_string();
139
140    let description = role_data
141        .get("description")
142        .and_then(|v| v.as_str())
143        .map(|s| s.to_string());
144
145    let permissions = role_data
146        .get("permissions")
147        .and_then(|v| v.as_array())
148        .map(|arr| {
149            arr.iter()
150                .filter_map(|v| v.as_str())
151                .map(|s| s.to_string())
152                .collect()
153        })
154        .unwrap_or_default();
155
156    let parent_roles = role_data
157        .get("parents")
158        .or_else(|| role_data.get("inherits"))
159        .and_then(|v| v.as_array())
160        .map(|arr| {
161            arr.iter()
162                .filter_map(|v| v.as_str())
163                .map(|s| s.to_string())
164                .collect()
165        })
166        .unwrap_or_default();
167
168    let mut metadata = HashMap::new();
169    if let Some(meta) = role_data.get("metadata").and_then(|v| v.as_object()) {
170        for (key, value) in meta {
171            if let Some(value_str) = value.as_str() {
172                metadata.insert(key.clone(), value_str.to_string());
173            }
174        }
175    }
176
177    Ok(LegacyRole {
178        id: role_id.to_string(),
179        name,
180        description,
181        permissions,
182        parent_roles,
183        metadata,
184    })
185}
186
187/// Extract permissions from JSON configuration
188fn extract_json_permissions(
189    permissions_section: &Value,
190) -> Result<Vec<LegacyPermission>, MigrationError> {
191    let mut permissions = Vec::new();
192
193    match permissions_section {
194        Value::Object(perms_map) => {
195            for (perm_id, perm_data) in perms_map {
196                let permission = parse_json_permission(perm_id, perm_data)?;
197                permissions.push(permission);
198            }
199        }
200        Value::Array(perms_array) => {
201            for perm_data in perms_array {
202                if let Some(perm_id) = perm_data.get("id").and_then(|v| v.as_str()) {
203                    let permission = parse_json_permission(perm_id, perm_data)?;
204                    permissions.push(permission);
205                }
206            }
207        }
208        _ => {
209            return Err(MigrationError::AnalysisError(
210                "Invalid permissions format".to_string(),
211            ));
212        }
213    }
214
215    Ok(permissions)
216}
217
218/// Parse individual permission from JSON
219fn parse_json_permission(
220    perm_id: &str,
221    perm_data: &Value,
222) -> Result<LegacyPermission, MigrationError> {
223    let action = perm_data
224        .get("action")
225        .and_then(|v| v.as_str())
226        .unwrap_or("unknown")
227        .to_string();
228
229    let resource = perm_data
230        .get("resource")
231        .and_then(|v| v.as_str())
232        .unwrap_or("unknown")
233        .to_string();
234
235    let mut conditions = HashMap::new();
236    if let Some(cond) = perm_data.get("conditions").and_then(|v| v.as_object()) {
237        for (key, value) in cond {
238            if let Some(value_str) = value.as_str() {
239                conditions.insert(key.clone(), value_str.to_string());
240            }
241        }
242    }
243
244    let mut metadata = HashMap::new();
245    if let Some(meta) = perm_data.get("metadata").and_then(|v| v.as_object()) {
246        for (key, value) in meta {
247            if let Some(value_str) = value.as_str() {
248                metadata.insert(key.clone(), value_str.to_string());
249            }
250        }
251    }
252
253    Ok(LegacyPermission {
254        id: perm_id.to_string(),
255        action,
256        resource,
257        conditions,
258        metadata,
259    })
260}
261
262/// Extract user assignments from JSON configuration
263fn extract_json_user_assignments(
264    users_section: &Value,
265) -> Result<Vec<LegacyUserAssignment>, MigrationError> {
266    let mut assignments = Vec::new();
267
268    match users_section {
269        Value::Object(users_map) => {
270            for (user_id, user_data) in users_map {
271                let assignment = parse_json_user_assignment(user_id, user_data)?;
272                assignments.push(assignment);
273            }
274        }
275        Value::Array(users_array) => {
276            for user_data in users_array {
277                if let Some(user_id) = user_data.get("id").and_then(|v| v.as_str()) {
278                    let assignment = parse_json_user_assignment(user_id, user_data)?;
279                    assignments.push(assignment);
280                }
281            }
282        }
283        _ => {
284            return Err(MigrationError::AnalysisError(
285                "Invalid users format".to_string(),
286            ));
287        }
288    }
289
290    Ok(assignments)
291}
292
293/// Parse individual user assignment from JSON
294fn parse_json_user_assignment(
295    user_id: &str,
296    user_data: &Value,
297) -> Result<LegacyUserAssignment, MigrationError> {
298    let role_id = user_data
299        .get("role")
300        .and_then(|v| v.as_str())
301        .map(|s| s.to_string());
302
303    let permissions = user_data
304        .get("permissions")
305        .and_then(|v| v.as_array())
306        .map(|arr| {
307            arr.iter()
308                .filter_map(|v| v.as_str())
309                .map(|s| s.to_string())
310                .collect()
311        })
312        .unwrap_or_default();
313
314    let mut attributes = HashMap::new();
315    if let Some(attrs) = user_data.get("attributes").and_then(|v| v.as_object()) {
316        for (key, value) in attrs {
317            if let Some(value_str) = value.as_str() {
318                attributes.insert(key.clone(), value_str.to_string());
319            }
320        }
321    }
322
323    let expiration = user_data
324        .get("expires_at")
325        .and_then(|v| v.as_str())
326        .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
327        .map(|dt| dt.with_timezone(&chrono::Utc));
328
329    Ok(LegacyUserAssignment {
330        user_id: user_id.to_string(),
331        role_id,
332        permissions,
333        attributes,
334        expiration,
335    })
336}
337
338/// Analyze TOML-based authorization configuration
339async fn analyze_toml_config(config: &Value) -> Result<LegacySystemAnalysis, MigrationError> {
340    // Convert TOML Value to similar analysis as JSON
341    // TOML and JSON structures are similar enough to reuse logic
342    analyze_json_config(config).await
343}
344
345/// Analyze YAML-based authorization configuration
346async fn analyze_yaml_config(config: &Value) -> Result<LegacySystemAnalysis, MigrationError> {
347    // Convert YAML Value to similar analysis as JSON
348    // YAML and JSON structures are similar enough to reuse logic
349    analyze_json_config(config).await
350}
351
352/// Analyze complexity and recommend migration strategy
353fn analyze_complexity_and_recommend_strategy(analysis: &mut LegacySystemAnalysis) {
354    let mut complexity_score = 1u8;
355
356    // Factor in number of roles
357    complexity_score += match analysis.role_count {
358        0..=10 => 1,
359        11..=50 => 2,
360        51..=200 => 3,
361        _ => 4,
362    };
363
364    // Factor in number of permissions
365    complexity_score += match analysis.permission_count {
366        0..=20 => 1,
367        21..=100 => 2,
368        101..=500 => 3,
369        _ => 4,
370    };
371
372    // Factor in hierarchy depth
373    let max_depth = calculate_hierarchy_depth(&analysis.roles);
374    analysis.hierarchy_depth = max_depth;
375    complexity_score += match max_depth {
376        0..=2 => 1,
377        3..=5 => 2,
378        6..=10 => 3,
379        _ => 4,
380    };
381
382    // Check for duplicates
383    analysis.duplicates_found = check_for_duplicates(&analysis.roles, &analysis.permissions);
384    if analysis.duplicates_found {
385        complexity_score += 1;
386    }
387
388    // Find orphaned permissions
389    analysis.orphaned_permissions =
390        find_orphaned_permissions(&analysis.roles, &analysis.permissions);
391    if !analysis.orphaned_permissions.is_empty() {
392        complexity_score += 1;
393    }
394
395    // Check for circular dependencies
396    analysis.circular_dependencies = find_circular_dependencies(&analysis.roles);
397    if !analysis.circular_dependencies.is_empty() {
398        complexity_score += 2;
399    }
400
401    // Collect custom attributes
402    for assignment in &analysis.user_assignments {
403        for key in assignment.attributes.keys() {
404            analysis.custom_attributes.insert(key.clone());
405        }
406    }
407
408    if !analysis.custom_attributes.is_empty() {
409        complexity_score += 1;
410    }
411
412    // Cap complexity score at 10
413    analysis.complexity_score = complexity_score.min(10);
414
415    // Recommend strategy based on complexity
416    analysis.recommended_strategy = match analysis.complexity_score {
417        1..=3 => MigrationStrategy::DirectMapping,
418        4..=6 => MigrationStrategy::GradualMigration,
419        7..=8 => MigrationStrategy::Rebuild,
420        _ => MigrationStrategy::Custom("High complexity requires custom approach".to_string()),
421    };
422}
423
424/// Calculate maximum hierarchy depth
425fn calculate_hierarchy_depth(roles: &[LegacyRole]) -> usize {
426    let mut max_depth = 0;
427    let mut role_map: HashMap<String, &LegacyRole> = HashMap::new();
428
429    for role in roles {
430        role_map.insert(role.id.clone(), role);
431    }
432
433    for role in roles {
434        let depth = calculate_role_depth(&role.id, &role_map, &mut HashSet::new());
435        max_depth = max_depth.max(depth);
436    }
437
438    max_depth
439}
440
441/// Calculate depth for a specific role
442fn calculate_role_depth(
443    role_id: &str,
444    role_map: &HashMap<String, &LegacyRole>,
445    visited: &mut HashSet<String>,
446) -> usize {
447    if visited.contains(role_id) {
448        return 0; // Circular dependency
449    }
450
451    visited.insert(role_id.to_string());
452
453    if let Some(role) = role_map.get(role_id) {
454        if role.parent_roles.is_empty() {
455            visited.remove(role_id);
456            return 0;
457        }
458
459        let mut max_parent_depth = 0;
460        for parent_id in &role.parent_roles {
461            let parent_depth = calculate_role_depth(parent_id, role_map, visited);
462            max_parent_depth = max_parent_depth.max(parent_depth);
463        }
464
465        visited.remove(role_id);
466        return max_parent_depth + 1;
467    }
468
469    visited.remove(role_id);
470    0
471}
472
473/// Check for duplicate roles and permissions
474fn check_for_duplicates(roles: &[LegacyRole], permissions: &[LegacyPermission]) -> bool {
475    let mut role_names = HashSet::new();
476    let mut permission_names = HashSet::new();
477
478    for role in roles {
479        if !role_names.insert(&role.name) {
480            return true; // Duplicate role name found
481        }
482    }
483
484    for permission in permissions {
485        let perm_key = format!("{}:{}", permission.action, permission.resource);
486        if !permission_names.insert(perm_key) {
487            return true; // Duplicate permission found
488        }
489    }
490
491    false
492}
493
494/// Find permissions not assigned to any role
495fn find_orphaned_permissions(
496    roles: &[LegacyRole],
497    permissions: &[LegacyPermission],
498) -> Vec<String> {
499    let mut assigned_permissions = HashSet::new();
500
501    for role in roles {
502        for permission in &role.permissions {
503            assigned_permissions.insert(permission.clone());
504        }
505    }
506
507    permissions
508        .iter()
509        .filter(|perm| !assigned_permissions.contains(&perm.id))
510        .map(|perm| perm.id.clone())
511        .collect()
512}
513
514/// Find circular dependencies in role hierarchy
515fn find_circular_dependencies(roles: &[LegacyRole]) -> Vec<Vec<String>> {
516    let mut circular_deps = Vec::new();
517    let role_map: HashMap<String, &LegacyRole> =
518        roles.iter().map(|role| (role.id.clone(), role)).collect();
519
520    for role in roles {
521        if let Some(cycle) = detect_cycle(&role.id, &role_map, &mut Vec::new(), &mut HashSet::new())
522            && !circular_deps.contains(&cycle)
523        {
524            circular_deps.push(cycle);
525        }
526    }
527
528    circular_deps
529}
530
531/// Detect cycle in role hierarchy
532fn detect_cycle(
533    role_id: &str,
534    role_map: &HashMap<String, &LegacyRole>,
535    path: &mut Vec<String>,
536    visited: &mut HashSet<String>,
537) -> Option<Vec<String>> {
538    if path.contains(&role_id.to_string()) {
539        // Found cycle
540        if let Some(cycle_start) = path.iter().position(|id| id == role_id) {
541            return Some(path[cycle_start..].to_vec());
542        }
543    }
544
545    if visited.contains(role_id) {
546        return None;
547    }
548
549    visited.insert(role_id.to_string());
550    path.push(role_id.to_string());
551
552    if let Some(role) = role_map.get(role_id) {
553        for parent_id in &role.parent_roles {
554            if let Some(cycle) = detect_cycle(parent_id, role_map, path, visited) {
555                return Some(cycle);
556            }
557        }
558    }
559
560    path.pop();
561    None
562}
563
564#[cfg(test)]
565mod tests {
566    use super::*;
567    use serde_json::json;
568
569    #[tokio::test]
570    async fn test_analyze_json_config() {
571        let config = json!({
572            "roles": {
573                "admin": {
574                    "name": "Administrator",
575                    "permissions": ["read", "write", "delete"]
576                },
577                "user": {
578                    "name": "User",
579                    "permissions": ["read"]
580                }
581            },
582            "permissions": {
583                "read": {
584                    "action": "read",
585                    "resource": "data"
586                }
587            }
588        });
589
590        let analysis = analyze_json_config(&config).await.unwrap();
591        assert_eq!(analysis.role_count, 2);
592        assert_eq!(analysis.permission_count, 1);
593        assert_eq!(analysis.system_type, LegacySystemType::BasicRbac);
594    }
595
596    #[test]
597    fn test_detect_json_system_type() {
598        let basic_rbac = json!({
599            "roles": {},
600            "permissions": {}
601        });
602        assert_eq!(
603            detect_json_system_type(&basic_rbac),
604            LegacySystemType::BasicRbac
605        );
606
607        let permission_based = json!({
608            "permissions": {}
609        });
610        assert_eq!(
611            detect_json_system_type(&permission_based),
612            LegacySystemType::PermissionBased
613        );
614    }
615
616    #[test]
617    fn test_calculate_hierarchy_depth() {
618        let roles = vec![
619            LegacyRole {
620                id: "admin".to_string(),
621                name: "Admin".to_string(),
622                description: None,
623                permissions: vec![],
624                parent_roles: vec!["super_admin".to_string()],
625                metadata: HashMap::new(),
626            },
627            LegacyRole {
628                id: "super_admin".to_string(),
629                name: "Super Admin".to_string(),
630                description: None,
631                permissions: vec![],
632                parent_roles: vec![],
633                metadata: HashMap::new(),
634            },
635        ];
636
637        let depth = calculate_hierarchy_depth(&roles);
638        assert_eq!(depth, 1);
639    }
640}
641
642