Coverage for src / local_deep_research / database / session_context.py: 57%
97 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-01-11 00:51 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2026-01-11 00:51 +0000
1"""
2Database session context manager and decorator for encrypted databases.
3Ensures all database access has proper encryption context.
4"""
6import functools
7from contextlib import contextmanager
8from typing import Callable, Optional
10from flask import g, has_app_context, session as flask_session
11from loguru import logger
12from sqlalchemy.orm import Session
14from ..utilities.thread_context import get_search_context
15from .encrypted_db import db_manager
17# Placeholder password used when accessing unencrypted databases.
18# This should only be used when LDR_ALLOW_UNENCRYPTED=true is set.
19UNENCRYPTED_DB_PLACEHOLDER = "unencrypted-mode"
22class DatabaseSessionError(Exception):
23 """Raised when database session cannot be established."""
25 pass
28@contextmanager
29def get_user_db_session(
30 username: Optional[str] = None, password: Optional[str] = None
31):
32 """
33 Context manager that ensures proper database session with encryption.
34 Now uses thread-local sessions for better performance.
36 Args:
37 username: Username (if not provided, gets from Flask session)
38 password: Password for encrypted database (required for first access)
40 Yields:
41 Database session for the user
43 Raises:
44 DatabaseSessionError: If session cannot be established
45 """
46 # Import here to avoid circular imports
47 from .thread_local_session import get_metrics_session
48 from .session_passwords import session_password_store
50 session = None
51 needs_close = False
53 try:
54 # Get username from Flask session if not provided (only in Flask context)
55 if not username and has_app_context():
56 username = flask_session.get("username")
58 if not username: 58 ↛ 59line 58 didn't jump to line 59 because the condition on line 58 was never true
59 raise DatabaseSessionError("No authenticated user")
61 # First, check if we have a session in Flask's g object (best performance)
62 if has_app_context() and hasattr(g, "db_session") and g.db_session:
63 # Use existing session from g - no need to close
64 session = g.db_session
65 needs_close = False
66 else:
67 # Get password if not provided
68 if not password and has_app_context():
69 # Try to get from g
70 if hasattr(g, "user_password"):
71 password = g.user_password
72 logger.debug(
73 f"Got password from g.user_password for {username}"
74 )
75 # Try session password store
76 elif flask_session.get("session_id"): 76 ↛ 77line 76 didn't jump to line 77 because the condition on line 76 was never true
77 session_id = flask_session.get("session_id")
78 logger.debug(
79 f"Trying session password store for {username}"
80 )
81 password = session_password_store.get_session_password(
82 username, session_id
83 )
84 if password:
85 logger.debug(
86 f"Got password from session store for {username}"
87 )
88 else:
89 logger.debug(
90 f"No password in session store for {username}"
91 )
93 # Try thread context (for background threads)
94 if not password:
95 thread_context = get_search_context()
96 if thread_context and thread_context.get("user_password"):
97 password = thread_context["user_password"]
98 logger.debug(
99 f"Got password from thread context for {username}"
100 )
102 if not password and db_manager.has_encryption:
103 raise DatabaseSessionError(
104 f"Encrypted database for {username} requires password"
105 )
106 elif not password: 106 ↛ 107line 106 didn't jump to line 107 because the condition on line 106 was never true
107 logger.warning(
108 f"Accessing unencrypted database for {username} - "
109 "ensure this is intentional (LDR_ALLOW_UNENCRYPTED=true)"
110 )
111 password = UNENCRYPTED_DB_PLACEHOLDER
113 # Use thread-local session (will reuse existing or create new)
114 session = get_metrics_session(username, password)
115 if not session:
116 raise DatabaseSessionError(
117 f"Could not establish session for {username}"
118 )
119 # Thread-local sessions are managed by the thread, don't close them
120 needs_close = False
122 # Store the password we successfully used
123 if password and has_app_context():
124 g.user_password = password
126 yield session
128 finally:
129 # Only close if we created a new session (which we don't anymore)
130 if session and needs_close: 130 ↛ 131line 130 didn't jump to line 131 because the condition on line 130 was never true
131 try:
132 session.close()
133 except Exception:
134 pass
137def with_user_database(func: Callable) -> Callable:
138 """
139 Decorator that ensures function has access to user's database.
140 Injects 'db_session' as first argument to the decorated function.
142 Usage:
143 @with_user_database
144 def get_user_settings(db_session, setting_key):
145 return db_session.query(Setting).filter_by(key=setting_key).first()
146 """
148 @functools.wraps(func)
149 def wrapper(*args, **kwargs):
150 # Check if username/password provided in kwargs
151 username = kwargs.pop("_username", None)
152 password = kwargs.pop("_password", None)
154 with get_user_db_session(username, password) as db_session:
155 return func(db_session, *args, **kwargs)
157 return wrapper
160def ensure_db_session(view_func: Callable) -> Callable:
161 """
162 Flask view decorator that ensures database session is available.
163 Sets g.db_session for use in the request.
165 Usage:
166 @app.route('/my-route')
167 @ensure_db_session
168 def my_view():
169 # g.db_session is available here
170 settings = g.db_session.query(Setting).all()
171 """
173 @functools.wraps(view_func)
174 def wrapper(*args, **kwargs):
175 username = flask_session.get("username")
177 if not username:
178 # Let the view handle unauthenticated users
179 return view_func(*args, **kwargs)
181 try:
182 # Try to get or create session
183 if username in db_manager.connections:
184 g.db_session = db_manager.get_session(username)
185 else:
186 # Database not open - for encrypted DBs this means user needs to re-login
187 if db_manager.has_encryption:
188 # Clear session to force re-login
189 flask_session.clear()
190 from flask import redirect, url_for
192 return redirect(url_for("auth.login"))
193 else:
194 # Try to reopen unencrypted database
195 logger.warning(
196 f"Reopening unencrypted database for {username} - "
197 "ensure this is intentional"
198 )
199 engine = db_manager.open_user_database(
200 username, UNENCRYPTED_DB_PLACEHOLDER
201 )
202 if engine:
203 g.db_session = db_manager.get_session(username)
205 except Exception:
206 logger.exception(
207 f"Failed to ensure database session for {username}"
208 )
210 return view_func(*args, **kwargs)
212 return wrapper
215class DatabaseAccessMixin:
216 """
217 Mixin class for services that need database access.
218 Provides convenient methods for database operations.
219 """
221 def get_db_session(
222 self, username: Optional[str] = None
223 ) -> Optional[Session]:
224 """Get database session for user."""
225 try:
226 with get_user_db_session(username) as session:
227 return session
228 except DatabaseSessionError:
229 return None
231 @with_user_database
232 def execute_with_db(
233 self, db_session: Session, query_func: Callable, *args, **kwargs
234 ):
235 """Execute a function with database session."""
236 return query_func(db_session, *args, **kwargs)