dataflow-core 2.1.7__py3-none-any.whl → 2.1.9__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.
- authenticator/dataflowhubauthenticator.py +19 -16
- dataflow/dataflow.py +155 -34
- dataflow/schemas/__init__.py +0 -0
- dataflow/schemas/connection.py +84 -0
- dataflow/schemas/git_ssh.py +47 -0
- dataflow/schemas/secret.py +44 -0
- dataflow/secrets_manager/__init__.py +13 -0
- dataflow/secrets_manager/factory.py +59 -0
- dataflow/secrets_manager/interface.py +22 -0
- dataflow/secrets_manager/providers/__init__.py +0 -0
- dataflow/secrets_manager/providers/aws_manager.py +164 -0
- dataflow/secrets_manager/providers/azure_manager.py +185 -0
- dataflow/secrets_manager/service.py +156 -0
- dataflow/utils/exceptions.py +112 -0
- {dataflow_core-2.1.7.dist-info → dataflow_core-2.1.9.dist-info}/METADATA +3 -1
- {dataflow_core-2.1.7.dist-info → dataflow_core-2.1.9.dist-info}/RECORD +19 -9
- dataflow/utils/aws_secrets_manager.py +0 -57
- dataflow/utils/json_handler.py +0 -33
- {dataflow_core-2.1.7.dist-info → dataflow_core-2.1.9.dist-info}/WHEEL +0 -0
- {dataflow_core-2.1.7.dist-info → dataflow_core-2.1.9.dist-info}/entry_points.txt +0 -0
- {dataflow_core-2.1.7.dist-info → dataflow_core-2.1.9.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
|
|
2
|
+
import boto3
|
|
3
|
+
import json
|
|
4
|
+
from botocore.exceptions import BotoCoreError, ClientError, EndpointConnectionError, NoCredentialsError
|
|
5
|
+
from ..interface import SecretManager
|
|
6
|
+
from ...utils.exceptions import (
|
|
7
|
+
SecretNotFoundException,
|
|
8
|
+
SecretAlreadyExistsException,
|
|
9
|
+
SecretManagerAuthException,
|
|
10
|
+
SecretManagerServiceException
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
class AWSSecretsManager(SecretManager):
|
|
14
|
+
def __init__(self):
|
|
15
|
+
try:
|
|
16
|
+
self.client = boto3.client('secretsmanager')
|
|
17
|
+
except EndpointConnectionError as e:
|
|
18
|
+
raise SecretManagerServiceException("initialize_aws_client", original_error=str(e))
|
|
19
|
+
except NoCredentialsError as e:
|
|
20
|
+
raise SecretManagerAuthException("initialize_aws_client", original_error=str(e))
|
|
21
|
+
except Exception as e:
|
|
22
|
+
raise SecretManagerServiceException("initialize_aws_client", original_error=str(e))
|
|
23
|
+
|
|
24
|
+
def create_secret(self, vault_path: str, secret_data: dict) -> str:
|
|
25
|
+
try:
|
|
26
|
+
# Convert dictionary to JSON string before saving
|
|
27
|
+
secret_string = json.dumps(secret_data)
|
|
28
|
+
|
|
29
|
+
self.client.create_secret(
|
|
30
|
+
Name=vault_path,
|
|
31
|
+
SecretString=secret_string,
|
|
32
|
+
Description=secret_data.get("description", "Created by AWSSecretsManager")
|
|
33
|
+
)
|
|
34
|
+
return "Secret created successfully"
|
|
35
|
+
except ClientError as e:
|
|
36
|
+
error_code = e.response['Error']['Code']
|
|
37
|
+
error_message = e.response['Error']['Message']
|
|
38
|
+
|
|
39
|
+
if error_code == 'ResourceExistsException':
|
|
40
|
+
raise SecretAlreadyExistsException("secret", vault_path, original_error=str(e))
|
|
41
|
+
elif error_code == 'InvalidRequestException' and 'scheduled for deletion' in error_message:
|
|
42
|
+
# Special case for secrets in recovery period
|
|
43
|
+
raise SecretAlreadyExistsException("secret", vault_path, original_error=str(e), is_scheduled_for_deletion=True)
|
|
44
|
+
elif error_code in ['AccessDeniedException', 'UnauthorizedOperation', 'UnrecognizedClientException']:
|
|
45
|
+
raise SecretManagerAuthException("create_secret", original_error=str(e))
|
|
46
|
+
elif error_code in ['InvalidRequestException', 'LimitExceededException']:
|
|
47
|
+
raise SecretManagerServiceException("create_secret", original_error=str(e))
|
|
48
|
+
elif error_code == 'InternalServiceErrorException':
|
|
49
|
+
raise SecretManagerServiceException("create_secret", original_error=str(e))
|
|
50
|
+
else:
|
|
51
|
+
raise SecretManagerServiceException("create_secret", original_error=str(e))
|
|
52
|
+
except BotoCoreError as e:
|
|
53
|
+
raise SecretManagerServiceException("create_secret", original_error=str(e))
|
|
54
|
+
except Exception as e:
|
|
55
|
+
raise SecretManagerServiceException("create_secret", original_error=str(e))
|
|
56
|
+
|
|
57
|
+
def get_secret_by_key(self, vault_path: str) -> dict:
|
|
58
|
+
try:
|
|
59
|
+
response = self.client.get_secret_value(SecretId=vault_path)
|
|
60
|
+
secret_string = response.get('SecretString')
|
|
61
|
+
|
|
62
|
+
# Convert JSON string back to dictionary before returning
|
|
63
|
+
secret_data = json.loads(secret_string)
|
|
64
|
+
return secret_data
|
|
65
|
+
except ClientError as e:
|
|
66
|
+
error_code = e.response['Error']['Code']
|
|
67
|
+
if error_code == 'ResourceNotFoundException':
|
|
68
|
+
raise SecretNotFoundException("secret", vault_path, original_error=str(e))
|
|
69
|
+
elif error_code in ['AccessDeniedException', 'UnauthorizedOperation', 'UnrecognizedClientException']:
|
|
70
|
+
raise SecretManagerAuthException("get_secret", original_error=str(e))
|
|
71
|
+
elif error_code in ['InvalidRequestException', 'DecryptionFailureException']:
|
|
72
|
+
raise SecretManagerServiceException("get_secret", original_error=str(e))
|
|
73
|
+
elif error_code == 'InternalServiceErrorException':
|
|
74
|
+
raise SecretManagerServiceException("get_secret", original_error=str(e))
|
|
75
|
+
else:
|
|
76
|
+
raise SecretManagerServiceException("get_secret", original_error=str(e))
|
|
77
|
+
except json.JSONDecodeError as e:
|
|
78
|
+
raise SecretManagerServiceException("get_secret", original_error=str(e))
|
|
79
|
+
except BotoCoreError as e:
|
|
80
|
+
raise SecretManagerServiceException("get_secret", original_error=str(e))
|
|
81
|
+
except Exception as e:
|
|
82
|
+
raise SecretManagerServiceException("get_secret", original_error=str(e))
|
|
83
|
+
|
|
84
|
+
def update_secret(self, vault_path: str, update_data: dict) -> str:
|
|
85
|
+
try:
|
|
86
|
+
# Get current secret data
|
|
87
|
+
current = self.client.get_secret_value(SecretId=vault_path)
|
|
88
|
+
current_string = current['SecretString']
|
|
89
|
+
|
|
90
|
+
# Convert current JSON string to dictionary
|
|
91
|
+
current_data = json.loads(current_string)
|
|
92
|
+
|
|
93
|
+
# Update with new data
|
|
94
|
+
current_data.update(update_data)
|
|
95
|
+
|
|
96
|
+
# Convert updated dictionary back to JSON string
|
|
97
|
+
updated_string = json.dumps(current_data)
|
|
98
|
+
|
|
99
|
+
self.client.update_secret(
|
|
100
|
+
SecretId=vault_path,
|
|
101
|
+
SecretString=updated_string,
|
|
102
|
+
Description=current_data.get('description', '')
|
|
103
|
+
)
|
|
104
|
+
return "Secret updated successfully"
|
|
105
|
+
except ClientError as e:
|
|
106
|
+
error_code = e.response['Error']['Code']
|
|
107
|
+
if error_code == 'ResourceNotFoundException':
|
|
108
|
+
raise SecretNotFoundException("secret", vault_path, original_error=str(e))
|
|
109
|
+
elif error_code in ['AccessDeniedException', 'UnauthorizedOperation', 'UnrecognizedClientException']:
|
|
110
|
+
raise SecretManagerAuthException("update_secret", original_error=str(e))
|
|
111
|
+
elif error_code in ['InvalidRequestException', 'DecryptionFailureException']:
|
|
112
|
+
raise SecretManagerServiceException("update_secret", original_error=str(e))
|
|
113
|
+
elif error_code == 'InternalServiceErrorException':
|
|
114
|
+
raise SecretManagerServiceException("update_secret", original_error=str(e))
|
|
115
|
+
else:
|
|
116
|
+
raise SecretManagerServiceException("update_secret", original_error=str(e))
|
|
117
|
+
except json.JSONDecodeError as e:
|
|
118
|
+
raise SecretManagerServiceException("update_secret", original_error=str(e))
|
|
119
|
+
except BotoCoreError as e:
|
|
120
|
+
raise SecretManagerServiceException("update_secret", original_error=str(e))
|
|
121
|
+
except Exception as e:
|
|
122
|
+
raise SecretManagerServiceException("update_secret", original_error=str(e))
|
|
123
|
+
|
|
124
|
+
def delete_secret(self, vault_path: str) -> str:
|
|
125
|
+
try:
|
|
126
|
+
if "git-ssh" in vault_path:
|
|
127
|
+
self.client.delete_secret(
|
|
128
|
+
SecretId=vault_path,
|
|
129
|
+
ForceDeleteWithoutRecovery=True
|
|
130
|
+
)
|
|
131
|
+
else:
|
|
132
|
+
self.client.delete_secret(
|
|
133
|
+
SecretId=vault_path,
|
|
134
|
+
RecoveryWindowInDays=7
|
|
135
|
+
)
|
|
136
|
+
return "Secret deleted successfully"
|
|
137
|
+
except ClientError as e:
|
|
138
|
+
error_code = e.response['Error']['Code']
|
|
139
|
+
if error_code == 'ResourceNotFoundException':
|
|
140
|
+
raise SecretNotFoundException("secret", vault_path, original_error=str(e))
|
|
141
|
+
elif error_code in ['AccessDeniedException', 'UnauthorizedOperation', 'UnrecognizedClientException']:
|
|
142
|
+
raise SecretManagerAuthException("delete_secret", original_error=str(e))
|
|
143
|
+
elif error_code == 'InvalidRequestException':
|
|
144
|
+
# Can occur if secret is already scheduled for deletion or in invalid state
|
|
145
|
+
raise SecretManagerServiceException("delete_secret", original_error=str(e))
|
|
146
|
+
elif error_code == 'InternalServiceErrorException':
|
|
147
|
+
raise SecretManagerServiceException("delete_secret", original_error=str(e))
|
|
148
|
+
else:
|
|
149
|
+
raise SecretManagerServiceException("delete_secret", original_error=str(e))
|
|
150
|
+
except BotoCoreError as e:
|
|
151
|
+
raise SecretManagerServiceException("delete_secret", original_error=str(e))
|
|
152
|
+
except Exception as e:
|
|
153
|
+
raise SecretManagerServiceException("delete_secret", original_error=str(e))
|
|
154
|
+
|
|
155
|
+
def test_connection(self, vault_path: str) -> str:
|
|
156
|
+
try:
|
|
157
|
+
secret = self.get_secret_by_key(vault_path)
|
|
158
|
+
return secret.get('status', 'Unknown')
|
|
159
|
+
except SecretNotFoundException:
|
|
160
|
+
raise
|
|
161
|
+
except (SecretManagerAuthException, SecretManagerServiceException):
|
|
162
|
+
raise
|
|
163
|
+
except Exception as e:
|
|
164
|
+
raise SecretManagerServiceException("test_connection", original_error=str(e))
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
|
|
2
|
+
from azure.keyvault.secrets import SecretClient
|
|
3
|
+
from azure.identity import DefaultAzureCredential
|
|
4
|
+
from azure.core.exceptions import (
|
|
5
|
+
ResourceNotFoundError,
|
|
6
|
+
ResourceExistsError,
|
|
7
|
+
HttpResponseError,
|
|
8
|
+
ClientAuthenticationError,
|
|
9
|
+
ServiceRequestError
|
|
10
|
+
)
|
|
11
|
+
from ..interface import SecretManager
|
|
12
|
+
from ...utils.exceptions import (
|
|
13
|
+
SecretNotFoundException,
|
|
14
|
+
SecretAlreadyExistsException,
|
|
15
|
+
SecretManagerAuthException,
|
|
16
|
+
SecretManagerServiceException
|
|
17
|
+
)
|
|
18
|
+
import json
|
|
19
|
+
|
|
20
|
+
class AzureKeyVault(SecretManager):
|
|
21
|
+
def __init__(self, vault_url: str):
|
|
22
|
+
try:
|
|
23
|
+
credential = DefaultAzureCredential(additionally_allowed_tenants=["*"])
|
|
24
|
+
self.client = SecretClient(vault_url=vault_url, credential=credential)
|
|
25
|
+
except ClientAuthenticationError as e:
|
|
26
|
+
raise SecretManagerAuthException("initialize_azure_client", original_error=str(e))
|
|
27
|
+
except ServiceRequestError as e:
|
|
28
|
+
raise SecretManagerServiceException("initialize_azure_client", original_error=str(e))
|
|
29
|
+
except Exception as e:
|
|
30
|
+
raise SecretManagerServiceException("initialize_azure_client", original_error=str(e))
|
|
31
|
+
|
|
32
|
+
def create_secret(self, vault_path: str, secret_data: dict) -> str:
|
|
33
|
+
try:
|
|
34
|
+
# Convert dictionary to JSON string before saving
|
|
35
|
+
secret_string = json.dumps(secret_data)
|
|
36
|
+
|
|
37
|
+
self.client.set_secret(
|
|
38
|
+
name=vault_path,
|
|
39
|
+
value=secret_string,
|
|
40
|
+
content_type="application/json",
|
|
41
|
+
tags={"description": secret_data.get("description", "Created by AzureKeyVault")}
|
|
42
|
+
)
|
|
43
|
+
return "Secret created successfully"
|
|
44
|
+
except ResourceExistsError as e:
|
|
45
|
+
raise SecretAlreadyExistsException("secret", vault_path, original_error=str(e))
|
|
46
|
+
except ClientAuthenticationError as e:
|
|
47
|
+
raise SecretManagerAuthException("create_secret", original_error=str(e))
|
|
48
|
+
except HttpResponseError as e:
|
|
49
|
+
if e.status_code == 403:
|
|
50
|
+
raise SecretManagerAuthException("create_secret", original_error=str(e))
|
|
51
|
+
elif e.status_code == 409:
|
|
52
|
+
# Check if it's a scheduled deletion case
|
|
53
|
+
if "deleted but not purged" in str(e) or "being recovered" in str(e):
|
|
54
|
+
raise SecretAlreadyExistsException("secret", vault_path, original_error=str(e), is_scheduled_for_deletion=True)
|
|
55
|
+
else:
|
|
56
|
+
raise SecretAlreadyExistsException("secret", vault_path, original_error=str(e))
|
|
57
|
+
elif e.status_code == 429:
|
|
58
|
+
raise SecretManagerServiceException("create_secret", original_error=str(e))
|
|
59
|
+
elif e.status_code >= 500:
|
|
60
|
+
raise SecretManagerServiceException("create_secret", original_error=str(e))
|
|
61
|
+
else:
|
|
62
|
+
raise SecretManagerServiceException("create_secret", original_error=str(e))
|
|
63
|
+
except ServiceRequestError as e:
|
|
64
|
+
raise SecretManagerServiceException("create_secret", original_error=str(e))
|
|
65
|
+
except Exception as e:
|
|
66
|
+
raise SecretManagerServiceException("create_secret", original_error=str(e))
|
|
67
|
+
|
|
68
|
+
def get_secret_by_key(self, vault_path: str) -> dict:
|
|
69
|
+
try:
|
|
70
|
+
secret = self.client.get_secret(vault_path)
|
|
71
|
+
secret_string = secret.value
|
|
72
|
+
|
|
73
|
+
# Convert JSON string back to dictionary before returning
|
|
74
|
+
secret_data = json.loads(secret_string)
|
|
75
|
+
return secret_data
|
|
76
|
+
except ResourceNotFoundError as e:
|
|
77
|
+
raise SecretNotFoundException("secret", vault_path, original_error=str(e))
|
|
78
|
+
except ClientAuthenticationError as e:
|
|
79
|
+
raise SecretManagerAuthException("get_secret", original_error=str(e))
|
|
80
|
+
except HttpResponseError as e:
|
|
81
|
+
if e.status_code == 403:
|
|
82
|
+
raise SecretManagerAuthException("get_secret", original_error=str(e))
|
|
83
|
+
elif e.status_code == 404:
|
|
84
|
+
raise SecretNotFoundException("secret", vault_path, original_error=str(e))
|
|
85
|
+
elif e.status_code >= 500:
|
|
86
|
+
raise SecretManagerServiceException("get_secret", original_error=str(e))
|
|
87
|
+
else:
|
|
88
|
+
raise SecretManagerServiceException("get_secret", original_error=str(e))
|
|
89
|
+
except json.JSONDecodeError as e:
|
|
90
|
+
raise SecretManagerServiceException("get_secret", original_error=str(e))
|
|
91
|
+
except ServiceRequestError as e:
|
|
92
|
+
raise SecretManagerServiceException("get_secret", original_error=str(e))
|
|
93
|
+
except Exception as e:
|
|
94
|
+
raise SecretManagerServiceException("get_secret", original_error=str(e))
|
|
95
|
+
|
|
96
|
+
def update_secret(self, vault_path: str, update_data: dict) -> str:
|
|
97
|
+
try:
|
|
98
|
+
# Get current secret data
|
|
99
|
+
current_secret = self.client.get_secret(vault_path)
|
|
100
|
+
current_string = current_secret.value
|
|
101
|
+
|
|
102
|
+
# Convert current JSON string to dictionary
|
|
103
|
+
current_data = json.loads(current_string)
|
|
104
|
+
|
|
105
|
+
# Update with new data
|
|
106
|
+
current_data.update(update_data)
|
|
107
|
+
|
|
108
|
+
# Convert updated dictionary back to JSON string
|
|
109
|
+
updated_string = json.dumps(current_data)
|
|
110
|
+
|
|
111
|
+
self.client.set_secret(
|
|
112
|
+
name=vault_path,
|
|
113
|
+
value=updated_string,
|
|
114
|
+
content_type="application/json",
|
|
115
|
+
tags={"description": current_data.get("description", "")}
|
|
116
|
+
)
|
|
117
|
+
return "Secret updated successfully"
|
|
118
|
+
except ResourceNotFoundError as e:
|
|
119
|
+
raise SecretNotFoundException("secret", vault_path, original_error=str(e))
|
|
120
|
+
except ClientAuthenticationError as e:
|
|
121
|
+
raise SecretManagerAuthException("update_secret", original_error=str(e))
|
|
122
|
+
except HttpResponseError as e:
|
|
123
|
+
if e.status_code == 403:
|
|
124
|
+
raise SecretManagerAuthException("update_secret", original_error=str(e))
|
|
125
|
+
elif e.status_code == 404:
|
|
126
|
+
raise SecretNotFoundException("secret", vault_path, original_error=str(e))
|
|
127
|
+
elif e.status_code >= 500:
|
|
128
|
+
raise SecretManagerServiceException("update_secret", original_error=str(e))
|
|
129
|
+
else:
|
|
130
|
+
raise SecretManagerServiceException("update_secret", original_error=str(e))
|
|
131
|
+
except json.JSONDecodeError as e:
|
|
132
|
+
raise SecretManagerServiceException("update_secret", original_error=str(e))
|
|
133
|
+
except ServiceRequestError as e:
|
|
134
|
+
raise SecretManagerServiceException("update_secret", original_error=str(e))
|
|
135
|
+
except Exception as e:
|
|
136
|
+
raise SecretManagerServiceException("update_secret", original_error=str(e))
|
|
137
|
+
|
|
138
|
+
def delete_secret(self, vault_path: str) -> str:
|
|
139
|
+
try:
|
|
140
|
+
if "git-ssh" in vault_path:
|
|
141
|
+
# For SSH keys, try to purge immediately after deletion
|
|
142
|
+
delete_poller = self.client.begin_delete_secret(vault_path)
|
|
143
|
+
delete_poller.wait() # Wait for deletion to complete
|
|
144
|
+
try:
|
|
145
|
+
# Try to purge immediately (only works if soft-delete is enabled and allows purging)
|
|
146
|
+
self.client.purge_deleted_secret(vault_path)
|
|
147
|
+
except (ResourceNotFoundError, HttpResponseError):
|
|
148
|
+
# Purge may fail if soft-delete is disabled or purging is not allowed - that's ok
|
|
149
|
+
pass
|
|
150
|
+
else:
|
|
151
|
+
# For other secrets, just delete (respects soft-delete settings)
|
|
152
|
+
delete_poller = self.client.begin_delete_secret(vault_path)
|
|
153
|
+
delete_poller.wait() # Wait for deletion to complete
|
|
154
|
+
return "Secret deleted successfully"
|
|
155
|
+
except ResourceNotFoundError as e:
|
|
156
|
+
raise SecretNotFoundException("secret", vault_path, original_error=str(e))
|
|
157
|
+
except ClientAuthenticationError as e:
|
|
158
|
+
raise SecretManagerAuthException("delete_secret", original_error=str(e))
|
|
159
|
+
except HttpResponseError as e:
|
|
160
|
+
if e.status_code == 403:
|
|
161
|
+
raise SecretManagerAuthException("delete_secret", original_error=str(e))
|
|
162
|
+
elif e.status_code == 404:
|
|
163
|
+
raise SecretNotFoundException("secret", vault_path, original_error=str(e))
|
|
164
|
+
elif e.status_code == 409:
|
|
165
|
+
# Can occur if secret is already being deleted or in invalid state
|
|
166
|
+
raise SecretManagerServiceException("delete_secret", original_error=str(e))
|
|
167
|
+
elif e.status_code >= 500:
|
|
168
|
+
raise SecretManagerServiceException("delete_secret", original_error=str(e))
|
|
169
|
+
else:
|
|
170
|
+
raise SecretManagerServiceException("delete_secret", original_error=str(e))
|
|
171
|
+
except ServiceRequestError as e:
|
|
172
|
+
raise SecretManagerServiceException("delete_secret", original_error=str(e))
|
|
173
|
+
except Exception as e:
|
|
174
|
+
raise SecretManagerServiceException("delete_secret", original_error=str(e))
|
|
175
|
+
|
|
176
|
+
def test_connection(self, vault_path: str) -> str:
|
|
177
|
+
try:
|
|
178
|
+
secret = self.get_secret_by_key(vault_path)
|
|
179
|
+
return secret.get('status', 'Unknown')
|
|
180
|
+
except SecretNotFoundException:
|
|
181
|
+
raise
|
|
182
|
+
except (SecretManagerAuthException, SecretManagerServiceException):
|
|
183
|
+
raise
|
|
184
|
+
except Exception as e:
|
|
185
|
+
raise SecretManagerServiceException("test_connection", original_error=str(e))
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from .interface import SecretManager
|
|
2
|
+
from ..schemas.connection import ConnectionSave, ConnectionUpdate, ConnectionRead
|
|
3
|
+
from ..schemas.secret import SecretSave, SecretUpdate, SecretRead
|
|
4
|
+
from ..schemas.git_ssh import SSHSave, SSHRead
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
# --------------------------------------------------------------------------
|
|
9
|
+
# BASE ACCESSOR
|
|
10
|
+
# This contains all the methods for CRUD operations on secrets, connections, and SSH keys.
|
|
11
|
+
# --------------------------------------------------------------------------
|
|
12
|
+
class _BaseAccessor(ABC):
|
|
13
|
+
def __init__(self, secret_manager: SecretManager):
|
|
14
|
+
self.secret_manager = secret_manager
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def _get_vault_path(self, secret_type: str, key: str) -> str:
|
|
18
|
+
"""This must be implemented by each specific context."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
# --------------------------------------------------------------------------
|
|
22
|
+
# CONNECTION CRUD OPERATIONS
|
|
23
|
+
# --------------------------------------------------------------------------
|
|
24
|
+
def create_connection(self, connection_data: ConnectionSave) -> str:
|
|
25
|
+
"""Create a new connection."""
|
|
26
|
+
vault_path = self._get_vault_path("connections", connection_data.conn_id)
|
|
27
|
+
# Convert schema to dict and then to JSON string for storage
|
|
28
|
+
connection_dict = connection_data.model_dump()
|
|
29
|
+
return self.secret_manager.create_secret(vault_path, connection_dict)
|
|
30
|
+
|
|
31
|
+
def get_connection(self, key: str) -> ConnectionRead:
|
|
32
|
+
"""Get a connection by key."""
|
|
33
|
+
vault_path = self._get_vault_path("connections", key)
|
|
34
|
+
raw_data = self.secret_manager.get_secret_by_key(vault_path)
|
|
35
|
+
# Convert stored dict back to schema
|
|
36
|
+
return ConnectionRead(**raw_data)
|
|
37
|
+
|
|
38
|
+
def update_connection(self, key: str, connection_data: ConnectionUpdate) -> str:
|
|
39
|
+
"""Update an existing connection."""
|
|
40
|
+
vault_path = self._get_vault_path("connections", key)
|
|
41
|
+
|
|
42
|
+
# First, get the existing connection data
|
|
43
|
+
existing_data: dict = self.secret_manager.get_secret_by_key(vault_path)
|
|
44
|
+
|
|
45
|
+
# Convert update schema to dict, only include non-None values
|
|
46
|
+
update_dict: dict = connection_data.model_dump(exclude_none=True)
|
|
47
|
+
|
|
48
|
+
# Merge existing data with update data
|
|
49
|
+
existing_data.update(update_dict)
|
|
50
|
+
|
|
51
|
+
# Send the complete merged data back to storage
|
|
52
|
+
return self.secret_manager.update_secret(vault_path, existing_data)
|
|
53
|
+
|
|
54
|
+
def delete_connection(self, key: str) -> str:
|
|
55
|
+
"""Delete a connection."""
|
|
56
|
+
vault_path = self._get_vault_path("connections", key)
|
|
57
|
+
return self.secret_manager.delete_secret(vault_path)
|
|
58
|
+
|
|
59
|
+
def test_connection(self, key: str):
|
|
60
|
+
"""Test a connection."""
|
|
61
|
+
vault_path = self._get_vault_path("connections", key)
|
|
62
|
+
return self.secret_manager.test_connection(vault_path)
|
|
63
|
+
|
|
64
|
+
# --------------------------------------------------------------------------
|
|
65
|
+
# SSH CRUD OPERATIONS
|
|
66
|
+
# --------------------------------------------------------------------------
|
|
67
|
+
def create_ssh(self, ssh_data: SSHSave) -> str:
|
|
68
|
+
"""Create a new SSH key."""
|
|
69
|
+
vault_path = self._get_vault_path("git-ssh", ssh_data.key_name)
|
|
70
|
+
# Convert schema to dict for storage
|
|
71
|
+
ssh_dict = ssh_data.model_dump()
|
|
72
|
+
return self.secret_manager.create_secret(vault_path, ssh_dict)
|
|
73
|
+
|
|
74
|
+
def get_ssh(self, key: str) -> SSHRead:
|
|
75
|
+
"""Get an SSH key by key."""
|
|
76
|
+
vault_path = self._get_vault_path("git-ssh", key)
|
|
77
|
+
raw_data = self.secret_manager.get_secret_by_key(vault_path)
|
|
78
|
+
# Convert stored dict back to schema
|
|
79
|
+
return SSHRead(**raw_data)
|
|
80
|
+
|
|
81
|
+
def delete_ssh(self, key: str) -> str:
|
|
82
|
+
"""Delete an SSH key."""
|
|
83
|
+
vault_path = self._get_vault_path("git-ssh", key)
|
|
84
|
+
return self.secret_manager.delete_secret(vault_path)
|
|
85
|
+
|
|
86
|
+
# --------------------------------------------------------------------------
|
|
87
|
+
# SECRET CRUD OPERATIONS
|
|
88
|
+
# --------------------------------------------------------------------------
|
|
89
|
+
def create_secret(self, secret_data: SecretSave) -> str:
|
|
90
|
+
"""Create a new secret."""
|
|
91
|
+
vault_path = self._get_vault_path("secrets", secret_data.key)
|
|
92
|
+
# Convert schema to dict for storage
|
|
93
|
+
secret_dict = secret_data.model_dump()
|
|
94
|
+
return self.secret_manager.create_secret(vault_path, secret_dict)
|
|
95
|
+
|
|
96
|
+
def get_secret(self, key: str) -> SecretRead:
|
|
97
|
+
"""Get a secret by key."""
|
|
98
|
+
vault_path = self._get_vault_path("secrets", key)
|
|
99
|
+
raw_data = self.secret_manager.get_secret_by_key(vault_path)
|
|
100
|
+
# Convert stored dict back to schema
|
|
101
|
+
return SecretRead(**raw_data)
|
|
102
|
+
|
|
103
|
+
def update_secret(self, key: str, secret_data: SecretUpdate) -> str:
|
|
104
|
+
"""Update an existing secret."""
|
|
105
|
+
vault_path = self._get_vault_path("secrets", key)
|
|
106
|
+
# Convert schema to dict, only include non-None values
|
|
107
|
+
update_dict = secret_data.model_dump(exclude_none=True)
|
|
108
|
+
return self.secret_manager.update_secret(vault_path, update_dict)
|
|
109
|
+
|
|
110
|
+
def delete_secret(self, key: str) -> str:
|
|
111
|
+
"""Delete a secret."""
|
|
112
|
+
vault_path = self._get_vault_path("secrets", key)
|
|
113
|
+
return self.secret_manager.delete_secret(vault_path)
|
|
114
|
+
|
|
115
|
+
# --------------------------------------------------------------------------
|
|
116
|
+
# CONTEXT-SPECIFIC ACCESSORS
|
|
117
|
+
# These classes implement the logic for building the vault path based on the context.
|
|
118
|
+
# --------------------------------------------------------------------------
|
|
119
|
+
class _RuntimeAccessor(_BaseAccessor):
|
|
120
|
+
def __init__(self, secret_manager: SecretManager, runtime_env: str, slug: str = None):
|
|
121
|
+
super().__init__(secret_manager)
|
|
122
|
+
self.runtime_env = runtime_env
|
|
123
|
+
self.slug = slug
|
|
124
|
+
|
|
125
|
+
def _get_vault_path(self, secret_type: str, key: str) -> str:
|
|
126
|
+
# Special case for git-ssh in runtime context
|
|
127
|
+
if secret_type == "git-ssh":
|
|
128
|
+
return f"{self.runtime_env}-{secret_type}-{self.slug}"
|
|
129
|
+
|
|
130
|
+
# Standard format for all other secret types
|
|
131
|
+
context = self.slug if self.slug else "global"
|
|
132
|
+
return f"{self.runtime_env}-{context}-{secret_type}-{key}"
|
|
133
|
+
|
|
134
|
+
class _StudioAccessor(_BaseAccessor):
|
|
135
|
+
def __init__(self, secret_manager: SecretManager, user_name: str):
|
|
136
|
+
super().__init__(secret_manager)
|
|
137
|
+
self.user_name = user_name
|
|
138
|
+
|
|
139
|
+
def _get_vault_path(self, secret_type: str, key: str) -> str:
|
|
140
|
+
return f"{self.user_name}-{secret_type}-{key}"
|
|
141
|
+
|
|
142
|
+
# --------------------------------------------------------------------------
|
|
143
|
+
# PUBLIC INTERFACE CLASS
|
|
144
|
+
# This is the class external systems will interact with.
|
|
145
|
+
# --------------------------------------------------------------------------
|
|
146
|
+
class SecretsService:
|
|
147
|
+
def __init__(self, secret_manager: SecretManager):
|
|
148
|
+
self._secret_manager = secret_manager
|
|
149
|
+
|
|
150
|
+
def runtime(self, env: str, slug: str = None) -> _RuntimeAccessor:
|
|
151
|
+
"""Sets the context to RUNTIME and returns the appropriate accessor."""
|
|
152
|
+
return _RuntimeAccessor(self._secret_manager, env, slug)
|
|
153
|
+
|
|
154
|
+
def studio(self, user: str) -> _StudioAccessor:
|
|
155
|
+
"""Sets the context to STUDIO and returns the appropriate accessor."""
|
|
156
|
+
return _StudioAccessor(self._secret_manager, user)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Custom exceptions for the dataflow secrets manager."""
|
|
2
|
+
|
|
3
|
+
from dataflow.utils.logger import CustomLogger
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SecretsManagerException(Exception):
|
|
8
|
+
"""Base exception for all secrets manager errors."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, message: str, details: Optional[str] = None, operation: Optional[str] = None):
|
|
11
|
+
self.message = message
|
|
12
|
+
self.details = details
|
|
13
|
+
self.operation = operation
|
|
14
|
+
self.logger = CustomLogger().get_logger(__name__)
|
|
15
|
+
|
|
16
|
+
# Log detailed error information
|
|
17
|
+
log_msg = f"SecretsManager Error"
|
|
18
|
+
if operation:
|
|
19
|
+
log_msg += f" in {operation}"
|
|
20
|
+
log_msg += f": {message}"
|
|
21
|
+
if details:
|
|
22
|
+
log_msg += f" | Details: {details}"
|
|
23
|
+
|
|
24
|
+
self.logger.error(log_msg)
|
|
25
|
+
super().__init__(message)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SecretNotFoundException(SecretsManagerException):
|
|
29
|
+
"""Raised when a requested secret is not found."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, secret_type: str, key: str, context: Optional[str] = None, original_error: Optional[str] = None):
|
|
32
|
+
message = f"{secret_type.capitalize()} not found"
|
|
33
|
+
|
|
34
|
+
details = f"Secret type: {secret_type}, Key: {key}"
|
|
35
|
+
if context:
|
|
36
|
+
details += f", Context: {context}"
|
|
37
|
+
if original_error:
|
|
38
|
+
details += f", Original error: {original_error}"
|
|
39
|
+
|
|
40
|
+
super().__init__(message, details, f"get_{secret_type}")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SecretAlreadyExistsException(SecretsManagerException):
|
|
44
|
+
"""Raised when trying to create a secret that already exists."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, secret_type: str, key: str, context: Optional[str] = None, original_error: Optional[str] = None, is_scheduled_for_deletion: bool = False):
|
|
47
|
+
if is_scheduled_for_deletion:
|
|
48
|
+
message = f"{secret_type.capitalize()} is in recovery mode. Please use another key name"
|
|
49
|
+
else:
|
|
50
|
+
message = f"{secret_type.capitalize()} already exists. Please use another key name"
|
|
51
|
+
|
|
52
|
+
details = f"Secret type: {secret_type}, Key: {key}"
|
|
53
|
+
if context:
|
|
54
|
+
details += f", Context: {context}"
|
|
55
|
+
if is_scheduled_for_deletion:
|
|
56
|
+
details += ", Status: Scheduled for deletion"
|
|
57
|
+
if original_error:
|
|
58
|
+
details += f", Original error: {original_error}"
|
|
59
|
+
|
|
60
|
+
super().__init__(message, details, f"create_{secret_type}")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class SecretValidationException(SecretsManagerException):
|
|
64
|
+
"""Raised when secret data validation fails."""
|
|
65
|
+
|
|
66
|
+
def __init__(self, secret_type: str, validation_error: str, original_error: Optional[str] = None):
|
|
67
|
+
message = f"Invalid {secret_type} data. Please check your input"
|
|
68
|
+
|
|
69
|
+
details = f"Secret type: {secret_type}, Validation error: {validation_error}"
|
|
70
|
+
if original_error:
|
|
71
|
+
details += f", Original error: {original_error}"
|
|
72
|
+
|
|
73
|
+
super().__init__(message, details, f"validate_{secret_type}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class SecretManagerAuthException(SecretsManagerException):
|
|
77
|
+
"""Raised when authentication or authorization fails."""
|
|
78
|
+
|
|
79
|
+
def __init__(self, operation: str, original_error: Optional[str] = None):
|
|
80
|
+
message = "Access denied. Please check your permissions"
|
|
81
|
+
|
|
82
|
+
details = f"Operation: {operation}"
|
|
83
|
+
if original_error:
|
|
84
|
+
details += f", Original error: {original_error}"
|
|
85
|
+
|
|
86
|
+
super().__init__(message, details, operation)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class SecretManagerServiceException(SecretsManagerException):
|
|
90
|
+
"""Raised when the secret manager service is unavailable or fails."""
|
|
91
|
+
|
|
92
|
+
def __init__(self, operation: str, original_error: Optional[str] = None):
|
|
93
|
+
message = "We're experiencing some issues. Our best minds are working on it!"
|
|
94
|
+
|
|
95
|
+
details = f"Operation: {operation}"
|
|
96
|
+
if original_error:
|
|
97
|
+
details += f", Original error: {original_error}"
|
|
98
|
+
|
|
99
|
+
super().__init__(message, details, operation)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class SecretManagerConfigException(SecretsManagerException):
|
|
103
|
+
"""Raised when there's a configuration error."""
|
|
104
|
+
|
|
105
|
+
def __init__(self, config_issue: str, original_error: Optional[str] = None):
|
|
106
|
+
message = "Configuration issue detected. Please contact support"
|
|
107
|
+
|
|
108
|
+
details = f"Config issue: {config_issue}"
|
|
109
|
+
if original_error:
|
|
110
|
+
details += f", Original error: {original_error}"
|
|
111
|
+
|
|
112
|
+
super().__init__(message, details, "configuration")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dataflow-core
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.9
|
|
4
4
|
Summary: Dataflow core package
|
|
5
5
|
Author: Dataflow
|
|
6
6
|
Author-email:
|
|
@@ -9,6 +9,8 @@ Requires-Dist: boto3
|
|
|
9
9
|
Requires-Dist: psycopg2-binary
|
|
10
10
|
Requires-Dist: pymysql
|
|
11
11
|
Requires-Dist: requests
|
|
12
|
+
Requires-Dist: azure-identity
|
|
13
|
+
Requires-Dist: azure-keyvault-secrets
|
|
12
14
|
Dynamic: author
|
|
13
15
|
Dynamic: requires-dist
|
|
14
16
|
Dynamic: summary
|