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
|
@@ -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
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
from typing import List, Union
|
|
2
|
+
|
|
3
|
+
from infisical_sdk.infisical_requests import InfisicalRequests
|
|
4
|
+
from infisical_sdk.api_types import ListSecretsResponse, SingleSecretResponse, BaseSecret
|
|
5
|
+
from infisical_sdk.util import SecretsCache
|
|
6
|
+
|
|
7
|
+
CACHE_KEY_LIST_SECRETS = "cache-list-secrets"
|
|
8
|
+
CACHE_KEY_SINGLE_SECRET = "cache-single-secret"
|
|
9
|
+
|
|
10
|
+
class V3RawSecrets:
|
|
11
|
+
def __init__(self, requests: InfisicalRequests, cache: SecretsCache) -> None:
|
|
12
|
+
self.requests = requests
|
|
13
|
+
self.cache = cache
|
|
14
|
+
|
|
15
|
+
def list_secrets(
|
|
16
|
+
self,
|
|
17
|
+
project_id: str,
|
|
18
|
+
environment_slug: str,
|
|
19
|
+
secret_path: str,
|
|
20
|
+
expand_secret_references: bool = True,
|
|
21
|
+
view_secret_value: bool = True,
|
|
22
|
+
recursive: bool = False,
|
|
23
|
+
include_imports: bool = True,
|
|
24
|
+
tag_filters: List[str] = []) -> ListSecretsResponse:
|
|
25
|
+
|
|
26
|
+
params = {
|
|
27
|
+
"workspaceId": project_id,
|
|
28
|
+
"environment": environment_slug,
|
|
29
|
+
"secretPath": secret_path,
|
|
30
|
+
"viewSecretValue": str(view_secret_value).lower(),
|
|
31
|
+
"expandSecretReferences": str(expand_secret_references).lower(),
|
|
32
|
+
"recursive": str(recursive).lower(),
|
|
33
|
+
"include_imports": str(include_imports).lower(),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if tag_filters:
|
|
37
|
+
params["tagSlugs"] = ",".join(tag_filters)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
cache_key = self.cache.compute_cache_key(CACHE_KEY_LIST_SECRETS, **params)
|
|
41
|
+
if self.cache.enabled:
|
|
42
|
+
cached_response = self.cache.get(cache_key)
|
|
43
|
+
|
|
44
|
+
if cached_response is not None and isinstance(cached_response, ListSecretsResponse):
|
|
45
|
+
return cached_response
|
|
46
|
+
|
|
47
|
+
result = self.requests.get(
|
|
48
|
+
path="/api/v3/secrets/raw",
|
|
49
|
+
params=params,
|
|
50
|
+
model=ListSecretsResponse
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if self.cache.enabled:
|
|
54
|
+
self.cache.set(cache_key, result.data)
|
|
55
|
+
|
|
56
|
+
return result.data
|
|
57
|
+
|
|
58
|
+
def get_secret_by_name(
|
|
59
|
+
self,
|
|
60
|
+
secret_name: str,
|
|
61
|
+
project_id: str,
|
|
62
|
+
environment_slug: str,
|
|
63
|
+
secret_path: str,
|
|
64
|
+
expand_secret_references: bool = True,
|
|
65
|
+
include_imports: bool = True,
|
|
66
|
+
view_secret_value: bool = True,
|
|
67
|
+
version: str = None) -> BaseSecret:
|
|
68
|
+
|
|
69
|
+
params = {
|
|
70
|
+
"workspaceId": project_id,
|
|
71
|
+
"viewSecretValue": str(view_secret_value).lower(),
|
|
72
|
+
"environment": environment_slug,
|
|
73
|
+
"secretPath": secret_path,
|
|
74
|
+
"expandSecretReferences": str(expand_secret_references).lower(),
|
|
75
|
+
"include_imports": str(include_imports).lower(),
|
|
76
|
+
"version": version
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
cache_params = {
|
|
80
|
+
"project_id": project_id,
|
|
81
|
+
"environment_slug": environment_slug,
|
|
82
|
+
"secret_path": secret_path,
|
|
83
|
+
"secret_name": secret_name,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
cache_key = self.cache.compute_cache_key(CACHE_KEY_SINGLE_SECRET, **cache_params)
|
|
87
|
+
|
|
88
|
+
if self.cache.enabled:
|
|
89
|
+
cached_response = self.cache.get(cache_key)
|
|
90
|
+
|
|
91
|
+
if cached_response is not None and isinstance(cached_response, BaseSecret):
|
|
92
|
+
return cached_response
|
|
93
|
+
|
|
94
|
+
result = self.requests.get(
|
|
95
|
+
path=f"/api/v3/secrets/raw/{secret_name}",
|
|
96
|
+
params=params,
|
|
97
|
+
model=SingleSecretResponse
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if self.cache.enabled:
|
|
101
|
+
self.cache.set(cache_key, result.data.secret)
|
|
102
|
+
|
|
103
|
+
return result.data.secret
|
|
104
|
+
|
|
105
|
+
def create_secret_by_name(
|
|
106
|
+
self,
|
|
107
|
+
secret_name: str,
|
|
108
|
+
project_id: str,
|
|
109
|
+
secret_path: str,
|
|
110
|
+
environment_slug: str,
|
|
111
|
+
secret_value: str = None,
|
|
112
|
+
secret_comment: str = None,
|
|
113
|
+
skip_multiline_encoding: bool = False,
|
|
114
|
+
secret_reminder_repeat_days: Union[float, int] = None,
|
|
115
|
+
secret_reminder_note: str = None) -> BaseSecret:
|
|
116
|
+
|
|
117
|
+
requestBody = {
|
|
118
|
+
"workspaceId": project_id,
|
|
119
|
+
"environment": environment_slug,
|
|
120
|
+
"secretPath": secret_path,
|
|
121
|
+
"secretValue": secret_value,
|
|
122
|
+
"secretComment": secret_comment,
|
|
123
|
+
"tagIds": None,
|
|
124
|
+
"skipMultilineEncoding": skip_multiline_encoding,
|
|
125
|
+
"type": "shared",
|
|
126
|
+
"secretReminderRepeatDays": secret_reminder_repeat_days,
|
|
127
|
+
"secretReminderNote": secret_reminder_note
|
|
128
|
+
}
|
|
129
|
+
result = self.requests.post(
|
|
130
|
+
path=f"/api/v3/secrets/raw/{secret_name}",
|
|
131
|
+
json=requestBody,
|
|
132
|
+
model=SingleSecretResponse
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
if self.cache.enabled:
|
|
137
|
+
cache_params = {
|
|
138
|
+
"project_id": project_id,
|
|
139
|
+
"environment_slug": environment_slug,
|
|
140
|
+
"secret_path": secret_path,
|
|
141
|
+
"secret_name": secret_name,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
cache_key = self.cache.compute_cache_key(CACHE_KEY_SINGLE_SECRET, **cache_params)
|
|
145
|
+
self.cache.set(cache_key, result.data.secret)
|
|
146
|
+
|
|
147
|
+
# Invalidates all list secret cache
|
|
148
|
+
self.cache.invalidate_operation(CACHE_KEY_LIST_SECRETS)
|
|
149
|
+
|
|
150
|
+
return result.data.secret
|
|
151
|
+
|
|
152
|
+
def update_secret_by_name(
|
|
153
|
+
self,
|
|
154
|
+
current_secret_name: str,
|
|
155
|
+
project_id: str,
|
|
156
|
+
secret_path: str,
|
|
157
|
+
environment_slug: str,
|
|
158
|
+
secret_value: str = None,
|
|
159
|
+
secret_comment: str = None,
|
|
160
|
+
skip_multiline_encoding: bool = False,
|
|
161
|
+
secret_reminder_repeat_days: Union[float, int] = None,
|
|
162
|
+
secret_reminder_note: str = None,
|
|
163
|
+
new_secret_name: str = None) -> BaseSecret:
|
|
164
|
+
|
|
165
|
+
requestBody = {
|
|
166
|
+
"workspaceId": project_id,
|
|
167
|
+
"environment": environment_slug,
|
|
168
|
+
"secretPath": secret_path,
|
|
169
|
+
"secretValue": secret_value,
|
|
170
|
+
"secretComment": secret_comment,
|
|
171
|
+
"newSecretName": new_secret_name,
|
|
172
|
+
"tagIds": None,
|
|
173
|
+
"skipMultilineEncoding": skip_multiline_encoding,
|
|
174
|
+
"type": "shared",
|
|
175
|
+
"secretReminderRepeatDays": secret_reminder_repeat_days,
|
|
176
|
+
"secretReminderNote": secret_reminder_note
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
result = self.requests.patch(
|
|
180
|
+
path=f"/api/v3/secrets/raw/{current_secret_name}",
|
|
181
|
+
json=requestBody,
|
|
182
|
+
model=SingleSecretResponse
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if self.cache.enabled:
|
|
186
|
+
cache_params = {
|
|
187
|
+
"project_id": project_id,
|
|
188
|
+
"environment_slug": environment_slug,
|
|
189
|
+
"secret_path": secret_path,
|
|
190
|
+
"secret_name": current_secret_name,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
cache_key = self.cache.compute_cache_key(CACHE_KEY_SINGLE_SECRET, **cache_params)
|
|
194
|
+
self.cache.unset(cache_key)
|
|
195
|
+
|
|
196
|
+
# Invalidates all list secret cache
|
|
197
|
+
self.cache.invalidate_operation(CACHE_KEY_LIST_SECRETS)
|
|
198
|
+
|
|
199
|
+
return result.data.secret
|
|
200
|
+
|
|
201
|
+
def delete_secret_by_name(
|
|
202
|
+
self,
|
|
203
|
+
secret_name: str,
|
|
204
|
+
project_id: str,
|
|
205
|
+
secret_path: str,
|
|
206
|
+
environment_slug: str) -> BaseSecret:
|
|
207
|
+
|
|
208
|
+
requestBody = {
|
|
209
|
+
"workspaceId": project_id,
|
|
210
|
+
"environment": environment_slug,
|
|
211
|
+
"secretPath": secret_path,
|
|
212
|
+
"type": "shared",
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
result = self.requests.delete(
|
|
216
|
+
path=f"/api/v3/secrets/raw/{secret_name}",
|
|
217
|
+
json=requestBody,
|
|
218
|
+
model=SingleSecretResponse
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if self.cache.enabled:
|
|
222
|
+
cache_params = {
|
|
223
|
+
"project_id": project_id,
|
|
224
|
+
"environment_slug": environment_slug,
|
|
225
|
+
"secret_path": secret_path,
|
|
226
|
+
"secret_name": secret_name,
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
cache_key = self.cache.compute_cache_key(CACHE_KEY_SINGLE_SECRET, **cache_params)
|
|
230
|
+
self.cache.unset(cache_key)
|
|
231
|
+
|
|
232
|
+
# Invalidates all list secret cache
|
|
233
|
+
self.cache.invalidate_operation(CACHE_KEY_LIST_SECRETS)
|
|
234
|
+
|
|
235
|
+
return result.data.secret
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .secrets_cache import SecretsCache
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from typing import Dict, Tuple, Any
|
|
2
|
+
|
|
3
|
+
from infisical_sdk.api_types import BaseSecret
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
import threading
|
|
7
|
+
from hashlib import sha256
|
|
8
|
+
import pickle
|
|
9
|
+
|
|
10
|
+
MAX_CACHE_SIZE = 1000
|
|
11
|
+
|
|
12
|
+
class SecretsCache:
|
|
13
|
+
def __init__(self, ttl_seconds: int = 60) -> None:
|
|
14
|
+
if ttl_seconds is None or ttl_seconds <= 0:
|
|
15
|
+
self.enabled = False
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
self.enabled = True
|
|
19
|
+
self.ttl = ttl_seconds
|
|
20
|
+
self.cleanup_interval = 60
|
|
21
|
+
|
|
22
|
+
self.cache: Dict[str, Tuple[bytes, float]] = {}
|
|
23
|
+
|
|
24
|
+
self.lock = threading.RLock()
|
|
25
|
+
|
|
26
|
+
self.stop_cleanup_thread = False
|
|
27
|
+
self.cleanup_thread = threading.Thread(target=self._cleanup_worker, daemon=True)
|
|
28
|
+
self.cleanup_thread.start()
|
|
29
|
+
|
|
30
|
+
def compute_cache_key(self, operation_name: str, **kwargs) -> str:
|
|
31
|
+
sorted_kwargs = sorted(kwargs.items())
|
|
32
|
+
json_str = json.dumps(sorted_kwargs)
|
|
33
|
+
|
|
34
|
+
return f"{operation_name}-{sha256(json_str.encode()).hexdigest()}"
|
|
35
|
+
|
|
36
|
+
def get(self, cache_key: str) -> Any:
|
|
37
|
+
if not self.enabled:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
with self.lock:
|
|
41
|
+
if cache_key in self.cache:
|
|
42
|
+
serialized_value, timestamp = self.cache[cache_key]
|
|
43
|
+
if time.time() - timestamp <= self.ttl:
|
|
44
|
+
return pickle.loads(serialized_value)
|
|
45
|
+
else:
|
|
46
|
+
self.cache.pop(cache_key, None)
|
|
47
|
+
return None
|
|
48
|
+
else:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def set(self, cache_key: str, value: Any) -> None:
|
|
53
|
+
if not self.enabled:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
with self.lock:
|
|
57
|
+
serialized_value = pickle.dumps(value)
|
|
58
|
+
self.cache[cache_key] = (serialized_value, time.time())
|
|
59
|
+
|
|
60
|
+
if len(self.cache) > MAX_CACHE_SIZE:
|
|
61
|
+
oldest_key = min(self.cache.keys(), key=lambda k: self.cache[k][1]) # oldest key based on timestamp
|
|
62
|
+
self.cache.pop(oldest_key)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def unset(self, cache_key: str) -> None:
|
|
67
|
+
if not self.enabled:
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
with self.lock:
|
|
71
|
+
self.cache.pop(cache_key, None)
|
|
72
|
+
|
|
73
|
+
def invalidate_operation(self, operation_name: str) -> None:
|
|
74
|
+
if not self.enabled:
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
with self.lock:
|
|
78
|
+
for key in list(self.cache.keys()):
|
|
79
|
+
if key.startswith(operation_name):
|
|
80
|
+
self.cache.pop(key, None)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _cleanup_expired_items(self) -> None:
|
|
84
|
+
"""Remove all expired items from the cache."""
|
|
85
|
+
current_time = time.time()
|
|
86
|
+
with self.lock:
|
|
87
|
+
expired_keys = [
|
|
88
|
+
key for key, (_, timestamp) in self.cache.items()
|
|
89
|
+
if current_time - timestamp > self.ttl
|
|
90
|
+
]
|
|
91
|
+
for key in expired_keys:
|
|
92
|
+
self.cache.pop(key, None)
|
|
93
|
+
|
|
94
|
+
def _cleanup_worker(self) -> None:
|
|
95
|
+
"""Background worker that periodically cleans up expired items."""
|
|
96
|
+
while not self.stop_cleanup_thread:
|
|
97
|
+
time.sleep(self.cleanup_interval)
|
|
98
|
+
self._cleanup_expired_items()
|
|
99
|
+
|
|
100
|
+
def __del__(self) -> None:
|
|
101
|
+
"""Ensure thread is properly stopped when the object is garbage collected."""
|
|
102
|
+
self.stop_cleanup_thread = True
|
|
103
|
+
if self.enabled and self.cleanup_thread.is_alive():
|
|
104
|
+
self.cleanup_thread.join(timeout=1.0)
|
|
105
|
+
|
|
106
|
+
|