dataflow-core 2.1.6__py3-none-any.whl → 2.1.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 dataflow-core might be problematic. Click here for more details.
- authenticator/dataflowhubauthenticator.py +19 -17
- dataflow/dataflow.py +158 -34
- dataflow/models/__init__.py +2 -1
- dataflow/models/dataflow_zone.py +19 -0
- dataflow/models/role.py +12 -2
- dataflow/models/role_zone.py +17 -0
- dataflow/models/user.py +2 -2
- dataflow/schemas/__init__.py +0 -0
- dataflow/schemas/connection.py +84 -0
- dataflow/schemas/git_ssh.py +50 -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/utils/get_current_user.py +2 -0
- {dataflow_core-2.1.6.dist-info → dataflow_core-2.1.8.dist-info}/METADATA +3 -1
- {dataflow_core-2.1.6.dist-info → dataflow_core-2.1.8.dist-info}/RECORD +25 -14
- dataflow/models/runtime.py +0 -11
- dataflow/utils/aws_secrets_manager.py +0 -57
- dataflow/utils/json_handler.py +0 -33
- {dataflow_core-2.1.6.dist-info → dataflow_core-2.1.8.dist-info}/WHEEL +0 -0
- {dataflow_core-2.1.6.dist-info → dataflow_core-2.1.8.dist-info}/entry_points.txt +0 -0
- {dataflow_core-2.1.6.dist-info → dataflow_core-2.1.8.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# secrets_manager/factory.py
|
|
2
|
+
import os
|
|
3
|
+
from .interface import SecretManager
|
|
4
|
+
from .providers.aws_manager import AWSSecretsManager
|
|
5
|
+
from .providers.azure_manager import AzureKeyVault
|
|
6
|
+
from ..configuration import ConfigurationManager
|
|
7
|
+
|
|
8
|
+
# A custom exception for clear error messages
|
|
9
|
+
class SecretProviderError(Exception):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
def get_secret_manager() -> SecretManager:
|
|
13
|
+
"""
|
|
14
|
+
Factory function to get the configured secret manager instance.
|
|
15
|
+
|
|
16
|
+
Reads the cloud provider configuration from dataflow_auth.cfg
|
|
17
|
+
to determine which cloud provider's secret manager to instantiate.
|
|
18
|
+
"""
|
|
19
|
+
try:
|
|
20
|
+
# dataflow_config = None
|
|
21
|
+
# if os.getenv('HOSTNAME'):
|
|
22
|
+
# dataflow_config = ConfigurationManager('/dataflow/app/auth_config/dataflow_auth.cfg')
|
|
23
|
+
# else:
|
|
24
|
+
dataflow_config = ConfigurationManager('/dataflow/app/config/dataflow.cfg')
|
|
25
|
+
except Exception as e:
|
|
26
|
+
raise SecretProviderError(
|
|
27
|
+
f"Failed to read cloud provider configuration: {str(e)}. "
|
|
28
|
+
"Please check that the configuration file exists and contains the 'cloud' section."
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
provider = dataflow_config.get_config_value('cloudProvider', 'cloud')
|
|
32
|
+
if not provider:
|
|
33
|
+
raise SecretProviderError(
|
|
34
|
+
"The cloud provider is not configured in config file. "
|
|
35
|
+
"Please set the 'cloud' value in the 'cloud' section to 'aws' or 'azure'."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
provider = provider.lower()
|
|
39
|
+
print(f"Initializing secret manager for provider: {provider}")
|
|
40
|
+
|
|
41
|
+
if provider == "aws":
|
|
42
|
+
return AWSSecretsManager()
|
|
43
|
+
|
|
44
|
+
elif provider == "azure":
|
|
45
|
+
vault_url = dataflow_config.get_config_value('cloudProvider', 'key_vault')
|
|
46
|
+
if not vault_url:
|
|
47
|
+
raise SecretProviderError(
|
|
48
|
+
"AZURE_VAULT_URL must be set when using the Azure provider."
|
|
49
|
+
)
|
|
50
|
+
return AzureKeyVault(vault_url=vault_url)
|
|
51
|
+
|
|
52
|
+
# You can easily add more providers here in the future
|
|
53
|
+
# elif provider == "gcp":
|
|
54
|
+
# return GCPSecretManager()
|
|
55
|
+
|
|
56
|
+
else:
|
|
57
|
+
raise SecretProviderError(
|
|
58
|
+
f"Unsupported secret provider: '{provider}'. Supported providers are: aws, azure."
|
|
59
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
class SecretManager(ABC):
|
|
4
|
+
@abstractmethod
|
|
5
|
+
def create_secret(self, vault_path: str, secret_data: dict) -> str:
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
@abstractmethod
|
|
9
|
+
def get_secret_by_key(self, vault_path: str):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def update_secret(self, vault_path: str, update_data):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def delete_secret(self, vault_path: str):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def test_connection(self, vault_path: str):
|
|
22
|
+
pass
|
|
File without changes
|
|
@@ -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)
|