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/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
- # Try importing keyring - it might not be available in container environments
19
- try:
20
- import keyring
21
- _KEYRING_AVAILABLE = True
22
- except ImportError:
23
- _KEYRING_AVAILABLE = False
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
- def set_to_keyring(key, value):
50
+
51
+
52
+ def _secure_state_file(state_path: str) -> None:
219
53
  """
220
- Set a value to keyring
54
+ Ensure the state file has secure permissions
221
55
 
222
56
  Args:
223
- key (str): Key in keyring
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
- keyring.set_password(_KEYRING_SERVICE_NAME, key, value)
234
- return True
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"Failed to set {key} to keyring: {e}")
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 _load_secrets(project_id=None):
123
+ def env_load(project_id=None, override=False):
289
124
  """
290
- Load secrets from Bitwarden
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
- # 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
141
+ secrets = load_secrets_env(client, organization_id, project_id)
363
142
 
364
- def env_load(project_id=None, override=False):
365
- """
366
- Load all secrets related to the project into environmental variables.
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 initialize Bitwarden client: {e}")
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, refresh=False, use_keyring=True):
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
- # Get ORGANIZATION_ID from keyring or environment variable
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
- # Build the service name for keyring storage
441
- service_name = f"vault_{organization_id or 'default'}"
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 secrets and their keys
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
- # Store secrets in keyring if available and requested
475
- keyring_usable = _KEYRING_AVAILABLE and use_keyring
476
- if keyring_usable:
477
- for key, value in all_secrets.items():
478
- keyring.set_password(service_name, key, value)
479
-
480
- # When keyring is unavailable or not requested (likely in container)
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(refresh=False, use_keyring=True):
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
- # Initialize the client
547
- client = _initialize_client()
548
- if not client:
549
- logging.error("Failed to initialize Bitwarden client")
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
- logging.error("Organization ID not found in keyring or environment variables")
222
+ logger.error("Organization ID not found in keyring or environment variables")
556
223
  return {}
557
224
 
558
225
  try:
559
- # Get all projects
560
- projects = client.projects_api.get_projects(organization_id)
561
- if not projects or not projects.data:
562
- logging.warning("No projects found in the organization")
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
- for project in projects.data:
568
- project_secrets = get(project.id, refresh=refresh, use_keyring=use_keyring)
569
- # Update the combined dictionary (note: this will overwrite duplicate keys)
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
- logging.error(f"Error retrieving projects: {str(e)}")
251
+ logger.error(f"Error retrieving projects: {str(e)}")
575
252
  return {}