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 +1 -1
- infisical_sdk/client.py +17 -502
- infisical_sdk/infisical_requests.py +59 -1
- infisical_sdk/resources/__init__.py +3 -0
- infisical_sdk/resources/auth.py +10 -0
- infisical_sdk/resources/auth_methods/__init__.py +2 -0
- infisical_sdk/resources/auth_methods/aws_auth.py +134 -0
- infisical_sdk/resources/auth_methods/universal_auth.py +35 -0
- infisical_sdk/resources/kms.py +177 -0
- infisical_sdk/resources/secrets.py +235 -0
- infisical_sdk/util/__init__.py +1 -0
- infisical_sdk/util/secrets_cache.py +106 -0
- {infisicalsdk-1.0.6.dist-info → infisicalsdk-1.0.8.dist-info}/METADATA +2 -2
- infisicalsdk-1.0.8.dist-info/RECORD +17 -0
- {infisicalsdk-1.0.6.dist-info → infisicalsdk-1.0.8.dist-info}/WHEEL +1 -1
- infisicalsdk-1.0.6.dist-info/RECORD +0 -8
- {infisicalsdk-1.0.6.dist-info → infisicalsdk-1.0.8.dist-info}/top_level.txt +0 -0
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 .
|
|
17
|
-
from .
|
|
18
|
-
from .
|
|
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,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)
|