Skip to main content

aster/permission/
manager.rs

1//! Tool Permission Manager Module
2//!
3//! This module implements the core `ToolPermissionManager` that provides
4//! fine-grained tool permission control for the AI Agent framework.
5//!
6//! Features:
7//! - Three-tier permission architecture (Global, Project, Session)
8//! - Parameter-level restrictions
9//! - Context-based condition evaluation
10//! - Permission merging with configurable strategies
11//! - Permission persistence (Global and Project scopes)
12//!
13//! Requirements: 1.1, 1.4, 1.5, 2.3, 2.4, 5.1, 5.2, 5.3, 5.4
14
15use super::condition::check_conditions;
16use super::merger::merge_permissions;
17use super::pattern::match_pattern;
18use super::policy::ToolPolicyManager;
19use super::restriction::check_parameter_restrictions;
20use super::types::{
21    PermissionContext, PermissionInheritance, PermissionResult, PermissionScope, RestrictionType,
22    ToolPermission,
23};
24use anyhow::{Context, Result};
25use serde::{Deserialize, Serialize};
26use serde_json::Value;
27use std::collections::HashMap;
28use std::fs::{self, File};
29use std::io::{BufReader, BufWriter};
30use std::path::PathBuf;
31
32/// Permission configuration file format
33///
34/// Used for serializing/deserializing permissions to/from JSON files
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct PermissionConfig {
37    /// Configuration version for future migrations
38    pub version: String,
39    /// Inheritance configuration
40    pub inheritance: PermissionInheritance,
41    /// List of permissions
42    pub permissions: Vec<ToolPermission>,
43}
44
45impl Default for PermissionConfig {
46    fn default() -> Self {
47        Self {
48            version: "1.0.0".to_string(),
49            inheritance: PermissionInheritance::default(),
50            permissions: Vec::new(),
51        }
52    }
53}
54
55/// File names for permission configuration
56const GLOBAL_PERMISSIONS_FILE: &str = "global_permissions.json";
57const PROJECT_PERMISSIONS_FILE: &str = "project_permissions.json";
58
59/// Tool Permission Manager
60///
61/// Manages tool permissions across three scopes: Global, Project, and Session.
62/// Provides permission checking, CRUD operations, and configuration management.
63///
64/// Requirements: 1.1
65pub struct ToolPermissionManager {
66    /// Global permissions (persisted to file)
67    global_permissions: HashMap<String, ToolPermission>,
68    /// Project permissions (persisted to file)
69    project_permissions: HashMap<String, ToolPermission>,
70    /// Session permissions (memory only)
71    session_permissions: HashMap<String, ToolPermission>,
72    /// Inheritance configuration
73    inheritance: PermissionInheritance,
74    /// Configuration directory for persistence
75    config_dir: Option<PathBuf>,
76    /// Custom template registry
77    /// Requirements: 7.5
78    template_registry: HashMap<String, Vec<ToolPermission>>,
79    /// Tool Policy Manager (optional, for new policy system)
80    /// Requirements: 5.1, 5.3
81    policy_manager: Option<ToolPolicyManager>,
82}
83
84impl ToolPermissionManager {
85    /// Create a new ToolPermissionManager
86    ///
87    /// # Arguments
88    /// * `config_dir` - Optional configuration directory for persistence
89    ///
90    /// # Returns
91    /// A new ToolPermissionManager instance with default settings
92    ///
93    /// Requirements: 1.1
94    pub fn new(config_dir: Option<PathBuf>) -> Self {
95        Self {
96            global_permissions: HashMap::new(),
97            project_permissions: HashMap::new(),
98            session_permissions: HashMap::new(),
99            inheritance: PermissionInheritance::default(),
100            config_dir,
101            template_registry: HashMap::new(),
102            policy_manager: None,
103        }
104    }
105
106    /// Enable the new Tool Policy system
107    ///
108    /// # Arguments
109    /// * `policy_manager` - The ToolPolicyManager to use
110    ///
111    /// Requirements: 5.1, 5.3
112    pub fn with_policy_manager(mut self, policy_manager: ToolPolicyManager) -> Self {
113        self.policy_manager = Some(policy_manager);
114        self
115    }
116
117    /// Set the policy manager
118    ///
119    /// Requirements: 5.1, 5.3
120    pub fn set_policy_manager(&mut self, policy_manager: ToolPolicyManager) {
121        self.policy_manager = Some(policy_manager);
122    }
123
124    /// Get the policy manager
125    pub fn policy_manager(&self) -> Option<&ToolPolicyManager> {
126        self.policy_manager.as_ref()
127    }
128
129    /// Get mutable policy manager
130    pub fn policy_manager_mut(&mut self) -> Option<&mut ToolPolicyManager> {
131        self.policy_manager.as_mut()
132    }
133
134    /// Get the configuration directory
135    pub fn config_dir(&self) -> Option<&PathBuf> {
136        self.config_dir.as_ref()
137    }
138
139    /// Get the inheritance configuration (reference)
140    pub fn inheritance(&self) -> &PermissionInheritance {
141        &self.inheritance
142    }
143
144    /// Get the inheritance configuration (cloned)
145    ///
146    /// Returns a clone of the current inheritance configuration.
147    /// Use this when you need to modify the configuration or pass it elsewhere.
148    ///
149    /// Requirements: 6.1, 6.2
150    pub fn get_inheritance(&self) -> PermissionInheritance {
151        self.inheritance.clone()
152    }
153
154    /// Set the inheritance configuration
155    ///
156    /// Updates the inheritance configuration that controls how permissions
157    /// are merged across scopes (Global, Project, Session).
158    ///
159    /// # Arguments
160    /// * `inheritance` - The new inheritance configuration
161    ///
162    /// Requirements: 6.1, 6.2
163    pub fn set_inheritance(&mut self, inheritance: PermissionInheritance) {
164        self.inheritance = inheritance;
165    }
166
167    /// Check if a tool is allowed to execute
168    ///
169    /// # Arguments
170    /// * `tool` - The tool name to check
171    /// * `params` - The tool parameters
172    /// * `context` - The permission context
173    ///
174    /// # Returns
175    /// A PermissionResult containing the decision and details
176    ///
177    /// # Behavior
178    /// 1. If policy_manager is set, check it first (new system takes precedence)
179    /// 2. Merge permissions from all scopes according to inheritance config
180    /// 3. Find matching rules by tool name (supports wildcards)
181    /// 4. Sort by priority (highest first)
182    /// 5. For each rule:
183    ///    - Skip if expired
184    ///    - Evaluate conditions
185    ///    - If conditions pass, check parameter restrictions
186    ///    - Return result based on rule's allowed flag
187    /// 6. If no rules match, allow by default
188    ///
189    /// Requirements: 2.3, 2.4, 5.1, 5.2, 5.3
190    pub fn is_allowed(
191        &self,
192        tool: &str,
193        params: &HashMap<String, Value>,
194        context: &PermissionContext,
195    ) -> PermissionResult {
196        // Step 0: Check policy manager first if enabled (Requirements: 5.1, 5.3)
197        if let Some(policy_manager) = &self.policy_manager {
198            let decision = policy_manager.is_allowed(tool);
199            if !decision.allowed {
200                return PermissionResult {
201                    allowed: false,
202                    reason: Some(decision.reason),
203                    restricted: false,
204                    suggestions: vec![format!(
205                        "Tool denied by policy layer: {:?}",
206                        decision.source_layer
207                    )],
208                    matched_rule: None,
209                    violations: Vec::new(),
210                };
211            }
212        }
213
214        // Step 1: Merge permissions from all scopes
215        let global_perms: Vec<ToolPermission> = self.global_permissions.values().cloned().collect();
216        let project_perms: Vec<ToolPermission> =
217            self.project_permissions.values().cloned().collect();
218        let session_perms: Vec<ToolPermission> =
219            self.session_permissions.values().cloned().collect();
220
221        let merged = merge_permissions(
222            &global_perms,
223            &project_perms,
224            &session_perms,
225            &self.inheritance,
226        );
227
228        // Step 2: Find matching rules by tool name
229        let mut matching_rules: Vec<&ToolPermission> = merged
230            .iter()
231            .filter(|perm| match_pattern(tool, &perm.tool))
232            .collect();
233
234        // Step 3: Sort by priority (highest first) - already sorted by merge_permissions
235        // but we re-sort to ensure correct order after filtering
236        matching_rules.sort_by(|a, b| b.priority.cmp(&a.priority));
237
238        // Step 4: Evaluate each rule
239        for rule in matching_rules {
240            // Skip expired rules
241            if let Some(expires_at) = rule.expires_at {
242                if context.timestamp > expires_at {
243                    continue;
244                }
245            }
246
247            // Evaluate conditions
248            if !check_conditions(&rule.conditions, context) {
249                continue;
250            }
251
252            // Conditions passed - this rule matches
253            // Check parameter restrictions
254            let restriction_result =
255                check_parameter_restrictions(&rule.parameter_restrictions, params);
256
257            match restriction_result {
258                Ok(()) => {
259                    // All restrictions passed
260                    if rule.allowed {
261                        return PermissionResult {
262                            allowed: true,
263                            reason: rule.reason.clone(),
264                            restricted: !rule.parameter_restrictions.is_empty(),
265                            suggestions: Vec::new(),
266                            matched_rule: Some(rule.clone()),
267                            violations: Vec::new(),
268                        };
269                    } else {
270                        // Tool is explicitly denied
271                        let suggestions = Self::generate_suggestions(rule, &[]);
272                        return PermissionResult {
273                            allowed: false,
274                            reason: rule.reason.clone().or_else(|| {
275                                Some(format!("Tool '{}' is denied by permission rule", tool))
276                            }),
277                            restricted: false,
278                            suggestions,
279                            matched_rule: Some(rule.clone()),
280                            violations: Vec::new(),
281                        };
282                    }
283                }
284                Err(violations) => {
285                    // Parameter restrictions violated
286                    let suggestions = Self::generate_suggestions(rule, &violations);
287                    return PermissionResult {
288                        allowed: false,
289                        reason: Some(format!(
290                            "Parameter restrictions violated for tool '{}'",
291                            tool
292                        )),
293                        restricted: true,
294                        suggestions,
295                        matched_rule: Some(rule.clone()),
296                        violations,
297                    };
298                }
299            }
300        }
301
302        // Step 5: No rules matched - allow by default
303        PermissionResult {
304            allowed: true,
305            reason: None,
306            restricted: false,
307            suggestions: Vec::new(),
308            matched_rule: None,
309            violations: Vec::new(),
310        }
311    }
312
313    /// Generate suggestions for resolving permission denials
314    ///
315    /// # Arguments
316    /// * `rule` - The matched permission rule
317    /// * `violations` - List of parameter violations
318    ///
319    /// # Returns
320    /// A list of suggestions for resolving the denial
321    ///
322    /// Requirements: 5.3, 5.4
323    pub fn generate_suggestions(rule: &ToolPermission, violations: &[String]) -> Vec<String> {
324        let mut suggestions = Vec::new();
325
326        // If tool is explicitly denied, suggest alternatives
327        if !rule.allowed {
328            if let Some(ref reason) = rule.reason {
329                suggestions.push(format!("Denial reason: {}", reason));
330            }
331
332            // Check if there are conditions that could be satisfied
333            if !rule.conditions.is_empty() {
334                suggestions.push(
335                    "This tool may be allowed under different conditions. \
336                     Check the permission conditions."
337                        .to_string(),
338                );
339            }
340
341            // Suggest checking scope
342            match rule.scope {
343                PermissionScope::Session => {
344                    suggestions.push(
345                        "This is a session-level restriction. \
346                         It will be reset when the session ends."
347                            .to_string(),
348                    );
349                }
350                PermissionScope::Project => {
351                    suggestions.push(
352                        "This is a project-level restriction. \
353                         Check project permission configuration."
354                            .to_string(),
355                    );
356                }
357                PermissionScope::Global => {
358                    suggestions.push(
359                        "This is a global restriction. \
360                         Contact administrator to modify global permissions."
361                            .to_string(),
362                    );
363                }
364            }
365        }
366
367        // Add suggestions based on violations
368        for violation in violations {
369            if violation.contains("whitelist") {
370                suggestions.push(format!(
371                    "Parameter value not in allowed list. {}",
372                    violation
373                ));
374            } else if violation.contains("blacklist") {
375                suggestions.push(format!(
376                    "Parameter value is blocked. Try a different value. {}",
377                    violation
378                ));
379            } else if violation.contains("pattern") {
380                suggestions.push(format!(
381                    "Parameter value doesn't match required format. {}",
382                    violation
383                ));
384            } else if violation.contains("range") {
385                suggestions.push(format!(
386                    "Parameter value is out of allowed range. {}",
387                    violation
388                ));
389            } else if violation.contains("Required") {
390                suggestions.push(format!("Missing required parameter. {}", violation));
391            } else {
392                suggestions.push(violation.clone());
393            }
394        }
395
396        // Add suggestion about parameter restrictions if present
397        if !rule.parameter_restrictions.is_empty() && violations.is_empty() {
398            suggestions.push(
399                "This tool has parameter restrictions. \
400                 Ensure all parameters meet the requirements."
401                    .to_string(),
402            );
403        }
404
405        suggestions
406    }
407
408    /// Add a permission rule
409    ///
410    /// # Arguments
411    /// * `permission` - The permission to add
412    /// * `scope` - The scope to add the permission to
413    pub fn add_permission(&mut self, permission: ToolPermission, scope: PermissionScope) {
414        let key = permission.tool.clone();
415        let mut perm = permission;
416        perm.scope = scope;
417
418        match scope {
419            PermissionScope::Global => {
420                self.global_permissions.insert(key, perm);
421            }
422            PermissionScope::Project => {
423                self.project_permissions.insert(key, perm);
424            }
425            PermissionScope::Session => {
426                self.session_permissions.insert(key, perm);
427            }
428        }
429    }
430
431    /// Remove a permission rule
432    ///
433    /// # Arguments
434    /// * `tool` - The tool name pattern to remove
435    /// * `scope` - Optional scope to remove from (None removes from all scopes)
436    pub fn remove_permission(&mut self, tool: &str, scope: Option<PermissionScope>) {
437        match scope {
438            Some(PermissionScope::Global) => {
439                self.global_permissions.remove(tool);
440            }
441            Some(PermissionScope::Project) => {
442                self.project_permissions.remove(tool);
443            }
444            Some(PermissionScope::Session) => {
445                self.session_permissions.remove(tool);
446            }
447            None => {
448                self.global_permissions.remove(tool);
449                self.project_permissions.remove(tool);
450                self.session_permissions.remove(tool);
451            }
452        }
453    }
454
455    /// Update a permission rule
456    ///
457    /// # Arguments
458    /// * `tool` - The tool name pattern to update
459    /// * `updates` - The partial updates to apply
460    /// * `scope` - The scope to update in
461    ///
462    /// # Returns
463    /// `true` if the permission was found and updated, `false` otherwise
464    ///
465    /// Requirements: 1.1
466    pub fn update_permission(
467        &mut self,
468        tool: &str,
469        updates: super::types::ToolPermissionUpdate,
470        scope: PermissionScope,
471    ) -> bool {
472        let permissions = match scope {
473            PermissionScope::Global => &mut self.global_permissions,
474            PermissionScope::Project => &mut self.project_permissions,
475            PermissionScope::Session => &mut self.session_permissions,
476        };
477
478        if let Some(perm) = permissions.get_mut(tool) {
479            // Apply updates
480            if let Some(allowed) = updates.allowed {
481                perm.allowed = allowed;
482            }
483            if let Some(priority) = updates.priority {
484                perm.priority = priority;
485            }
486            if let Some(conditions) = updates.conditions {
487                perm.conditions = conditions;
488            }
489            if let Some(restrictions) = updates.parameter_restrictions {
490                perm.parameter_restrictions = restrictions;
491            }
492            if let Some(reason) = updates.reason {
493                perm.reason = reason;
494            }
495            if let Some(expires_at) = updates.expires_at {
496                perm.expires_at = expires_at;
497            }
498            if let Some(metadata) = updates.metadata {
499                perm.metadata = metadata;
500            }
501            true
502        } else {
503            false
504        }
505    }
506
507    /// Get all permissions
508    ///
509    /// # Arguments
510    /// * `scope` - Optional scope filter (None returns all)
511    ///
512    /// # Returns
513    /// A vector of permissions matching the scope filter
514    pub fn get_permissions(&self, scope: Option<PermissionScope>) -> Vec<ToolPermission> {
515        match scope {
516            Some(PermissionScope::Global) => self.global_permissions.values().cloned().collect(),
517            Some(PermissionScope::Project) => self.project_permissions.values().cloned().collect(),
518            Some(PermissionScope::Session) => self.session_permissions.values().cloned().collect(),
519            None => {
520                let mut all = Vec::new();
521                all.extend(self.global_permissions.values().cloned());
522                all.extend(self.project_permissions.values().cloned());
523                all.extend(self.session_permissions.values().cloned());
524                all
525            }
526        }
527    }
528
529    /// Get permission for a specific tool
530    ///
531    /// # Arguments
532    /// * `tool` - The tool name to look up
533    ///
534    /// # Returns
535    /// The first matching permission (Session > Project > Global priority)
536    pub fn get_tool_permission(&self, tool: &str) -> Option<ToolPermission> {
537        // Check session first (highest priority)
538        if let Some(perm) = self.session_permissions.get(tool) {
539            return Some(perm.clone());
540        }
541
542        // Check project
543        if let Some(perm) = self.project_permissions.get(tool) {
544            return Some(perm.clone());
545        }
546
547        // Check global
548        if let Some(perm) = self.global_permissions.get(tool) {
549            return Some(perm.clone());
550        }
551
552        None
553    }
554
555    /// Get the number of permissions in each scope
556    pub fn permission_counts(&self) -> (usize, usize, usize) {
557        (
558            self.global_permissions.len(),
559            self.project_permissions.len(),
560            self.session_permissions.len(),
561        )
562    }
563
564    /// Clear all permissions in a specific scope
565    pub fn clear_scope(&mut self, scope: PermissionScope) {
566        match scope {
567            PermissionScope::Global => self.global_permissions.clear(),
568            PermissionScope::Project => self.project_permissions.clear(),
569            PermissionScope::Session => self.session_permissions.clear(),
570        }
571    }
572
573    /// Clear all permissions
574    pub fn clear_all(&mut self) {
575        self.global_permissions.clear();
576        self.project_permissions.clear();
577        self.session_permissions.clear();
578    }
579
580    // ========================================================================
581    // Template Methods
582    // ========================================================================
583
584    /// Register a custom permission template
585    ///
586    /// Registers a named template that can be applied later using `apply_template`.
587    /// If a template with the same name already exists, it will be replaced.
588    ///
589    /// # Arguments
590    /// * `name` - The name to register the template under
591    /// * `template` - The vector of permissions that make up the template
592    ///
593    /// # Example
594    /// ```ignore
595    /// let mut manager = ToolPermissionManager::new(None);
596    /// let custom_template = vec![
597    ///     ToolPermission {
598    ///         tool: "custom_tool".to_string(),
599    ///         allowed: true,
600    ///         ..Default::default()
601    ///     },
602    /// ];
603    /// manager.register_template("my_template", custom_template);
604    /// ```
605    ///
606    /// Requirements: 7.5
607    pub fn register_template(&mut self, name: &str, template: Vec<ToolPermission>) {
608        self.template_registry.insert(name.to_string(), template);
609    }
610
611    /// Apply a registered template to a specific scope
612    ///
613    /// Applies all permissions from the named template to the specified scope.
614    /// Each permission's scope field is updated to match the target scope.
615    ///
616    /// # Arguments
617    /// * `name` - The name of the registered template to apply
618    /// * `scope` - The scope to apply the template permissions to
619    ///
620    /// # Returns
621    /// `true` if the template was found and applied, `false` if the template doesn't exist
622    ///
623    /// # Example
624    /// ```ignore
625    /// let mut manager = ToolPermissionManager::new(None);
626    /// manager.register_template("my_template", vec![...]);
627    /// manager.apply_template("my_template", PermissionScope::Project);
628    /// ```
629    ///
630    /// Requirements: 7.5
631    pub fn apply_template(&mut self, name: &str, scope: PermissionScope) -> bool {
632        let Some(template) = self.template_registry.get(name).cloned() else {
633            return false;
634        };
635
636        for mut perm in template {
637            perm.scope = scope;
638            self.add_permission(perm, scope);
639        }
640
641        true
642    }
643
644    /// Get a registered template by name
645    ///
646    /// # Arguments
647    /// * `name` - The name of the template to retrieve
648    ///
649    /// # Returns
650    /// The template permissions if found, None otherwise
651    ///
652    /// Requirements: 7.5
653    pub fn get_template(&self, name: &str) -> Option<&Vec<ToolPermission>> {
654        self.template_registry.get(name)
655    }
656
657    /// Remove a registered template
658    ///
659    /// # Arguments
660    /// * `name` - The name of the template to remove
661    ///
662    /// # Returns
663    /// The removed template if it existed, None otherwise
664    ///
665    /// Requirements: 7.5
666    pub fn remove_template(&mut self, name: &str) -> Option<Vec<ToolPermission>> {
667        self.template_registry.remove(name)
668    }
669
670    /// List all registered template names
671    ///
672    /// # Returns
673    /// A vector of all registered template names
674    ///
675    /// Requirements: 7.5
676    pub fn list_templates(&self) -> Vec<&String> {
677        self.template_registry.keys().collect()
678    }
679
680    /// Check if a template is registered
681    ///
682    /// # Arguments
683    /// * `name` - The name of the template to check
684    ///
685    /// # Returns
686    /// `true` if the template exists, `false` otherwise
687    ///
688    /// Requirements: 7.5
689    pub fn has_template(&self, name: &str) -> bool {
690        self.template_registry.contains_key(name)
691    }
692
693    // ========================================================================
694    // Statistics and Query Methods
695    // ========================================================================
696
697    /// Get permission statistics
698    ///
699    /// Calculates and returns statistics about the current permission configuration.
700    ///
701    /// # Returns
702    /// A `PermissionStats` struct containing:
703    /// - total_permissions: Total number of permissions across all scopes
704    /// - allowed_tools: Number of permissions with allowed=true
705    /// - denied_tools: Number of permissions with allowed=false
706    /// - conditional_tools: Number of permissions with at least one condition
707    /// - restricted_parameters: Number of permissions with at least one parameter restriction
708    ///
709    /// Requirements: 9.1
710    pub fn get_stats(&self) -> super::types::PermissionStats {
711        let all_permissions = self.get_permissions(None);
712
713        let total_permissions = all_permissions.len();
714        let allowed_tools = all_permissions.iter().filter(|p| p.allowed).count();
715        let denied_tools = all_permissions.iter().filter(|p| !p.allowed).count();
716        let conditional_tools = all_permissions
717            .iter()
718            .filter(|p| !p.conditions.is_empty())
719            .count();
720        let restricted_parameters = all_permissions
721            .iter()
722            .filter(|p| !p.parameter_restrictions.is_empty())
723            .count();
724
725        super::types::PermissionStats {
726            total_permissions,
727            allowed_tools,
728            denied_tools,
729            conditional_tools,
730            restricted_parameters,
731        }
732    }
733
734    /// Query permissions with filters
735    ///
736    /// Returns all permissions that match the specified filter criteria.
737    /// All filter conditions are combined with AND logic.
738    ///
739    /// # Arguments
740    /// * `filter` - The filter criteria to apply
741    ///
742    /// # Returns
743    /// A vector of permissions matching all specified filter criteria.
744    ///
745    /// # Filter Behavior
746    /// - `allowed`: Filter by allowed flag (true/false)
747    /// - `scope`: Filter by permission scope (Global/Project/Session)
748    /// - `has_conditions`: Filter by whether permission has conditions
749    /// - `has_restrictions`: Filter by whether permission has parameter restrictions
750    /// - `tool_pattern`: Filter by tool name pattern (supports wildcards)
751    ///
752    /// Requirements: 9.2, 9.3
753    pub fn query_permissions(&self, filter: super::types::PermissionFilter) -> Vec<ToolPermission> {
754        let all_permissions = self.get_permissions(filter.scope);
755
756        all_permissions
757            .into_iter()
758            .filter(|perm| {
759                // Filter by allowed
760                if let Some(allowed) = filter.allowed {
761                    if perm.allowed != allowed {
762                        return false;
763                    }
764                }
765
766                // Filter by has_conditions
767                if let Some(has_conditions) = filter.has_conditions {
768                    let perm_has_conditions = !perm.conditions.is_empty();
769                    if perm_has_conditions != has_conditions {
770                        return false;
771                    }
772                }
773
774                // Filter by has_restrictions
775                if let Some(has_restrictions) = filter.has_restrictions {
776                    let perm_has_restrictions = !perm.parameter_restrictions.is_empty();
777                    if perm_has_restrictions != has_restrictions {
778                        return false;
779                    }
780                }
781
782                // Filter by tool_pattern
783                if let Some(ref pattern) = filter.tool_pattern {
784                    if !match_pattern(&perm.tool, pattern) {
785                        return false;
786                    }
787                }
788
789                true
790            })
791            .collect()
792    }
793
794    // ========================================================================
795    // Persistence Methods
796    // ========================================================================
797
798    /// Load permissions from configuration files
799    ///
800    /// Loads Global permissions from the config directory and Project permissions
801    /// from the project-specific configuration. Session permissions are not loaded
802    /// as they are memory-only.
803    ///
804    /// # Behavior
805    /// - If config_dir is None, no permissions are loaded
806    /// - If a config file doesn't exist, that scope starts empty
807    /// - If a config file is invalid, an error is logged and that scope starts empty
808    ///
809    /// Requirements: 1.4
810    pub fn load_permissions(&mut self) {
811        let Some(config_dir) = &self.config_dir else {
812            return;
813        };
814
815        // Load global permissions
816        let global_path = config_dir.join(GLOBAL_PERMISSIONS_FILE);
817        if global_path.exists() {
818            match Self::load_config_file(&global_path) {
819                Ok(config) => {
820                    self.inheritance = config.inheritance;
821                    for perm in config.permissions {
822                        let key = perm.tool.clone();
823                        self.global_permissions.insert(key, perm);
824                    }
825                }
826                Err(e) => {
827                    tracing::warn!(
828                        "Failed to load global permissions from {:?}: {}",
829                        global_path,
830                        e
831                    );
832                }
833            }
834        }
835
836        // Load project permissions
837        let project_path = config_dir.join(PROJECT_PERMISSIONS_FILE);
838        if project_path.exists() {
839            match Self::load_config_file(&project_path) {
840                Ok(config) => {
841                    for perm in config.permissions {
842                        let key = perm.tool.clone();
843                        self.project_permissions.insert(key, perm);
844                    }
845                }
846                Err(e) => {
847                    tracing::warn!(
848                        "Failed to load project permissions from {:?}: {}",
849                        project_path,
850                        e
851                    );
852                }
853            }
854        }
855
856        // Session permissions are NOT loaded - they are memory-only (Requirement 1.5)
857    }
858
859    /// Load a permission configuration file
860    fn load_config_file(path: &PathBuf) -> Result<PermissionConfig> {
861        let file = File::open(path)
862            .with_context(|| format!("Failed to open permission config file: {:?}", path))?;
863        let reader = BufReader::new(file);
864        let config: PermissionConfig = serde_json::from_reader(reader)
865            .with_context(|| format!("Failed to parse permission config file: {:?}", path))?;
866        Ok(config)
867    }
868
869    /// Save permissions to configuration files
870    ///
871    /// Saves permissions to the appropriate configuration file based on scope.
872    /// Session permissions are NOT saved as they are memory-only.
873    ///
874    /// # Arguments
875    /// * `scope` - The scope to save (Global or Project only)
876    ///
877    /// # Returns
878    /// * `Ok(())` if save was successful
879    /// * `Err` if save failed or if trying to save Session scope
880    ///
881    /// # Behavior
882    /// - Creates the config directory if it doesn't exist
883    /// - Writes to a temporary file first, then atomically renames
884    /// - Session scope returns an error (memory-only)
885    ///
886    /// Requirements: 1.4, 1.5
887    pub fn save_permissions(&self, scope: PermissionScope) -> Result<()> {
888        // Session permissions are NOT persisted (Requirement 1.5)
889        if scope == PermissionScope::Session {
890            anyhow::bail!("Session permissions cannot be persisted - they are memory-only");
891        }
892
893        let Some(config_dir) = &self.config_dir else {
894            anyhow::bail!("No config directory configured for persistence");
895        };
896
897        // Ensure config directory exists
898        fs::create_dir_all(config_dir)
899            .with_context(|| format!("Failed to create config directory: {:?}", config_dir))?;
900
901        let (file_name, permissions) = match scope {
902            PermissionScope::Global => (GLOBAL_PERMISSIONS_FILE, &self.global_permissions),
903            PermissionScope::Project => (PROJECT_PERMISSIONS_FILE, &self.project_permissions),
904            PermissionScope::Session => unreachable!(), // Already handled above
905        };
906
907        let config = PermissionConfig {
908            version: "1.0.0".to_string(),
909            inheritance: self.inheritance.clone(),
910            permissions: permissions.values().cloned().collect(),
911        };
912
913        let file_path = config_dir.join(file_name);
914        let temp_path = file_path.with_extension("tmp");
915
916        // Write to temporary file first
917        let file = File::create(&temp_path)
918            .with_context(|| format!("Failed to create temp file: {:?}", temp_path))?;
919        let writer = BufWriter::new(file);
920        serde_json::to_writer_pretty(writer, &config)
921            .with_context(|| format!("Failed to write permission config: {:?}", temp_path))?;
922
923        // Atomically rename temp file to target file
924        fs::rename(&temp_path, &file_path)
925            .with_context(|| format!("Failed to rename temp file to: {:?}", file_path))?;
926
927        Ok(())
928    }
929
930    /// Get the path to a permission config file
931    pub fn get_config_path(&self, scope: PermissionScope) -> Option<PathBuf> {
932        self.config_dir.as_ref().map(|dir| {
933            let file_name = match scope {
934                PermissionScope::Global => GLOBAL_PERMISSIONS_FILE,
935                PermissionScope::Project => PROJECT_PERMISSIONS_FILE,
936                PermissionScope::Session => return dir.join("session_permissions.json"), // Not actually used
937            };
938            dir.join(file_name)
939        })
940    }
941
942    /// Check if a config file exists for the given scope
943    pub fn config_exists(&self, scope: PermissionScope) -> bool {
944        self.get_config_path(scope)
945            .map(|p| p.exists())
946            .unwrap_or(false)
947    }
948
949    // ========================================================================
950    // Import/Export Methods
951    // ========================================================================
952
953    /// Export permissions to JSON format
954    ///
955    /// Exports permissions from the specified scope(s) to a JSON string.
956    /// The exported configuration includes version information for future migrations.
957    ///
958    /// # Arguments
959    /// * `scope` - Optional scope filter. If None, exports all scopes.
960    ///
961    /// # Returns
962    /// * `Ok(String)` - JSON string containing the exported permissions
963    /// * `Err` - If serialization fails
964    ///
965    /// # Format
966    /// The exported JSON follows the PermissionConfig format:
967    /// ```json
968    /// {
969    ///   "version": "1.0.0",
970    ///   "inheritance": { ... },
971    ///   "permissions": [ ... ]
972    /// }
973    /// ```
974    ///
975    /// Requirements: 8.1, 8.5
976    pub fn export(&self, scope: Option<PermissionScope>) -> Result<String> {
977        let permissions = self.get_permissions(scope);
978
979        let config = PermissionConfig {
980            version: "1.0.0".to_string(),
981            inheritance: self.inheritance.clone(),
982            permissions,
983        };
984
985        serde_json::to_string_pretty(&config).context("Failed to serialize permissions to JSON")
986    }
987
988    /// Import permissions from JSON format
989    ///
990    /// Imports permissions from a JSON string into the specified scope.
991    /// The import validates the configuration format before applying changes.
992    /// If validation fails, existing permissions remain unchanged.
993    ///
994    /// # Arguments
995    /// * `config_json` - JSON string containing the permission configuration
996    /// * `scope` - The scope to import permissions into
997    ///
998    /// # Returns
999    /// * `Ok(())` - If import was successful
1000    /// * `Err` - If validation or parsing fails (existing permissions unchanged)
1001    ///
1002    /// # Behavior
1003    /// - Validates JSON format before modifying any permissions
1004    /// - Replaces all permissions in the target scope with imported ones
1005    /// - Updates inheritance configuration from the imported config
1006    /// - Sets the scope field of all imported permissions to the target scope
1007    ///
1008    /// Requirements: 8.2, 8.3, 8.4
1009    pub fn import(&mut self, config_json: &str, scope: PermissionScope) -> Result<()> {
1010        // Parse and validate the configuration first (before modifying anything)
1011        let config: PermissionConfig = serde_json::from_str(config_json)
1012            .context("Failed to parse permission configuration JSON")?;
1013
1014        // Validate version (for future compatibility)
1015        Self::validate_config_version(&config.version)?;
1016
1017        // Validate all permissions in the config
1018        for perm in &config.permissions {
1019            Self::validate_permission(perm)?;
1020        }
1021
1022        // All validation passed - now apply the changes
1023        // Clear existing permissions in the target scope
1024        self.clear_scope(scope);
1025
1026        // Import permissions with the target scope
1027        for mut perm in config.permissions {
1028            perm.scope = scope;
1029            let key = perm.tool.clone();
1030            match scope {
1031                PermissionScope::Global => {
1032                    self.global_permissions.insert(key, perm);
1033                }
1034                PermissionScope::Project => {
1035                    self.project_permissions.insert(key, perm);
1036                }
1037                PermissionScope::Session => {
1038                    self.session_permissions.insert(key, perm);
1039                }
1040            }
1041        }
1042
1043        // Update inheritance configuration
1044        self.inheritance = config.inheritance;
1045
1046        Ok(())
1047    }
1048
1049    /// Validate configuration version
1050    ///
1051    /// Checks if the configuration version is supported.
1052    /// Currently supports version "1.0.0".
1053    fn validate_config_version(version: &str) -> Result<()> {
1054        // For now, we only support version 1.0.0
1055        // Future versions can add migration logic here
1056        match version {
1057            "1.0.0" => Ok(()),
1058            _ => anyhow::bail!(
1059                "Unsupported configuration version: {}. Supported versions: 1.0.0",
1060                version
1061            ),
1062        }
1063    }
1064
1065    /// Validate a single permission
1066    ///
1067    /// Performs basic validation on a permission to ensure it's well-formed.
1068    fn validate_permission(perm: &ToolPermission) -> Result<()> {
1069        // Tool name must not be empty
1070        if perm.tool.is_empty() {
1071            anyhow::bail!("Permission tool name cannot be empty");
1072        }
1073
1074        // Validate parameter restrictions
1075        for restriction in &perm.parameter_restrictions {
1076            if restriction.parameter.is_empty() {
1077                anyhow::bail!("Parameter restriction parameter name cannot be empty");
1078            }
1079
1080            // Range restrictions must have at least min or max
1081            if restriction.restriction_type == RestrictionType::Range
1082                && restriction.min.is_none()
1083                && restriction.max.is_none()
1084            {
1085                anyhow::bail!(
1086                    "Range restriction for parameter '{}' must have at least min or max",
1087                    restriction.parameter
1088                );
1089            }
1090
1091            // Pattern restrictions must have a pattern
1092            if restriction.restriction_type == RestrictionType::Pattern
1093                && restriction.pattern.is_none()
1094            {
1095                anyhow::bail!(
1096                    "Pattern restriction for parameter '{}' must have a pattern",
1097                    restriction.parameter
1098                );
1099            }
1100
1101            // Whitelist/Blacklist restrictions should have values
1102            if (restriction.restriction_type == RestrictionType::Whitelist
1103                || restriction.restriction_type == RestrictionType::Blacklist)
1104                && restriction.values.is_none()
1105            {
1106                anyhow::bail!(
1107                    "{:?} restriction for parameter '{}' must have values",
1108                    restriction.restriction_type,
1109                    restriction.parameter
1110                );
1111            }
1112        }
1113
1114        Ok(())
1115    }
1116}
1117
1118impl Default for ToolPermissionManager {
1119    fn default() -> Self {
1120        Self::new(None)
1121    }
1122}
1123
1124#[cfg(test)]
1125mod tests {
1126    use super::*;
1127    use crate::permission::types::{
1128        ConditionOperator, ConditionType, ParameterRestriction, PermissionCondition,
1129        RestrictionType,
1130    };
1131
1132    fn create_test_context() -> PermissionContext {
1133        PermissionContext {
1134            working_directory: PathBuf::from("/home/user/project"),
1135            session_id: "test-session".to_string(),
1136            timestamp: 1700000000,
1137            user: Some("testuser".to_string()),
1138            environment: HashMap::new(),
1139            metadata: HashMap::new(),
1140        }
1141    }
1142
1143    fn create_simple_permission(
1144        tool: &str,
1145        allowed: bool,
1146        scope: PermissionScope,
1147    ) -> ToolPermission {
1148        ToolPermission {
1149            tool: tool.to_string(),
1150            allowed,
1151            priority: 0,
1152            conditions: Vec::new(),
1153            parameter_restrictions: Vec::new(),
1154            scope,
1155            reason: None,
1156            expires_at: None,
1157            metadata: HashMap::new(),
1158        }
1159    }
1160
1161    #[test]
1162    fn test_new_manager() {
1163        let manager = ToolPermissionManager::new(None);
1164        assert!(manager.config_dir().is_none());
1165        assert_eq!(manager.permission_counts(), (0, 0, 0));
1166    }
1167
1168    #[test]
1169    fn test_new_manager_with_config_dir() {
1170        let config_dir = PathBuf::from("/tmp/config");
1171        let manager = ToolPermissionManager::new(Some(config_dir.clone()));
1172        assert_eq!(manager.config_dir(), Some(&config_dir));
1173    }
1174
1175    #[test]
1176    fn test_add_permission() {
1177        let mut manager = ToolPermissionManager::new(None);
1178        let perm = create_simple_permission("bash", true, PermissionScope::Global);
1179
1180        manager.add_permission(perm, PermissionScope::Global);
1181
1182        assert_eq!(manager.permission_counts(), (1, 0, 0));
1183        assert!(manager.get_tool_permission("bash").is_some());
1184    }
1185
1186    #[test]
1187    fn test_add_permission_different_scopes() {
1188        let mut manager = ToolPermissionManager::new(None);
1189
1190        manager.add_permission(
1191            create_simple_permission("bash", true, PermissionScope::Global),
1192            PermissionScope::Global,
1193        );
1194        manager.add_permission(
1195            create_simple_permission("file_read", true, PermissionScope::Project),
1196            PermissionScope::Project,
1197        );
1198        manager.add_permission(
1199            create_simple_permission("http_get", true, PermissionScope::Session),
1200            PermissionScope::Session,
1201        );
1202
1203        assert_eq!(manager.permission_counts(), (1, 1, 1));
1204    }
1205
1206    #[test]
1207    fn test_remove_permission_specific_scope() {
1208        let mut manager = ToolPermissionManager::new(None);
1209        manager.add_permission(
1210            create_simple_permission("bash", true, PermissionScope::Global),
1211            PermissionScope::Global,
1212        );
1213        manager.add_permission(
1214            create_simple_permission("bash", false, PermissionScope::Session),
1215            PermissionScope::Session,
1216        );
1217
1218        manager.remove_permission("bash", Some(PermissionScope::Global));
1219
1220        assert_eq!(manager.permission_counts(), (0, 0, 1));
1221    }
1222
1223    #[test]
1224    fn test_remove_permission_all_scopes() {
1225        let mut manager = ToolPermissionManager::new(None);
1226        manager.add_permission(
1227            create_simple_permission("bash", true, PermissionScope::Global),
1228            PermissionScope::Global,
1229        );
1230        manager.add_permission(
1231            create_simple_permission("bash", false, PermissionScope::Session),
1232            PermissionScope::Session,
1233        );
1234
1235        manager.remove_permission("bash", None);
1236
1237        assert_eq!(manager.permission_counts(), (0, 0, 0));
1238    }
1239
1240    #[test]
1241    fn test_get_permissions_by_scope() {
1242        let mut manager = ToolPermissionManager::new(None);
1243        manager.add_permission(
1244            create_simple_permission("bash", true, PermissionScope::Global),
1245            PermissionScope::Global,
1246        );
1247        manager.add_permission(
1248            create_simple_permission("file_read", true, PermissionScope::Project),
1249            PermissionScope::Project,
1250        );
1251
1252        let global = manager.get_permissions(Some(PermissionScope::Global));
1253        let project = manager.get_permissions(Some(PermissionScope::Project));
1254        let all = manager.get_permissions(None);
1255
1256        assert_eq!(global.len(), 1);
1257        assert_eq!(project.len(), 1);
1258        assert_eq!(all.len(), 2);
1259    }
1260
1261    #[test]
1262    fn test_get_tool_permission_priority() {
1263        let mut manager = ToolPermissionManager::new(None);
1264        manager.add_permission(
1265            create_simple_permission("bash", true, PermissionScope::Global),
1266            PermissionScope::Global,
1267        );
1268        manager.add_permission(
1269            create_simple_permission("bash", false, PermissionScope::Session),
1270            PermissionScope::Session,
1271        );
1272
1273        let perm = manager.get_tool_permission("bash").unwrap();
1274        // Session has higher priority
1275        assert!(!perm.allowed);
1276        assert_eq!(perm.scope, PermissionScope::Session);
1277    }
1278
1279    #[test]
1280    fn test_is_allowed_no_rules() {
1281        let manager = ToolPermissionManager::new(None);
1282        let context = create_test_context();
1283        let params = HashMap::new();
1284
1285        let result = manager.is_allowed("any_tool", &params, &context);
1286
1287        assert!(result.allowed);
1288        assert!(result.matched_rule.is_none());
1289    }
1290
1291    #[test]
1292    fn test_is_allowed_explicit_allow() {
1293        let mut manager = ToolPermissionManager::new(None);
1294        manager.add_permission(
1295            create_simple_permission("bash", true, PermissionScope::Global),
1296            PermissionScope::Global,
1297        );
1298
1299        let context = create_test_context();
1300        let params = HashMap::new();
1301
1302        let result = manager.is_allowed("bash", &params, &context);
1303
1304        assert!(result.allowed);
1305        assert!(result.matched_rule.is_some());
1306    }
1307
1308    #[test]
1309    fn test_is_allowed_explicit_deny() {
1310        let mut manager = ToolPermissionManager::new(None);
1311        let mut perm = create_simple_permission("bash", false, PermissionScope::Global);
1312        perm.reason = Some("Dangerous command".to_string());
1313        manager.add_permission(perm, PermissionScope::Global);
1314
1315        let context = create_test_context();
1316        let params = HashMap::new();
1317
1318        let result = manager.is_allowed("bash", &params, &context);
1319
1320        assert!(!result.allowed);
1321        assert!(result.matched_rule.is_some());
1322        assert!(result.reason.is_some());
1323    }
1324
1325    #[test]
1326    fn test_is_allowed_wildcard_pattern() {
1327        let mut manager = ToolPermissionManager::new(None);
1328        manager.add_permission(
1329            create_simple_permission("file_*", true, PermissionScope::Global),
1330            PermissionScope::Global,
1331        );
1332
1333        let context = create_test_context();
1334        let params = HashMap::new();
1335
1336        assert!(manager.is_allowed("file_read", &params, &context).allowed);
1337        assert!(manager.is_allowed("file_write", &params, &context).allowed);
1338        assert!(manager.is_allowed("file_delete", &params, &context).allowed);
1339    }
1340
1341    #[test]
1342    fn test_is_allowed_expired_rule() {
1343        let mut manager = ToolPermissionManager::new(None);
1344        let mut perm = create_simple_permission("bash", false, PermissionScope::Global);
1345        perm.expires_at = Some(1600000000); // Expired
1346        manager.add_permission(perm, PermissionScope::Global);
1347
1348        let context = create_test_context(); // timestamp = 1700000000
1349
1350        let params = HashMap::new();
1351        let result = manager.is_allowed("bash", &params, &context);
1352
1353        // Expired rule should be skipped, default allow
1354        assert!(result.allowed);
1355    }
1356
1357    #[test]
1358    fn test_is_allowed_with_conditions() {
1359        let mut manager = ToolPermissionManager::new(None);
1360        let mut perm = create_simple_permission("bash", true, PermissionScope::Global);
1361        perm.conditions = vec![PermissionCondition {
1362            condition_type: ConditionType::Context,
1363            field: Some("working_directory".to_string()),
1364            operator: ConditionOperator::Contains,
1365            value: serde_json::json!("project"),
1366            validator: None,
1367            description: None,
1368        }];
1369        manager.add_permission(perm, PermissionScope::Global);
1370
1371        let context = create_test_context(); // working_directory contains "project"
1372        let params = HashMap::new();
1373
1374        let result = manager.is_allowed("bash", &params, &context);
1375        assert!(result.allowed);
1376    }
1377
1378    #[test]
1379    fn test_is_allowed_conditions_not_met() {
1380        let mut manager = ToolPermissionManager::new(None);
1381        let mut perm = create_simple_permission("bash", true, PermissionScope::Global);
1382        perm.conditions = vec![PermissionCondition {
1383            condition_type: ConditionType::Context,
1384            field: Some("working_directory".to_string()),
1385            operator: ConditionOperator::Contains,
1386            value: serde_json::json!("safe_directory"),
1387            validator: None,
1388            description: None,
1389        }];
1390        manager.add_permission(perm, PermissionScope::Global);
1391
1392        let context = create_test_context(); // working_directory does NOT contain "safe_directory"
1393        let params = HashMap::new();
1394
1395        let result = manager.is_allowed("bash", &params, &context);
1396        // Condition not met, rule skipped, default allow
1397        assert!(result.allowed);
1398        assert!(result.matched_rule.is_none());
1399    }
1400
1401    #[test]
1402    fn test_is_allowed_parameter_restriction_pass() {
1403        let mut manager = ToolPermissionManager::new(None);
1404        let mut perm = create_simple_permission("bash", true, PermissionScope::Global);
1405        perm.parameter_restrictions = vec![ParameterRestriction {
1406            parameter: "command".to_string(),
1407            restriction_type: RestrictionType::Whitelist,
1408            values: Some(vec![serde_json::json!("ls"), serde_json::json!("cat")]),
1409            pattern: None,
1410            validator: None,
1411            min: None,
1412            max: None,
1413            required: false,
1414            description: None,
1415        }];
1416        manager.add_permission(perm, PermissionScope::Global);
1417
1418        let context = create_test_context();
1419        let mut params = HashMap::new();
1420        params.insert("command".to_string(), serde_json::json!("ls"));
1421
1422        let result = manager.is_allowed("bash", &params, &context);
1423        assert!(result.allowed);
1424        assert!(result.restricted);
1425    }
1426
1427    #[test]
1428    fn test_is_allowed_parameter_restriction_fail() {
1429        let mut manager = ToolPermissionManager::new(None);
1430        let mut perm = create_simple_permission("bash", true, PermissionScope::Global);
1431        perm.parameter_restrictions = vec![ParameterRestriction {
1432            parameter: "command".to_string(),
1433            restriction_type: RestrictionType::Whitelist,
1434            values: Some(vec![serde_json::json!("ls"), serde_json::json!("cat")]),
1435            pattern: None,
1436            validator: None,
1437            min: None,
1438            max: None,
1439            required: false,
1440            description: None,
1441        }];
1442        manager.add_permission(perm, PermissionScope::Global);
1443
1444        let context = create_test_context();
1445        let mut params = HashMap::new();
1446        params.insert("command".to_string(), serde_json::json!("rm -rf"));
1447
1448        let result = manager.is_allowed("bash", &params, &context);
1449        assert!(!result.allowed);
1450        assert!(result.restricted);
1451        assert!(!result.violations.is_empty());
1452    }
1453
1454    #[test]
1455    fn test_is_allowed_priority_order() {
1456        let mut manager = ToolPermissionManager::new(None);
1457
1458        // Low priority: allow
1459        let mut low_perm = create_simple_permission("bash", true, PermissionScope::Global);
1460        low_perm.priority = 1;
1461        manager.add_permission(low_perm, PermissionScope::Global);
1462
1463        // High priority: deny
1464        let mut high_perm = create_simple_permission("bash", false, PermissionScope::Session);
1465        high_perm.priority = 10;
1466        manager.add_permission(high_perm, PermissionScope::Session);
1467
1468        let context = create_test_context();
1469        let params = HashMap::new();
1470
1471        let result = manager.is_allowed("bash", &params, &context);
1472        // High priority rule should win
1473        assert!(!result.allowed);
1474    }
1475
1476    #[test]
1477    fn test_generate_suggestions_denied() {
1478        let mut perm = create_simple_permission("bash", false, PermissionScope::Global);
1479        perm.reason = Some("Security policy".to_string());
1480
1481        let suggestions = ToolPermissionManager::generate_suggestions(&perm, &[]);
1482
1483        assert!(!suggestions.is_empty());
1484        assert!(suggestions.iter().any(|s| s.contains("Security policy")));
1485    }
1486
1487    #[test]
1488    fn test_generate_suggestions_with_violations() {
1489        let perm = create_simple_permission("bash", true, PermissionScope::Global);
1490        let violations = vec!["Parameter 'command' value \"rm\" is not in whitelist".to_string()];
1491
1492        let suggestions = ToolPermissionManager::generate_suggestions(&perm, &violations);
1493
1494        assert!(!suggestions.is_empty());
1495        assert!(suggestions.iter().any(|s| s.contains("whitelist")));
1496    }
1497
1498    #[test]
1499    fn test_clear_scope() {
1500        let mut manager = ToolPermissionManager::new(None);
1501        manager.add_permission(
1502            create_simple_permission("bash", true, PermissionScope::Global),
1503            PermissionScope::Global,
1504        );
1505        manager.add_permission(
1506            create_simple_permission("file", true, PermissionScope::Session),
1507            PermissionScope::Session,
1508        );
1509
1510        manager.clear_scope(PermissionScope::Global);
1511
1512        assert_eq!(manager.permission_counts(), (0, 0, 1));
1513    }
1514
1515    #[test]
1516    fn test_clear_all() {
1517        let mut manager = ToolPermissionManager::new(None);
1518        manager.add_permission(
1519            create_simple_permission("bash", true, PermissionScope::Global),
1520            PermissionScope::Global,
1521        );
1522        manager.add_permission(
1523            create_simple_permission("file", true, PermissionScope::Session),
1524            PermissionScope::Session,
1525        );
1526
1527        manager.clear_all();
1528
1529        assert_eq!(manager.permission_counts(), (0, 0, 0));
1530    }
1531
1532    #[test]
1533    fn test_set_inheritance() {
1534        let mut manager = ToolPermissionManager::new(None);
1535        let new_inheritance = PermissionInheritance {
1536            inherit_global: false,
1537            inherit_project: true,
1538            override_global: false,
1539            merge_strategy: crate::permission::types::MergeStrategy::Merge,
1540        };
1541
1542        manager.set_inheritance(new_inheritance.clone());
1543
1544        assert_eq!(manager.inheritance(), &new_inheritance);
1545    }
1546
1547    #[test]
1548    fn test_update_permission_allowed() {
1549        use crate::permission::types::ToolPermissionUpdate;
1550
1551        let mut manager = ToolPermissionManager::new(None);
1552        manager.add_permission(
1553            create_simple_permission("bash", true, PermissionScope::Global),
1554            PermissionScope::Global,
1555        );
1556
1557        let update = ToolPermissionUpdate::new().with_allowed(false);
1558        let result = manager.update_permission("bash", update, PermissionScope::Global);
1559
1560        assert!(result);
1561        let perm = manager.get_tool_permission("bash").unwrap();
1562        assert!(!perm.allowed);
1563    }
1564
1565    #[test]
1566    fn test_update_permission_priority() {
1567        use crate::permission::types::ToolPermissionUpdate;
1568
1569        let mut manager = ToolPermissionManager::new(None);
1570        manager.add_permission(
1571            create_simple_permission("bash", true, PermissionScope::Global),
1572            PermissionScope::Global,
1573        );
1574
1575        let update = ToolPermissionUpdate::new().with_priority(100);
1576        let result = manager.update_permission("bash", update, PermissionScope::Global);
1577
1578        assert!(result);
1579        let perm = manager.get_tool_permission("bash").unwrap();
1580        assert_eq!(perm.priority, 100);
1581    }
1582
1583    #[test]
1584    fn test_update_permission_reason() {
1585        use crate::permission::types::ToolPermissionUpdate;
1586
1587        let mut manager = ToolPermissionManager::new(None);
1588        manager.add_permission(
1589            create_simple_permission("bash", true, PermissionScope::Global),
1590            PermissionScope::Global,
1591        );
1592
1593        let update = ToolPermissionUpdate::new().with_reason(Some("Updated reason".to_string()));
1594        let result = manager.update_permission("bash", update, PermissionScope::Global);
1595
1596        assert!(result);
1597        let perm = manager.get_tool_permission("bash").unwrap();
1598        assert_eq!(perm.reason, Some("Updated reason".to_string()));
1599    }
1600
1601    #[test]
1602    fn test_update_permission_not_found() {
1603        use crate::permission::types::ToolPermissionUpdate;
1604
1605        let mut manager = ToolPermissionManager::new(None);
1606
1607        let update = ToolPermissionUpdate::new().with_allowed(false);
1608        let result = manager.update_permission("nonexistent", update, PermissionScope::Global);
1609
1610        assert!(!result);
1611    }
1612
1613    #[test]
1614    fn test_update_permission_wrong_scope() {
1615        use crate::permission::types::ToolPermissionUpdate;
1616
1617        let mut manager = ToolPermissionManager::new(None);
1618        manager.add_permission(
1619            create_simple_permission("bash", true, PermissionScope::Global),
1620            PermissionScope::Global,
1621        );
1622
1623        // Try to update in Session scope where it doesn't exist
1624        let update = ToolPermissionUpdate::new().with_allowed(false);
1625        let result = manager.update_permission("bash", update, PermissionScope::Session);
1626
1627        assert!(!result);
1628        // Original permission should be unchanged
1629        let perm = manager.get_tool_permission("bash").unwrap();
1630        assert!(perm.allowed);
1631    }
1632
1633    #[test]
1634    fn test_update_permission_multiple_fields() {
1635        use crate::permission::types::ToolPermissionUpdate;
1636
1637        let mut manager = ToolPermissionManager::new(None);
1638        manager.add_permission(
1639            create_simple_permission("bash", true, PermissionScope::Project),
1640            PermissionScope::Project,
1641        );
1642
1643        let update = ToolPermissionUpdate::new()
1644            .with_allowed(false)
1645            .with_priority(50)
1646            .with_reason(Some("Security update".to_string()))
1647            .with_expires_at(Some(1800000000));
1648
1649        let result = manager.update_permission("bash", update, PermissionScope::Project);
1650
1651        assert!(result);
1652        let perm = manager.get_tool_permission("bash").unwrap();
1653        assert!(!perm.allowed);
1654        assert_eq!(perm.priority, 50);
1655        assert_eq!(perm.reason, Some("Security update".to_string()));
1656        assert_eq!(perm.expires_at, Some(1800000000));
1657    }
1658
1659    #[test]
1660    fn test_update_permission_clear_reason() {
1661        use crate::permission::types::ToolPermissionUpdate;
1662
1663        let mut manager = ToolPermissionManager::new(None);
1664        let mut perm = create_simple_permission("bash", true, PermissionScope::Global);
1665        perm.reason = Some("Initial reason".to_string());
1666        manager.add_permission(perm, PermissionScope::Global);
1667
1668        // Clear the reason by setting it to None
1669        let update = ToolPermissionUpdate::new().with_reason(None);
1670        let result = manager.update_permission("bash", update, PermissionScope::Global);
1671
1672        assert!(result);
1673        let perm = manager.get_tool_permission("bash").unwrap();
1674        assert!(perm.reason.is_none());
1675    }
1676
1677    // ========================================================================
1678    // Export/Import Tests
1679    // ========================================================================
1680
1681    #[test]
1682    fn test_export_empty() {
1683        let manager = ToolPermissionManager::new(None);
1684        let result = manager.export(None);
1685
1686        assert!(result.is_ok());
1687        let json = result.unwrap();
1688        assert!(json.contains("\"version\": \"1.0.0\""));
1689        assert!(json.contains("\"permissions\": []"));
1690    }
1691
1692    #[test]
1693    fn test_export_with_permissions() {
1694        let mut manager = ToolPermissionManager::new(None);
1695        manager.add_permission(
1696            create_simple_permission("bash", true, PermissionScope::Global),
1697            PermissionScope::Global,
1698        );
1699        manager.add_permission(
1700            create_simple_permission("file_read", false, PermissionScope::Project),
1701            PermissionScope::Project,
1702        );
1703
1704        let result = manager.export(None);
1705
1706        assert!(result.is_ok());
1707        let json = result.unwrap();
1708        assert!(json.contains("\"version\": \"1.0.0\""));
1709        assert!(json.contains("\"bash\""));
1710        assert!(json.contains("\"file_read\""));
1711    }
1712
1713    #[test]
1714    fn test_export_specific_scope() {
1715        let mut manager = ToolPermissionManager::new(None);
1716        manager.add_permission(
1717            create_simple_permission("bash", true, PermissionScope::Global),
1718            PermissionScope::Global,
1719        );
1720        manager.add_permission(
1721            create_simple_permission("file_read", false, PermissionScope::Project),
1722            PermissionScope::Project,
1723        );
1724
1725        let result = manager.export(Some(PermissionScope::Global));
1726
1727        assert!(result.is_ok());
1728        let json = result.unwrap();
1729        assert!(json.contains("\"bash\""));
1730        assert!(!json.contains("\"file_read\""));
1731    }
1732
1733    #[test]
1734    fn test_import_valid_config() {
1735        let mut manager = ToolPermissionManager::new(None);
1736        let config_json = r#"{
1737            "version": "1.0.0",
1738            "inheritance": {
1739                "inherit_global": true,
1740                "inherit_project": true,
1741                "override_global": true,
1742                "merge_strategy": "Override"
1743            },
1744            "permissions": [
1745                {
1746                    "tool": "bash",
1747                    "allowed": true,
1748                    "priority": 10,
1749                    "conditions": [],
1750                    "parameter_restrictions": [],
1751                    "scope": "Global",
1752                    "reason": "Test permission",
1753                    "expires_at": null,
1754                    "metadata": {}
1755                }
1756            ]
1757        }"#;
1758
1759        let result = manager.import(config_json, PermissionScope::Global);
1760
1761        assert!(result.is_ok());
1762        assert_eq!(manager.permission_counts(), (1, 0, 0));
1763        let perm = manager.get_tool_permission("bash").unwrap();
1764        assert!(perm.allowed);
1765        assert_eq!(perm.priority, 10);
1766    }
1767
1768    #[test]
1769    fn test_import_invalid_json() {
1770        let mut manager = ToolPermissionManager::new(None);
1771        manager.add_permission(
1772            create_simple_permission("existing", true, PermissionScope::Global),
1773            PermissionScope::Global,
1774        );
1775
1776        let result = manager.import("invalid json", PermissionScope::Global);
1777
1778        assert!(result.is_err());
1779        // Existing permissions should remain unchanged
1780        assert_eq!(manager.permission_counts(), (1, 0, 0));
1781    }
1782
1783    #[test]
1784    fn test_import_invalid_version() {
1785        let mut manager = ToolPermissionManager::new(None);
1786        let config_json = r#"{
1787            "version": "99.0.0",
1788            "inheritance": {
1789                "inherit_global": true,
1790                "inherit_project": true,
1791                "override_global": true,
1792                "merge_strategy": "Override"
1793            },
1794            "permissions": []
1795        }"#;
1796
1797        let result = manager.import(config_json, PermissionScope::Global);
1798
1799        assert!(result.is_err());
1800        assert!(result
1801            .unwrap_err()
1802            .to_string()
1803            .contains("Unsupported configuration version"));
1804    }
1805
1806    #[test]
1807    fn test_import_empty_tool_name() {
1808        let mut manager = ToolPermissionManager::new(None);
1809        let config_json = r#"{
1810            "version": "1.0.0",
1811            "inheritance": {
1812                "inherit_global": true,
1813                "inherit_project": true,
1814                "override_global": true,
1815                "merge_strategy": "Override"
1816            },
1817            "permissions": [
1818                {
1819                    "tool": "",
1820                    "allowed": true,
1821                    "priority": 0,
1822                    "conditions": [],
1823                    "parameter_restrictions": [],
1824                    "scope": "Global",
1825                    "reason": null,
1826                    "expires_at": null,
1827                    "metadata": {}
1828                }
1829            ]
1830        }"#;
1831
1832        let result = manager.import(config_json, PermissionScope::Global);
1833
1834        assert!(result.is_err());
1835        assert!(result
1836            .unwrap_err()
1837            .to_string()
1838            .contains("tool name cannot be empty"));
1839    }
1840
1841    #[test]
1842    fn test_import_replaces_existing() {
1843        let mut manager = ToolPermissionManager::new(None);
1844        manager.add_permission(
1845            create_simple_permission("old_tool", true, PermissionScope::Global),
1846            PermissionScope::Global,
1847        );
1848
1849        let config_json = r#"{
1850            "version": "1.0.0",
1851            "inheritance": {
1852                "inherit_global": true,
1853                "inherit_project": true,
1854                "override_global": true,
1855                "merge_strategy": "Override"
1856            },
1857            "permissions": [
1858                {
1859                    "tool": "new_tool",
1860                    "allowed": false,
1861                    "priority": 5,
1862                    "conditions": [],
1863                    "parameter_restrictions": [],
1864                    "scope": "Global",
1865                    "reason": null,
1866                    "expires_at": null,
1867                    "metadata": {}
1868                }
1869            ]
1870        }"#;
1871
1872        let result = manager.import(config_json, PermissionScope::Global);
1873
1874        assert!(result.is_ok());
1875        assert_eq!(manager.permission_counts(), (1, 0, 0));
1876        assert!(manager.get_tool_permission("old_tool").is_none());
1877        assert!(manager.get_tool_permission("new_tool").is_some());
1878    }
1879
1880    #[test]
1881    fn test_import_sets_target_scope() {
1882        let mut manager = ToolPermissionManager::new(None);
1883        let config_json = r#"{
1884            "version": "1.0.0",
1885            "inheritance": {
1886                "inherit_global": true,
1887                "inherit_project": true,
1888                "override_global": true,
1889                "merge_strategy": "Override"
1890            },
1891            "permissions": [
1892                {
1893                    "tool": "bash",
1894                    "allowed": true,
1895                    "priority": 0,
1896                    "conditions": [],
1897                    "parameter_restrictions": [],
1898                    "scope": "Global",
1899                    "reason": null,
1900                    "expires_at": null,
1901                    "metadata": {}
1902                }
1903            ]
1904        }"#;
1905
1906        // Import into Session scope (different from the scope in JSON)
1907        let result = manager.import(config_json, PermissionScope::Session);
1908
1909        assert!(result.is_ok());
1910        assert_eq!(manager.permission_counts(), (0, 0, 1));
1911        let perm = manager.get_tool_permission("bash").unwrap();
1912        assert_eq!(perm.scope, PermissionScope::Session);
1913    }
1914
1915    #[test]
1916    fn test_export_import_round_trip() {
1917        let mut manager = ToolPermissionManager::new(None);
1918        let mut perm = create_simple_permission("bash_*", true, PermissionScope::Global);
1919        perm.priority = 42;
1920        perm.reason = Some("Test reason".to_string());
1921        manager.add_permission(perm, PermissionScope::Global);
1922
1923        // Export
1924        let exported = manager.export(Some(PermissionScope::Global)).unwrap();
1925
1926        // Create new manager and import
1927        let mut new_manager = ToolPermissionManager::new(None);
1928        let result = new_manager.import(&exported, PermissionScope::Global);
1929
1930        assert!(result.is_ok());
1931        let imported_perm = new_manager.get_tool_permission("bash_*").unwrap();
1932        assert_eq!(imported_perm.tool, "bash_*");
1933        assert!(imported_perm.allowed);
1934        assert_eq!(imported_perm.priority, 42);
1935        assert_eq!(imported_perm.reason, Some("Test reason".to_string()));
1936    }
1937
1938    #[test]
1939    fn test_import_invalid_range_restriction() {
1940        let mut manager = ToolPermissionManager::new(None);
1941        let config_json = r#"{
1942            "version": "1.0.0",
1943            "inheritance": {
1944                "inherit_global": true,
1945                "inherit_project": true,
1946                "override_global": true,
1947                "merge_strategy": "Override"
1948            },
1949            "permissions": [
1950                {
1951                    "tool": "bash",
1952                    "allowed": true,
1953                    "priority": 0,
1954                    "conditions": [],
1955                    "parameter_restrictions": [
1956                        {
1957                            "parameter": "count",
1958                            "restriction_type": "Range",
1959                            "values": null,
1960                            "pattern": null,
1961                            "min": null,
1962                            "max": null,
1963                            "required": false,
1964                            "description": null
1965                        }
1966                    ],
1967                    "scope": "Global",
1968                    "reason": null,
1969                    "expires_at": null,
1970                    "metadata": {}
1971                }
1972            ]
1973        }"#;
1974
1975        let result = manager.import(config_json, PermissionScope::Global);
1976
1977        assert!(result.is_err());
1978        assert!(result
1979            .unwrap_err()
1980            .to_string()
1981            .contains("must have at least min or max"));
1982    }
1983
1984    #[test]
1985    fn test_import_invalid_pattern_restriction() {
1986        let mut manager = ToolPermissionManager::new(None);
1987        let config_json = r#"{
1988            "version": "1.0.0",
1989            "inheritance": {
1990                "inherit_global": true,
1991                "inherit_project": true,
1992                "override_global": true,
1993                "merge_strategy": "Override"
1994            },
1995            "permissions": [
1996                {
1997                    "tool": "bash",
1998                    "allowed": true,
1999                    "priority": 0,
2000                    "conditions": [],
2001                    "parameter_restrictions": [
2002                        {
2003                            "parameter": "command",
2004                            "restriction_type": "Pattern",
2005                            "values": null,
2006                            "pattern": null,
2007                            "min": null,
2008                            "max": null,
2009                            "required": false,
2010                            "description": null
2011                        }
2012                    ],
2013                    "scope": "Global",
2014                    "reason": null,
2015                    "expires_at": null,
2016                    "metadata": {}
2017                }
2018            ]
2019        }"#;
2020
2021        let result = manager.import(config_json, PermissionScope::Global);
2022
2023        assert!(result.is_err());
2024        assert!(result
2025            .unwrap_err()
2026            .to_string()
2027            .contains("must have a pattern"));
2028    }
2029
2030    #[test]
2031    fn test_import_invalid_whitelist_restriction() {
2032        let mut manager = ToolPermissionManager::new(None);
2033        let config_json = r#"{
2034            "version": "1.0.0",
2035            "inheritance": {
2036                "inherit_global": true,
2037                "inherit_project": true,
2038                "override_global": true,
2039                "merge_strategy": "Override"
2040            },
2041            "permissions": [
2042                {
2043                    "tool": "bash",
2044                    "allowed": true,
2045                    "priority": 0,
2046                    "conditions": [],
2047                    "parameter_restrictions": [
2048                        {
2049                            "parameter": "command",
2050                            "restriction_type": "Whitelist",
2051                            "values": null,
2052                            "pattern": null,
2053                            "min": null,
2054                            "max": null,
2055                            "required": false,
2056                            "description": null
2057                        }
2058                    ],
2059                    "scope": "Global",
2060                    "reason": null,
2061                    "expires_at": null,
2062                    "metadata": {}
2063                }
2064            ]
2065        }"#;
2066
2067        let result = manager.import(config_json, PermissionScope::Global);
2068
2069        assert!(result.is_err());
2070        assert!(result.unwrap_err().to_string().contains("must have values"));
2071    }
2072
2073    // ========================================================================
2074    // Template Tests
2075    // ========================================================================
2076
2077    #[test]
2078    fn test_register_template() {
2079        let mut manager = ToolPermissionManager::new(None);
2080        let template = vec![create_simple_permission(
2081            "custom_tool",
2082            true,
2083            PermissionScope::Global,
2084        )];
2085
2086        manager.register_template("my_template", template);
2087
2088        assert!(manager.has_template("my_template"));
2089        assert!(manager.get_template("my_template").is_some());
2090    }
2091
2092    #[test]
2093    fn test_register_template_replaces_existing() {
2094        let mut manager = ToolPermissionManager::new(None);
2095        let template1 = vec![create_simple_permission(
2096            "tool1",
2097            true,
2098            PermissionScope::Global,
2099        )];
2100        let template2 = vec![create_simple_permission(
2101            "tool2",
2102            false,
2103            PermissionScope::Global,
2104        )];
2105
2106        manager.register_template("my_template", template1);
2107        manager.register_template("my_template", template2);
2108
2109        let template = manager.get_template("my_template").unwrap();
2110        assert_eq!(template.len(), 1);
2111        assert_eq!(template[0].tool, "tool2");
2112    }
2113
2114    #[test]
2115    fn test_apply_template() {
2116        let mut manager = ToolPermissionManager::new(None);
2117        let template = vec![
2118            create_simple_permission("tool1", true, PermissionScope::Global),
2119            create_simple_permission("tool2", false, PermissionScope::Global),
2120        ];
2121
2122        manager.register_template("my_template", template);
2123        let result = manager.apply_template("my_template", PermissionScope::Project);
2124
2125        assert!(result);
2126        assert_eq!(manager.permission_counts(), (0, 2, 0));
2127
2128        // Check that scope was updated
2129        let perm = manager.get_tool_permission("tool1").unwrap();
2130        assert_eq!(perm.scope, PermissionScope::Project);
2131    }
2132
2133    #[test]
2134    fn test_apply_template_not_found() {
2135        let mut manager = ToolPermissionManager::new(None);
2136
2137        let result = manager.apply_template("nonexistent", PermissionScope::Global);
2138
2139        assert!(!result);
2140        assert_eq!(manager.permission_counts(), (0, 0, 0));
2141    }
2142
2143    #[test]
2144    fn test_remove_template() {
2145        let mut manager = ToolPermissionManager::new(None);
2146        let template = vec![create_simple_permission(
2147            "tool1",
2148            true,
2149            PermissionScope::Global,
2150        )];
2151
2152        manager.register_template("my_template", template);
2153        let removed = manager.remove_template("my_template");
2154
2155        assert!(removed.is_some());
2156        assert!(!manager.has_template("my_template"));
2157    }
2158
2159    #[test]
2160    fn test_remove_template_not_found() {
2161        let mut manager = ToolPermissionManager::new(None);
2162
2163        let removed = manager.remove_template("nonexistent");
2164
2165        assert!(removed.is_none());
2166    }
2167
2168    #[test]
2169    fn test_list_templates() {
2170        let mut manager = ToolPermissionManager::new(None);
2171        manager.register_template("template1", vec![]);
2172        manager.register_template("template2", vec![]);
2173        manager.register_template("template3", vec![]);
2174
2175        let templates = manager.list_templates();
2176
2177        assert_eq!(templates.len(), 3);
2178        assert!(templates.iter().any(|t| *t == "template1"));
2179        assert!(templates.iter().any(|t| *t == "template2"));
2180        assert!(templates.iter().any(|t| *t == "template3"));
2181    }
2182
2183    #[test]
2184    fn test_has_template() {
2185        let mut manager = ToolPermissionManager::new(None);
2186        manager.register_template("exists", vec![]);
2187
2188        assert!(manager.has_template("exists"));
2189        assert!(!manager.has_template("not_exists"));
2190    }
2191
2192    #[test]
2193    fn test_apply_template_to_different_scopes() {
2194        let mut manager = ToolPermissionManager::new(None);
2195        let template = vec![create_simple_permission(
2196            "tool",
2197            true,
2198            PermissionScope::Global,
2199        )];
2200
2201        manager.register_template("my_template", template);
2202
2203        // Apply to Global
2204        manager.apply_template("my_template", PermissionScope::Global);
2205        assert_eq!(manager.permission_counts(), (1, 0, 0));
2206
2207        // Apply to Session
2208        manager.apply_template("my_template", PermissionScope::Session);
2209        assert_eq!(manager.permission_counts(), (1, 0, 1));
2210
2211        // Check scopes are correct
2212        let global_perms = manager.get_permissions(Some(PermissionScope::Global));
2213        let session_perms = manager.get_permissions(Some(PermissionScope::Session));
2214
2215        assert_eq!(global_perms[0].scope, PermissionScope::Global);
2216        assert_eq!(session_perms[0].scope, PermissionScope::Session);
2217    }
2218}