infisicalsdk 1.0.6__tar.gz → 1.0.8__tar.gz

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.

Files changed (25) hide show
  1. {infisicalsdk-1.0.6 → infisicalsdk-1.0.8}/PKG-INFO +2 -2
  2. {infisicalsdk-1.0.6 → infisicalsdk-1.0.8}/README.md +17 -0
  3. {infisicalsdk-1.0.6 → infisicalsdk-1.0.8}/infisical_sdk/__init__.py +1 -1
  4. infisicalsdk-1.0.8/infisical_sdk/client.py +40 -0
  5. {infisicalsdk-1.0.6 → infisicalsdk-1.0.8}/infisical_sdk/infisical_requests.py +59 -1
  6. infisicalsdk-1.0.8/infisical_sdk/resources/__init__.py +3 -0
  7. infisicalsdk-1.0.8/infisical_sdk/resources/auth.py +10 -0
  8. infisicalsdk-1.0.8/infisical_sdk/resources/auth_methods/__init__.py +2 -0
  9. infisicalsdk-1.0.8/infisical_sdk/resources/auth_methods/aws_auth.py +134 -0
  10. infisicalsdk-1.0.8/infisical_sdk/resources/auth_methods/universal_auth.py +35 -0
  11. infisicalsdk-1.0.8/infisical_sdk/resources/kms.py +177 -0
  12. infisicalsdk-1.0.8/infisical_sdk/resources/secrets.py +235 -0
  13. infisicalsdk-1.0.8/infisical_sdk/util/__init__.py +1 -0
  14. infisicalsdk-1.0.8/infisical_sdk/util/secrets_cache.py +106 -0
  15. {infisicalsdk-1.0.6 → infisicalsdk-1.0.8}/infisicalsdk.egg-info/PKG-INFO +2 -2
  16. infisicalsdk-1.0.8/infisicalsdk.egg-info/SOURCES.txt +22 -0
  17. {infisicalsdk-1.0.6 → infisicalsdk-1.0.8}/setup.cfg +1 -1
  18. {infisicalsdk-1.0.6 → infisicalsdk-1.0.8}/setup.py +1 -1
  19. infisicalsdk-1.0.6/infisical_sdk/client.py +0 -525
  20. infisicalsdk-1.0.6/infisicalsdk.egg-info/SOURCES.txt +0 -13
  21. {infisicalsdk-1.0.6 → infisicalsdk-1.0.8}/infisical_sdk/api_types.py +0 -0
  22. {infisicalsdk-1.0.6 → infisicalsdk-1.0.8}/infisicalsdk.egg-info/dependency_links.txt +0 -0
  23. {infisicalsdk-1.0.6 → infisicalsdk-1.0.8}/infisicalsdk.egg-info/requires.txt +0 -0
  24. {infisicalsdk-1.0.6 → infisicalsdk-1.0.8}/infisicalsdk.egg-info/top_level.txt +0 -0
  25. {infisicalsdk-1.0.6 → infisicalsdk-1.0.8}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: infisicalsdk
3
- Version: 1.0.6
3
+ Version: 1.0.8
4
4
  Summary: Infisical API Client
5
5
  Home-page: https://github.com/Infisical/python-sdk-official
6
6
  Author: Infisical
@@ -45,12 +45,29 @@ client.auth.universal_auth.login(
45
45
  secrets = client.secrets.list_secrets(project_id="<project-id>", environment_slug="dev", secret_path="/")
46
46
  ```
47
47
 
48
+ ## InfisicalSDKClient Parameters
49
+
50
+ The `InfisicalSDKClient` takes the following parameters, which are used as a global configuration for the lifetime of the SDK instance.
51
+
52
+ - **host** (`str`, _Optional_): The host URL for your Infisical instance. Defaults to `https://app.infisical.com`.
53
+ - **token** (`str`, _Optional_): Specify an authentication token to use for all requests. If provided, you will not need to call any of the `auth` methods. Defaults to `None`
54
+ - **cache_ttl** (`int`, _Optional_): The SDK has built-in client-side caching for secrets, greatly improving response times. By default, secrets are cached for 1 minute (60 seconds). You can disable caching by setting `cache_ttl` to `None`, or adjust the duration in seconds as needed.
55
+
56
+ ```python
57
+ client = InfisicalSDKClient(
58
+ host="https://app.infisical.com", # Defaults to https://app.infisical.com
59
+ token="<optional-auth-token>", # If not set, use the client.auth() methods.
60
+ cache_ttl = 300 # `None` to disable caching
61
+ )
62
+ ```
63
+
48
64
  ## Core Methods
49
65
 
50
66
  The SDK methods are organized into the following high-level categories:
51
67
 
52
68
  1. `auth`: Handles authentication methods.
53
69
  2. `secrets`: Manages CRUD operations for secrets.
70
+ 3. `kms`: Perform cryptographic operations with Infisical KMS.
54
71
 
55
72
  ### `auth`
56
73
 
@@ -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
@@ -0,0 +1,40 @@
1
+ from .infisical_requests import InfisicalRequests
2
+
3
+ from infisical_sdk.resources import Auth
4
+ from infisical_sdk.resources import V3RawSecrets
5
+ from infisical_sdk.resources import KMS
6
+
7
+ from infisical_sdk.util import SecretsCache
8
+
9
+ class InfisicalSDKClient:
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
+
19
+ self.host = host
20
+ self.access_token = token
21
+
22
+ self.api = InfisicalRequests(host=host, token=token)
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)
27
+
28
+ def set_token(self, token: str):
29
+ """
30
+ Set the access token for future requests.
31
+ """
32
+ self.api.set_token(token)
33
+ self.access_token = token
34
+
35
+ def get_token(self):
36
+ """
37
+ Set the access token for future requests.
38
+ """
39
+ return self.access_token
40
+
@@ -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
@@ -0,0 +1,134 @@
1
+ from botocore.auth import SigV4Auth
2
+ from botocore.awsrequest import AWSRequest
3
+ from botocore.exceptions import NoCredentialsError
4
+
5
+ from infisical_sdk.infisical_requests import InfisicalRequests
6
+ from infisical_sdk.api_types import MachineIdentityLoginResponse
7
+
8
+ from typing import Callable
9
+
10
+ import requests
11
+ import boto3
12
+ import base64
13
+ import json
14
+ import os
15
+ import datetime
16
+
17
+ from typing import Dict, Any
18
+
19
+
20
+ class AWSAuth:
21
+ def __init__(self, requests: InfisicalRequests, setToken: Callable[[str], None]) -> None:
22
+ self.requests = requests
23
+ self.setToken = setToken
24
+
25
+ def login(self, identity_id: str) -> MachineIdentityLoginResponse:
26
+ """
27
+ Login with AWS Authentication.
28
+
29
+ Args:
30
+ identity_id (str): Your Machine Identity ID that has AWS Auth configured.
31
+
32
+ Returns:
33
+ Dict: A dictionary containing the access token and related information.
34
+ """
35
+
36
+ identity_id = identity_id or os.getenv("INFISICAL_AWS_IAM_AUTH_IDENTITY_ID")
37
+ if not identity_id:
38
+ raise ValueError(
39
+ "Identity ID must be provided or set in the environment variable" +
40
+ "INFISICAL_AWS_IAM_AUTH_IDENTITY_ID."
41
+ )
42
+
43
+ aws_region = self.get_aws_region()
44
+ session = boto3.Session(region_name=aws_region)
45
+
46
+ credentials = self._get_aws_credentials(session)
47
+
48
+ iam_request_url = f"https://sts.{aws_region}.amazonaws.com/"
49
+ iam_request_body = "Action=GetCallerIdentity&Version=2011-06-15"
50
+
51
+ request_headers = self._prepare_aws_request(
52
+ iam_request_url,
53
+ iam_request_body,
54
+ credentials,
55
+ aws_region
56
+ )
57
+
58
+ requestBody = {
59
+ "identityId": identity_id,
60
+ "iamRequestBody": base64.b64encode(iam_request_body.encode()).decode(),
61
+ "iamRequestHeaders": base64.b64encode(json.dumps(request_headers).encode()).decode(),
62
+ "iamHttpRequestMethod": "POST"
63
+ }
64
+
65
+ result = self.requests.post(
66
+ path="/api/v1/auth/aws-auth/login",
67
+ json=requestBody,
68
+ model=MachineIdentityLoginResponse
69
+ )
70
+
71
+ self.setToken(result.data.accessToken)
72
+
73
+ return result.data
74
+
75
+ def _get_aws_credentials(self, session: boto3.Session) -> Any:
76
+ try:
77
+ credentials = session.get_credentials()
78
+ if credentials is None:
79
+ raise NoCredentialsError("AWS credentials not found.")
80
+ return credentials.get_frozen_credentials()
81
+ except NoCredentialsError as e:
82
+ raise RuntimeError(f"AWS IAM Auth Login failed: {str(e)}")
83
+
84
+ def _prepare_aws_request(
85
+ self,
86
+ url: str,
87
+ body: str,
88
+ credentials: Any,
89
+ region: str) -> Dict[str, str]:
90
+
91
+ current_time = datetime.datetime.now(datetime.timezone.utc)
92
+ amz_date = current_time.strftime('%Y%m%dT%H%M%SZ')
93
+
94
+ request = AWSRequest(method="POST", url=url, data=body)
95
+ request.headers["X-Amz-Date"] = amz_date
96
+ request.headers["Host"] = f"sts.{region}.amazonaws.com"
97
+ request.headers["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8"
98
+ request.headers["Content-Length"] = str(len(body))
99
+
100
+ signer = SigV4Auth(credentials, "sts", region)
101
+ signer.add_auth(request)
102
+
103
+ return {k: v for k, v in request.headers.items() if k.lower() != "content-length"}
104
+
105
+ @staticmethod
106
+ def get_aws_region() -> str:
107
+ region = os.getenv("AWS_REGION") # Typically found in lambda runtime environment
108
+ if region:
109
+ return region
110
+
111
+ try:
112
+ return AWSAuth._get_aws_ec2_identity_document_region()
113
+ except Exception as e:
114
+ raise Exception("Failed to retrieve AWS region") from e
115
+
116
+ @staticmethod
117
+ def _get_aws_ec2_identity_document_region(timeout: int = 5000) -> str:
118
+ session = requests.Session()
119
+ token_response = session.put(
120
+ "http://169.254.169.254/latest/api/token",
121
+ headers={"X-aws-ec2-metadata-token-ttl-seconds": "21600"},
122
+ timeout=timeout / 1000
123
+ )
124
+ token_response.raise_for_status()
125
+ metadata_token = token_response.text
126
+
127
+ identity_response = session.get(
128
+ "http://169.254.169.254/latest/dynamic/instance-identity/document",
129
+ headers={"X-aws-ec2-metadata-token": metadata_token, "Accept": "application/json"},
130
+ timeout=timeout / 1000
131
+ )
132
+
133
+ identity_response.raise_for_status()
134
+ return identity_response.json().get("region")
@@ -0,0 +1,35 @@
1
+ from infisical_sdk.api_types import MachineIdentityLoginResponse
2
+
3
+ from typing import Callable
4
+ from infisical_sdk.infisical_requests import InfisicalRequests
5
+ class UniversalAuth:
6
+ def __init__(self, requests: InfisicalRequests, setToken: Callable[[str], None]):
7
+ self.requests = requests
8
+ self.setToken = setToken
9
+
10
+ def login(self, client_id: str, client_secret: str) -> MachineIdentityLoginResponse:
11
+ """
12
+ Login with Universal Auth.
13
+
14
+ Args:
15
+ client_id (str): Your Machine Identity Client ID.
16
+ client_secret (str): Your Machine Identity Client Secret.
17
+
18
+ Returns:
19
+ Dict: A dictionary containing the access token and related information.
20
+ """
21
+
22
+ requestBody = {
23
+ "clientId": client_id,
24
+ "clientSecret": client_secret
25
+ }
26
+
27
+ result = self.requests.post(
28
+ path="/api/v1/auth/universal-auth/login",
29
+ json=requestBody,
30
+ model=MachineIdentityLoginResponse
31
+ )
32
+
33
+ self.setToken(result.data.accessToken)
34
+
35
+ return result.data
@@ -0,0 +1,177 @@
1
+ from infisical_sdk.api_types import SymmetricEncryption, KmsKeysOrderBy, OrderDirection
2
+ from infisical_sdk.api_types import ListKmsKeysResponse, SingleKmsKeyResponse
3
+ from infisical_sdk.api_types import KmsKey, KmsKeyEncryptDataResponse, KmsKeyDecryptDataResponse
4
+
5
+ from infisical_sdk.infisical_requests import InfisicalRequests
6
+
7
+
8
+ class KMS:
9
+ def __init__(self, requests: InfisicalRequests) -> None:
10
+ self.requests = requests
11
+
12
+ def list_keys(
13
+ self,
14
+ project_id: str,
15
+ offset: int = 0,
16
+ limit: int = 100,
17
+ order_by: KmsKeysOrderBy = KmsKeysOrderBy.NAME,
18
+ order_direction: OrderDirection = OrderDirection.ASC,
19
+ search: str = None) -> ListKmsKeysResponse:
20
+
21
+ params = {
22
+ "projectId": project_id,
23
+ "search": search,
24
+ "offset": offset,
25
+ "limit": limit,
26
+ "orderBy": order_by,
27
+ "orderDirection": order_direction,
28
+ }
29
+
30
+ result = self.requests.get(
31
+ path="/api/v1/kms/keys",
32
+ params=params,
33
+ model=ListKmsKeysResponse
34
+ )
35
+
36
+ return result.data
37
+
38
+ def get_key_by_id(
39
+ self,
40
+ key_id: str) -> KmsKey:
41
+
42
+ result = self.requests.get(
43
+ path=f"/api/v1/kms/keys/{key_id}",
44
+ model=SingleKmsKeyResponse
45
+ )
46
+
47
+ return result.data.key
48
+
49
+ def get_key_by_name(
50
+ self,
51
+ key_name: str,
52
+ project_id: str) -> KmsKey:
53
+
54
+ params = {
55
+ "projectId": project_id,
56
+ }
57
+
58
+ result = self.requests.get(
59
+ path=f"/api/v1/kms/keys/key-name/{key_name}",
60
+ params=params,
61
+ model=SingleKmsKeyResponse
62
+ )
63
+
64
+ return result.data.key
65
+
66
+ def create_key(
67
+ self,
68
+ name: str,
69
+ project_id: str,
70
+ encryption_algorithm: SymmetricEncryption,
71
+ description: str = None) -> KmsKey:
72
+
73
+ request_body = {
74
+ "name": name,
75
+ "projectId": project_id,
76
+ "encryptionAlgorithm": encryption_algorithm,
77
+ "description": description,
78
+ }
79
+
80
+ result = self.requests.post(
81
+ path="/api/v1/kms/keys",
82
+ json=request_body,
83
+ model=SingleKmsKeyResponse
84
+ )
85
+
86
+ return result.data.key
87
+
88
+ def update_key(
89
+ self,
90
+ key_id: str,
91
+ name: str = None,
92
+ is_disabled: bool = None,
93
+ description: str = None) -> KmsKey:
94
+
95
+ request_body = {
96
+ "name": name,
97
+ "isDisabled": is_disabled,
98
+ "description": description,
99
+ }
100
+
101
+ result = self.requests.patch(
102
+ path=f"/api/v1/kms/keys/{key_id}",
103
+ json=request_body,
104
+ model=SingleKmsKeyResponse
105
+ )
106
+
107
+ return result.data.key
108
+
109
+ def delete_key(
110
+ self,
111
+ key_id: str) -> KmsKey:
112
+
113
+ result = self.requests.delete(
114
+ path=f"/api/v1/kms/keys/{key_id}",
115
+ json={},
116
+ model=SingleKmsKeyResponse
117
+ )
118
+
119
+ return result.data.key
120
+
121
+ def encrypt_data(
122
+ self,
123
+ key_id: str,
124
+ base64EncodedPlaintext: str) -> str:
125
+ """
126
+ Encrypt data with the specified KMS key.
127
+
128
+ :param key_id: The ID of the key to decrypt the ciphertext with
129
+ :type key_id: str
130
+ :param base64EncodedPlaintext: The base64 encoded plaintext to encrypt
131
+ :type plaintext: str
132
+
133
+
134
+ :return: The encrypted base64 encoded plaintext (ciphertext)
135
+ :rtype: str
136
+ """
137
+
138
+ request_body = {
139
+ "plaintext": base64EncodedPlaintext
140
+ }
141
+
142
+ result = self.requests.post(
143
+ path=f"/api/v1/kms/keys/{key_id}/encrypt",
144
+ json=request_body,
145
+ model=KmsKeyEncryptDataResponse
146
+ )
147
+
148
+ return result.data.ciphertext
149
+
150
+ def decrypt_data(
151
+ self,
152
+ key_id: str,
153
+ ciphertext: str) -> str:
154
+ """
155
+ Decrypt data with the specified KMS key.
156
+
157
+ :param key_id: The ID of the key to decrypt the ciphertext with
158
+ :type key_id: str
159
+ :param ciphertext: The encrypted base64 plaintext to decrypt
160
+ :type ciphertext: str
161
+
162
+
163
+ :return: The base64 encoded plaintext
164
+ :rtype: str
165
+ """
166
+
167
+ request_body = {
168
+ "ciphertext": ciphertext
169
+ }
170
+
171
+ result = self.requests.post(
172
+ path=f"/api/v1/kms/keys/{key_id}/decrypt",
173
+ json=request_body,
174
+ model=KmsKeyDecryptDataResponse
175
+ )
176
+
177
+ return result.data.plaintext