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

1""" 

2Session-based password storage for metrics access. 

3 

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. 

7 

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

15 

16from typing import Optional 

17 

18from loguru import logger 

19 

20from .credential_store_base import CredentialStoreBase 

21 

22 

23class SessionPasswordStore(CredentialStoreBase): 

24 """ 

25 Stores passwords temporarily for active sessions. 

26 Used to allow background threads to write metrics to encrypted databases. 

27 """ 

28 

29 def __init__(self, ttl_hours: int = 24): 

30 """ 

31 Initialize the session password store. 

32 

33 Args: 

34 ttl_hours: How long to keep passwords (default 24 hours) 

35 """ 

36 super().__init__(ttl_hours * 3600) # Convert to seconds 

37 

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. 

43 

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

54 

55 def get_session_password( 

56 self, username: str, session_id: str 

57 ) -> Optional[str]: 

58 """ 

59 Retrieve a password for an active session. 

60 

61 Args: 

62 username: The username 

63 session_id: The Flask session ID 

64 

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 

71 

72 def clear_session(self, username: str, session_id: str) -> None: 

73 """ 

74 Clear password for a specific session (on logout). 

75 

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

83 

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] 

90 

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) 

95 

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) 

99 

100 

101# Global instance 

102session_password_store = SessionPasswordStore() 

103 

104 

105def capture_request_db_password(username: str) -> Optional[str]: 

106 """Best-effort capture of the encrypted-DB password from the request 

107 thread. 

108 

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. 

115 

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 ) 

127 

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