Coverage for src / local_deep_research / security / security_settings.py: 92%

56 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-14 23:55 +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: 

37 logger.warning( 

38 f"Failed to load security settings from {_SETTINGS_PATH}" 

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 if target_type is int: 

59 return int(value) 

60 if target_type is float: 

61 return float(value) 

62 return str(value) 

63 except (ValueError, TypeError): 

64 logger.warning( 

65 f"Invalid value for {key}: '{value}' cannot be converted to {target_type.__name__}" 

66 ) 

67 return None 

68 

69 

70def _validate_bounds( 

71 value: Union[int, float], 

72 min_value: Optional[Union[int, float]], 

73 max_value: Optional[Union[int, float]], 

74 key: str, 

75) -> Union[int, float]: 

76 """Validate that value is within min/max bounds. 

77 

78 Args: 

79 value: Value to validate 

80 min_value: Minimum allowed value (inclusive) 

81 max_value: Maximum allowed value (inclusive) 

82 key: Setting key for error messages 

83 

84 Returns: 

85 Clamped value within bounds 

86 """ 

87 original = value 

88 

89 if min_value is not None and value < min_value: 

90 value = min_value 

91 logger.warning( 

92 f"Value {original} for {key} is below minimum {min_value}, using {value}" 

93 ) 

94 

95 if max_value is not None and value > max_value: 

96 value = max_value 

97 logger.warning( 

98 f"Value {original} for {key} is above maximum {max_value}, using {value}" 

99 ) 

100 

101 return value 

102 

103 

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

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

106 

107 Priority order: 

108 1. Environment variable (LDR_SECURITY_<KEY>) 

109 2. JSON defaults file value 

110 3. Provided default 

111 

112 Environment variables are validated against min/max bounds defined 

113 in the JSON settings file. 

114 

115 Args: 

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

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

118 

119 Returns: 

120 Setting value of same type as default 

121 

122 Example: 

123 >>> get_security_default("security.session_remember_me_days", 30) 

124 30 # or value from env/JSON 

125 """ 

126 settings = _load_security_settings() 

127 setting_data = settings.get(key, {}) 

128 

129 # Get min/max bounds from settings schema 

130 min_value = ( 

131 setting_data.get("min_value") 

132 if isinstance(setting_data, dict) 

133 else None 

134 ) 

135 max_value = ( 

136 setting_data.get("max_value") 

137 if isinstance(setting_data, dict) 

138 else None 

139 ) 

140 

141 # Check environment variable first (e.g., LDR_SECURITY_SESSION_REMEMBER_ME_DAYS) 

142 env_key = f"LDR_{key.upper().replace('.', '_')}" 

143 env_value = check_env_setting(key) 

144 

145 if env_value is not None: 

146 converted = _convert_value(env_value, type(default), env_key) 

147 if converted is not None: 

148 # Validate bounds for numeric types 

149 if isinstance(converted, (int, float)) and ( 149 ↛ 152line 149 didn't jump to line 152 because the condition on line 149 was never true

150 min_value is not None or max_value is not None 

151 ): 

152 converted = _validate_bounds( 

153 converted, min_value, max_value, env_key 

154 ) 

155 return converted # type: ignore 

156 

157 # Load from JSON defaults 

158 if key in settings: 

159 if isinstance(setting_data, dict) and "value" in setting_data: 159 ↛ 161line 159 didn't jump to line 161 because the condition on line 159 was always true

160 return setting_data["value"] 

161 return setting_data 

162 

163 return default