OpenHands/openhands/server/settings_validation.py

123 lines
4.4 KiB
Python

"""Settings validation utilities for LLM settings access control."""
from openhands.core.logger import openhands_logger as logger
from openhands.server.shared import server_config
from openhands.server.types import AppMode
from openhands.storage.data_models.settings import Settings
def _is_llm_setting_changing(setting_name: str, new_value, existing_settings) -> bool:
"""Check if a specific LLM setting is being changed from its existing value.
Args:
setting_name: Name of the setting to check
new_value: New value being set
existing_settings: Existing settings object (can be None)
Returns:
bool: True if the setting is being changed, False otherwise
"""
if new_value is None:
return False
# Handle special case for enable_default_condenser with default value
if setting_name == 'enable_default_condenser':
if not existing_settings:
# First time setting - only validate if setting to non-default value
return not new_value
else:
# Changing existing value
return new_value != existing_settings.enable_default_condenser
# For other settings, validate if explicitly provided and different from existing
if not existing_settings:
return True
existing_value = getattr(existing_settings, setting_name, None)
return new_value != existing_value
def check_llm_settings_changes(settings: Settings, existing_settings) -> bool:
"""Check if any LLM-related settings are being changed.
Validates both core LLM settings (model, API key, base URL) and advanced settings
shown to SaaS users (confirmation mode, security analyzer, memory condenser settings).
Args:
settings: New settings being applied
existing_settings: Current settings (can be None)
Returns:
bool: True if any LLM settings are being changed, False otherwise
"""
# Core LLM settings - always validate if provided
core_llm_changes = any(
[
settings.llm_model is not None,
settings.llm_api_key is not None,
settings.llm_base_url is not None,
]
)
if core_llm_changes:
return True
# Additional LLM settings shown to SaaS users - validate if actually changing
advanced_llm_changes = any(
[
_is_llm_setting_changing(
'confirmation_mode', settings.confirmation_mode, existing_settings
),
_is_llm_setting_changing(
'security_analyzer', settings.security_analyzer, existing_settings
),
_is_llm_setting_changing(
'enable_default_condenser',
settings.enable_default_condenser,
existing_settings,
),
_is_llm_setting_changing(
'condenser_max_size', settings.condenser_max_size, existing_settings
),
]
)
return advanced_llm_changes
async def validate_llm_settings_access(user_id: str) -> bool:
"""Validate if user has access to modify LLM settings in SaaS mode.
In SaaS mode, only pro users with active subscriptions can modify LLM settings.
Args:
user_id: The user ID to check subscription for
Returns:
bool: True if user can modify LLM settings, False otherwise
"""
# Skip validation in non-SaaS mode
if server_config.app_mode != AppMode.SAAS:
return True
# In SaaS mode, check for active subscription for ANY LLM settings changes
try:
# Import here to avoid circular imports and handle enterprise mode gracefully
from enterprise.server.routes.billing import get_subscription_access
subscription = await get_subscription_access(user_id)
# The get_subscription_access function already filters for ACTIVE status,
# so if we get a subscription back, it means it's active
return subscription is not None
except ImportError:
# Enterprise billing module not available - in SaaS mode, this means
# we can't validate subscriptions, so deny access to be safe
logger.warning(
'Enterprise billing module not available in SaaS mode, denying LLM settings access'
)
return False
except Exception as e:
# On error, deny access to be safe
logger.warning(f'Error checking subscription access for user {user_id}: {e}')
return False