Coverage for src/local_deep_research/database/session_passwords.py: 88%
40 statements
« prev ^ index » next coverage.py v7.15.1, created at 2026-07-20 01:24 +0000
« prev ^ index » next coverage.py v7.15.1, created at 2026-07-20 01:24 +0000
1"""
2Session-based password storage for metrics access.
4This module provides a way to temporarily store passwords in memory
5for the duration of a user's session, allowing background threads
6to access encrypted databases for metrics writing.
8SECURITY NOTES:
91. Passwords are only stored in memory, never on disk
102. Passwords are stored in plain text in memory (encryption removed as it
11 provided no real security benefit - see issue #593)
123. Passwords are automatically cleared on logout
134. This is only used for metrics/logging, not user data access
14"""
16from typing import Optional
18from loguru import logger
20from .credential_store_base import CredentialStoreBase
23class SessionPasswordStore(CredentialStoreBase):
24 """
25 Stores passwords temporarily for active sessions.
26 Used to allow background threads to write metrics to encrypted databases.
27 """
29 def __init__(self, ttl_hours: int = 24):
30 """
31 Initialize the session password store.
33 Args:
34 ttl_hours: How long to keep passwords (default 24 hours)
35 """
36 super().__init__(ttl_hours * 3600) # Convert to seconds
38 def store_session_password(
39 self, username: str, session_id: str, password: str
40 ) -> None:
41 """
42 Store a password for an active session.
44 Args:
45 username: The username
46 session_id: The Flask session ID
47 password: The password to store
48 """
49 key = (username, session_id)
50 self._store_credentials(
51 key, {"username": username, "password": password}
52 )
53 logger.debug(f"Stored session password for {username}")
55 def get_session_password(
56 self, username: str, session_id: str
57 ) -> Optional[str]:
58 """
59 Retrieve a password for an active session.
61 Args:
62 username: The username
63 session_id: The Flask session ID
65 Returns:
66 The decrypted password or None if not found/expired
67 """
68 key = (username, session_id)
69 result = self._retrieve_credentials(key, remove=False)
70 return result[1] if result else None
72 def clear_session(self, username: str, session_id: str) -> None:
73 """
74 Clear password for a specific session (on logout).
76 Args:
77 username: The username
78 session_id: The Flask session ID
79 """
80 key = (username, session_id)
81 self.clear_entry(key)
82 logger.debug(f"Cleared session password for {username}")
84 def clear_all_for_user(self, username: str) -> None:
85 """Remove all stored passwords for a user (idle connection cleanup)."""
86 with self._lock:
87 keys_to_remove = [k for k in self._store if k[0] == username]
88 for k in keys_to_remove:
89 del self._store[k]
91 # Implement abstract methods for compatibility
92 def store(self, username: str, session_id: str, password: str) -> None:
93 """Alias for store_session_password."""
94 self.store_session_password(username, session_id, password)
96 def retrieve(self, username: str, session_id: str) -> Optional[str]:
97 """Alias for get_session_password."""
98 return self.get_session_password(username, session_id)
101# Global instance
102session_password_store = SessionPasswordStore()
105def capture_request_db_password(username: str) -> Optional[str]:
106 """Best-effort capture of the encrypted-DB password from the request
107 thread.
109 ``ThreadPoolExecutor.submit`` does NOT copy the calling thread's
110 ``ContextVar`` snapshot, so background workers cannot recover the
111 password through the usual request-context probes. Callers that need
112 to hand a password to a worker (or to a service like the RAG factory
113 that must open the encrypted DB to read index config) capture it here
114 while still on the request thread and pass it explicitly.
116 Returns ``None`` if the password cannot be located — on encrypted-DB
117 installs the caller should then skip the DB-touching work rather than
118 fail loudly.
119 """
120 try:
121 from flask import (
122 g,
123 has_app_context,
124 has_request_context,
125 session as flask_session,
126 )
128 if has_app_context() and hasattr(g, "user_password"): 128 ↛ 129line 128 didn't jump to line 129 because the condition on line 128 was never true
129 return g.user_password
130 if has_request_context():
131 session_id = flask_session.get("session_id")
132 if session_id: 132 ↛ 133line 132 didn't jump to line 133 because the condition on line 132 was never true
133 return session_password_store.get_session_password(
134 username, session_id
135 )
136 except Exception:
137 logger.debug(
138 "Failed to capture DB password from request context",
139 exc_info=True,
140 )
141 return None