toru-vault 0.1.3__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- toru_vault/__init__.py +1 -0
- toru_vault/__main__.py +29 -6
- toru_vault/in_env.py +173 -0
- toru_vault/in_memory.py +379 -0
- toru_vault/lazy_dict.py +10 -15
- toru_vault/vault.py +101 -424
- {toru_vault-0.1.3.dist-info → toru_vault-0.2.0.dist-info}/METADATA +21 -14
- toru_vault-0.2.0.dist-info/RECORD +13 -0
- {toru_vault-0.1.3.dist-info → toru_vault-0.2.0.dist-info}/WHEEL +1 -1
- toru_vault-0.1.3.dist-info/RECORD +0 -11
- {toru_vault-0.1.3.dist-info → toru_vault-0.2.0.dist-info}/entry_points.txt +0 -0
- {toru_vault-0.1.3.dist-info → toru_vault-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {toru_vault-0.1.3.dist-info → toru_vault-0.2.0.dist-info}/top_level.txt +0 -0
toru_vault/vault.py
CHANGED
@@ -1,28 +1,18 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
import os
|
3
3
|
import logging
|
4
|
-
import time
|
5
|
-
import json
|
6
|
-
import tempfile
|
7
4
|
import stat
|
8
|
-
import atexit
|
9
|
-
import secrets as pysecrets
|
10
|
-
from typing import Dict, Optional, Tuple
|
11
5
|
from bitwarden_sdk import BitwardenClient, DeviceType, client_settings_from_dict
|
12
|
-
from cryptography.fernet import Fernet
|
13
|
-
from cryptography.hazmat.primitives import hashes
|
14
|
-
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
15
|
-
import base64
|
16
|
-
from .lazy_dict import LazySecretsDict
|
17
6
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
7
|
+
from .in_memory import (
|
8
|
+
_KEYRING_AVAILABLE,
|
9
|
+
create_secrets_dict
|
10
|
+
)
|
11
|
+
from .in_env import (
|
12
|
+
set_env_vars,
|
13
|
+
load_secrets_env
|
14
|
+
)
|
24
15
|
|
25
|
-
# Setup minimal logging
|
26
16
|
logger = logging.getLogger(__name__)
|
27
17
|
|
28
18
|
# Constants for keyring storage
|
@@ -31,164 +21,6 @@ _KEYRING_BWS_TOKEN_KEY = "bws_token"
|
|
31
21
|
_KEYRING_ORG_ID_KEY = "organization_id"
|
32
22
|
_KEYRING_STATE_FILE_KEY = "state_file"
|
33
23
|
|
34
|
-
# Secure cache configuration
|
35
|
-
_SECRET_CACHE_TIMEOUT = 300 # 5 minutes
|
36
|
-
_secrets_cache: Dict[str, Tuple[float, Dict[str, str]]] = {}
|
37
|
-
|
38
|
-
def _generate_encryption_key(salt: bytes = None) -> Tuple[bytes, bytes]:
|
39
|
-
"""
|
40
|
-
Generate an encryption key for securing the cache
|
41
|
-
|
42
|
-
Args:
|
43
|
-
salt (bytes, optional): Salt for key derivation
|
44
|
-
|
45
|
-
Returns:
|
46
|
-
Tuple[bytes, bytes]: Key and salt
|
47
|
-
"""
|
48
|
-
if salt is None:
|
49
|
-
salt = os.urandom(16)
|
50
|
-
|
51
|
-
# Generate a key from the machine-specific information and random salt
|
52
|
-
machine_id = _get_machine_id()
|
53
|
-
password = machine_id.encode()
|
54
|
-
|
55
|
-
kdf = PBKDF2HMAC(
|
56
|
-
algorithm=hashes.SHA256(),
|
57
|
-
length=32,
|
58
|
-
salt=salt,
|
59
|
-
iterations=100000,
|
60
|
-
)
|
61
|
-
key = base64.urlsafe_b64encode(kdf.derive(password))
|
62
|
-
return key, salt
|
63
|
-
|
64
|
-
def _get_machine_id() -> str:
|
65
|
-
"""Get a unique identifier for the current machine"""
|
66
|
-
# Try platform-specific methods to get a machine ID
|
67
|
-
machine_id = ""
|
68
|
-
|
69
|
-
if os.path.exists('/etc/machine-id'):
|
70
|
-
with open('/etc/machine-id', 'r') as f:
|
71
|
-
machine_id = f.read().strip()
|
72
|
-
elif os.path.exists('/var/lib/dbus/machine-id'):
|
73
|
-
with open('/var/lib/dbus/machine-id', 'r') as f:
|
74
|
-
machine_id = f.read().strip()
|
75
|
-
elif os.name == 'nt': # Windows
|
76
|
-
import subprocess
|
77
|
-
try:
|
78
|
-
result = subprocess.run(['wmic', 'csproduct', 'get', 'UUID'], capture_output=True, text=True)
|
79
|
-
if result.returncode == 0:
|
80
|
-
machine_id = result.stdout.strip().split('\n')[-1].strip()
|
81
|
-
except (FileNotFoundError, subprocess.SubprocessError):
|
82
|
-
pass
|
83
|
-
|
84
|
-
# Fallback if we couldn't get a machine ID
|
85
|
-
if not machine_id:
|
86
|
-
# Use a combination of hostname and a persisted random value
|
87
|
-
import socket
|
88
|
-
hostname = socket.gethostname()
|
89
|
-
|
90
|
-
# Create a persistent random ID
|
91
|
-
id_file = os.path.join(tempfile.gettempdir(), '.vault_machine_id')
|
92
|
-
if os.path.exists(id_file):
|
93
|
-
try:
|
94
|
-
with open(id_file, 'r') as f:
|
95
|
-
random_id = f.read().strip()
|
96
|
-
except Exception:
|
97
|
-
random_id = pysecrets.token_hex(16)
|
98
|
-
else:
|
99
|
-
random_id = pysecrets.token_hex(16)
|
100
|
-
try:
|
101
|
-
# Try to save it with restricted permissions
|
102
|
-
with open(id_file, 'w') as f:
|
103
|
-
f.write(random_id)
|
104
|
-
os.chmod(id_file, stat.S_IRUSR | stat.S_IWUSR) # 0600 permissions
|
105
|
-
except Exception:
|
106
|
-
pass
|
107
|
-
|
108
|
-
machine_id = f"{hostname}-{random_id}"
|
109
|
-
|
110
|
-
return machine_id
|
111
|
-
|
112
|
-
def _encrypt_secrets(secrets_dict: Dict[str, str]) -> Optional[str]:
|
113
|
-
"""
|
114
|
-
Encrypt secrets dictionary
|
115
|
-
|
116
|
-
Args:
|
117
|
-
secrets_dict (Dict[str, str]): Dictionary of secrets
|
118
|
-
|
119
|
-
Returns:
|
120
|
-
Optional[str]: Encrypted data or None if encryption fails
|
121
|
-
"""
|
122
|
-
try:
|
123
|
-
key, salt = _generate_encryption_key()
|
124
|
-
if not key:
|
125
|
-
return None
|
126
|
-
|
127
|
-
# Encrypt the serialized secrets
|
128
|
-
f = Fernet(key)
|
129
|
-
encrypted_data = f.encrypt(json.dumps(secrets_dict).encode())
|
130
|
-
|
131
|
-
# Store along with the salt
|
132
|
-
return base64.urlsafe_b64encode(salt).decode() + ":" + encrypted_data.decode()
|
133
|
-
except Exception as e:
|
134
|
-
logger.warning(f"Failed to encrypt secrets: {e}")
|
135
|
-
return None
|
136
|
-
|
137
|
-
def _decrypt_secrets(encrypted_data: str) -> Optional[Dict[str, str]]:
|
138
|
-
"""
|
139
|
-
Decrypt secrets
|
140
|
-
|
141
|
-
Args:
|
142
|
-
encrypted_data (str): Encrypted data
|
143
|
-
|
144
|
-
Returns:
|
145
|
-
Optional[Dict[str, str]]: Decrypted secrets dictionary or None if decryption fails
|
146
|
-
"""
|
147
|
-
try:
|
148
|
-
# Split salt and encrypted data
|
149
|
-
salt_b64, encrypted = encrypted_data.split(":", 1)
|
150
|
-
salt = base64.urlsafe_b64decode(salt_b64)
|
151
|
-
|
152
|
-
# Regenerate the key with the same salt
|
153
|
-
key, _ = _generate_encryption_key(salt)
|
154
|
-
if not key:
|
155
|
-
return None
|
156
|
-
|
157
|
-
# Decrypt the data
|
158
|
-
f = Fernet(key)
|
159
|
-
decrypted_data = f.decrypt(encrypted.encode())
|
160
|
-
|
161
|
-
return json.loads(decrypted_data.decode())
|
162
|
-
except Exception as e:
|
163
|
-
logger.warning(f"Failed to decrypt secrets: {e}")
|
164
|
-
return None
|
165
|
-
|
166
|
-
def _secure_state_file(state_path: str) -> None:
|
167
|
-
"""
|
168
|
-
Ensure the state file has secure permissions
|
169
|
-
|
170
|
-
Args:
|
171
|
-
state_path (str): Path to the state file
|
172
|
-
"""
|
173
|
-
try:
|
174
|
-
if os.path.exists(state_path):
|
175
|
-
if os.name == 'posix': # Linux/Mac
|
176
|
-
os.chmod(state_path, stat.S_IRUSR | stat.S_IWUSR) # 0600 permissions
|
177
|
-
elif os.name == 'nt': # Windows
|
178
|
-
import subprocess
|
179
|
-
subprocess.run(['icacls', state_path, '/inheritance:r', '/grant:r', f'{os.getlogin()}:(F)'],
|
180
|
-
capture_output=True)
|
181
|
-
except Exception as e:
|
182
|
-
logger.warning(f"Could not set secure permissions on state file: {e}")
|
183
|
-
|
184
|
-
def _clear_cache() -> None:
|
185
|
-
"""Clear the secrets cache on exit"""
|
186
|
-
global _secrets_cache
|
187
|
-
_secrets_cache = {}
|
188
|
-
|
189
|
-
# Register the cache clearing function to run on exit
|
190
|
-
atexit.register(_clear_cache)
|
191
|
-
|
192
24
|
def _get_from_keyring_or_env(key, env_var):
|
193
25
|
"""
|
194
26
|
Get a value from keyring or environment variable
|
@@ -205,56 +37,59 @@ def _get_from_keyring_or_env(key, env_var):
|
|
205
37
|
# Try keyring first if available
|
206
38
|
if _KEYRING_AVAILABLE:
|
207
39
|
try:
|
40
|
+
import keyring
|
208
41
|
value = keyring.get_password(_KEYRING_SERVICE_NAME, key)
|
209
42
|
except Exception as e:
|
210
43
|
logger.warning(f"Failed to get {key} from keyring: {e}")
|
211
44
|
|
212
|
-
# Fall back to environment variable
|
213
45
|
if not value:
|
214
46
|
value = os.getenv(env_var)
|
215
47
|
|
216
48
|
return value
|
217
49
|
|
218
|
-
|
50
|
+
|
51
|
+
|
52
|
+
def _secure_state_file(state_path: str) -> None:
|
219
53
|
"""
|
220
|
-
|
54
|
+
Ensure the state file has secure permissions
|
221
55
|
|
222
56
|
Args:
|
223
|
-
|
224
|
-
value (str): Value to store
|
225
|
-
|
226
|
-
Returns:
|
227
|
-
bool: True if successful, False otherwise
|
57
|
+
state_path (str): Path to the state file
|
228
58
|
"""
|
229
|
-
if not _KEYRING_AVAILABLE:
|
230
|
-
return False
|
231
|
-
|
232
59
|
try:
|
233
|
-
|
234
|
-
|
60
|
+
if os.path.exists(state_path):
|
61
|
+
if os.name == 'posix': # Linux/Mac
|
62
|
+
os.chmod(state_path, stat.S_IRUSR | stat.S_IWUSR) # 0600 permissions
|
63
|
+
elif os.name == 'nt': # Windows
|
64
|
+
import subprocess
|
65
|
+
# /inheritance:r - Removes all inherited ACEs (Access Control Entries).
|
66
|
+
# /grant:r - Grants specified user rights, replacing any previous explicit ACEs for that user.
|
67
|
+
# <os.getlogin()>:(F) - Grants the current user (F)ull control.
|
68
|
+
result = subprocess.run(['icacls', state_path, '/inheritance:r', '/grant:r', f'{os.getlogin()}:(F)'],
|
69
|
+
capture_output=True)
|
70
|
+
if result.returncode != 0:
|
71
|
+
raise Exception(f"Could not set secure permissions on state file: {result.stderr.decode()}")
|
72
|
+
|
235
73
|
except Exception as e:
|
236
|
-
logger.warning(f"
|
237
|
-
return False
|
74
|
+
logger.warning(f"Could not set secure permissions on state file: {e}")
|
238
75
|
|
239
76
|
def _initialize_client():
|
240
77
|
"""
|
241
78
|
Initialize the Bitwarden client
|
242
79
|
"""
|
243
|
-
# Get environment variables with defaults
|
244
80
|
api_url = os.getenv("API_URL", "https://api.bitwarden.com")
|
245
81
|
identity_url = os.getenv("IDENTITY_URL", "https://identity.bitwarden.com")
|
246
82
|
|
247
|
-
# Get BWS_TOKEN from keyring or environment variable
|
248
83
|
bws_token = _get_from_keyring_or_env(_KEYRING_BWS_TOKEN_KEY, "BWS_TOKEN")
|
249
|
-
|
250
|
-
# Get STATE_FILE from keyring or environment variable
|
251
84
|
state_path = _get_from_keyring_or_env(_KEYRING_STATE_FILE_KEY, "STATE_FILE")
|
85
|
+
org_id = _get_from_keyring_or_env(_KEYRING_ORG_ID_KEY, "ORGANIZATION_ID")
|
252
86
|
|
253
|
-
# Validate required environment variables
|
254
87
|
if not bws_token:
|
255
88
|
raise ValueError("BWS_TOKEN not found in keyring or environment variable")
|
256
89
|
if not state_path:
|
257
90
|
raise ValueError("STATE_FILE not found in keyring or environment variable")
|
91
|
+
if not org_id:
|
92
|
+
raise ValueError("ORGANIZATION_ID not found in keyring or environment variable")
|
258
93
|
|
259
94
|
# Ensure state file directory exists
|
260
95
|
state_dir = os.path.dirname(state_path)
|
@@ -267,10 +102,8 @@ def _initialize_client():
|
|
267
102
|
except Exception as e:
|
268
103
|
logger.warning(f"Could not create state directory with secure permissions: {e}")
|
269
104
|
|
270
|
-
# Secure the state file
|
271
105
|
_secure_state_file(state_path)
|
272
106
|
|
273
|
-
# Create and initialize the client
|
274
107
|
client = BitwardenClient(
|
275
108
|
client_settings_from_dict({
|
276
109
|
"apiUrl": api_url,
|
@@ -280,102 +113,36 @@ def _initialize_client():
|
|
280
113
|
})
|
281
114
|
)
|
282
115
|
|
283
|
-
# Authenticate with the Secrets Manager Access Token
|
284
116
|
client.auth().login_access_token(bws_token, state_path)
|
285
117
|
|
118
|
+
del bws_token
|
119
|
+
del org_id
|
120
|
+
|
286
121
|
return client
|
287
122
|
|
288
|
-
def
|
123
|
+
def env_load(project_id=None, override=False):
|
289
124
|
"""
|
290
|
-
Load secrets
|
125
|
+
Load all secrets related to the project into environmental variables.
|
291
126
|
|
292
127
|
Args:
|
293
|
-
project_id (str): Project ID to filter secrets
|
294
|
-
|
295
|
-
Returns:
|
296
|
-
dict: Dictionary of secrets with their names as keys
|
128
|
+
project_id (str, optional): Project ID to filter secrets
|
129
|
+
override (bool, optional): Whether to override existing environment variables
|
297
130
|
"""
|
298
|
-
# Initialize client with credentials from environment or keyring
|
299
131
|
try:
|
300
132
|
client = _initialize_client()
|
301
133
|
except Exception as e:
|
302
134
|
logger.error(f"Failed to initialize Bitwarden client: {e}")
|
303
|
-
return
|
304
|
-
|
305
|
-
# Get ORGANIZATION_ID from keyring or environment variable
|
135
|
+
return
|
306
136
|
organization_id = _get_from_keyring_or_env(_KEYRING_ORG_ID_KEY, "ORGANIZATION_ID")
|
307
137
|
if not organization_id:
|
308
138
|
logger.error("ORGANIZATION_ID not found in keyring or environment variable")
|
309
|
-
return
|
139
|
+
return
|
310
140
|
|
311
|
-
|
312
|
-
try:
|
313
|
-
# Sync secrets to ensure we have the latest
|
314
|
-
client.secrets().sync(organization_id, None)
|
315
|
-
|
316
|
-
# Initialize empty secrets dictionary
|
317
|
-
secrets = {}
|
318
|
-
|
319
|
-
# Retrieve all secrets
|
320
|
-
all_secrets = client.secrets().list(organization_id)
|
321
|
-
|
322
|
-
# Validate response format
|
323
|
-
if not hasattr(all_secrets, 'data') or not hasattr(all_secrets.data, 'data'):
|
324
|
-
return {}
|
325
|
-
|
326
|
-
# We need to collect all secret IDs first
|
327
|
-
secret_ids = []
|
328
|
-
for secret in all_secrets.data.data:
|
329
|
-
secret_ids.append(secret.id)
|
330
|
-
|
331
|
-
# If we have secret IDs, fetch their values
|
332
|
-
if secret_ids:
|
333
|
-
# Get detailed information for all secrets by their IDs
|
334
|
-
secrets_detailed = client.secrets().get_by_ids(secret_ids)
|
335
|
-
|
336
|
-
# Validate response format
|
337
|
-
if not hasattr(secrets_detailed, 'data') or not hasattr(secrets_detailed.data, 'data'):
|
338
|
-
return {}
|
339
|
-
|
340
|
-
# Process each secret
|
341
|
-
for secret in secrets_detailed.data.data:
|
342
|
-
# Extract the project ID
|
343
|
-
secret_project_id = getattr(secret, 'project_id', None)
|
344
|
-
|
345
|
-
# Check if this secret belongs to the specified project
|
346
|
-
if project_id and secret_project_id is not None and project_id != str(secret_project_id):
|
347
|
-
continue
|
348
|
-
|
349
|
-
# Add the secret to our dictionary
|
350
|
-
secrets[secret.key] = secret.value
|
351
|
-
|
352
|
-
# Update the cache with encryption
|
353
|
-
encrypted_data = _encrypt_secrets(secrets)
|
354
|
-
if encrypted_data:
|
355
|
-
_secrets_cache[f"{organization_id}:{project_id or ''}"] = (time.time(), encrypted_data)
|
356
|
-
else:
|
357
|
-
_secrets_cache[f"{organization_id}:{project_id or ''}"] = (time.time(), secrets.copy())
|
358
|
-
|
359
|
-
return secrets
|
360
|
-
except Exception as e:
|
361
|
-
logger.error(f"Error loading secrets: {e}")
|
362
|
-
raise
|
141
|
+
secrets = load_secrets_env(client, organization_id, project_id)
|
363
142
|
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
Args:
|
369
|
-
project_id (str, optional): Project ID to filter secrets
|
370
|
-
override (bool, optional): Whether to override existing environment variables
|
371
|
-
"""
|
372
|
-
# Get all secrets from BWS
|
373
|
-
secrets = _load_secrets(project_id)
|
374
|
-
|
375
|
-
# Set environment variables
|
376
|
-
for key, value in secrets.items():
|
377
|
-
if override or key not in os.environ:
|
378
|
-
os.environ[key] = value
|
143
|
+
set_env_vars(secrets, override)
|
144
|
+
|
145
|
+
del secrets
|
379
146
|
|
380
147
|
def env_load_all(override=False):
|
381
148
|
"""
|
@@ -384,192 +151,102 @@ def env_load_all(override=False):
|
|
384
151
|
Args:
|
385
152
|
override (bool, optional): Whether to override existing environment variables
|
386
153
|
"""
|
387
|
-
# Get ORGANIZATION_ID from keyring or environment variable
|
388
154
|
organization_id = _get_from_keyring_or_env(_KEYRING_ORG_ID_KEY, "ORGANIZATION_ID")
|
155
|
+
if not organization_id:
|
156
|
+
logger.error("ORGANIZATION_ID not found in keyring or environment variable")
|
157
|
+
return
|
389
158
|
|
390
|
-
# Initialize Bitwarden client
|
391
159
|
try:
|
392
160
|
client = _initialize_client()
|
161
|
+
from .in_env import load_secrets_env_all
|
162
|
+
secrets = load_secrets_env_all(client, organization_id)
|
163
|
+
set_env_vars(secrets, override)
|
164
|
+
del secrets
|
393
165
|
except Exception as e:
|
394
|
-
logger.error(f"Failed to
|
166
|
+
logger.error(f"Failed to load all secrets: {e}")
|
395
167
|
return
|
396
|
-
|
397
|
-
try:
|
398
|
-
# Sync to ensure we have the latest data
|
399
|
-
client.secrets().sync(organization_id, None)
|
400
|
-
|
401
|
-
# Get all projects
|
402
|
-
projects_response = client.projects().list(organization_id)
|
403
|
-
|
404
|
-
# Validate response format
|
405
|
-
if not hasattr(projects_response, 'data') or not hasattr(projects_response.data, 'data'):
|
406
|
-
logger.warning(f"No projects found in organization {organization_id}")
|
407
|
-
return
|
408
|
-
|
409
|
-
# Process each project
|
410
|
-
for project in projects_response.data.data:
|
411
|
-
if hasattr(project, 'id'):
|
412
|
-
project_id = str(project.id)
|
413
|
-
|
414
|
-
# Load environment variables for this project
|
415
|
-
try:
|
416
|
-
# Get the secrets for this project and set them as environment variables
|
417
|
-
env_load(project_id=project_id, override=override)
|
418
|
-
logger.info(f"Loaded secrets from project: {getattr(project, 'name', project_id)}")
|
419
|
-
except Exception as e:
|
420
|
-
logger.warning(f"Failed to load secrets from project {project_id}: {e}")
|
421
|
-
|
422
|
-
except Exception as e:
|
423
|
-
logger.error(f"Failed to load all secrets into environment variables: {e}")
|
424
168
|
|
425
|
-
def get(project_id=None,
|
169
|
+
def get(project_id=None, use_keyring=True):
|
426
170
|
"""
|
427
|
-
Return a dictionary of all project secrets
|
171
|
+
Return a dictionary of all project secrets with JIT decryption
|
428
172
|
|
429
173
|
Args:
|
430
174
|
project_id (str, optional): Project ID to filter secrets
|
431
|
-
refresh (bool, optional): Force refresh the secrets cache
|
432
175
|
use_keyring (bool, optional): Whether to use system keyring (True) or in-memory encryption (False)
|
433
176
|
|
434
177
|
Returns:
|
435
|
-
dict: Dictionary of secrets with their names as keys, using lazy loading
|
178
|
+
dict: Dictionary of secrets with their names as keys, using lazy loading with JIT decryption
|
436
179
|
"""
|
437
|
-
|
180
|
+
try:
|
181
|
+
client = _initialize_client()
|
182
|
+
except Exception as e:
|
183
|
+
logger.error(f"Failed to initialize Bitwarden client: {e}")
|
184
|
+
return {}
|
185
|
+
|
438
186
|
organization_id = _get_from_keyring_or_env(_KEYRING_ORG_ID_KEY, "ORGANIZATION_ID")
|
187
|
+
if not organization_id:
|
188
|
+
logger.error("ORGANIZATION_ID not found in keyring or environment variable")
|
189
|
+
return {}
|
439
190
|
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
# Function to either fetch from keyring or decrypt from cache based on container flag
|
444
|
-
def _load_decrypted_secrets():
|
445
|
-
# Check if we need to force a refresh
|
446
|
-
if refresh:
|
447
|
-
return _load_secrets(project_id)
|
448
|
-
|
449
|
-
# Otherwise try to use cached values first
|
450
|
-
cache_key = f"{organization_id}:{project_id or ''}"
|
451
|
-
current_time = time.time()
|
452
|
-
|
453
|
-
if cache_key in _secrets_cache:
|
454
|
-
timestamp, encrypted_secrets = _secrets_cache[cache_key]
|
455
|
-
|
456
|
-
# If cache hasn't expired
|
457
|
-
if current_time - timestamp < _SECRET_CACHE_TIMEOUT:
|
458
|
-
# If we have encryption, try to decrypt
|
459
|
-
if encrypted_secrets:
|
460
|
-
decrypted_secrets = _decrypt_secrets(encrypted_secrets)
|
461
|
-
if decrypted_secrets:
|
462
|
-
return decrypted_secrets
|
463
|
-
# Otherwise return the unencrypted data (backward compatibility)
|
464
|
-
elif isinstance(encrypted_secrets, dict):
|
465
|
-
return encrypted_secrets.copy()
|
466
|
-
|
467
|
-
# If we couldn't get from cache, load fresh
|
468
|
-
return _load_secrets(project_id)
|
191
|
+
from .in_memory import load_secrets_memory
|
192
|
+
all_secrets = load_secrets_memory(client, organization_id, project_id)
|
469
193
|
|
470
|
-
# Get all
|
471
|
-
all_secrets = _load_decrypted_secrets()
|
194
|
+
# Get all secret keys - values will be decrypted JIT when accessed
|
472
195
|
secret_keys = set(all_secrets.keys())
|
473
196
|
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
if not keyring_usable:
|
482
|
-
# Create a dictionary of cached secrets for container mode
|
483
|
-
container_secrets = {}
|
484
|
-
encrypted_data = None
|
485
|
-
cache_key = f"{organization_id}:{project_id or ''}"
|
486
|
-
|
487
|
-
# If we have a cached encrypted version, use that
|
488
|
-
if cache_key in _secrets_cache:
|
489
|
-
_, encrypted_data = _secrets_cache[cache_key]
|
490
|
-
|
491
|
-
# Create getter function for container mode
|
492
|
-
def _container_getter(key):
|
493
|
-
if key in container_secrets:
|
494
|
-
return container_secrets[key]
|
495
|
-
|
496
|
-
# If not in memory cache, check if we have pre-loaded decrypted secrets
|
497
|
-
if all_secrets and key in all_secrets:
|
498
|
-
container_secrets[key] = all_secrets[key]
|
499
|
-
return container_secrets[key]
|
500
|
-
|
501
|
-
# Otherwise, try to decrypt from cache
|
502
|
-
if encrypted_data and not isinstance(encrypted_data, dict):
|
503
|
-
decrypted = _decrypt_secrets(encrypted_data)
|
504
|
-
if decrypted and key in decrypted:
|
505
|
-
container_secrets[key] = decrypted[key]
|
506
|
-
return container_secrets[key]
|
507
|
-
|
508
|
-
# If all else fails, load from API
|
509
|
-
fresh_secrets = _load_secrets(project_id)
|
510
|
-
if key in fresh_secrets:
|
511
|
-
container_secrets[key] = fresh_secrets[key]
|
512
|
-
return container_secrets[key]
|
513
|
-
|
514
|
-
return None
|
515
|
-
|
516
|
-
# Create the lazy dictionary with container getter
|
517
|
-
return LazySecretsDict(secret_keys, _container_getter)
|
518
|
-
else:
|
519
|
-
# Create getter function for keyring mode
|
520
|
-
def _keyring_getter(key):
|
521
|
-
return keyring.get_password(service_name, key)
|
522
|
-
|
523
|
-
# Create setter function for keyring mode
|
524
|
-
def _keyring_setter(key, value):
|
525
|
-
keyring.set_password(service_name, key, value)
|
526
|
-
|
527
|
-
# Create deleter function for keyring mode
|
528
|
-
def _keyring_deleter(key):
|
529
|
-
keyring.delete_password(service_name, key)
|
530
|
-
|
531
|
-
# Create the lazy dictionary with keyring getter/setter/deleter
|
532
|
-
return LazySecretsDict(secret_keys, _keyring_getter, _keyring_setter, _keyring_deleter)
|
533
|
-
|
197
|
+
return create_secrets_dict(
|
198
|
+
secret_keys,
|
199
|
+
organization_id,
|
200
|
+
project_id or "",
|
201
|
+
all_secrets,
|
202
|
+
use_keyring
|
203
|
+
)
|
534
204
|
|
535
|
-
def get_all(
|
205
|
+
def get_all(use_keyring=True):
|
536
206
|
"""
|
537
|
-
Return a combined dictionary of secrets from all projects that user has access to
|
207
|
+
Return a combined dictionary of secrets from all projects that user has access to with JIT decryption
|
538
208
|
|
539
209
|
Args:
|
540
|
-
refresh (bool, optional): Force refresh the secrets cache
|
541
210
|
use_keyring (bool, optional): Whether to use system keyring (True) or in-memory encryption (False)
|
542
211
|
|
543
212
|
Returns:
|
544
|
-
dict: Dictionary of secrets with their names as keys, using lazy loading
|
213
|
+
dict: Dictionary of secrets with their names as keys, using lazy loading with JIT decryption
|
545
214
|
"""
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
215
|
+
try:
|
216
|
+
client = _initialize_client()
|
217
|
+
except Exception as e:
|
218
|
+
logger.error(f"Failed to initialize Bitwarden client: {e}")
|
550
219
|
return {}
|
551
|
-
|
552
|
-
# Get organization ID
|
553
|
-
organization_id = _get_from_keyring_or_env("ORGANIZATION_ID", "ORGANIZATION_ID")
|
220
|
+
organization_id = _get_from_keyring_or_env(_KEYRING_ORG_ID_KEY, "ORGANIZATION_ID")
|
554
221
|
if not organization_id:
|
555
|
-
|
222
|
+
logger.error("Organization ID not found in keyring or environment variables")
|
556
223
|
return {}
|
557
224
|
|
558
225
|
try:
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
226
|
+
projects_response = client.projects().list(organization_id)
|
227
|
+
|
228
|
+
# Validate response format
|
229
|
+
if not hasattr(projects_response, 'data') or not hasattr(projects_response.data, 'data'):
|
230
|
+
logger.warning(f"No projects found in organization {organization_id}")
|
563
231
|
return {}
|
564
232
|
|
565
233
|
# Create a combined dictionary with all secrets
|
566
234
|
all_secrets = {}
|
567
|
-
|
568
|
-
|
569
|
-
|
235
|
+
project_ids = []
|
236
|
+
|
237
|
+
# First collect all project IDs
|
238
|
+
for project in projects_response.data.data:
|
239
|
+
if hasattr(project, 'id'):
|
240
|
+
project_ids.append(str(project.id))
|
241
|
+
|
242
|
+
# Create merged dictionary of all secrets with JIT decryption
|
243
|
+
for project_id in project_ids:
|
244
|
+
# Get secrets for this project
|
245
|
+
project_secrets = get(project_id, use_keyring=use_keyring)
|
246
|
+
# Update the combined dictionary (this will overwrite duplicate keys)
|
570
247
|
all_secrets.update(project_secrets)
|
571
248
|
|
572
249
|
return all_secrets
|
573
250
|
except Exception as e:
|
574
|
-
|
251
|
+
logger.error(f"Error retrieving projects: {str(e)}")
|
575
252
|
return {}
|