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

1"""Security settings utilities. 

2 

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""" 

7 

8import json 

9from functools import lru_cache 

10from pathlib import Path 

11from typing import Any, Optional, TypeVar, Union 

12 

13from loguru import logger 

14 

15from local_deep_research.settings.manager import check_env_setting 

16 

17T = TypeVar("T", int, float, str, bool) 

18 

19# Path to security settings JSON 

20_SETTINGS_PATH = ( 

21 Path(__file__).parent.parent / "defaults" / "settings_security.json" 

22) 

23 

24 

25@lru_cache(maxsize=1) 

26def _load_security_settings() -> dict: 

27 """Load and cache security settings from JSON file. 

28 

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 {} 

41 

42 

43def _convert_value(value: str, target_type: type, key: str) -> Optional[Any]: 

44 """Convert string value to target type with proper error handling. 

45 

46 Args: 

47 value: String value to convert 

48 target_type: Target type (int, float, str, bool) 

49 key: Setting key for error messages 

50 

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 

69 

70 

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. 

78 

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 

84 

85 Returns: 

86 Clamped value within bounds 

87 """ 

88 original = value 

89 

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 ) 

95 

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 ) 

101 

102 return value 

103 

104 

105def get_security_default(key: str, default: T) -> T: 

106 """Load a security setting with environment variable override. 

107 

108 Priority order: 

109 1. Environment variable (LDR_SECURITY_<KEY>) 

110 2. JSON defaults file value 

111 3. Provided default 

112 

113 Environment variables are validated against min/max bounds defined 

114 in the JSON settings file. 

115 

116 Args: 

117 key: Setting key (e.g., "security.session_remember_me_days") 

118 default: Default value if setting not found (also determines type) 

119 

120 Returns: 

121 Setting value of same type as default 

122 

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, {}) 

129 

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 ) 

141 

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) 

145 

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 

157 

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 

163 

164 return default