infisicalsdk 1.0.6__py3-none-any.whl → 1.0.8__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.

Potentially problematic release.


This version of infisicalsdk might be problematic. Click here for more details.

infisical_sdk/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from .client import InfisicalSDKClient # noqa
2
2
  from .infisical_requests import InfisicalError # noqa
3
- from .api_types import SingleSecretResponse, ListSecretsResponse, BaseSecret # noqa
3
+ from .api_types import SingleSecretResponse, ListSecretsResponse, BaseSecret, SymmetricEncryption # noqa
infisical_sdk/client.py CHANGED
@@ -1,34 +1,29 @@
1
- import base64
2
- import json
3
- from typing import List, Union
4
- import os
5
- import datetime
6
- from typing import Dict, Any
7
-
8
- import requests
9
- import boto3
10
- from botocore.auth import SigV4Auth
11
- from botocore.awsrequest import AWSRequest
12
- from botocore.exceptions import NoCredentialsError
13
-
14
1
  from .infisical_requests import InfisicalRequests
15
2
 
16
- from .api_types import ListSecretsResponse, SingleSecretResponse, BaseSecret
17
- from .api_types import SymmetricEncryption, KmsKeysOrderBy, OrderDirection
18
- from .api_types import ListKmsKeysResponse, SingleKmsKeyResponse, MachineIdentityLoginResponse
19
- from .api_types import KmsKey, KmsKeyEncryptDataResponse, KmsKeyDecryptDataResponse
3
+ from infisical_sdk.resources import Auth
4
+ from infisical_sdk.resources import V3RawSecrets
5
+ from infisical_sdk.resources import KMS
20
6
 
7
+ from infisical_sdk.util import SecretsCache
21
8
 
22
9
  class InfisicalSDKClient:
23
- def __init__(self, host: str, token: str = None):
10
+ def __init__(self, host: str, token: str = None, cache_ttl: int = 60):
11
+ """
12
+ Initialize the Infisical SDK client.
13
+
14
+ :param str host: The host URL for your Infisical instance. Will default to `https://app.infisical.com` if not specified.
15
+ :param str token: The authentication token for the client. If not specified, you can use the `auth` methods to authenticate.
16
+ :param int cache_ttl: The time to live for the secrets cache. This is the number of seconds that secrets fetched from the API will be cached for. Set to `None` to disable caching. Defaults to `60` seconds.
17
+ """
18
+
24
19
  self.host = host
25
20
  self.access_token = token
26
21
 
27
22
  self.api = InfisicalRequests(host=host, token=token)
28
-
29
- self.auth = Auth(self)
30
- self.secrets = V3RawSecrets(self)
31
- self.kms = KMS(self)
23
+ self.cache = SecretsCache(cache_ttl)
24
+ self.auth = Auth(self.api, self.set_token)
25
+ self.secrets = V3RawSecrets(self.api, self.cache)
26
+ self.kms = KMS(self.api)
32
27
 
33
28
  def set_token(self, token: str):
34
29
  """
@@ -43,483 +38,3 @@ class InfisicalSDKClient:
43
38
  """
44
39
  return self.access_token
45
40
 
46
-
47
- class UniversalAuth:
48
- def __init__(self, client: InfisicalSDKClient):
49
- self.client = client
50
-
51
- def login(self, client_id: str, client_secret: str) -> MachineIdentityLoginResponse:
52
- """
53
- Login with Universal Auth.
54
-
55
- Args:
56
- client_id (str): Your Machine Identity Client ID.
57
- client_secret (str): Your Machine Identity Client Secret.
58
-
59
- Returns:
60
- Dict: A dictionary containing the access token and related information.
61
- """
62
-
63
- requestBody = {
64
- "clientId": client_id,
65
- "clientSecret": client_secret
66
- }
67
-
68
- result = self.client.api.post(
69
- path="/api/v1/auth/universal-auth/login",
70
- json=requestBody,
71
- model=MachineIdentityLoginResponse
72
- )
73
-
74
- self.client.set_token(result.data.accessToken)
75
-
76
- return result.data
77
-
78
-
79
- class AWSAuth:
80
- def __init__(self, client: InfisicalSDKClient) -> None:
81
- self.client = client
82
-
83
- def login(self, identity_id: str) -> MachineIdentityLoginResponse:
84
- """
85
- Login with AWS Authentication.
86
-
87
- Args:
88
- identity_id (str): Your Machine Identity ID that has AWS Auth configured.
89
-
90
- Returns:
91
- Dict: A dictionary containing the access token and related information.
92
- """
93
-
94
- identity_id = identity_id or os.getenv("INFISICAL_AWS_IAM_AUTH_IDENTITY_ID")
95
- if not identity_id:
96
- raise ValueError(
97
- "Identity ID must be provided or set in the environment variable" +
98
- "INFISICAL_AWS_IAM_AUTH_IDENTITY_ID."
99
- )
100
-
101
- aws_region = self.get_aws_region()
102
- session = boto3.Session(region_name=aws_region)
103
-
104
- credentials = self._get_aws_credentials(session)
105
-
106
- iam_request_url = f"https://sts.{aws_region}.amazonaws.com/"
107
- iam_request_body = "Action=GetCallerIdentity&Version=2011-06-15"
108
-
109
- request_headers = self._prepare_aws_request(
110
- iam_request_url,
111
- iam_request_body,
112
- credentials,
113
- aws_region
114
- )
115
-
116
- requestBody = {
117
- "identityId": identity_id,
118
- "iamRequestBody": base64.b64encode(iam_request_body.encode()).decode(),
119
- "iamRequestHeaders": base64.b64encode(json.dumps(request_headers).encode()).decode(),
120
- "iamHttpRequestMethod": "POST"
121
- }
122
-
123
- result = self.client.api.post(
124
- path="/api/v1/auth/aws-auth/login",
125
- json=requestBody,
126
- model=MachineIdentityLoginResponse
127
- )
128
-
129
- self.client.set_token(result.data.accessToken)
130
-
131
- return result.data
132
-
133
- def _get_aws_credentials(self, session: boto3.Session) -> Any:
134
- try:
135
- credentials = session.get_credentials()
136
- if credentials is None:
137
- raise NoCredentialsError("AWS credentials not found.")
138
- return credentials.get_frozen_credentials()
139
- except NoCredentialsError as e:
140
- raise RuntimeError(f"AWS IAM Auth Login failed: {str(e)}")
141
-
142
- def _prepare_aws_request(
143
- self,
144
- url: str,
145
- body: str,
146
- credentials: Any,
147
- region: str) -> Dict[str, str]:
148
-
149
- current_time = datetime.datetime.now(datetime.timezone.utc)
150
- amz_date = current_time.strftime('%Y%m%dT%H%M%SZ')
151
-
152
- request = AWSRequest(method="POST", url=url, data=body)
153
- request.headers["X-Amz-Date"] = amz_date
154
- request.headers["Host"] = f"sts.{region}.amazonaws.com"
155
- request.headers["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8"
156
- request.headers["Content-Length"] = str(len(body))
157
-
158
- signer = SigV4Auth(credentials, "sts", region)
159
- signer.add_auth(request)
160
-
161
- return {k: v for k, v in request.headers.items() if k.lower() != "content-length"}
162
-
163
- @staticmethod
164
- def get_aws_region() -> str:
165
- region = os.getenv("AWS_REGION") # Typically found in lambda runtime environment
166
- if region:
167
- return region
168
-
169
- try:
170
- return AWSAuth._get_aws_ec2_identity_document_region()
171
- except Exception as e:
172
- raise Exception("Failed to retrieve AWS region") from e
173
-
174
- @staticmethod
175
- def _get_aws_ec2_identity_document_region(timeout: int = 5000) -> str:
176
- session = requests.Session()
177
- token_response = session.put(
178
- "http://169.254.169.254/latest/api/token",
179
- headers={"X-aws-ec2-metadata-token-ttl-seconds": "21600"},
180
- timeout=timeout / 1000
181
- )
182
- token_response.raise_for_status()
183
- metadata_token = token_response.text
184
-
185
- identity_response = session.get(
186
- "http://169.254.169.254/latest/dynamic/instance-identity/document",
187
- headers={"X-aws-ec2-metadata-token": metadata_token, "Accept": "application/json"},
188
- timeout=timeout / 1000
189
- )
190
-
191
- identity_response.raise_for_status()
192
- return identity_response.json().get("region")
193
-
194
-
195
- class Auth:
196
- def __init__(self, client):
197
- self.client = client
198
- self.aws_auth = AWSAuth(client)
199
- self.universal_auth = UniversalAuth(client)
200
-
201
-
202
- class V3RawSecrets:
203
- def __init__(self, client: InfisicalSDKClient) -> None:
204
- self.client = client
205
-
206
- def list_secrets(
207
- self,
208
- project_id: str,
209
- environment_slug: str,
210
- secret_path: str,
211
- expand_secret_references: bool = True,
212
- view_secret_value: bool = True,
213
- recursive: bool = False,
214
- include_imports: bool = True,
215
- tag_filters: List[str] = []) -> ListSecretsResponse:
216
-
217
- params = {
218
- "workspaceId": project_id,
219
- "environment": environment_slug,
220
- "secretPath": secret_path,
221
- "viewSecretValue": str(view_secret_value).lower(),
222
- "expandSecretReferences": str(expand_secret_references).lower(),
223
- "recursive": str(recursive).lower(),
224
- "include_imports": str(include_imports).lower(),
225
- }
226
-
227
- if tag_filters:
228
- params["tagSlugs"] = ",".join(tag_filters)
229
-
230
- result = self.client.api.get(
231
- path="/api/v3/secrets/raw",
232
- params=params,
233
- model=ListSecretsResponse
234
- )
235
-
236
- return result.data
237
-
238
- def get_secret_by_name(
239
- self,
240
- secret_name: str,
241
- project_id: str,
242
- environment_slug: str,
243
- secret_path: str,
244
- expand_secret_references: bool = True,
245
- include_imports: bool = True,
246
- view_secret_value: bool = True,
247
- version: str = None) -> BaseSecret:
248
-
249
- params = {
250
- "workspaceId": project_id,
251
- "viewSecretValue": str(view_secret_value).lower(),
252
- "environment": environment_slug,
253
- "secretPath": secret_path,
254
- "expandSecretReferences": str(expand_secret_references).lower(),
255
- "include_imports": str(include_imports).lower(),
256
- "version": version
257
- }
258
-
259
- result = self.client.api.get(
260
- path=f"/api/v3/secrets/raw/{secret_name}",
261
- params=params,
262
- model=SingleSecretResponse
263
- )
264
-
265
- return result.data.secret
266
-
267
- def create_secret_by_name(
268
- self,
269
- secret_name: str,
270
- project_id: str,
271
- secret_path: str,
272
- environment_slug: str,
273
- secret_value: str = None,
274
- secret_comment: str = None,
275
- skip_multiline_encoding: bool = False,
276
- secret_reminder_repeat_days: Union[float, int] = None,
277
- secret_reminder_note: str = None) -> BaseSecret:
278
-
279
- requestBody = {
280
- "workspaceId": project_id,
281
- "environment": environment_slug,
282
- "secretPath": secret_path,
283
- "secretValue": secret_value,
284
- "secretComment": secret_comment,
285
- "tagIds": None,
286
- "skipMultilineEncoding": skip_multiline_encoding,
287
- "type": "shared",
288
- "secretReminderRepeatDays": secret_reminder_repeat_days,
289
- "secretReminderNote": secret_reminder_note
290
- }
291
- result = self.client.api.post(
292
- path=f"/api/v3/secrets/raw/{secret_name}",
293
- json=requestBody,
294
- model=SingleSecretResponse
295
- )
296
-
297
- return result.data.secret
298
-
299
- def update_secret_by_name(
300
- self,
301
- current_secret_name: str,
302
- project_id: str,
303
- secret_path: str,
304
- environment_slug: str,
305
- secret_value: str = None,
306
- secret_comment: str = None,
307
- skip_multiline_encoding: bool = False,
308
- secret_reminder_repeat_days: Union[float, int] = None,
309
- secret_reminder_note: str = None,
310
- new_secret_name: str = None) -> BaseSecret:
311
-
312
- requestBody = {
313
- "workspaceId": project_id,
314
- "environment": environment_slug,
315
- "secretPath": secret_path,
316
- "secretValue": secret_value,
317
- "secretComment": secret_comment,
318
- "newSecretName": new_secret_name,
319
- "tagIds": None,
320
- "skipMultilineEncoding": skip_multiline_encoding,
321
- "type": "shared",
322
- "secretReminderRepeatDays": secret_reminder_repeat_days,
323
- "secretReminderNote": secret_reminder_note
324
- }
325
-
326
- result = self.client.api.patch(
327
- path=f"/api/v3/secrets/raw/{current_secret_name}",
328
- json=requestBody,
329
- model=SingleSecretResponse
330
- )
331
- return result.data.secret
332
-
333
- def delete_secret_by_name(
334
- self,
335
- secret_name: str,
336
- project_id: str,
337
- secret_path: str,
338
- environment_slug: str) -> BaseSecret:
339
-
340
- requestBody = {
341
- "workspaceId": project_id,
342
- "environment": environment_slug,
343
- "secretPath": secret_path,
344
- "type": "shared",
345
- }
346
-
347
- result = self.client.api.delete(
348
- path=f"/api/v3/secrets/raw/{secret_name}",
349
- json=requestBody,
350
- model=SingleSecretResponse
351
- )
352
-
353
- return result.data.secret
354
-
355
-
356
- class KMS:
357
- def __init__(self, client: InfisicalSDKClient) -> None:
358
- self.client = client
359
-
360
- def list_keys(
361
- self,
362
- project_id: str,
363
- offset: int = 0,
364
- limit: int = 100,
365
- order_by: KmsKeysOrderBy = KmsKeysOrderBy.NAME,
366
- order_direction: OrderDirection = OrderDirection.ASC,
367
- search: str = None) -> ListKmsKeysResponse:
368
-
369
- params = {
370
- "projectId": project_id,
371
- "search": search,
372
- "offset": offset,
373
- "limit": limit,
374
- "orderBy": order_by,
375
- "orderDirection": order_direction,
376
- }
377
-
378
- result = self.client.api.get(
379
- path="/api/v1/kms/keys",
380
- params=params,
381
- model=ListKmsKeysResponse
382
- )
383
-
384
- return result.data
385
-
386
- def get_key_by_id(
387
- self,
388
- key_id: str) -> KmsKey:
389
-
390
- result = self.client.api.get(
391
- path=f"/api/v1/kms/keys/{key_id}",
392
- model=SingleKmsKeyResponse
393
- )
394
-
395
- return result.data.key
396
-
397
- def get_key_by_name(
398
- self,
399
- key_name: str,
400
- project_id: str) -> KmsKey:
401
-
402
- params = {
403
- "projectId": project_id,
404
- }
405
-
406
- result = self.client.api.get(
407
- path=f"/api/v1/kms/keys/key-name/{key_name}",
408
- params=params,
409
- model=SingleKmsKeyResponse
410
- )
411
-
412
- return result.data.key
413
-
414
- def create_key(
415
- self,
416
- name: str,
417
- project_id: str,
418
- encryption_algorithm: SymmetricEncryption,
419
- description: str = None) -> KmsKey:
420
-
421
- request_body = {
422
- "name": name,
423
- "projectId": project_id,
424
- "encryptionAlgorithm": encryption_algorithm,
425
- "description": description,
426
- }
427
-
428
- result = self.client.api.post(
429
- path="/api/v1/kms/keys",
430
- json=request_body,
431
- model=SingleKmsKeyResponse
432
- )
433
-
434
- return result.data.key
435
-
436
- def update_key(
437
- self,
438
- key_id: str,
439
- name: str = None,
440
- is_disabled: bool = None,
441
- description: str = None) -> KmsKey:
442
-
443
- request_body = {
444
- "name": name,
445
- "isDisabled": is_disabled,
446
- "description": description,
447
- }
448
-
449
- result = self.client.api.patch(
450
- path=f"/api/v1/kms/keys/{key_id}",
451
- json=request_body,
452
- model=SingleKmsKeyResponse
453
- )
454
-
455
- return result.data.key
456
-
457
- def delete_key(
458
- self,
459
- key_id: str) -> KmsKey:
460
-
461
- result = self.client.api.delete(
462
- path=f"/api/v1/kms/keys/{key_id}",
463
- json={},
464
- model=SingleKmsKeyResponse
465
- )
466
-
467
- return result.data.key
468
-
469
- def encrypt_data(
470
- self,
471
- key_id: str,
472
- base64EncodedPlaintext: str) -> str:
473
- """
474
- Encrypt data with the specified KMS key.
475
-
476
- :param key_id: The ID of the key to decrypt the ciphertext with
477
- :type key_id: str
478
- :param base64EncodedPlaintext: The base64 encoded plaintext to encrypt
479
- :type plaintext: str
480
-
481
-
482
- :return: The encrypted base64 encoded plaintext (ciphertext)
483
- :rtype: str
484
- """
485
-
486
- request_body = {
487
- "plaintext": base64EncodedPlaintext
488
- }
489
-
490
- result = self.client.api.post(
491
- path=f"/api/v1/kms/keys/{key_id}/encrypt",
492
- json=request_body,
493
- model=KmsKeyEncryptDataResponse
494
- )
495
-
496
- return result.data.ciphertext
497
-
498
- def decrypt_data(
499
- self,
500
- key_id: str,
501
- ciphertext: str) -> str:
502
- """
503
- Decrypt data with the specified KMS key.
504
-
505
- :param key_id: The ID of the key to decrypt the ciphertext with
506
- :type key_id: str
507
- :param ciphertext: The encrypted base64 plaintext to decrypt
508
- :type ciphertext: str
509
-
510
-
511
- :return: The base64 encoded plaintext
512
- :rtype: str
513
- """
514
-
515
- request_body = {
516
- "ciphertext": ciphertext
517
- }
518
-
519
- result = self.client.api.post(
520
- path=f"/api/v1/kms/keys/{key_id}/decrypt",
521
- json=request_body,
522
- model=KmsKeyDecryptDataResponse
523
- )
524
-
525
- return result.data.plaintext
@@ -1,9 +1,27 @@
1
- from typing import Any, Dict, Generic, Optional, TypeVar, Type
1
+ from typing import Any, Dict, Generic, Optional, TypeVar, Type, Callable, List
2
+ import socket
2
3
  import requests
4
+ import functools
3
5
  from dataclasses import dataclass
6
+ import time
7
+ import random
4
8
 
5
9
  T = TypeVar("T")
6
10
 
11
+ # List of network-related exceptions that should trigger retries
12
+ NETWORK_ERRORS = [
13
+ requests.exceptions.ConnectionError,
14
+ requests.exceptions.ChunkedEncodingError,
15
+ requests.exceptions.ReadTimeout,
16
+ requests.exceptions.ConnectTimeout,
17
+ socket.gaierror,
18
+ socket.timeout,
19
+ ConnectionResetError,
20
+ ConnectionRefusedError,
21
+ ConnectionError,
22
+ ConnectionAbortedError,
23
+ ]
24
+
7
25
  def join_url(base: str, path: str) -> str:
8
26
  """
9
27
  Join base URL and path properly, handling slashes appropriately.
@@ -49,6 +67,42 @@ class APIResponse(Generic[T]):
49
67
  headers=data['headers']
50
68
  )
51
69
 
70
+ def with_retry(
71
+ max_retries: int = 3,
72
+ base_delay: float = 1.0,
73
+ network_errors: Optional[List[Type[Exception]]] = None
74
+ ) -> Callable:
75
+ """
76
+ Decorator to add retry logic with exponential backoff to requests methods.
77
+ """
78
+ if network_errors is None:
79
+ network_errors = NETWORK_ERRORS
80
+
81
+ def decorator(func: Callable) -> Callable:
82
+ @functools.wraps(func)
83
+ def wrapper(*args, **kwargs):
84
+ retry_count = 0
85
+
86
+ while True:
87
+ try:
88
+ return func(*args, **kwargs)
89
+ except tuple(network_errors) as error:
90
+ retry_count += 1
91
+ if retry_count > max_retries:
92
+ raise
93
+
94
+ base_delay_with_backoff = base_delay * (2 ** (retry_count - 1))
95
+
96
+ # +/-20% jitter
97
+ jitter = random.uniform(-0.2, 0.2) * base_delay_with_backoff
98
+ delay = base_delay_with_backoff + jitter
99
+
100
+ time.sleep(delay)
101
+
102
+ return wrapper
103
+
104
+ return decorator
105
+
52
106
 
53
107
  class InfisicalRequests:
54
108
  def __init__(self, host: str, token: Optional[str] = None):
@@ -93,6 +147,7 @@ class InfisicalRequests:
93
147
  except ValueError:
94
148
  raise InfisicalError("Invalid JSON response")
95
149
 
150
+ @with_retry(max_retries=4, base_delay=1.0)
96
151
  def get(
97
152
  self,
98
153
  path: str,
@@ -119,6 +174,7 @@ class InfisicalRequests:
119
174
  headers=dict(response.headers)
120
175
  )
121
176
 
177
+ @with_retry(max_retries=4, base_delay=1.0)
122
178
  def post(
123
179
  self,
124
180
  path: str,
@@ -143,6 +199,7 @@ class InfisicalRequests:
143
199
  headers=dict(response.headers)
144
200
  )
145
201
 
202
+ @with_retry(max_retries=4, base_delay=1.0)
146
203
  def patch(
147
204
  self,
148
205
  path: str,
@@ -167,6 +224,7 @@ class InfisicalRequests:
167
224
  headers=dict(response.headers)
168
225
  )
169
226
 
227
+ @with_retry(max_retries=4, base_delay=1.0)
170
228
  def delete(
171
229
  self,
172
230
  path: str,
@@ -0,0 +1,3 @@
1
+ from .secrets import V3RawSecrets
2
+ from .kms import KMS
3
+ from .auth import Auth
@@ -0,0 +1,10 @@
1
+ from infisical_sdk.infisical_requests import InfisicalRequests
2
+ from infisical_sdk.resources.auth_methods import AWSAuth
3
+ from infisical_sdk.resources.auth_methods import UniversalAuth
4
+
5
+ from typing import Callable
6
+ class Auth:
7
+ def __init__(self, requests: InfisicalRequests, setToken: Callable[[str], None]):
8
+ self.requests = requests
9
+ self.aws_auth = AWSAuth(requests, setToken)
10
+ self.universal_auth = UniversalAuth(requests, setToken)
@@ -0,0 +1,2 @@
1
+ from .aws_auth import AWSAuth
2
+ from .universal_auth import UniversalAuth