Coverage for src / local_deep_research / security / security_settings.py: 92%
56 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-25 01:07 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-25 01:07 +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.settings.manager import check_env_setting
17T = TypeVar("T", int, float, str, bool)
19# Path to security settings JSON
20_SETTINGS_PATH = (
21 Path(__file__).parent.parent / "defaults" / "settings_security.json"
22)
25@lru_cache(maxsize=1)
26def _load_security_settings() -> dict:
27 """Load and cache security settings from JSON file.
29 Returns:
30 Dictionary of security settings, or empty dict on error.
31 """
32 try:
33 if _SETTINGS_PATH.exists():
34 with open(_SETTINGS_PATH, "r") as f:
35 return json.load(f)
36 except Exception as e:
37 logger.warning(
38 f"Failed to load security settings from {_SETTINGS_PATH}: {e}"
39 )
40 return {}
43def _convert_value(value: str, target_type: type, key: str) -> Optional[Any]:
44 """Convert string value to target type with proper error handling.
46 Args:
47 value: String value to convert
48 target_type: Target type (int, float, str, bool)
49 key: Setting key for error messages
51 Returns:
52 Converted value or None if conversion fails
53 """
54 try:
55 if target_type is bool:
56 # Handle boolean strings properly
57 return value.lower() in ("true", "1", "yes", "on")
58 elif target_type is int:
59 return int(value)
60 elif target_type is float:
61 return float(value)
62 else:
63 return str(value)
64 except (ValueError, TypeError) as e:
65 logger.warning(
66 f"Invalid value for {key}: '{value}' cannot be converted to {target_type.__name__}: {e}"
67 )
68 return None
71def _validate_bounds(
72 value: Union[int, float],
73 min_value: Optional[Union[int, float]],
74 max_value: Optional[Union[int, float]],
75 key: str,
76) -> Union[int, float]:
77 """Validate that value is within min/max bounds.
79 Args:
80 value: Value to validate
81 min_value: Minimum allowed value (inclusive)
82 max_value: Maximum allowed value (inclusive)
83 key: Setting key for error messages
85 Returns:
86 Clamped value within bounds
87 """
88 original = value
90 if min_value is not None and value < min_value:
91 value = min_value
92 logger.warning(
93 f"Value {original} for {key} is below minimum {min_value}, using {value}"
94 )
96 if max_value is not None and value > max_value:
97 value = max_value
98 logger.warning(
99 f"Value {original} for {key} is above maximum {max_value}, using {value}"
100 )
102 return value
105def get_security_default(key: str, default: T) -> T:
106 """Load a security setting with environment variable override.
108 Priority order:
109 1. Environment variable (LDR_SECURITY_<KEY>)
110 2. JSON defaults file value
111 3. Provided default
113 Environment variables are validated against min/max bounds defined
114 in the JSON settings file.
116 Args:
117 key: Setting key (e.g., "security.session_remember_me_days")
118 default: Default value if setting not found (also determines type)
120 Returns:
121 Setting value of same type as default
123 Example:
124 >>> get_security_default("security.session_remember_me_days", 30)
125 30 # or value from env/JSON
126 """
127 settings = _load_security_settings()
128 setting_data = settings.get(key, {})
130 # Get min/max bounds from settings schema
131 min_value = (
132 setting_data.get("min_value")
133 if isinstance(setting_data, dict)
134 else None
135 )
136 max_value = (
137 setting_data.get("max_value")
138 if isinstance(setting_data, dict)
139 else None
140 )
142 # Check environment variable first (e.g., LDR_SECURITY_SESSION_REMEMBER_ME_DAYS)
143 env_key = f"LDR_{key.upper().replace('.', '_')}"
144 env_value = check_env_setting(key)
146 if env_value is not None:
147 converted = _convert_value(env_value, type(default), env_key)
148 if converted is not None:
149 # Validate bounds for numeric types
150 if isinstance(converted, (int, float)) and ( 150 ↛ 153line 150 didn't jump to line 153 because the condition on line 150 was never true
151 min_value is not None or max_value is not None
152 ):
153 converted = _validate_bounds(
154 converted, min_value, max_value, env_key
155 )
156 return converted # type: ignore
158 # Load from JSON defaults
159 if key in settings:
160 if isinstance(setting_data, dict) and "value" in setting_data: 160 ↛ 162line 160 didn't jump to line 162 because the condition on line 160 was always true
161 return setting_data["value"]
162 return setting_data
164 return default