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/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
@@ -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
- def set_to_keyring(key, value):
51
+
52
+
53
+ def _secure_state_file(state_path: str) -> None:
219
54
  """
220
- Set a value to keyring
55
+ Ensure the state file has secure permissions
221
56
 
222
57
  Args:
223
- key (str): Key in keyring
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
- keyring.set_password(_KEYRING_SERVICE_NAME, key, value)
234
- return True
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"Failed to set {key} to keyring: {e}")
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 _load_secrets(project_id=None):
124
+ def env_load(project_id=None, override=False):
289
125
  """
290
- Load secrets from Bitwarden
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
- 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)
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
- # Set environment variables
376
- for key, value in secrets.items():
377
- if override or key not in os.environ:
378
- os.environ[key] = value
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 initialize Bitwarden client: {e}")
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, refresh=False, use_keyring=True):
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
- # Get ORGANIZATION_ID from keyring or environment variable
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
- # Build the service name for keyring storage
441
- service_name = f"vault_{organization_id or 'default'}"
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
- # 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)
200
+ from .in_memory import load_secrets_memory
201
+ all_secrets = load_secrets_memory(client, organization_id, project_id)
469
202
 
470
- # Get all secrets and their keys
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
- # 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
-
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(refresh=False, use_keyring=True):
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
- # Initialize the client
547
- client = _initialize_client()
548
- if not client:
549
- logging.error("Failed to initialize Bitwarden client")
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
- logging.error("Organization ID not found in keyring or environment variables")
231
+ logger.error("Organization ID not found in keyring or environment variables")
556
232
  return {}
557
233
 
558
234
  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")
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
- 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)
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
- logging.error(f"Error retrieving projects: {str(e)}")
260
+ logger.error(f"Error retrieving projects: {str(e)}")
575
261
  return {}