Files
doorman/backend-services/utils/security_settings_util.py
2025-12-10 23:09:05 -05:00

169 lines
5.0 KiB
Python

"""
Utilities to manage security-related settings and schedule auto-save of memory dumps.
"""
import asyncio
import logging
import os
from pathlib import Path
from typing import Any
from .database import database, db
from .memory_dump_util import dump_memory_to_file
logger = logging.getLogger('doorman.gateway')
_CACHE: dict[str, Any] = {}
_AUTO_TASK: asyncio.Task | None = None
_STOP_EVENT: asyncio.Event | None = None
_PROJECT_ROOT = Path(__file__).resolve().parent.parent
_GEN_DIR = _PROJECT_ROOT / 'generated'
DEFAULTS = {
'type': 'security_settings',
'enable_auto_save': False,
'auto_save_frequency_seconds': 900,
'dump_path': os.getenv('MEM_DUMP_PATH', str(_GEN_DIR / 'memory_dump.bin')),
'ip_whitelist': [],
'ip_blacklist': [],
'trust_x_forwarded_for': False,
'xff_trusted_proxies': [],
'allow_localhost_bypass': (os.getenv('LOCAL_HOST_IP_BYPASS', 'false').lower() == 'true'),
}
SETTINGS_FILE = os.getenv('SECURITY_SETTINGS_FILE', str(_GEN_DIR / 'security_settings.json'))
def _get_collection():
return db.settings if not database.memory_only else database.db.settings
def _merge_settings(doc: dict[str, Any]) -> dict[str, Any]:
merged = DEFAULTS.copy()
if doc:
merged.update({k: v for k, v in doc.items() if v is not None})
return merged
def get_cached_settings() -> dict[str, Any]:
global _CACHE
if not _CACHE:
_CACHE = DEFAULTS.copy()
return _CACHE
def _load_from_file() -> dict[str, Any] | None:
try:
if not os.path.exists(SETTINGS_FILE):
return None
with open(SETTINGS_FILE, encoding='utf-8') as f:
data = f.read().strip()
if not data:
return None
import json
obj = json.loads(data)
if isinstance(obj, dict):
return obj
except Exception as e:
logger.error('Failed to read settings file %s: %s', SETTINGS_FILE, e)
return None
def _save_to_file(settings: dict[str, Any]) -> None:
try:
os.makedirs(os.path.dirname(SETTINGS_FILE) or '.', exist_ok=True)
import json
with open(SETTINGS_FILE, 'w', encoding='utf-8') as f:
f.write(json.dumps(settings, separators=(',', ':')))
except Exception as e:
logger.error('Failed to write settings file %s: %s', SETTINGS_FILE, e)
async def load_settings() -> dict[str, Any]:
coll = _get_collection()
doc = coll.find_one({'type': 'security_settings'})
if not doc and database.memory_only:
file_obj = _load_from_file()
if file_obj:
try:
to_set = _merge_settings(file_obj)
coll.update_one({'type': 'security_settings'}, {'$set': to_set})
doc = to_set
except Exception:
doc = file_obj
settings = _merge_settings(doc or {})
_CACHE.update(settings)
return settings
async def save_settings(partial: dict[str, Any]) -> dict[str, Any]:
coll = _get_collection()
current = _merge_settings(coll.find_one({'type': 'security_settings'}) or {})
current.update({k: v for k, v in partial.items() if v is not None})
result = coll.update_one({'type': 'security_settings'}, {'$set': current})
try:
modified = getattr(result, 'modified_count', 0)
except Exception:
modified = 0
if not modified and not coll.find_one({'type': 'security_settings'}):
coll.insert_one(current)
_CACHE.update(current)
_save_to_file(_CACHE)
await restart_auto_save_task()
return current
async def _auto_save_loop(stop_event: asyncio.Event):
while not stop_event.is_set():
try:
settings = get_cached_settings()
freq = int(settings.get('auto_save_frequency_seconds', 0) or 0)
if database.memory_only and freq > 0:
try:
dump_memory_to_file(settings.get('dump_path'))
logger.info('Auto-saved memory dump to %s', settings.get('dump_path'))
except Exception as e:
logger.error('Auto-save memory dump failed: %s', e)
await asyncio.wait_for(stop_event.wait(), timeout=max(freq, 60) if freq > 0 else 60)
except TimeoutError:
continue
except Exception as e:
logger.error('Auto-save loop error: %s', e)
await asyncio.sleep(60)
async def start_auto_save_task():
global _AUTO_TASK, _STOP_EVENT
if _AUTO_TASK and not _AUTO_TASK.done():
return
_STOP_EVENT = asyncio.Event()
_AUTO_TASK = asyncio.create_task(_auto_save_loop(_STOP_EVENT))
logger.info('Security auto-save task started')
async def stop_auto_save_task():
global _AUTO_TASK, _STOP_EVENT
if _STOP_EVENT:
_STOP_EVENT.set()
if _AUTO_TASK:
try:
await asyncio.wait_for(_AUTO_TASK, timeout=5)
except Exception:
pass
_AUTO_TASK = None
_STOP_EVENT = None
async def restart_auto_save_task():
await stop_auto_save_task()
await start_auto_save_task()