""" Settings management endpoints """ from typing import List, Optional, Dict, Any from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel, Field from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, update, delete from app.db.database import get_db from app.models.user import User from app.core.security import get_current_user from app.services.permission_manager import require_permission from app.services.audit_service import log_audit_event from app.core.logging import get_logger from app.core.config import settings as app_settings logger = get_logger(__name__) router = APIRouter() # Pydantic models class SettingValue(BaseModel): value: Any value_type: str = Field(..., pattern="^(string|integer|float|boolean|json|list)$") description: Optional[str] = None is_secret: bool = False class SettingResponse(BaseModel): key: str value: Any value_type: str description: Optional[str] = None is_secret: bool = False category: str is_system: bool = False created_at: str updated_at: Optional[str] = None class SettingUpdate(BaseModel): value: Any description: Optional[str] = None class SystemInfoResponse(BaseModel): version: str environment: str database_status: str redis_status: str llm_service_status: str modules_loaded: int active_users: int total_api_keys: int uptime_seconds: int class PlatformConfigResponse(BaseModel): app_name: str debug_mode: bool log_level: str cors_origins: List[str] rate_limiting_enabled: bool max_upload_size: int session_timeout_minutes: int api_key_prefix: str features: Dict[str, bool] maintenance_mode: bool = False maintenance_message: Optional[str] = None class SecurityConfigResponse(BaseModel): password_min_length: int password_require_special: bool password_require_numbers: bool password_require_uppercase: bool session_timeout_minutes: int max_login_attempts: int lockout_duration_minutes: int require_2fa: bool = False allowed_domains: List[str] = Field(default_factory=list) ip_whitelist_enabled: bool = False # Global settings storage (in a real app, this would be in database) SETTINGS_STORE: Dict[str, Dict[str, Any]] = { "platform": { "app_name": {"value": "Confidential Empire", "type": "string", "description": "Application name"}, "maintenance_mode": {"value": False, "type": "boolean", "description": "Enable maintenance mode"}, "maintenance_message": {"value": None, "type": "string", "description": "Maintenance mode message"}, "debug_mode": {"value": False, "type": "boolean", "description": "Enable debug mode"}, "max_upload_size": {"value": 10485760, "type": "integer", "description": "Maximum upload size in bytes"}, }, "api": { # Security Settings "security_enabled": {"value": True, "type": "boolean", "description": "Enable API security system"}, "threat_detection_enabled": {"value": True, "type": "boolean", "description": "Enable threat detection analysis"}, "rate_limiting_enabled": {"value": True, "type": "boolean", "description": "Enable rate limiting"}, "ip_reputation_enabled": {"value": True, "type": "boolean", "description": "Enable IP reputation checking"}, "anomaly_detection_enabled": {"value": True, "type": "boolean", "description": "Enable anomaly detection"}, "security_headers_enabled": {"value": True, "type": "boolean", "description": "Enable security headers"}, # Rate Limiting by Authentication Level "rate_limit_authenticated_per_minute": {"value": 200, "type": "integer", "description": "Rate limit for authenticated users per minute"}, "rate_limit_authenticated_per_hour": {"value": 5000, "type": "integer", "description": "Rate limit for authenticated users per hour"}, "rate_limit_api_key_per_minute": {"value": 1000, "type": "integer", "description": "Rate limit for API key users per minute"}, "rate_limit_api_key_per_hour": {"value": 20000, "type": "integer", "description": "Rate limit for API key users per hour"}, "rate_limit_premium_per_minute": {"value": 5000, "type": "integer", "description": "Rate limit for premium users per minute"}, "rate_limit_premium_per_hour": {"value": 100000, "type": "integer", "description": "Rate limit for premium users per hour"}, # Security Thresholds "security_risk_threshold": {"value": 0.8, "type": "float", "description": "Risk score threshold for blocking requests (0.0-1.0)"}, "security_warning_threshold": {"value": 0.6, "type": "float", "description": "Risk score threshold for warnings (0.0-1.0)"}, "anomaly_threshold": {"value": 0.7, "type": "float", "description": "Anomaly severity threshold (0.0-1.0)"}, # Request Settings "max_request_size_mb": {"value": 10, "type": "integer", "description": "Maximum request size in MB for standard users"}, "max_request_size_premium_mb": {"value": 50, "type": "integer", "description": "Maximum request size in MB for premium users"}, "enable_cors": {"value": True, "type": "boolean", "description": "Enable CORS headers"}, "cors_origins": {"value": ["http://localhost:3000", "http://localhost:53000"], "type": "list", "description": "Allowed CORS origins"}, "api_key_expiry_days": {"value": 90, "type": "integer", "description": "Default API key expiry in days"}, # IP Security "blocked_ips": {"value": [], "type": "list", "description": "List of blocked IP addresses"}, "allowed_ips": {"value": [], "type": "list", "description": "List of allowed IP addresses (empty = allow all)"}, "csp_header": {"value": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';", "type": "string", "description": "Content Security Policy header"}, }, "security": { "password_min_length": {"value": 8, "type": "integer", "description": "Minimum password length"}, "password_require_special": {"value": True, "type": "boolean", "description": "Require special characters in passwords"}, "password_require_numbers": {"value": True, "type": "boolean", "description": "Require numbers in passwords"}, "password_require_uppercase": {"value": True, "type": "boolean", "description": "Require uppercase letters in passwords"}, "max_login_attempts": {"value": 5, "type": "integer", "description": "Maximum login attempts before lockout"}, "lockout_duration_minutes": {"value": 15, "type": "integer", "description": "Account lockout duration in minutes"}, "require_2fa": {"value": False, "type": "boolean", "description": "Require two-factor authentication"}, "ip_whitelist_enabled": {"value": False, "type": "boolean", "description": "Enable IP whitelist"}, "allowed_domains": {"value": [], "type": "list", "description": "Allowed email domains for registration"}, }, "features": { "user_registration": {"value": True, "type": "boolean", "description": "Allow user registration"}, "api_key_creation": {"value": True, "type": "boolean", "description": "Allow API key creation"}, "budget_enforcement": {"value": True, "type": "boolean", "description": "Enable budget enforcement"}, "audit_logging": {"value": True, "type": "boolean", "description": "Enable audit logging"}, "module_hot_reload": {"value": True, "type": "boolean", "description": "Enable module hot reload"}, "tee_support": {"value": True, "type": "boolean", "description": "Enable TEE (Trusted Execution Environment) support"}, "advanced_analytics": {"value": True, "type": "boolean", "description": "Enable advanced analytics"}, }, "notifications": { "email_enabled": {"value": False, "type": "boolean", "description": "Enable email notifications"}, "slack_enabled": {"value": False, "type": "boolean", "description": "Enable Slack notifications"}, "webhook_enabled": {"value": False, "type": "boolean", "description": "Enable webhook notifications"}, "budget_alerts": {"value": True, "type": "boolean", "description": "Enable budget alert notifications"}, "security_alerts": {"value": True, "type": "boolean", "description": "Enable security alert notifications"}, } } # Settings management endpoints @router.get("/") async def list_settings( category: Optional[str] = None, include_secrets: bool = False, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """List all settings or settings in a specific category""" # Check permissions require_permission(current_user.get("permissions", []), "platform:settings:read") result = {} for cat, settings in SETTINGS_STORE.items(): if category and cat != category: continue result[cat] = {} for key, setting in settings.items(): # Hide secret values unless specifically requested and user has permission if setting.get("is_secret", False) and not include_secrets: if not any(perm in current_user.get("permissions", []) for perm in ["platform:settings:admin", "platform:*"]): continue result[cat][key] = { "value": setting["value"], "type": setting["type"], "description": setting.get("description", ""), "is_secret": setting.get("is_secret", False) } # Log audit event await log_audit_event( db=db, user_id=current_user['id'], action="list_settings", resource_type="setting", details={"category": category, "include_secrets": include_secrets} ) return result @router.get("/system-info", response_model=SystemInfoResponse) async def get_system_info( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Get system information and status""" # Check permissions require_permission(current_user.get("permissions", []), "platform:settings:read") import psutil import time from app.models.api_key import APIKey # Get database status try: await db.execute(select(1)) database_status = "healthy" except Exception: database_status = "error" # Get Redis status (simplified check) redis_status = "healthy" # Would implement actual Redis check # Get LLM service status try: from app.services.llm.service import llm_service health_summary = llm_service.get_health_summary() llm_service_status = health_summary.get("service_status", "unknown") except Exception: llm_service_status = "unavailable" # Get modules loaded (from module manager) modules_loaded = 8 # Would get from actual module manager # Get active users count (last 24 hours) from datetime import datetime, timedelta yesterday = datetime.utcnow() - timedelta(days=1) active_users_query = select(User.id).where(User.last_login >= yesterday) active_users_result = await db.execute(active_users_query) active_users = len(active_users_result.fetchall()) # Get total API keys total_api_keys_query = select(APIKey.id) total_api_keys_result = await db.execute(total_api_keys_query) total_api_keys = len(total_api_keys_result.fetchall()) # Get uptime (simplified - would track actual start time) uptime_seconds = int(time.time()) % 86400 # Placeholder # Log audit event await log_audit_event( db=db, user_id=current_user['id'], action="get_system_info", resource_type="system" ) return SystemInfoResponse( version="1.0.0", environment="production", database_status=database_status, redis_status=redis_status, llm_service_status=llm_service_status, modules_loaded=modules_loaded, active_users=active_users, total_api_keys=total_api_keys, uptime_seconds=uptime_seconds ) @router.get("/platform-config", response_model=PlatformConfigResponse) async def get_platform_config( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Get platform configuration""" # Basic users can see non-sensitive platform config platform_settings = SETTINGS_STORE.get("platform", {}) feature_settings = SETTINGS_STORE.get("features", {}) features = {key: setting["value"] for key, setting in feature_settings.items()} # Get API settings for rate limiting api_settings = SETTINGS_STORE.get("api", {}) return PlatformConfigResponse( app_name=platform_settings.get("app_name", {}).get("value", "Confidential Empire"), debug_mode=platform_settings.get("debug_mode", {}).get("value", False), log_level=app_settings.LOG_LEVEL, cors_origins=app_settings.CORS_ORIGINS, rate_limiting_enabled=api_settings.get("rate_limiting_enabled", {}).get("value", True), max_upload_size=platform_settings.get("max_upload_size", {}).get("value", 10485760), session_timeout_minutes=app_settings.SESSION_EXPIRE_MINUTES, api_key_prefix=app_settings.API_KEY_PREFIX, features=features, maintenance_mode=platform_settings.get("maintenance_mode", {}).get("value", False), maintenance_message=platform_settings.get("maintenance_message", {}).get("value") ) @router.get("/security-config", response_model=SecurityConfigResponse) async def get_security_config( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Get security configuration""" # Check permissions for sensitive security settings require_permission(current_user.get("permissions", []), "platform:settings:read") security_settings = SETTINGS_STORE.get("security", {}) return SecurityConfigResponse( password_min_length=security_settings.get("password_min_length", {}).get("value", 8), password_require_special=security_settings.get("password_require_special", {}).get("value", True), password_require_numbers=security_settings.get("password_require_numbers", {}).get("value", True), password_require_uppercase=security_settings.get("password_require_uppercase", {}).get("value", True), session_timeout_minutes=app_settings.SESSION_EXPIRE_MINUTES, max_login_attempts=security_settings.get("max_login_attempts", {}).get("value", 5), lockout_duration_minutes=security_settings.get("lockout_duration_minutes", {}).get("value", 15), require_2fa=security_settings.get("require_2fa", {}).get("value", False), allowed_domains=security_settings.get("allowed_domains", {}).get("value", []), ip_whitelist_enabled=security_settings.get("ip_whitelist_enabled", {}).get("value", False) ) @router.get("/{category}/{key}") async def get_setting( category: str, key: str, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Get a specific setting value""" # Check permissions require_permission(current_user.get("permissions", []), "platform:settings:read") if category not in SETTINGS_STORE: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Settings category '{category}' not found" ) if key not in SETTINGS_STORE[category]: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Setting '{key}' not found in category '{category}'" ) setting = SETTINGS_STORE[category][key] # Check if it's a secret setting if setting.get("is_secret", False): require_permission(current_user.get("permissions", []), "platform:settings:admin") # Log audit event await log_audit_event( db=db, user_id=current_user['id'], action="get_setting", resource_type="setting", resource_id=f"{category}.{key}" ) return { "category": category, "key": key, "value": setting["value"], "type": setting["type"], "description": setting.get("description", ""), "is_secret": setting.get("is_secret", False) } @router.put("/{category}") async def update_category_settings( category: str, settings_data: Dict[str, Any], current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Update multiple settings in a category""" # Check permissions require_permission(current_user.get("permissions", []), "platform:settings:update") if category not in SETTINGS_STORE: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Settings category '{category}' not found" ) updated_settings = [] errors = [] for key, new_value in settings_data.items(): if key not in SETTINGS_STORE[category]: errors.append(f"Setting '{key}' not found in category '{category}'") continue setting = SETTINGS_STORE[category][key] # Check if it's a secret setting if setting.get("is_secret", False): require_permission(current_user.get("permissions", []), "platform:settings:admin") # Store original value for audit original_value = setting["value"] # Validate value type expected_type = setting["type"] try: if expected_type == "integer" and not isinstance(new_value, int): if isinstance(new_value, str) and new_value.isdigit(): new_value = int(new_value) else: errors.append(f"Setting '{key}' expects an integer value") continue elif expected_type == "boolean" and not isinstance(new_value, bool): if isinstance(new_value, str): new_value = new_value.lower() in ('true', '1', 'yes', 'on') else: errors.append(f"Setting '{key}' expects a boolean value") continue elif expected_type == "float" and not isinstance(new_value, (int, float)): if isinstance(new_value, str): try: new_value = float(new_value) except ValueError: errors.append(f"Setting '{key}' expects a numeric value") continue else: errors.append(f"Setting '{key}' expects a numeric value") continue elif expected_type == "list" and not isinstance(new_value, list): errors.append(f"Setting '{key}' expects a list value") continue # Update setting SETTINGS_STORE[category][key]["value"] = new_value updated_settings.append({ "key": key, "original_value": original_value, "new_value": new_value }) except Exception as e: errors.append(f"Error updating setting '{key}': {str(e)}") # Log audit event for bulk update await log_audit_event( db=db, user_id=current_user['id'], action="bulk_update_settings", resource_type="setting", resource_id=category, details={ "updated_count": len(updated_settings), "errors_count": len(errors), "updated_settings": updated_settings, "errors": errors } ) logger.info(f"Bulk settings updated in category '{category}': {len(updated_settings)} settings by {current_user['username']}") if errors and not updated_settings: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"No settings were updated. Errors: {errors}" ) return { "category": category, "updated_count": len(updated_settings), "errors_count": len(errors), "updated_settings": [{"key": s["key"], "new_value": s["new_value"]} for s in updated_settings], "errors": errors, "message": f"Updated {len(updated_settings)} settings in category '{category}'" } @router.put("/{category}/{key}") async def update_setting( category: str, key: str, setting_update: SettingUpdate, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Update a specific setting""" # Check permissions require_permission(current_user.get("permissions", []), "platform:settings:update") if category not in SETTINGS_STORE: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Settings category '{category}' not found" ) if key not in SETTINGS_STORE[category]: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Setting '{key}' not found in category '{category}'" ) setting = SETTINGS_STORE[category][key] # Check if it's a secret setting if setting.get("is_secret", False): require_permission(current_user.get("permissions", []), "platform:settings:admin") # Store original value for audit original_value = setting["value"] # Validate value type expected_type = setting["type"] new_value = setting_update.value if expected_type == "integer" and not isinstance(new_value, int): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Setting '{key}' expects an integer value" ) elif expected_type == "boolean" and not isinstance(new_value, bool): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Setting '{key}' expects a boolean value" ) elif expected_type == "float" and not isinstance(new_value, (int, float)): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Setting '{key}' expects a numeric value" ) elif expected_type == "list" and not isinstance(new_value, list): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Setting '{key}' expects a list value" ) # Update setting SETTINGS_STORE[category][key]["value"] = new_value if setting_update.description is not None: SETTINGS_STORE[category][key]["description"] = setting_update.description # Log audit event await log_audit_event( db=db, user_id=current_user['id'], action="update_setting", resource_type="setting", resource_id=f"{category}.{key}", details={ "original_value": original_value, "new_value": new_value, "description_updated": setting_update.description is not None } ) logger.info(f"Setting updated: {category}.{key} by {current_user['username']}") return { "category": category, "key": key, "value": new_value, "type": expected_type, "description": SETTINGS_STORE[category][key].get("description", ""), "message": "Setting updated successfully" } @router.post("/reset-defaults") async def reset_to_defaults( category: Optional[str] = None, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Reset settings to default values""" # Check permissions require_permission(current_user.get("permissions", []), "platform:settings:admin") # Define default values defaults = { "platform": { "app_name": {"value": "Confidential Empire", "type": "string"}, "maintenance_mode": {"value": False, "type": "boolean"}, "debug_mode": {"value": False, "type": "boolean"}, "max_upload_size": {"value": 10485760, "type": "integer"}, }, "api": { # Security Settings "security_enabled": {"value": True, "type": "boolean"}, "threat_detection_enabled": {"value": True, "type": "boolean"}, "rate_limiting_enabled": {"value": True, "type": "boolean"}, "ip_reputation_enabled": {"value": True, "type": "boolean"}, "anomaly_detection_enabled": {"value": True, "type": "boolean"}, "security_headers_enabled": {"value": True, "type": "boolean"}, # Rate Limiting by Authentication Level "rate_limit_authenticated_per_minute": {"value": 200, "type": "integer"}, "rate_limit_authenticated_per_hour": {"value": 5000, "type": "integer"}, "rate_limit_api_key_per_minute": {"value": 1000, "type": "integer"}, "rate_limit_api_key_per_hour": {"value": 20000, "type": "integer"}, "rate_limit_premium_per_minute": {"value": 5000, "type": "integer"}, "rate_limit_premium_per_hour": {"value": 100000, "type": "integer"}, # Security Thresholds "security_risk_threshold": {"value": 0.8, "type": "float"}, "security_warning_threshold": {"value": 0.6, "type": "float"}, "anomaly_threshold": {"value": 0.7, "type": "float"}, # Request Settings "max_request_size_mb": {"value": 10, "type": "integer"}, "max_request_size_premium_mb": {"value": 50, "type": "integer"}, "enable_cors": {"value": True, "type": "boolean"}, "cors_origins": {"value": ["http://localhost:3000", "http://localhost:53000"], "type": "list"}, "api_key_expiry_days": {"value": 90, "type": "integer"}, # IP Security "blocked_ips": {"value": [], "type": "list"}, "allowed_ips": {"value": [], "type": "list"}, "csp_header": {"value": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';", "type": "string"}, }, "security": { "password_min_length": {"value": 8, "type": "integer"}, "password_require_special": {"value": True, "type": "boolean"}, "password_require_numbers": {"value": True, "type": "boolean"}, "password_require_uppercase": {"value": True, "type": "boolean"}, "max_login_attempts": {"value": 5, "type": "integer"}, "lockout_duration_minutes": {"value": 15, "type": "integer"}, "require_2fa": {"value": False, "type": "boolean"}, "ip_whitelist_enabled": {"value": False, "type": "boolean"}, "allowed_domains": {"value": [], "type": "list"}, }, "features": { "user_registration": {"value": True, "type": "boolean"}, "api_key_creation": {"value": True, "type": "boolean"}, "budget_enforcement": {"value": True, "type": "boolean"}, "audit_logging": {"value": True, "type": "boolean"}, "module_hot_reload": {"value": True, "type": "boolean"}, "tee_support": {"value": True, "type": "boolean"}, "advanced_analytics": {"value": True, "type": "boolean"}, } } reset_categories = [category] if category else list(defaults.keys()) for cat in reset_categories: if cat in defaults and cat in SETTINGS_STORE: for key, default_setting in defaults[cat].items(): if key in SETTINGS_STORE[cat]: SETTINGS_STORE[cat][key]["value"] = default_setting["value"] # Log audit event await log_audit_event( db=db, user_id=current_user['id'], action="reset_settings_to_defaults", resource_type="setting", details={"categories_reset": reset_categories} ) logger.info(f"Settings reset to defaults: {reset_categories} by {current_user['username']}") return { "message": f"Settings reset to defaults for categories: {reset_categories}", "categories_reset": reset_categories } @router.post("/export") async def export_settings( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Export all settings to JSON""" # Check permissions require_permission(current_user.get("permissions", []), "platform:settings:export") # Export all settings (excluding secrets for non-admin users) export_data = {} for category, settings in SETTINGS_STORE.items(): export_data[category] = {} for key, setting in settings.items(): # Skip secret settings for non-admin users if setting.get("is_secret", False): if not any(perm in current_user.get("permissions", []) for perm in ["platform:settings:admin", "platform:*"]): continue export_data[category][key] = { "value": setting["value"], "type": setting["type"], "description": setting.get("description", ""), "is_secret": setting.get("is_secret", False) } # Log audit event await log_audit_event( db=db, user_id=current_user['id'], action="export_settings", resource_type="setting", details={"categories_exported": list(export_data.keys())} ) return { "settings": export_data, "exported_at": datetime.utcnow().isoformat(), "exported_by": current_user['username'] } @router.post("/import") async def import_settings( settings_data: Dict[str, Dict[str, Dict[str, Any]]], current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Import settings from JSON""" # Check permissions require_permission(current_user.get("permissions", []), "platform:settings:admin") imported_count = 0 errors = [] for category, settings in settings_data.items(): if category not in SETTINGS_STORE: errors.append(f"Unknown category: {category}") continue for key, setting_data in settings.items(): if key not in SETTINGS_STORE[category]: errors.append(f"Unknown setting: {category}.{key}") continue try: # Validate and import expected_type = SETTINGS_STORE[category][key]["type"] new_value = setting_data.get("value") # Basic type validation if expected_type == "integer" and not isinstance(new_value, int): errors.append(f"Invalid type for {category}.{key}: expected integer") continue elif expected_type == "boolean" and not isinstance(new_value, bool): errors.append(f"Invalid type for {category}.{key}: expected boolean") continue SETTINGS_STORE[category][key]["value"] = new_value if "description" in setting_data: SETTINGS_STORE[category][key]["description"] = setting_data["description"] imported_count += 1 except Exception as e: errors.append(f"Error importing {category}.{key}: {str(e)}") # Log audit event await log_audit_event( db=db, user_id=current_user['id'], action="import_settings", resource_type="setting", details={ "imported_count": imported_count, "errors_count": len(errors), "errors": errors } ) logger.info(f"Settings imported: {imported_count} settings by {current_user['username']}") return { "message": f"Import completed. {imported_count} settings imported.", "imported_count": imported_count, "errors": errors }