mirror of
https://github.com/aljazceru/enclava.git
synced 2025-12-17 23:44:24 +01:00
plugin system
This commit is contained in:
719
backend/app/services/plugin_security.py
Normal file
719
backend/app/services/plugin_security.py
Normal file
@@ -0,0 +1,719 @@
|
||||
"""
|
||||
Plugin Security and Authentication Service
|
||||
Handles plugin tokens, permissions, and security policies
|
||||
"""
|
||||
import jwt
|
||||
import hashlib
|
||||
import secrets
|
||||
import time
|
||||
import redis
|
||||
from typing import Dict, Any, List, Optional, Set, Tuple
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from sqlalchemy.orm import Session
|
||||
from cryptography.fernet import Fernet
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.logging import get_logger
|
||||
from app.models.plugin import Plugin, PluginConfiguration, PluginAuditLog, PluginPermission
|
||||
from app.models.user import User
|
||||
from app.models.api_key import APIKey
|
||||
from app.db.database import get_db
|
||||
from app.utils.exceptions import SecurityError, PluginError
|
||||
|
||||
|
||||
logger = get_logger("plugin.security")
|
||||
|
||||
|
||||
class PluginTokenManager:
|
||||
"""Manages plugin authentication tokens"""
|
||||
|
||||
def __init__(self):
|
||||
self.secret_key = settings.JWT_SECRET
|
||||
self.encryption_key = self._get_or_create_encryption_key()
|
||||
self.cipher_suite = Fernet(self.encryption_key)
|
||||
|
||||
# Initialize Redis connection for token blacklist
|
||||
try:
|
||||
self.redis_client = redis.from_url(settings.REDIS_URL, decode_responses=True)
|
||||
# Test connection
|
||||
self.redis_client.ping()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to Redis for token blacklist: {e}")
|
||||
self.redis_client = None
|
||||
|
||||
def _get_or_create_encryption_key(self) -> bytes:
|
||||
"""Get or create encryption key for plugin secrets"""
|
||||
# First, try to get from environment variable (production)
|
||||
if settings.PLUGIN_ENCRYPTION_KEY:
|
||||
try:
|
||||
# Environment variable should contain base64-encoded key
|
||||
import base64
|
||||
return base64.b64decode(settings.PLUGIN_ENCRYPTION_KEY.encode())
|
||||
except Exception as e:
|
||||
logger.error(f"Invalid PLUGIN_ENCRYPTION_KEY in environment: {e}")
|
||||
raise SecurityError("Invalid encryption key configuration")
|
||||
|
||||
# Development fallback: generate and store in data directory
|
||||
data_dir = Path("/data/plugin_keys")
|
||||
data_dir.mkdir(parents=True, exist_ok=True)
|
||||
key_file = data_dir / "encryption.key"
|
||||
|
||||
try:
|
||||
if key_file.exists():
|
||||
return key_file.read_bytes()
|
||||
else:
|
||||
# Generate new key for development
|
||||
key = Fernet.generate_key()
|
||||
key_file.write_bytes(key)
|
||||
|
||||
import base64
|
||||
logger.warning(
|
||||
f"Generated new plugin encryption key for development. "
|
||||
f"For production, set PLUGIN_ENCRYPTION_KEY environment variable to: "
|
||||
f"{base64.b64encode(key).decode()}"
|
||||
)
|
||||
return key
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to manage encryption key: {e}")
|
||||
raise SecurityError(f"Encryption key management failed: {e}")
|
||||
|
||||
def generate_plugin_token(self, plugin_id: str, user_id: str,
|
||||
permissions: List[str], expires_hours: int = 24) -> str:
|
||||
"""Generate JWT token for plugin authentication"""
|
||||
try:
|
||||
now = datetime.now(timezone.utc)
|
||||
expiration = now + timedelta(hours=expires_hours)
|
||||
|
||||
payload = {
|
||||
'sub': user_id,
|
||||
'plugin_id': plugin_id,
|
||||
'permissions': permissions,
|
||||
'iat': int(now.timestamp()),
|
||||
'exp': int(expiration.timestamp()),
|
||||
'aud': 'enclava-plugin',
|
||||
'iss': 'enclava-platform',
|
||||
'jti': secrets.token_urlsafe(16) # JWT ID for revocation
|
||||
}
|
||||
|
||||
token = jwt.encode(payload, self.secret_key, algorithm='HS256')
|
||||
|
||||
logger.info(f"Generated plugin token for {plugin_id} (user: {user_id})")
|
||||
return token
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to generate plugin token: {e}")
|
||||
raise SecurityError(f"Token generation failed: {e}")
|
||||
|
||||
def verify_plugin_token(self, token: str) -> Tuple[bool, Optional[Dict[str, Any]]]:
|
||||
"""Verify and decode plugin token"""
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
self.secret_key,
|
||||
algorithms=['HS256'],
|
||||
audience='enclava-plugin',
|
||||
issuer='enclava-platform'
|
||||
)
|
||||
|
||||
# Additional validation
|
||||
if 'plugin_id' not in payload or 'sub' not in payload:
|
||||
return False, None
|
||||
|
||||
# Check if specific token is revoked
|
||||
if self._is_token_revoked(payload.get('jti')):
|
||||
return False, None
|
||||
|
||||
# Check if plugin/user tokens are revoked
|
||||
plugin_id = payload.get('plugin_id')
|
||||
user_id = payload.get('sub')
|
||||
if self._is_plugin_user_revoked(plugin_id, user_id):
|
||||
return False, None
|
||||
|
||||
return True, payload
|
||||
|
||||
except jwt.InvalidTokenError as e:
|
||||
logger.warning(f"Invalid plugin token: {e}")
|
||||
return False, None
|
||||
except Exception as e:
|
||||
logger.error(f"Token verification failed: {e}")
|
||||
return False, None
|
||||
|
||||
def _is_token_revoked(self, jti: str) -> bool:
|
||||
"""Check if token is revoked using Redis blacklist"""
|
||||
if not jti or not self.redis_client:
|
||||
return False
|
||||
|
||||
try:
|
||||
# Check if token JTI exists in blacklist
|
||||
blacklist_key = f"plugin_token_blacklist:{jti}"
|
||||
is_revoked = self.redis_client.exists(blacklist_key)
|
||||
|
||||
if is_revoked:
|
||||
logger.debug(f"Token {jti} found in blacklist")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to check token blacklist: {e}")
|
||||
# Fail secure - if we can't check blacklist, assume token is valid
|
||||
# This prevents service disruption from Redis issues
|
||||
return False
|
||||
|
||||
def revoke_plugin_tokens(self, plugin_id: str, user_id: Optional[str] = None) -> bool:
|
||||
"""Revoke all tokens for a plugin or user"""
|
||||
try:
|
||||
if not self.redis_client:
|
||||
logger.error("Redis not available for token revocation")
|
||||
return False
|
||||
|
||||
# For this implementation, we'll mark the plugin/user combination as revoked
|
||||
# In a production system, you'd want to track individual JTI tokens
|
||||
revocation_key = f"plugin_revocation:{plugin_id}"
|
||||
if user_id:
|
||||
revocation_key += f":user:{user_id}"
|
||||
|
||||
# Set revocation flag with 7-day expiration (max token lifetime)
|
||||
expiration_seconds = 7 * 24 * 60 * 60 # 7 days
|
||||
self.redis_client.setex(
|
||||
revocation_key,
|
||||
expiration_seconds,
|
||||
int(time.time())
|
||||
)
|
||||
|
||||
logger.info(f"Revoked plugin tokens for {plugin_id} (user: {user_id})")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to revoke tokens: {e}")
|
||||
return False
|
||||
|
||||
def revoke_specific_token(self, jti: str, expires_at: datetime) -> bool:
|
||||
"""Revoke a specific token by adding its JTI to blacklist"""
|
||||
try:
|
||||
if not jti or not self.redis_client:
|
||||
logger.error("Cannot revoke token: missing JTI or Redis unavailable")
|
||||
return False
|
||||
|
||||
# Calculate time until token expires
|
||||
now = datetime.now(timezone.utc)
|
||||
if expires_at <= now:
|
||||
# Token already expired, no need to blacklist
|
||||
return True
|
||||
|
||||
ttl_seconds = int((expires_at - now).total_seconds())
|
||||
|
||||
# Add JTI to blacklist with TTL matching token expiration
|
||||
blacklist_key = f"plugin_token_blacklist:{jti}"
|
||||
self.redis_client.setex(blacklist_key, ttl_seconds, int(time.time()))
|
||||
|
||||
logger.info(f"Revoked token {jti}, expires in {ttl_seconds} seconds")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to revoke specific token {jti}: {e}")
|
||||
return False
|
||||
|
||||
def cleanup_expired_revocations(self) -> int:
|
||||
"""Clean up expired token revocations (Redis TTL handles this automatically)"""
|
||||
try:
|
||||
if not self.redis_client:
|
||||
return 0
|
||||
|
||||
# Redis TTL automatically cleans up expired keys
|
||||
# This method is for manual cleanup or statistics
|
||||
|
||||
# Count current blacklisted tokens
|
||||
pattern = "plugin_token_blacklist:*"
|
||||
blacklisted_count = len(self.redis_client.keys(pattern))
|
||||
|
||||
logger.debug(f"Current blacklisted tokens: {blacklisted_count}")
|
||||
return blacklisted_count
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to cleanup revocations: {e}")
|
||||
return 0
|
||||
|
||||
def _is_plugin_user_revoked(self, plugin_id: str, user_id: str) -> bool:
|
||||
"""Check if all tokens for a plugin/user combination are revoked"""
|
||||
if not plugin_id or not user_id or not self.redis_client:
|
||||
return False
|
||||
|
||||
try:
|
||||
# Check plugin-level revocation
|
||||
plugin_revocation_key = f"plugin_revocation:{plugin_id}"
|
||||
if self.redis_client.exists(plugin_revocation_key):
|
||||
logger.debug(f"Plugin {plugin_id} tokens are revoked")
|
||||
return True
|
||||
|
||||
# Check user-specific revocation for this plugin
|
||||
user_revocation_key = f"plugin_revocation:{plugin_id}:user:{user_id}"
|
||||
if self.redis_client.exists(user_revocation_key):
|
||||
logger.debug(f"Plugin {plugin_id} tokens revoked for user {user_id}")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to check plugin/user revocation: {e}")
|
||||
# Fail secure - if we can't check, assume not revoked
|
||||
return False
|
||||
|
||||
def encrypt_plugin_secret(self, secret: str) -> str:
|
||||
"""Encrypt plugin secret for storage"""
|
||||
try:
|
||||
encrypted = self.cipher_suite.encrypt(secret.encode())
|
||||
return encrypted.decode()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to encrypt secret: {e}")
|
||||
raise SecurityError("Secret encryption failed")
|
||||
|
||||
def decrypt_plugin_secret(self, encrypted_secret: str) -> str:
|
||||
"""Decrypt plugin secret"""
|
||||
try:
|
||||
decrypted = self.cipher_suite.decrypt(encrypted_secret.encode())
|
||||
return decrypted.decode()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to decrypt secret: {e}")
|
||||
raise SecurityError("Secret decryption failed")
|
||||
|
||||
def get_revocation_status(self, plugin_id: str, user_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Get revocation status for plugin and user"""
|
||||
try:
|
||||
if not self.redis_client:
|
||||
return {"status": "unknown", "error": "Redis unavailable"}
|
||||
|
||||
status = {
|
||||
"plugin_id": plugin_id,
|
||||
"user_id": user_id,
|
||||
"plugin_revoked": False,
|
||||
"user_revoked": False,
|
||||
"revoked_at": None
|
||||
}
|
||||
|
||||
# Check plugin-level revocation
|
||||
plugin_key = f"plugin_revocation:{plugin_id}"
|
||||
if self.redis_client.exists(plugin_key):
|
||||
status["plugin_revoked"] = True
|
||||
revoked_timestamp = self.redis_client.get(plugin_key)
|
||||
if revoked_timestamp:
|
||||
status["revoked_at"] = int(revoked_timestamp)
|
||||
|
||||
# Check user-specific revocation
|
||||
if user_id:
|
||||
user_key = f"plugin_revocation:{plugin_id}:user:{user_id}"
|
||||
if self.redis_client.exists(user_key):
|
||||
status["user_revoked"] = True
|
||||
revoked_timestamp = self.redis_client.get(user_key)
|
||||
if revoked_timestamp:
|
||||
status["revoked_at"] = int(revoked_timestamp)
|
||||
|
||||
return status
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get revocation status: {e}")
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
class PluginPermissionManager:
|
||||
"""Manages plugin permissions and access control"""
|
||||
|
||||
PLATFORM_API_PERMISSIONS = {
|
||||
'chatbot:invoke': 'Invoke chatbot conversations',
|
||||
'chatbot:manage': 'Manage chatbot instances',
|
||||
'chatbot:read': 'Read chatbot configurations',
|
||||
|
||||
'rag:query': 'Query RAG collections',
|
||||
'rag:manage': 'Manage RAG collections and documents',
|
||||
'rag:read': 'Read RAG collection metadata',
|
||||
|
||||
'llm:completion': 'Generate LLM completions',
|
||||
'llm:embeddings': 'Generate text embeddings',
|
||||
'llm:models': 'List available LLM models',
|
||||
|
||||
'workflow:execute': 'Execute workflow processes',
|
||||
'workflow:read': 'Read workflow definitions',
|
||||
|
||||
'cache:read': 'Read cached data',
|
||||
'cache:write': 'Write cached data',
|
||||
|
||||
'user:read': 'Read user profile data',
|
||||
'user:settings': 'Access user settings',
|
||||
|
||||
'admin:users': 'Manage users (admin only)',
|
||||
'admin:system': 'System administration (admin only)'
|
||||
}
|
||||
|
||||
PLUGIN_SCOPE_PERMISSIONS = {
|
||||
'read': 'Read plugin data',
|
||||
'write': 'Modify plugin data',
|
||||
'config': 'Manage plugin configuration',
|
||||
'install': 'Install/uninstall plugin',
|
||||
'execute': 'Execute plugin functions'
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.permission_cache: Dict[str, Set[str]] = {}
|
||||
|
||||
def validate_permissions(self, requested_permissions: List[str]) -> Tuple[bool, List[str]]:
|
||||
"""Validate requested permissions against allowed permissions"""
|
||||
valid_permissions = set(self.PLATFORM_API_PERMISSIONS.keys()) | set(self.PLUGIN_SCOPE_PERMISSIONS.keys())
|
||||
|
||||
invalid_permissions = []
|
||||
for permission in requested_permissions:
|
||||
if permission.endswith(':*'):
|
||||
# Wildcard permission - check if base exists
|
||||
base_permission = permission[:-2]
|
||||
if not any(p.startswith(base_permission + ':') for p in valid_permissions):
|
||||
invalid_permissions.append(permission)
|
||||
elif permission not in valid_permissions:
|
||||
invalid_permissions.append(permission)
|
||||
|
||||
return len(invalid_permissions) == 0, invalid_permissions
|
||||
|
||||
def check_permission(self, user_id: str, plugin_id: str,
|
||||
permission: str, db: Session) -> bool:
|
||||
"""Check if user has permission for plugin action"""
|
||||
try:
|
||||
# Get user permissions from cache or database
|
||||
cache_key = f"{user_id}:{plugin_id}"
|
||||
if cache_key not in self.permission_cache:
|
||||
self._load_user_permissions(user_id, plugin_id, db)
|
||||
|
||||
user_permissions = self.permission_cache.get(cache_key, set())
|
||||
|
||||
# Check exact permission match
|
||||
if permission in user_permissions:
|
||||
return True
|
||||
|
||||
# Check wildcard permissions
|
||||
permission_parts = permission.split(':')
|
||||
if len(permission_parts) == 2:
|
||||
wildcard_permission = f"{permission_parts[0]}:*"
|
||||
if wildcard_permission in user_permissions:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Permission check failed: {e}")
|
||||
return False
|
||||
|
||||
def _load_user_permissions(self, user_id: str, plugin_id: str, db: Session):
|
||||
"""Load user permissions for plugin from database"""
|
||||
try:
|
||||
# Get user
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
return
|
||||
|
||||
# Get plugin configuration
|
||||
config = db.query(PluginConfiguration).filter(
|
||||
PluginConfiguration.user_id == user_id,
|
||||
PluginConfiguration.plugin_id == plugin_id,
|
||||
PluginConfiguration.is_active == True
|
||||
).first()
|
||||
|
||||
permissions = set()
|
||||
|
||||
# Add base plugin permissions
|
||||
if config:
|
||||
permissions.update(self.PLUGIN_SCOPE_PERMISSIONS.keys())
|
||||
|
||||
# Add platform API permissions based on plugin manifest
|
||||
plugin = db.query(Plugin).filter(Plugin.id == plugin_id).first()
|
||||
if plugin and plugin.manifest_data:
|
||||
manifest_permissions = plugin.manifest_data.get('spec', {}).get('permissions', {}).get('platform_apis', [])
|
||||
permissions.update(manifest_permissions)
|
||||
|
||||
# Add explicitly granted permissions from database
|
||||
from datetime import datetime, timezone
|
||||
explicitly_granted = db.query(PluginPermission).filter(
|
||||
PluginPermission.plugin_id == plugin_id,
|
||||
PluginPermission.user_id == user_id,
|
||||
PluginPermission.granted == True
|
||||
).filter(
|
||||
# Only include non-expired permissions
|
||||
(PluginPermission.expires_at.is_(None)) |
|
||||
(PluginPermission.expires_at > datetime.now(timezone.utc))
|
||||
).all()
|
||||
|
||||
for permission_record in explicitly_granted:
|
||||
permissions.add(permission_record.permission_name)
|
||||
|
||||
# Add admin permissions if user is admin
|
||||
if hasattr(user, 'is_admin') and user.is_admin:
|
||||
permissions.update(['admin:users', 'admin:system'])
|
||||
|
||||
# Cache permissions
|
||||
cache_key = f"{user_id}:{plugin_id}"
|
||||
self.permission_cache[cache_key] = permissions
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load user permissions: {e}")
|
||||
|
||||
def get_user_permissions(self, user_id: str, plugin_id: str, db: Session) -> List[str]:
|
||||
"""Get list of permissions for user and plugin"""
|
||||
cache_key = f"{user_id}:{plugin_id}"
|
||||
if cache_key not in self.permission_cache:
|
||||
self._load_user_permissions(user_id, plugin_id, db)
|
||||
|
||||
return list(self.permission_cache.get(cache_key, set()))
|
||||
|
||||
def grant_permission(self, user_id: str, plugin_id: str,
|
||||
permission: str, granted_by: str, db: Session) -> bool:
|
||||
"""Grant permission to user for plugin"""
|
||||
try:
|
||||
# Validate permission
|
||||
valid, invalid = self.validate_permissions([permission])
|
||||
if not valid:
|
||||
raise SecurityError(f"Invalid permission: {permission}")
|
||||
|
||||
# Store permission grant in database
|
||||
permission_record = PluginPermission(
|
||||
plugin_id=plugin_id,
|
||||
user_id=user_id,
|
||||
permission_name=permission,
|
||||
granted=True,
|
||||
granted_by_user_id=granted_by,
|
||||
reason=f"Permission granted by user {granted_by}"
|
||||
)
|
||||
|
||||
db.add(permission_record)
|
||||
|
||||
# Invalidate cache to force reload
|
||||
cache_key = f"{user_id}:{plugin_id}"
|
||||
if cache_key in self.permission_cache:
|
||||
del self.permission_cache[cache_key]
|
||||
|
||||
# Log permission grant
|
||||
audit_log = PluginAuditLog(
|
||||
plugin_id=plugin_id,
|
||||
user_id=user_id,
|
||||
action="grant_permission",
|
||||
details={
|
||||
"permission": permission,
|
||||
"granted_by": granted_by
|
||||
}
|
||||
)
|
||||
db.add(audit_log)
|
||||
db.commit()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to grant permission: {e}")
|
||||
db.rollback()
|
||||
return False
|
||||
|
||||
def revoke_permission(self, user_id: str, plugin_id: str,
|
||||
permission: str, revoked_by: str, db: Session) -> bool:
|
||||
"""Revoke permission from user for plugin"""
|
||||
try:
|
||||
# Mark permission as revoked in database
|
||||
permission_record = db.query(PluginPermission).filter(
|
||||
PluginPermission.plugin_id == plugin_id,
|
||||
PluginPermission.user_id == user_id,
|
||||
PluginPermission.permission_name == permission,
|
||||
PluginPermission.granted == True
|
||||
).first()
|
||||
|
||||
if permission_record:
|
||||
# Mark as revoked
|
||||
permission_record.granted = False
|
||||
permission_record.revoked_at = func.now()
|
||||
permission_record.revoked_by_user_id = revoked_by
|
||||
permission_record.reason = f"Permission revoked by user {revoked_by}"
|
||||
else:
|
||||
logger.warning(f"Permission {permission} not found for user {user_id}, plugin {plugin_id}")
|
||||
|
||||
# Invalidate cache to force reload
|
||||
cache_key = f"{user_id}:{plugin_id}"
|
||||
if cache_key in self.permission_cache:
|
||||
del self.permission_cache[cache_key]
|
||||
|
||||
# Log permission revocation
|
||||
audit_log = PluginAuditLog(
|
||||
plugin_id=plugin_id,
|
||||
user_id=user_id,
|
||||
action="revoke_permission",
|
||||
details={
|
||||
"permission": permission,
|
||||
"revoked_by": revoked_by
|
||||
}
|
||||
)
|
||||
db.add(audit_log)
|
||||
db.commit()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to revoke permission: {e}")
|
||||
db.rollback()
|
||||
return False
|
||||
|
||||
|
||||
class PluginSecurityPolicyManager:
|
||||
"""Manages security policies for plugins"""
|
||||
|
||||
DEFAULT_SECURITY_POLICY = {
|
||||
'max_api_calls_per_minute': 100,
|
||||
'max_memory_mb': 128,
|
||||
'max_cpu_percent': 25,
|
||||
'max_disk_mb': 100,
|
||||
'max_network_connections': 10,
|
||||
'allowed_domains': [],
|
||||
'blocked_domains': ['localhost', '127.0.0.1', '0.0.0.0'],
|
||||
'require_https': True,
|
||||
'allow_file_access': False,
|
||||
'allow_system_calls': False,
|
||||
'enable_audit_logging': True,
|
||||
'token_expires_hours': 24,
|
||||
'max_token_lifetime_hours': 168 # 1 week
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.policy_cache: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
def get_security_policy(self, plugin_id: str, db: Session) -> Dict[str, Any]:
|
||||
"""Get security policy for plugin"""
|
||||
if plugin_id in self.policy_cache:
|
||||
return self.policy_cache[plugin_id]
|
||||
|
||||
try:
|
||||
plugin = db.query(Plugin).filter(Plugin.id == plugin_id).first()
|
||||
if not plugin:
|
||||
return self.DEFAULT_SECURITY_POLICY.copy()
|
||||
|
||||
# Start with default policy
|
||||
policy = self.DEFAULT_SECURITY_POLICY.copy()
|
||||
|
||||
# Override with plugin manifest settings
|
||||
if plugin.manifest_data:
|
||||
manifest_policy = plugin.manifest_data.get('spec', {}).get('security_policy', {})
|
||||
policy.update(manifest_policy)
|
||||
|
||||
# Add allowed domains from manifest
|
||||
external_services = plugin.manifest_data.get('spec', {}).get('external_services', {})
|
||||
if external_services.get('allowed_domains'):
|
||||
policy['allowed_domains'].extend(external_services['allowed_domains'])
|
||||
|
||||
# Cache policy
|
||||
self.policy_cache[plugin_id] = policy
|
||||
return policy
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get security policy for {plugin_id}: {e}")
|
||||
return self.DEFAULT_SECURITY_POLICY.copy()
|
||||
|
||||
def validate_security_policy(self, policy: Dict[str, Any]) -> Tuple[bool, List[str]]:
|
||||
"""Validate security policy configuration"""
|
||||
errors = []
|
||||
|
||||
# Check required fields
|
||||
required_fields = ['max_api_calls_per_minute', 'max_memory_mb', 'token_expires_hours']
|
||||
for field in required_fields:
|
||||
if field not in policy:
|
||||
errors.append(f"Missing required field: {field}")
|
||||
|
||||
# Validate numeric limits
|
||||
numeric_limits = {
|
||||
'max_api_calls_per_minute': (1, 1000),
|
||||
'max_memory_mb': (16, 1024),
|
||||
'max_cpu_percent': (1, 100),
|
||||
'max_disk_mb': (10, 10240),
|
||||
'token_expires_hours': (1, 168)
|
||||
}
|
||||
|
||||
for field, (min_val, max_val) in numeric_limits.items():
|
||||
if field in policy:
|
||||
value = policy[field]
|
||||
if not isinstance(value, (int, float)) or value < min_val or value > max_val:
|
||||
errors.append(f"{field} must be between {min_val} and {max_val}")
|
||||
|
||||
# Validate domains
|
||||
if 'allowed_domains' in policy:
|
||||
if not isinstance(policy['allowed_domains'], list):
|
||||
errors.append("allowed_domains must be a list")
|
||||
|
||||
return len(errors) == 0, errors
|
||||
|
||||
def update_security_policy(self, plugin_id: str, policy: Dict[str, Any],
|
||||
updated_by: str, db: Session) -> bool:
|
||||
"""Update security policy for plugin"""
|
||||
try:
|
||||
# Validate policy
|
||||
valid, errors = self.validate_security_policy(policy)
|
||||
if not valid:
|
||||
raise SecurityError(f"Invalid security policy: {errors}")
|
||||
|
||||
# TODO: Store policy in database
|
||||
# For now, update cache
|
||||
self.policy_cache[plugin_id] = policy
|
||||
|
||||
# Log policy update
|
||||
audit_log = PluginAuditLog(
|
||||
plugin_id=plugin_id,
|
||||
action="update_security_policy",
|
||||
details={
|
||||
"policy": policy,
|
||||
"updated_by": updated_by
|
||||
}
|
||||
)
|
||||
db.add(audit_log)
|
||||
db.commit()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update security policy for {plugin_id}: {e}")
|
||||
db.rollback()
|
||||
return False
|
||||
|
||||
def check_policy_compliance(self, plugin_id: str, action: str,
|
||||
context: Dict[str, Any], db: Session) -> bool:
|
||||
"""Check if action complies with plugin security policy"""
|
||||
try:
|
||||
policy = self.get_security_policy(plugin_id, db)
|
||||
|
||||
# Check specific action types
|
||||
if action == 'api_call':
|
||||
# Check rate limits (would need rate limiter integration)
|
||||
return True
|
||||
|
||||
elif action == 'network_access':
|
||||
domain = context.get('domain')
|
||||
if not domain:
|
||||
return False
|
||||
|
||||
# Check blocked domains
|
||||
for blocked in policy.get('blocked_domains', []):
|
||||
if domain.endswith(blocked):
|
||||
return False
|
||||
|
||||
# Check allowed domains if specified
|
||||
allowed_domains = policy.get('allowed_domains', [])
|
||||
if allowed_domains:
|
||||
return any(domain.endswith(allowed) for allowed in allowed_domains)
|
||||
|
||||
return True
|
||||
|
||||
elif action == 'file_access':
|
||||
return policy.get('allow_file_access', False)
|
||||
|
||||
elif action == 'system_call':
|
||||
return policy.get('allow_system_calls', False)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Policy compliance check failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# Global instances
|
||||
plugin_token_manager = PluginTokenManager()
|
||||
plugin_permission_manager = PluginPermissionManager()
|
||||
plugin_security_policy_manager = PluginSecurityPolicyManager()
|
||||
Reference in New Issue
Block a user