toru-vault 0.1.4__py3-none-any.whl → 0.3.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 +110 -424
- {toru_vault-0.1.4.dist-info → toru_vault-0.3.0.dist-info}/METADATA +19 -9
- toru_vault-0.3.0.dist-info/RECORD +13 -0
- {toru_vault-0.1.4.dist-info → toru_vault-0.3.0.dist-info}/WHEEL +1 -1
- toru_vault-0.1.4.dist-info/RECORD +0 -11
- {toru_vault-0.1.4.dist-info → toru_vault-0.3.0.dist-info}/entry_points.txt +0 -0
- {toru_vault-0.1.4.dist-info → toru_vault-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {toru_vault-0.1.4.dist-info → toru_vault-0.3.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
|
@@ -30,164 +20,7 @@ _KEYRING_SERVICE_NAME = "bitwarden_vault"
|
|
30
20
|
_KEYRING_BWS_TOKEN_KEY = "bws_token"
|
31
21
|
_KEYRING_ORG_ID_KEY = "organization_id"
|
32
22
|
_KEYRING_STATE_FILE_KEY = "state_file"
|
33
|
-
|
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)
|
23
|
+
_KEYRING_PROJECT_ID_KEY = "project_id"
|
191
24
|
|
192
25
|
def _get_from_keyring_or_env(key, env_var):
|
193
26
|
"""
|
@@ -205,56 +38,59 @@ def _get_from_keyring_or_env(key, env_var):
|
|
205
38
|
# Try keyring first if available
|
206
39
|
if _KEYRING_AVAILABLE:
|
207
40
|
try:
|
41
|
+
import keyring
|
208
42
|
value = keyring.get_password(_KEYRING_SERVICE_NAME, key)
|
209
43
|
except Exception as e:
|
210
44
|
logger.warning(f"Failed to get {key} from keyring: {e}")
|
211
45
|
|
212
|
-
# Fall back to environment variable
|
213
46
|
if not value:
|
214
47
|
value = os.getenv(env_var)
|
215
48
|
|
216
49
|
return value
|
217
50
|
|
218
|
-
|
51
|
+
|
52
|
+
|
53
|
+
def _secure_state_file(state_path: str) -> None:
|
219
54
|
"""
|
220
|
-
|
55
|
+
Ensure the state file has secure permissions
|
221
56
|
|
222
57
|
Args:
|
223
|
-
|
224
|
-
value (str): Value to store
|
225
|
-
|
226
|
-
Returns:
|
227
|
-
bool: True if successful, False otherwise
|
58
|
+
state_path (str): Path to the state file
|
228
59
|
"""
|
229
|
-
if not _KEYRING_AVAILABLE:
|
230
|
-
return False
|
231
|
-
|
232
60
|
try:
|
233
|
-
|
234
|
-
|
61
|
+
if os.path.exists(state_path):
|
62
|
+
if os.name == 'posix': # Linux/Mac
|
63
|
+
os.chmod(state_path, stat.S_IRUSR | stat.S_IWUSR) # 0600 permissions
|
64
|
+
elif os.name == 'nt': # Windows
|
65
|
+
import subprocess
|
66
|
+
# /inheritance:r - Removes all inherited ACEs (Access Control Entries).
|
67
|
+
# /grant:r - Grants specified user rights, replacing any previous explicit ACEs for that user.
|
68
|
+
# <os.getlogin()>:(F) - Grants the current user (F)ull control.
|
69
|
+
result = subprocess.run(['icacls', state_path, '/inheritance:r', '/grant:r', f'{os.getlogin()}:(F)'],
|
70
|
+
capture_output=True)
|
71
|
+
if result.returncode != 0:
|
72
|
+
raise Exception(f"Could not set secure permissions on state file: {result.stderr.decode()}")
|
73
|
+
|
235
74
|
except Exception as e:
|
236
|
-
logger.warning(f"
|
237
|
-
return False
|
75
|
+
logger.warning(f"Could not set secure permissions on state file: {e}")
|
238
76
|
|
239
77
|
def _initialize_client():
|
240
78
|
"""
|
241
79
|
Initialize the Bitwarden client
|
242
80
|
"""
|
243
|
-
# Get environment variables with defaults
|
244
81
|
api_url = os.getenv("API_URL", "https://api.bitwarden.com")
|
245
82
|
identity_url = os.getenv("IDENTITY_URL", "https://identity.bitwarden.com")
|
246
83
|
|
247
|
-
# Get BWS_TOKEN from keyring or environment variable
|
248
84
|
bws_token = _get_from_keyring_or_env(_KEYRING_BWS_TOKEN_KEY, "BWS_TOKEN")
|
249
|
-
|
250
|
-
# Get STATE_FILE from keyring or environment variable
|
251
85
|
state_path = _get_from_keyring_or_env(_KEYRING_STATE_FILE_KEY, "STATE_FILE")
|
86
|
+
org_id = _get_from_keyring_or_env(_KEYRING_ORG_ID_KEY, "ORGANIZATION_ID")
|
252
87
|
|
253
|
-
# Validate required environment variables
|
254
88
|
if not bws_token:
|
255
89
|
raise ValueError("BWS_TOKEN not found in keyring or environment variable")
|
256
90
|
if not state_path:
|
257
91
|
raise ValueError("STATE_FILE not found in keyring or environment variable")
|
92
|
+
if not org_id:
|
93
|
+
raise ValueError("ORGANIZATION_ID not found in keyring or environment variable")
|
258
94
|
|
259
95
|
# Ensure state file directory exists
|
260
96
|
state_dir = os.path.dirname(state_path)
|
@@ -267,10 +103,8 @@ def _initialize_client():
|
|
267
103
|
except Exception as e:
|
268
104
|
logger.warning(f"Could not create state directory with secure permissions: {e}")
|
269
105
|
|
270
|
-
# Secure the state file
|
271
106
|
_secure_state_file(state_path)
|
272
107
|
|
273
|
-
# Create and initialize the client
|
274
108
|
client = BitwardenClient(
|
275
109
|
client_settings_from_dict({
|
276
110
|
"apiUrl": api_url,
|
@@ -280,102 +114,40 @@ def _initialize_client():
|
|
280
114
|
})
|
281
115
|
)
|
282
116
|
|
283
|
-
# Authenticate with the Secrets Manager Access Token
|
284
117
|
client.auth().login_access_token(bws_token, state_path)
|
285
118
|
|
119
|
+
del bws_token
|
120
|
+
del org_id
|
121
|
+
|
286
122
|
return client
|
287
123
|
|
288
|
-
def
|
124
|
+
def env_load(project_id=None, override=False):
|
289
125
|
"""
|
290
|
-
Load secrets
|
126
|
+
Load all secrets related to the project into environmental variables.
|
291
127
|
|
292
128
|
Args:
|
293
|
-
project_id (str): Project ID to filter secrets
|
294
|
-
|
295
|
-
Returns:
|
296
|
-
dict: Dictionary of secrets with their names as keys
|
129
|
+
project_id (str, optional): Project ID to filter secrets. If None, will try to get from keyring or PROJECT_ID environment variable
|
130
|
+
override (bool, optional): Whether to override existing environment variables
|
297
131
|
"""
|
298
|
-
# Initialize client with credentials from environment or keyring
|
299
132
|
try:
|
300
133
|
client = _initialize_client()
|
301
134
|
except Exception as e:
|
302
135
|
logger.error(f"Failed to initialize Bitwarden client: {e}")
|
303
|
-
return
|
304
|
-
|
305
|
-
# Get ORGANIZATION_ID from keyring or environment variable
|
136
|
+
return
|
306
137
|
organization_id = _get_from_keyring_or_env(_KEYRING_ORG_ID_KEY, "ORGANIZATION_ID")
|
307
138
|
if not organization_id:
|
308
139
|
logger.error("ORGANIZATION_ID not found in keyring or environment variable")
|
309
|
-
return
|
310
|
-
|
311
|
-
# Get secrets from BitWarden
|
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
|
363
|
-
|
364
|
-
def env_load(project_id=None, override=False):
|
365
|
-
"""
|
366
|
-
Load all secrets related to the project into environmental variables.
|
140
|
+
return
|
367
141
|
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
"""
|
372
|
-
# Get all secrets from BWS
|
373
|
-
secrets = _load_secrets(project_id)
|
142
|
+
# If project_id is not provided, try to get it from keyring or environment variable
|
143
|
+
if project_id is None:
|
144
|
+
project_id = _get_from_keyring_or_env(_KEYRING_PROJECT_ID_KEY, "PROJECT_ID")
|
374
145
|
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
146
|
+
secrets = load_secrets_env(client, organization_id, project_id)
|
147
|
+
|
148
|
+
set_env_vars(secrets, override)
|
149
|
+
|
150
|
+
del secrets
|
379
151
|
|
380
152
|
def env_load_all(override=False):
|
381
153
|
"""
|
@@ -384,192 +156,106 @@ def env_load_all(override=False):
|
|
384
156
|
Args:
|
385
157
|
override (bool, optional): Whether to override existing environment variables
|
386
158
|
"""
|
387
|
-
# Get ORGANIZATION_ID from keyring or environment variable
|
388
159
|
organization_id = _get_from_keyring_or_env(_KEYRING_ORG_ID_KEY, "ORGANIZATION_ID")
|
160
|
+
if not organization_id:
|
161
|
+
logger.error("ORGANIZATION_ID not found in keyring or environment variable")
|
162
|
+
return
|
389
163
|
|
390
|
-
# Initialize Bitwarden client
|
391
164
|
try:
|
392
165
|
client = _initialize_client()
|
166
|
+
from .in_env import load_secrets_env_all
|
167
|
+
secrets = load_secrets_env_all(client, organization_id)
|
168
|
+
set_env_vars(secrets, override)
|
169
|
+
del secrets
|
393
170
|
except Exception as e:
|
394
|
-
logger.error(f"Failed to
|
171
|
+
logger.error(f"Failed to load all secrets: {e}")
|
395
172
|
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
173
|
|
425
|
-
def get(project_id=None,
|
174
|
+
def get(project_id=None, use_keyring=True):
|
426
175
|
"""
|
427
|
-
Return a dictionary of all project secrets
|
176
|
+
Return a dictionary of all project secrets with JIT decryption
|
428
177
|
|
429
178
|
Args:
|
430
|
-
project_id (str, optional): Project ID to filter secrets
|
431
|
-
refresh (bool, optional): Force refresh the secrets cache
|
179
|
+
project_id (str, optional): Project ID to filter secrets. If None, will try to get from keyring or PROJECT_ID environment variable
|
432
180
|
use_keyring (bool, optional): Whether to use system keyring (True) or in-memory encryption (False)
|
433
181
|
|
434
182
|
Returns:
|
435
|
-
dict: Dictionary of secrets with their names as keys, using lazy loading
|
183
|
+
dict: Dictionary of secrets with their names as keys, using lazy loading with JIT decryption
|
436
184
|
"""
|
437
|
-
|
185
|
+
try:
|
186
|
+
client = _initialize_client()
|
187
|
+
except Exception as e:
|
188
|
+
logger.error(f"Failed to initialize Bitwarden client: {e}")
|
189
|
+
return {}
|
190
|
+
|
438
191
|
organization_id = _get_from_keyring_or_env(_KEYRING_ORG_ID_KEY, "ORGANIZATION_ID")
|
192
|
+
if not organization_id:
|
193
|
+
logger.error("ORGANIZATION_ID not found in keyring or environment variable")
|
194
|
+
return {}
|
439
195
|
|
440
|
-
#
|
441
|
-
|
196
|
+
# If project_id is not provided, try to get it from keyring or environment variable
|
197
|
+
if project_id is None:
|
198
|
+
project_id = _get_from_keyring_or_env(_KEYRING_PROJECT_ID_KEY, "PROJECT_ID")
|
442
199
|
|
443
|
-
|
444
|
-
|
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)
|
200
|
+
from .in_memory import load_secrets_memory
|
201
|
+
all_secrets = load_secrets_memory(client, organization_id, project_id)
|
469
202
|
|
470
|
-
# Get all
|
471
|
-
all_secrets = _load_decrypted_secrets()
|
203
|
+
# Get all secret keys - values will be decrypted JIT when accessed
|
472
204
|
secret_keys = set(all_secrets.keys())
|
473
205
|
|
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
|
-
|
206
|
+
return create_secrets_dict(
|
207
|
+
secret_keys,
|
208
|
+
organization_id,
|
209
|
+
project_id or "",
|
210
|
+
all_secrets,
|
211
|
+
use_keyring
|
212
|
+
)
|
534
213
|
|
535
|
-
def get_all(
|
214
|
+
def get_all(use_keyring=True):
|
536
215
|
"""
|
537
|
-
Return a combined dictionary of secrets from all projects that user has access to
|
216
|
+
Return a combined dictionary of secrets from all projects that user has access to with JIT decryption
|
538
217
|
|
539
218
|
Args:
|
540
|
-
refresh (bool, optional): Force refresh the secrets cache
|
541
219
|
use_keyring (bool, optional): Whether to use system keyring (True) or in-memory encryption (False)
|
542
220
|
|
543
221
|
Returns:
|
544
|
-
dict: Dictionary of secrets with their names as keys, using lazy loading
|
222
|
+
dict: Dictionary of secrets with their names as keys, using lazy loading with JIT decryption
|
545
223
|
"""
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
224
|
+
try:
|
225
|
+
client = _initialize_client()
|
226
|
+
except Exception as e:
|
227
|
+
logger.error(f"Failed to initialize Bitwarden client: {e}")
|
550
228
|
return {}
|
551
|
-
|
552
|
-
# Get organization ID
|
553
|
-
organization_id = _get_from_keyring_or_env("ORGANIZATION_ID", "ORGANIZATION_ID")
|
229
|
+
organization_id = _get_from_keyring_or_env(_KEYRING_ORG_ID_KEY, "ORGANIZATION_ID")
|
554
230
|
if not organization_id:
|
555
|
-
|
231
|
+
logger.error("Organization ID not found in keyring or environment variables")
|
556
232
|
return {}
|
557
233
|
|
558
234
|
try:
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
235
|
+
projects_response = client.projects().list(organization_id)
|
236
|
+
|
237
|
+
# Validate response format
|
238
|
+
if not hasattr(projects_response, 'data') or not hasattr(projects_response.data, 'data'):
|
239
|
+
logger.warning(f"No projects found in organization {organization_id}")
|
563
240
|
return {}
|
564
241
|
|
565
242
|
# Create a combined dictionary with all secrets
|
566
243
|
all_secrets = {}
|
567
|
-
|
568
|
-
|
569
|
-
|
244
|
+
project_ids = []
|
245
|
+
|
246
|
+
# First collect all project IDs
|
247
|
+
for project in projects_response.data.data:
|
248
|
+
if hasattr(project, 'id'):
|
249
|
+
project_ids.append(str(project.id))
|
250
|
+
|
251
|
+
# Create merged dictionary of all secrets with JIT decryption
|
252
|
+
for project_id in project_ids:
|
253
|
+
# Get secrets for this project
|
254
|
+
project_secrets = get(project_id, use_keyring=use_keyring)
|
255
|
+
# Update the combined dictionary (this will overwrite duplicate keys)
|
570
256
|
all_secrets.update(project_secrets)
|
571
257
|
|
572
258
|
return all_secrets
|
573
259
|
except Exception as e:
|
574
|
-
|
260
|
+
logger.error(f"Error retrieving projects: {str(e)}")
|
575
261
|
return {}
|