Coverage for src / moai_adk / core / unified_permission_manager.py: 28.12%
313 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
1"""
2Unified Permission Manager for MoAI-ADK
4Production-ready permission management system that addresses agent permission validation
5errors identified in Claude Code debug logs. Provides automatic correction, validation,
6and monitoring of agent permissions and access control.
8Author: MoAI-ADK Core Team
9Version: 1.0.0
10"""
12import json
13import logging
14import os
15import time
16from dataclasses import dataclass, field
17from enum import Enum
18from typing import Any, Dict, List, Optional
20# Configure logging
21logger = logging.getLogger(__name__)
24class PermissionMode(Enum):
25 """Valid permission modes for agents"""
26 ACCEPT_EDITS = "acceptEdits"
27 BYPASS_PERMISSIONS = "bypassPermissions"
28 DEFAULT = "default"
29 DONT_ASK = "dontAsk"
30 PLAN = "plan"
33class PermissionSeverity(Enum):
34 """Permission validation severity levels"""
35 LOW = "low"
36 MEDIUM = "medium"
37 HIGH = "high"
38 CRITICAL = "critical"
41class ResourceType(Enum):
42 """Types of resources that can be protected"""
43 AGENT = "agent"
44 TOOL = "tool"
45 FILE = "file"
46 COMMAND = "command"
47 SETTING = "setting"
50@dataclass
51class PermissionRule:
52 """Individual permission rule"""
53 resource_type: ResourceType
54 resource_name: str
55 action: str
56 allowed: bool
57 conditions: Optional[Dict[str, Any]] = None
58 expires_at: Optional[float] = None
61@dataclass
62class ValidationResult:
63 """Result of permission validation"""
64 valid: bool
65 corrected_mode: Optional[str] = None
66 warnings: List[str] = field(default_factory=list)
67 errors: List[str] = field(default_factory=list)
68 severity: PermissionSeverity = PermissionSeverity.LOW
69 auto_corrected: bool = False
72@dataclass
73class PermissionAudit:
74 """Audit log entry for permission changes"""
75 timestamp: float
76 user_id: Optional[str]
77 resource_type: ResourceType
78 resource_name: str
79 action: str
80 old_permissions: Optional[Dict[str, Any]]
81 new_permissions: Optional[Dict[str, Any]]
82 reason: str
83 auto_corrected: bool
86class UnifiedPermissionManager:
87 """
88 Production-ready permission management system that addresses Claude Code
89 agent permission validation errors with automatic correction and monitoring.
91 Key Features:
92 - Automatic permission mode validation and correction
93 - Role-based access control with inheritance
94 - Real-time permission monitoring and auditing
95 - Configuration file auto-recovery
96 - Security-focused fail-safe behavior
97 """
99 # Valid permission modes from Claude Code
100 VALID_PERMISSION_MODES = {
101 "acceptEdits",
102 "bypassPermissions",
103 "default",
104 "dontAsk",
105 "plan"
106 }
108 # Default permission mappings
109 DEFAULT_PERMISSIONS = {
110 "backend-expert": PermissionMode.ACCEPT_EDITS,
111 "frontend-expert": PermissionMode.ACCEPT_EDITS,
112 "security-expert": PermissionMode.ACCEPT_EDITS,
113 "api-designer": PermissionMode.PLAN,
114 "database-expert": PermissionMode.ACCEPT_EDITS,
115 "docs-manager": PermissionMode.ACCEPT_EDITS,
116 "tdd-implementer": PermissionMode.ACCEPT_EDITS,
117 "spec-builder": PermissionMode.ACCEPT_EDITS,
118 "quality-gate": PermissionMode.ACCEPT_EDITS,
119 "default": PermissionMode.DEFAULT,
120 }
122 def __init__(self, config_path: Optional[str] = None, enable_logging: bool = True):
123 self.config_path = config_path or ".claude/settings.json"
124 self.enable_logging = enable_logging
125 self.permission_cache = {}
126 self.audit_log: List[PermissionAudit] = []
127 self.stats = {
128 'validations_performed': 0,
129 'auto_corrections': 0,
130 'security_violations': 0,
131 'permission_denied': 0
132 }
134 # Role hierarchy for inheritance
135 self.role_hierarchy = {
136 "admin": ["developer", "user"],
137 "developer": ["user"],
138 "user": []
139 }
141 # Load and validate current configuration
142 self.config = self._load_configuration()
143 self._validate_all_permissions()
145 def _load_configuration(self) -> Dict[str, Any]:
146 """Load configuration from file with error handling"""
147 try:
148 if os.path.exists(self.config_path):
149 with open(self.config_path, 'r', encoding='utf-8') as f:
150 config = json.load(f)
152 if self.enable_logging:
153 logger.info(f"Loaded configuration from {self.config_path}")
155 return config
156 else:
157 if self.enable_logging:
158 logger.warning(f"Configuration file not found: {self.config_path}")
159 return {}
161 except json.JSONDecodeError as e:
162 if self.enable_logging:
163 logger.error(f"Invalid JSON in configuration file: {e}")
164 return {}
165 except Exception as e:
166 if self.enable_logging:
167 logger.error(f"Error loading configuration: {e}")
168 return {}
170 def _validate_all_permissions(self) -> None:
171 """Validate all permissions in the current configuration"""
172 corrections_made = False
174 # Check agent permissions
175 agents_config = self.config.get('agents', {})
176 for agent_name, agent_config in agents_config.items():
177 result = self.validate_agent_permission(agent_name, agent_config)
178 if result.auto_corrected:
179 corrections_made = True
180 if self.enable_logging:
181 logger.info(f"Auto-corrected permissions for agent: {agent_name}")
183 # Check settings permissions
184 settings_config = self.config.get('projectSettings', {})
185 if 'allowedTools' in settings_config:
186 result = self.validate_tool_permissions(settings_config['allowedTools'])
187 if result.auto_corrected:
188 corrections_made = True
190 # Save corrections if any were made
191 if corrections_made:
192 self._save_configuration()
193 if self.enable_logging:
194 logger.info("Saved corrected configuration")
196 def validate_agent_permission(self, agent_name: str, agent_config: Dict[str, Any]) -> ValidationResult:
197 """
198 Validate and auto-correct agent permission configuration.
200 Addresses the permissionMode validation errors from debug logs:
201 - Lines 50-80: Multiple agents with invalid permission modes ('ask', 'auto')
202 """
203 self.stats['validations_performed'] += 1
205 result = ValidationResult(valid=True)
207 # Extract current permission mode
208 current_mode = agent_config.get('permissionMode', 'default')
210 # Validate permission mode
211 if current_mode not in self.VALID_PERMISSION_MODES:
212 # Auto-correct to appropriate default
213 suggested_mode = self._suggest_permission_mode(agent_name)
215 result.errors.append(
216 f"Invalid permissionMode '{current_mode}' for agent '{agent_name}'. "
217 f"Valid options: {sorted(self.VALID_PERMISSION_MODES)}"
218 )
220 # Auto-correction
221 agent_config['permissionMode'] = suggested_mode
222 result.corrected_mode = suggested_mode
223 result.auto_corrected = True
224 result.severity = PermissionSeverity.HIGH
226 self.stats['auto_corrections'] += 1
227 self._audit_permission_change(
228 resource_type=ResourceType.AGENT,
229 resource_name=agent_name,
230 action="permission_mode_correction",
231 old_permissions={"permissionMode": current_mode},
232 new_permissions={"permissionMode": suggested_mode},
233 reason=f"Invalid permission mode '{current_mode}' auto-corrected to '{suggested_mode}'",
234 auto_corrected=True
235 )
237 if self.enable_logging:
238 logger.warning(
239 f"Auto-corrected agent '{agent_name}' permissionMode from "
240 f"'{current_mode}' to '{suggested_mode}'"
241 )
243 # Validate other agent configuration
244 if 'model' in agent_config:
245 model = agent_config['model']
246 if not isinstance(model, str) or not model.strip():
247 result.errors.append(f"Invalid model configuration for agent '{agent_name}'")
248 result.severity = PermissionSeverity.MEDIUM
250 # Check for required fields
251 required_fields = ['description', 'systemPrompt']
252 for req_field in required_fields:
253 if req_field not in agent_config or not agent_config[req_field]:
254 result.warnings.append(
255 f"Missing or empty '{req_field}' for agent '{agent_name}'"
256 )
258 return result
260 def _suggest_permission_mode(self, agent_name: str) -> str:
261 """
262 Suggest appropriate permission mode based on agent name and function.
264 This addresses the core issue from the debug logs where agents had
265 invalid permission modes like 'ask' and 'auto'.
266 """
267 # Check if agent name matches known patterns
268 agent_lower = agent_name.lower()
270 # Security and compliance focused agents should be more restrictive
271 if any(keyword in agent_lower for keyword in ['security', 'audit', 'compliance']):
272 return PermissionMode.PLAN.value
274 # Code execution and modification agents should accept edits
275 if any(keyword in agent_lower for keyword in ['expert', 'implementer', 'builder']):
276 return PermissionMode.ACCEPT_EDITS.value
278 # Planning and analysis agents should use plan mode
279 if any(keyword in agent_lower for keyword in ['planner', 'analyzer', 'designer']):
280 return PermissionMode.PLAN.value
282 # Management agents should have appropriate permissions
283 if any(keyword in agent_lower for keyword in ['manager', 'coordinator']):
284 return PermissionMode.ACCEPT_EDITS.value
286 # Check against our default mappings
287 if agent_name in self.DEFAULT_PERMISSIONS:
288 return self.DEFAULT_PERMISSIONS[agent_name].value
290 # Default to safe option
291 return PermissionMode.DEFAULT.value
293 def validate_tool_permissions(self, allowed_tools: List[str]) -> ValidationResult:
294 """Validate list of allowed tools for security compliance"""
295 result = ValidationResult(valid=True)
297 # Define dangerous tools that should require explicit approval
298 dangerous_tools = {
299 'Bash(rm -rf:*)',
300 'Bash(sudo:*)',
301 'Bash(chmod -R 777:*)',
302 'Bash(dd:*)',
303 'Bash(mkfs:*)',
304 'Bash(fdisk:*)',
305 'Bash(reboot:*)',
306 'Bash(shutdown:*)',
307 'Bash(git push --force:*)',
308 'Bash(git reset --hard:*)'
309 }
311 for tool in allowed_tools:
312 if tool in dangerous_tools:
313 result.warnings.append(
314 f"Dangerous tool allowed: {tool}. Consider restricting access."
315 )
316 result.severity = PermissionSeverity.HIGH
317 self.stats['security_violations'] += 1
319 return result
321 def check_tool_permission(self, user_role: str, tool_name: str, operation: str) -> bool:
322 """
323 Check if a user role is permitted to use a specific tool.
325 Implements unified permission checking with role hierarchy support.
326 """
327 self.stats['validations_performed'] += 1
329 # Check cache first
330 cache_key = f"{user_role}:{tool_name}:{operation}"
331 if cache_key in self.permission_cache:
332 return self.permission_cache[cache_key]
334 # Check direct permissions
335 permitted = self._check_direct_permission(user_role, tool_name, operation)
337 # If not directly permitted, check role hierarchy
338 if not permitted:
339 for subordinate_role in self.role_hierarchy.get(user_role, []):
340 if self._check_direct_permission(subordinate_role, tool_name, operation):
341 permitted = True
342 break
344 # Cache the result
345 self.permission_cache[cache_key] = permitted
347 if not permitted:
348 self.stats['permission_denied'] += 1
349 if self.enable_logging:
350 logger.warning(f"Permission denied: {user_role} cannot {operation} with {tool_name}")
352 return permitted
354 def _check_direct_permission(self, role: str, tool_name: str, operation: str) -> bool:
355 """Check direct permissions for a specific role"""
356 # Default permissions by role
357 role_permissions = {
358 "admin": ["*"], # All tools
359 "developer": ["Task", "Read", "Write", "Edit", "Bash", "AskUserQuestion"],
360 "user": ["Task", "Read", "AskUserQuestion"]
361 }
363 allowed_tools = role_permissions.get(role, [])
365 # Wildcard permission
366 if "*" in allowed_tools:
367 return True
369 # Exact match
370 if tool_name in allowed_tools:
371 return True
373 # Pattern matching for Bash commands
374 if tool_name.startswith("Bash(") and "Bash" in allowed_tools:
375 return True
377 return False
379 def validate_configuration(self, config_path: Optional[str] = None) -> ValidationResult:
380 """
381 Validate Claude Code configuration file for security and compliance.
383 This addresses the configuration security gaps identified in the analysis.
384 """
385 config_to_validate = config_path or self.config_path
386 result = ValidationResult(valid=True)
388 try:
389 with open(config_to_validate, 'r', encoding='utf-8') as f:
390 config = json.load(f)
391 except FileNotFoundError:
392 result.errors.append(f"Configuration file not found: {config_to_validate}")
393 result.valid = False
394 result.severity = PermissionSeverity.CRITICAL
395 return result
396 except json.JSONDecodeError as e:
397 result.errors.append(f"Invalid JSON in configuration file: {e}")
398 result.valid = False
399 result.severity = PermissionSeverity.CRITICAL
400 return result
401 except Exception as e:
402 result.errors.append(f"Error reading configuration file: {e}")
403 result.valid = False
404 result.severity = PermissionSeverity.HIGH
405 return result
407 # Security validations
408 security_checks = [
409 self._validate_file_permissions,
410 self._validate_allowed_tools,
411 self._validate_sandbox_settings,
412 self._validate_mcp_servers
413 ]
415 for check in security_checks:
416 check_result = check(config)
417 if not check_result:
418 result.valid = False
419 result.severity = PermissionSeverity.CRITICAL
421 return result
423 def _validate_file_permissions(self, config: Dict[str, Any]) -> bool:
424 """Validate file permission settings"""
425 permissions = config.get('permissions', {})
427 # Check for overly permissive settings
428 if 'deniedTools' in permissions:
429 denied_tools = permissions['deniedTools']
430 # Ensure dangerous operations are denied
431 dangerous_patterns = ['rm -rf', 'sudo', 'chmod 777', 'format', 'mkfs']
433 for pattern in dangerous_patterns:
434 found = any(pattern in tool for tool in denied_tools)
435 if not found:
436 logger.warning(f"Dangerous operation not denied: {pattern}")
437 # Don't fail validation for this - just warn
438 # return False
440 return True
442 def _validate_allowed_tools(self, config: Dict[str, Any]) -> bool:
443 """Validate allowed tools configuration"""
444 permissions = config.get('permissions', {})
445 allowed_tools = permissions.get('allowedTools', [])
447 # Ensure essential tools are available (but don't fail validation)
448 essential_tools = ['Task', 'Read', 'AskUserQuestion']
449 for tool in essential_tools:
450 if tool not in allowed_tools:
451 logger.warning(f"Essential tool not allowed: {tool}")
452 # Don't fail validation for this - just warn
453 # return False
455 return True
457 def _validate_sandbox_settings(self, config: Dict[str, Any]) -> bool:
458 """Validate sandbox security settings"""
459 sandbox = config.get('sandbox', {})
461 # Ensure sandbox is enabled
462 if not sandbox.get('allowUnsandboxedCommands', False):
463 return True
465 # If sandbox is disabled, ensure validated commands are restricted
466 validated_commands = sandbox.get('validatedCommands', [])
467 dangerous_commands = ['rm -rf', 'sudo', 'format', 'mkfs']
469 for dangerous_cmd in dangerous_commands:
470 if any(dangerous_cmd in validated_cmd for validated_cmd in validated_commands):
471 logger.warning(f"Dangerous command in validated commands: {dangerous_cmd}")
472 return False
474 return True
476 def _validate_mcp_servers(self, config: Dict[str, Any]) -> bool:
477 """Validate MCP server configuration for security"""
478 mcp_servers = config.get('mcpServers', {})
480 for server_name, server_config in mcp_servers.items():
481 # Ensure command doesn't use dangerous flags
482 if 'command' in server_config:
483 command = server_config['command']
484 dangerous_flags = ['--insecure', '--allow-all', '--disable-ssl']
486 for flag in dangerous_flags:
487 if flag in command:
488 logger.warning(f"Dangerous flag in MCP server {server_name}: {flag}")
489 return False
491 return True
493 def auto_fix_agent_permissions(self, agent_name: str) -> ValidationResult:
494 """
495 Automatically fix agent permission configuration.
497 This is the main method to address the permissionMode errors
498 from the debug logs (Lines 50-80).
499 """
500 # Get current agent configuration
501 agents_config = self.config.setdefault('agents', {})
502 agent_config = agents_config.get(agent_name, {})
504 # Validate and fix
505 result = self.validate_agent_permission(agent_name, agent_config)
507 # Save configuration if corrections were made
508 if result.auto_corrected:
509 agents_config[agent_name] = agent_config
510 self._save_configuration()
512 if self.enable_logging:
513 logger.info(f"Fixed permissions for agent: {agent_name}")
515 return result
517 def auto_fix_all_agents(self) -> Dict[str, ValidationResult]:
518 """Auto-fix all agent permissions in the configuration"""
519 results = {}
521 agents_config = self.config.get('agents', {})
522 for agent_name in agents_config:
523 results[agent_name] = self.auto_fix_agent_permissions(agent_name)
525 # Also check for agents mentioned in the debug log that might not be in config
526 debug_log_agents = [
527 "backend-expert", "security-expert", "api-designer", "monitoring-expert",
528 "performance-engineer", "migration-expert", "mcp-playwright-integrator",
529 "quality-gate", "frontend-expert", "debug-helper", "ui-ux-expert",
530 "trust-checker", "project-manager", "mcp-context7-integrator",
531 "mcp-figma-integrator", "tdd-implementer", "format-expert",
532 "mcp-notion-integrator", "devops-expert", "docs-manager",
533 "implementation-planner", "skill-factory", "component-designer",
534 "database-expert", "agent-factory", "git-manager", "sync-manager",
535 "spec-builder", "doc-syncer", "accessibility-expert", "cc-manager"
536 ]
538 for agent_name in debug_log_agents:
539 if agent_name not in agents_config:
540 # Create default configuration for missing agents
541 agents_config[agent_name] = {
542 "permissionMode": self._suggest_permission_mode(agent_name),
543 "description": f"Auto-generated configuration for {agent_name}",
544 "systemPrompt": f"Default system prompt for {agent_name}"
545 }
547 results[agent_name] = ValidationResult(
548 valid=True,
549 auto_corrected=True,
550 warnings=[f"Created default configuration for agent: {agent_name}"]
551 )
553 if any(result.auto_corrected for result in results.values()):
554 self._save_configuration()
556 return results
558 def _save_configuration(self) -> None:
559 """Save current configuration to file"""
560 try:
561 # Create backup
562 if os.path.exists(self.config_path):
563 backup_path = f"{self.config_path}.backup.{int(time.time())}"
564 os.rename(self.config_path, backup_path)
565 if self.enable_logging:
566 logger.info(f"Created configuration backup: {backup_path}")
568 # Save updated configuration
569 with open(self.config_path, 'w', encoding='utf-8') as f:
570 json.dump(self.config, f, indent=2, ensure_ascii=False)
572 if self.enable_logging:
573 logger.info(f"Saved configuration to {self.config_path}")
575 except Exception as e:
576 if self.enable_logging:
577 logger.error(f"Error saving configuration: {e}")
579 def _audit_permission_change(self, resource_type: ResourceType, resource_name: str,
580 action: str, old_permissions: Optional[Dict[str, Any]],
581 new_permissions: Optional[Dict[str, Any]], reason: str,
582 auto_corrected: bool) -> None:
583 """Log permission changes for audit trail"""
584 audit_entry = PermissionAudit(
585 timestamp=time.time(),
586 user_id=None, # System correction
587 resource_type=resource_type,
588 resource_name=resource_name,
589 action=action,
590 old_permissions=old_permissions,
591 new_permissions=new_permissions,
592 reason=reason,
593 auto_corrected=auto_corrected
594 )
596 self.audit_log.append(audit_entry)
598 # Keep audit log size manageable
599 if len(self.audit_log) > 1000:
600 self.audit_log = self.audit_log[-1000:]
602 def get_permission_stats(self) -> Dict[str, Any]:
603 """Get permission management statistics"""
604 return {
605 **self.stats,
606 'cached_permissions': len(self.permission_cache),
607 'audit_log_entries': len(self.audit_log),
608 'configured_agents': len(self.config.get('agents', {}))
609 }
611 def get_recent_audits(self, limit: int = 50) -> List[PermissionAudit]:
612 """Get recent permission audit entries"""
613 return self.audit_log[-limit:]
615 def export_audit_report(self, output_path: str) -> None:
616 """Export audit report to file"""
617 report = {
618 'generated_at': time.time(),
619 'stats': self.get_permission_stats(),
620 'recent_audits': [
621 {
622 'timestamp': audit.timestamp,
623 'resource_type': audit.resource_type.value,
624 'resource_name': audit.resource_name,
625 'action': audit.action,
626 'reason': audit.reason,
627 'auto_corrected': audit.auto_corrected
628 }
629 for audit in self.get_recent_audits()
630 ]
631 }
633 with open(output_path, 'w', encoding='utf-8') as f:
634 json.dump(report, f, indent=2, ensure_ascii=False)
636 if self.enable_logging:
637 logger.info(f"Exported audit report to {output_path}")
640# Global instance for easy import
641permission_manager = UnifiedPermissionManager()
644def validate_agent_permission(agent_name: str, agent_config: Dict[str, Any]) -> ValidationResult:
645 """Convenience function to validate agent permissions"""
646 return permission_manager.validate_agent_permission(agent_name, agent_config)
649def check_tool_permission(user_role: str, tool_name: str, operation: str) -> bool:
650 """Convenience function to check tool permissions"""
651 return permission_manager.check_tool_permission(user_role, tool_name, operation)
654def auto_fix_all_agent_permissions() -> Dict[str, ValidationResult]:
655 """Convenience function to auto-fix all agent permissions"""
656 return permission_manager.auto_fix_all_agents()
659def get_permission_stats() -> Dict[str, Any]:
660 """Convenience function to get permission statistics"""
661 return permission_manager.get_permission_stats()
664if __name__ == "__main__":
665 # Demo script for testing the permission manager
666 print("🔧 MoAI-ADK Unified Permission Manager Demo")
667 print("=" * 50)
669 # Test agent permission validation
670 test_agents = [
671 {
672 "name": "backend-expert",
673 "config": {"permissionMode": "ask", "description": "Backend expert agent"}
674 },
675 {
676 "name": "security-expert",
677 "config": {"permissionMode": "auto", "description": "Security expert agent"}
678 },
679 {
680 "name": "api-designer",
681 "config": {"permissionMode": "plan", "description": "API designer agent"}
682 }
683 ]
685 print("Testing agent permission validation and auto-correction...")
687 for agent in test_agents:
688 print(f"\nTesting agent: {agent['name']}")
689 print(f"Original permissionMode: {agent['config'].get('permissionMode', 'default')}")
691 result = permission_manager.validate_agent_permission(agent['name'], agent['config'])
693 print(f"Valid: {result.valid}")
694 print(f"Auto-corrected: {result.auto_corrected}")
696 if result.corrected_mode:
697 print(f"Corrected to: {result.corrected_mode}")
699 if result.errors:
700 print(f"Errors: {result.errors}")
702 if result.warnings:
703 print(f"Warnings: {result.warnings}")
705 print("\n📊 Permission Statistics:")
706 stats = permission_manager.get_permission_stats()
707 for key, value in stats.items():
708 print(f" {key}: {value}")
710 print("\n✨ Demo completed! The Unified Permission Manager addresses")
711 print(" the agent permission validation errors from the debug logs.")