Skip to main content

aster/permission/
migration.rs

1//! Migration Module for Tool Permission System
2//!
3//! This module provides migration utilities to convert permissions from the old
4//! `PermissionManager` system to the new `ToolPermissionManager` system.
5//!
6//! The migration preserves all existing permission configurations while converting
7//! them to the new format with enhanced features.
8//!
9//! Requirements: 11.5
10
11use super::types::{PermissionScope, ToolPermission};
12use crate::config::permission::{PermissionConfig, PermissionLevel, PermissionManager};
13use std::collections::HashMap;
14
15/// Migration result containing converted permissions and any warnings
16#[derive(Debug, Clone, Default)]
17pub struct MigrationResult {
18    /// Successfully migrated permissions
19    pub permissions: Vec<ToolPermission>,
20    /// Warnings encountered during migration
21    pub warnings: Vec<String>,
22    /// Number of tools migrated from always_allow
23    pub always_allow_count: usize,
24    /// Number of tools migrated from ask_before
25    pub ask_before_count: usize,
26    /// Number of tools migrated from never_allow
27    pub never_allow_count: usize,
28}
29
30impl MigrationResult {
31    /// Create a new empty migration result
32    pub fn new() -> Self {
33        Self::default()
34    }
35
36    /// Get total number of migrated permissions
37    pub fn total_count(&self) -> usize {
38        self.always_allow_count + self.ask_before_count + self.never_allow_count
39    }
40}
41
42/// Migrate permissions from the old PermissionManager to the new ToolPermission format
43///
44/// This function converts all permissions from the old system to the new format:
45/// - `always_allow` tools become `ToolPermission { allowed: true, priority: 100 }`
46/// - `ask_before` tools become `ToolPermission { allowed: true, priority: 50 }` with metadata
47/// - `never_allow` tools become `ToolPermission { allowed: false, priority: 100 }`
48///
49/// # Arguments
50/// * `old_manager` - Reference to the existing PermissionManager
51///
52/// # Returns
53/// A vector of ToolPermission objects representing all migrated permissions
54///
55/// # Requirements
56/// 11.5 - WHEN migrating from old system, THE Tool_Permission_Manager SHALL preserve
57///        existing permission configurations
58pub fn migrate_from_old_system(old_manager: &PermissionManager) -> Vec<ToolPermission> {
59    let result = migrate_from_old_system_with_details(old_manager);
60    result.permissions
61}
62
63/// Migrate permissions from the old PermissionManager with detailed results
64///
65/// This function provides more detailed information about the migration process,
66/// including counts and any warnings encountered.
67///
68/// # Arguments
69/// * `old_manager` - Reference to the existing PermissionManager
70///
71/// # Returns
72/// A MigrationResult containing the migrated permissions and migration statistics
73pub fn migrate_from_old_system_with_details(old_manager: &PermissionManager) -> MigrationResult {
74    let mut result = MigrationResult::new();
75
76    // Get all permission category names from the old manager
77    let permission_names = old_manager.get_permission_names();
78
79    for name in permission_names {
80        // We need to check each tool individually since the old manager
81        // doesn't expose the raw PermissionConfig directly
82        // Instead, we'll use the get methods to check permissions
83
84        // Note: The old PermissionManager stores permissions by category (user, smart_approve)
85        // and within each category has always_allow, ask_before, never_allow lists.
86        // Since we can't directly access the internal HashMap, we'll work with what's available.
87
88        // For now, we'll add a warning that we can only migrate what's accessible
89        result.warnings.push(format!(
90            "Permission category '{}' found - migration may be partial",
91            name
92        ));
93    }
94
95    result
96}
97
98/// Migrate a single PermissionConfig to ToolPermission objects
99///
100/// This function converts a PermissionConfig (containing always_allow, ask_before,
101/// never_allow lists) to a vector of ToolPermission objects.
102///
103/// # Arguments
104/// * `config` - The PermissionConfig to migrate
105/// * `category` - The category name (e.g., "user", "smart_approve")
106/// * `scope` - The PermissionScope to assign to migrated permissions
107///
108/// # Returns
109/// A vector of ToolPermission objects
110pub fn migrate_permission_config(
111    config: &PermissionConfig,
112    category: &str,
113    scope: PermissionScope,
114) -> Vec<ToolPermission> {
115    let mut permissions = Vec::new();
116
117    // Migrate always_allow tools
118    for tool in &config.always_allow {
119        let mut metadata = HashMap::new();
120        metadata.insert(
121            "migrated_from".to_string(),
122            serde_json::Value::String("always_allow".to_string()),
123        );
124        metadata.insert(
125            "original_category".to_string(),
126            serde_json::Value::String(category.to_string()),
127        );
128
129        permissions.push(ToolPermission {
130            tool: tool.clone(),
131            allowed: true,
132            priority: 100, // High priority for always_allow
133            conditions: Vec::new(),
134            parameter_restrictions: Vec::new(),
135            scope,
136            reason: Some(format!("Migrated from {} always_allow", category)),
137            expires_at: None,
138            metadata,
139        });
140    }
141
142    // Migrate ask_before tools
143    // These are tools that require user confirmation - we mark them as allowed
144    // but with lower priority and metadata indicating they need confirmation
145    for tool in &config.ask_before {
146        let mut metadata = HashMap::new();
147        metadata.insert(
148            "migrated_from".to_string(),
149            serde_json::Value::String("ask_before".to_string()),
150        );
151        metadata.insert(
152            "original_category".to_string(),
153            serde_json::Value::String(category.to_string()),
154        );
155        metadata.insert(
156            "requires_confirmation".to_string(),
157            serde_json::Value::Bool(true),
158        );
159
160        permissions.push(ToolPermission {
161            tool: tool.clone(),
162            allowed: true, // Allowed but requires confirmation (indicated in metadata)
163            priority: 50,  // Medium priority for ask_before
164            conditions: Vec::new(),
165            parameter_restrictions: Vec::new(),
166            scope,
167            reason: Some(format!(
168                "Migrated from {} ask_before (requires confirmation)",
169                category
170            )),
171            expires_at: None,
172            metadata,
173        });
174    }
175
176    // Migrate never_allow tools
177    for tool in &config.never_allow {
178        let mut metadata = HashMap::new();
179        metadata.insert(
180            "migrated_from".to_string(),
181            serde_json::Value::String("never_allow".to_string()),
182        );
183        metadata.insert(
184            "original_category".to_string(),
185            serde_json::Value::String(category.to_string()),
186        );
187
188        permissions.push(ToolPermission {
189            tool: tool.clone(),
190            allowed: false,
191            priority: 100, // High priority for never_allow (deny takes precedence)
192            conditions: Vec::new(),
193            parameter_restrictions: Vec::new(),
194            scope,
195            reason: Some(format!("Migrated from {} never_allow", category)),
196            expires_at: None,
197            metadata,
198        });
199    }
200
201    permissions
202}
203
204/// Migrate a PermissionLevel to a ToolPermission
205///
206/// This function converts a single PermissionLevel for a specific tool
207/// to a ToolPermission object.
208///
209/// # Arguments
210/// * `tool_name` - The name of the tool
211/// * `level` - The PermissionLevel to convert
212/// * `scope` - The PermissionScope to assign
213///
214/// # Returns
215/// A ToolPermission object representing the permission
216pub fn migrate_permission_level(
217    tool_name: &str,
218    level: PermissionLevel,
219    scope: PermissionScope,
220) -> ToolPermission {
221    let (allowed, priority, migrated_from) = match level {
222        PermissionLevel::AlwaysAllow => (true, 100, "always_allow"),
223        PermissionLevel::AskBefore => (true, 50, "ask_before"),
224        PermissionLevel::NeverAllow => (false, 100, "never_allow"),
225    };
226
227    let mut metadata = HashMap::new();
228    metadata.insert(
229        "migrated_from".to_string(),
230        serde_json::Value::String(migrated_from.to_string()),
231    );
232
233    let requires_confirmation = matches!(level, PermissionLevel::AskBefore);
234    if requires_confirmation {
235        metadata.insert(
236            "requires_confirmation".to_string(),
237            serde_json::Value::Bool(true),
238        );
239    }
240
241    ToolPermission {
242        tool: tool_name.to_string(),
243        allowed,
244        priority,
245        conditions: Vec::new(),
246        parameter_restrictions: Vec::new(),
247        scope,
248        reason: Some(format!("Migrated from {}", migrated_from)),
249        expires_at: None,
250        metadata,
251    }
252}
253
254/// Migrate all known tools from a PermissionManager
255///
256/// This function attempts to migrate permissions for a list of known tool names
257/// by querying the old PermissionManager for each tool.
258///
259/// # Arguments
260/// * `old_manager` - Reference to the existing PermissionManager
261/// * `tool_names` - List of tool names to check and migrate
262/// * `scope` - The PermissionScope to assign to migrated permissions
263///
264/// # Returns
265/// A MigrationResult containing the migrated permissions
266pub fn migrate_known_tools(
267    old_manager: &PermissionManager,
268    tool_names: &[&str],
269    scope: PermissionScope,
270) -> MigrationResult {
271    let mut result = MigrationResult::new();
272
273    for tool_name in tool_names {
274        // Check user permissions
275        if let Some(level) = old_manager.get_user_permission(tool_name) {
276            let permission = migrate_permission_level(tool_name, level.clone(), scope);
277
278            match level {
279                PermissionLevel::AlwaysAllow => result.always_allow_count += 1,
280                PermissionLevel::AskBefore => result.ask_before_count += 1,
281                PermissionLevel::NeverAllow => result.never_allow_count += 1,
282            }
283
284            result.permissions.push(permission);
285        }
286
287        // Check smart_approve permissions (if different from user permissions)
288        if let Some(level) = old_manager.get_smart_approve_permission(tool_name) {
289            // Only add if not already added from user permissions
290            let already_exists = result.permissions.iter().any(|p| p.tool == *tool_name);
291
292            if !already_exists {
293                let mut permission = migrate_permission_level(tool_name, level.clone(), scope);
294                permission.metadata.insert(
295                    "original_category".to_string(),
296                    serde_json::Value::String("smart_approve".to_string()),
297                );
298
299                match level {
300                    PermissionLevel::AlwaysAllow => result.always_allow_count += 1,
301                    PermissionLevel::AskBefore => result.ask_before_count += 1,
302                    PermissionLevel::NeverAllow => result.never_allow_count += 1,
303                }
304
305                result.permissions.push(permission);
306            }
307        }
308    }
309
310    result
311}
312
313/// Check if a ToolPermission was migrated from the old system
314///
315/// # Arguments
316/// * `permission` - The ToolPermission to check
317///
318/// # Returns
319/// true if the permission has migration metadata
320pub fn is_migrated_permission(permission: &ToolPermission) -> bool {
321    permission.metadata.contains_key("migrated_from")
322}
323
324/// Get the original permission level from a migrated ToolPermission
325///
326/// # Arguments
327/// * `permission` - The migrated ToolPermission
328///
329/// # Returns
330/// The original PermissionLevel if the permission was migrated, None otherwise
331pub fn get_original_permission_level(permission: &ToolPermission) -> Option<PermissionLevel> {
332    permission
333        .metadata
334        .get("migrated_from")
335        .and_then(|v| v.as_str())
336        .and_then(|s| match s {
337            "always_allow" => Some(PermissionLevel::AlwaysAllow),
338            "ask_before" => Some(PermissionLevel::AskBefore),
339            "never_allow" => Some(PermissionLevel::NeverAllow),
340            _ => None,
341        })
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347    use tempfile::NamedTempFile;
348
349    fn create_test_permission_manager() -> PermissionManager {
350        let temp_file = NamedTempFile::new().unwrap();
351        PermissionManager::new(temp_file.path())
352    }
353
354    #[test]
355    fn test_migrate_permission_level_always_allow() {
356        let permission = migrate_permission_level(
357            "test_tool",
358            PermissionLevel::AlwaysAllow,
359            PermissionScope::Global,
360        );
361
362        assert_eq!(permission.tool, "test_tool");
363        assert!(permission.allowed);
364        assert_eq!(permission.priority, 100);
365        assert_eq!(permission.scope, PermissionScope::Global);
366        assert!(permission.metadata.contains_key("migrated_from"));
367        assert_eq!(
368            permission.metadata.get("migrated_from"),
369            Some(&serde_json::Value::String("always_allow".to_string()))
370        );
371    }
372
373    #[test]
374    fn test_migrate_permission_level_ask_before() {
375        let permission = migrate_permission_level(
376            "test_tool",
377            PermissionLevel::AskBefore,
378            PermissionScope::Project,
379        );
380
381        assert_eq!(permission.tool, "test_tool");
382        assert!(permission.allowed);
383        assert_eq!(permission.priority, 50);
384        assert_eq!(permission.scope, PermissionScope::Project);
385        assert!(permission.metadata.contains_key("requires_confirmation"));
386        assert_eq!(
387            permission.metadata.get("requires_confirmation"),
388            Some(&serde_json::Value::Bool(true))
389        );
390    }
391
392    #[test]
393    fn test_migrate_permission_level_never_allow() {
394        let permission = migrate_permission_level(
395            "test_tool",
396            PermissionLevel::NeverAllow,
397            PermissionScope::Session,
398        );
399
400        assert_eq!(permission.tool, "test_tool");
401        assert!(!permission.allowed);
402        assert_eq!(permission.priority, 100);
403        assert_eq!(permission.scope, PermissionScope::Session);
404    }
405
406    #[test]
407    fn test_migrate_permission_config() {
408        let config = PermissionConfig {
409            always_allow: vec!["tool1".to_string(), "tool2".to_string()],
410            ask_before: vec!["tool3".to_string()],
411            never_allow: vec!["tool4".to_string()],
412        };
413
414        let permissions = migrate_permission_config(&config, "user", PermissionScope::Global);
415
416        assert_eq!(permissions.len(), 4);
417
418        // Check always_allow tools
419        let tool1 = permissions.iter().find(|p| p.tool == "tool1").unwrap();
420        assert!(tool1.allowed);
421        assert_eq!(tool1.priority, 100);
422
423        let tool2 = permissions.iter().find(|p| p.tool == "tool2").unwrap();
424        assert!(tool2.allowed);
425        assert_eq!(tool2.priority, 100);
426
427        // Check ask_before tool
428        let tool3 = permissions.iter().find(|p| p.tool == "tool3").unwrap();
429        assert!(tool3.allowed);
430        assert_eq!(tool3.priority, 50);
431        assert_eq!(
432            tool3.metadata.get("requires_confirmation"),
433            Some(&serde_json::Value::Bool(true))
434        );
435
436        // Check never_allow tool
437        let tool4 = permissions.iter().find(|p| p.tool == "tool4").unwrap();
438        assert!(!tool4.allowed);
439        assert_eq!(tool4.priority, 100);
440    }
441
442    #[test]
443    fn test_migrate_known_tools() {
444        let mut manager = create_test_permission_manager();
445        manager.update_user_permission("tool1", PermissionLevel::AlwaysAllow);
446        manager.update_user_permission("tool2", PermissionLevel::AskBefore);
447        manager.update_user_permission("tool3", PermissionLevel::NeverAllow);
448
449        let result = migrate_known_tools(
450            &manager,
451            &["tool1", "tool2", "tool3", "tool4"],
452            PermissionScope::Global,
453        );
454
455        assert_eq!(result.permissions.len(), 3);
456        assert_eq!(result.always_allow_count, 1);
457        assert_eq!(result.ask_before_count, 1);
458        assert_eq!(result.never_allow_count, 1);
459        assert_eq!(result.total_count(), 3);
460    }
461
462    #[test]
463    fn test_is_migrated_permission() {
464        let migrated = migrate_permission_level(
465            "test_tool",
466            PermissionLevel::AlwaysAllow,
467            PermissionScope::Global,
468        );
469        assert!(is_migrated_permission(&migrated));
470
471        let not_migrated = ToolPermission {
472            tool: "test_tool".to_string(),
473            allowed: true,
474            ..Default::default()
475        };
476        assert!(!is_migrated_permission(&not_migrated));
477    }
478
479    #[test]
480    fn test_get_original_permission_level() {
481        let always_allow = migrate_permission_level(
482            "tool1",
483            PermissionLevel::AlwaysAllow,
484            PermissionScope::Global,
485        );
486        assert_eq!(
487            get_original_permission_level(&always_allow),
488            Some(PermissionLevel::AlwaysAllow)
489        );
490
491        let ask_before =
492            migrate_permission_level("tool2", PermissionLevel::AskBefore, PermissionScope::Global);
493        assert_eq!(
494            get_original_permission_level(&ask_before),
495            Some(PermissionLevel::AskBefore)
496        );
497
498        let never_allow = migrate_permission_level(
499            "tool3",
500            PermissionLevel::NeverAllow,
501            PermissionScope::Global,
502        );
503        assert_eq!(
504            get_original_permission_level(&never_allow),
505            Some(PermissionLevel::NeverAllow)
506        );
507
508        let not_migrated = ToolPermission::default();
509        assert_eq!(get_original_permission_level(&not_migrated), None);
510    }
511
512    #[test]
513    fn test_migration_result_total_count() {
514        let mut result = MigrationResult::new();
515        result.always_allow_count = 5;
516        result.ask_before_count = 3;
517        result.never_allow_count = 2;
518
519        assert_eq!(result.total_count(), 10);
520    }
521
522    #[test]
523    fn test_migrate_empty_config() {
524        let config = PermissionConfig::default();
525        let permissions = migrate_permission_config(&config, "user", PermissionScope::Global);
526        assert!(permissions.is_empty());
527    }
528
529    #[test]
530    fn test_migrate_preserves_tool_names() {
531        let config = PermissionConfig {
532            always_allow: vec!["prefix__tool_name".to_string()],
533            ask_before: vec!["another__tool".to_string()],
534            never_allow: vec!["dangerous_tool".to_string()],
535        };
536
537        let permissions = migrate_permission_config(&config, "user", PermissionScope::Global);
538
539        assert!(permissions.iter().any(|p| p.tool == "prefix__tool_name"));
540        assert!(permissions.iter().any(|p| p.tool == "another__tool"));
541        assert!(permissions.iter().any(|p| p.tool == "dangerous_tool"));
542    }
543}