Coverage for src/local_deep_research/security/security_settings.py: 95%
55 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-22 21:20 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-22 21:20 +0000
1"""Security settings utilities.
3Provides functions to load security settings from JSON defaults with
4environment variable overrides. Used during app initialization before
5the full settings system is available.
6"""
8import json
9from functools import lru_cache
10from pathlib import Path
11from typing import Any, Optional, TypeVar, Union
13from loguru import logger
15from local_deep_research.utilities.type_utils import unwrap_setting
16from local_deep_research.settings.manager import check_env_setting
18T = TypeVar("T", int, float, str, bool)
20# Path to security settings JSON
21_SETTINGS_PATH = (
22 Path(__file__).parent.parent / "defaults" / "settings_security.json"
23)
26@lru_cache(maxsize=1)
27def _load_security_settings() -> dict:
28 """Load and cache security settings from JSON file.
30 Returns:
31 Dictionary of security settings, or empty dict on error.
32 """
33 try:
34 if _SETTINGS_PATH.exists():
35 # nosemgrep: semgrep.rules.path-traversal-risk, reason: Fixed settings filename below the installed package __file__ directory
36 with open(_SETTINGS_PATH, "r", encoding="utf-8-sig") as f:
37 return json.load(f)
38 except Exception:
39 logger.warning(
40 f"Failed to load security settings from {_SETTINGS_PATH}"
41 )
42 return {}
45def _convert_value(value: str, target_type: type, key: str) -> Optional[Any]:
46 """Convert string value to target type with proper error handling.
48 Args:
49 value: String value to convert
50 target_type: Target type (int, float, str, bool)
51 key: Setting key for error messages
53 Returns:
54 Converted value or None if conversion fails
55 """
56 try:
57 if target_type is bool:
58 # Handle boolean strings properly
59 return value.lower() in ("true", "1", "yes", "on")
60 if target_type is int:
61 return int(value)
62 if target_type is float:
63 return float(value)
64 return str(value)
65 except (ValueError, TypeError):
66 logger.warning(
67 f"Invalid value for {key}: '{value}' cannot be converted to {target_type.__name__}"
68 )
69 return None
72def _validate_bounds(
73 value: Union[int, float],
74 min_value: Optional[Union[int, float]],
75 max_value: Optional[Union[int, float]],
76 key: str,
77) -> Union[int, float]:
78 """Validate that value is within min/max bounds.
80 Args:
81 value: Value to validate
82 min_value: Minimum allowed value (inclusive)
83 max_value: Maximum allowed value (inclusive)
84 key: Setting key for error messages
86 Returns:
87 Clamped value within bounds
88 """
89 original = value
91 if min_value is not None and value < min_value:
92 value = min_value
93 logger.warning(
94 f"Value {original} for {key} is below minimum {min_value}, using {value}"
95 )
97 if max_value is not None and value > max_value:
98 value = max_value
99 logger.warning(
100 f"Value {original} for {key} is above maximum {max_value}, using {value}"
101 )
103 return value
106def get_security_default(key: str, default: T) -> T:
107 """Load a security setting with environment variable override.
109 Priority order:
110 1. Environment variable (LDR_SECURITY_<KEY>)
111 2. JSON defaults file value
112 3. Provided default
114 Environment variables are validated against min/max bounds defined
115 in the JSON settings file.
117 Args:
118 key: Setting key (e.g., "security.session_remember_me_days")
119 default: Default value if setting not found (also determines type)
121 Returns:
122 Setting value of same type as default
124 Example:
125 >>> get_security_default("security.session_remember_me_days", 30)
126 30 # or value from env/JSON
127 """
128 settings = _load_security_settings()
129 setting_data = settings.get(key, {})
131 # Get min/max bounds from settings schema
132 min_value = (
133 setting_data.get("min_value")
134 if isinstance(setting_data, dict)
135 else None
136 )
137 max_value = (
138 setting_data.get("max_value")
139 if isinstance(setting_data, dict)
140 else None
141 )
143 # Check environment variable first (e.g., LDR_SECURITY_SESSION_REMEMBER_ME_DAYS)
144 env_key = f"LDR_{key.upper().replace('.', '_')}"
145 env_value = check_env_setting(key)
147 if env_value is not None:
148 converted = _convert_value(env_value, type(default), env_key)
149 if converted is not None:
150 # Validate bounds for numeric types
151 if isinstance(converted, (int, float)) and ( 151 ↛ 154line 151 didn't jump to line 154 because the condition on line 151 was never true
152 min_value is not None or max_value is not None
153 ):
154 converted = _validate_bounds(
155 converted, min_value, max_value, env_key
156 )
157 return converted # type: ignore
159 # Load from JSON defaults
160 if key in settings:
161 return unwrap_setting(setting_data)
163 return default