dataflow-core 2.1.7__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 -16
- dataflow/dataflow.py +158 -34
- 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_core-2.1.7.dist-info → dataflow_core-2.1.8.dist-info}/METADATA +3 -1
- {dataflow_core-2.1.7.dist-info → dataflow_core-2.1.8.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.8.dist-info}/WHEEL +0 -0
- {dataflow_core-2.1.7.dist-info → dataflow_core-2.1.8.dist-info}/entry_points.txt +0 -0
- {dataflow_core-2.1.7.dist-info → dataflow_core-2.1.8.dist-info}/top_level.txt +0 -0
|
@@ -109,7 +109,7 @@ class DataflowBaseAuthenticator(Authenticator):
|
|
|
109
109
|
return None
|
|
110
110
|
|
|
111
111
|
username = self.extract_username_from_email(email)
|
|
112
|
-
username = re.sub(r'[^
|
|
112
|
+
username = re.sub(r'[^a-z0-9]', '', username.lower())
|
|
113
113
|
if not username:
|
|
114
114
|
self.log.error("Cannot create user: Username is empty")
|
|
115
115
|
return None
|
|
@@ -247,7 +247,11 @@ class DataflowAzureAuthenticator(DataflowBaseAuthenticator, AzureAdOAuthenticato
|
|
|
247
247
|
azure_client_secret = Unicode(config=True, help="Azure AD OAuth client secret")
|
|
248
248
|
azure_tenant_id = Unicode(config=True, help="Azure AD tenant ID")
|
|
249
249
|
azure_scope = Unicode("openid profile email", config=True, help="Azure AD OAuth scopes")
|
|
250
|
-
|
|
250
|
+
dataflow_oauth_type = Unicode(
|
|
251
|
+
default_value="google",
|
|
252
|
+
config=True,
|
|
253
|
+
help="The OAuth provider type for DataflowHub (e.g., github, google)"
|
|
254
|
+
)
|
|
251
255
|
def __init__(self, **kwargs):
|
|
252
256
|
super().__init__(**kwargs)
|
|
253
257
|
self.client_id = self.azure_client_id
|
|
@@ -269,48 +273,47 @@ class DataflowAzureAuthenticator(DataflowBaseAuthenticator, AzureAdOAuthenticato
|
|
|
269
273
|
if not user:
|
|
270
274
|
self.log.warning("Azure AD OAuth authentication failed: No user data returned")
|
|
271
275
|
return None
|
|
272
|
-
|
|
273
|
-
|
|
276
|
+
|
|
277
|
+
auth_state = user.get("auth_state", {})
|
|
278
|
+
user_info = auth_state.get("user", {}) if auth_state else {}
|
|
279
|
+
email = user_info.get("upn")
|
|
274
280
|
if not email:
|
|
275
|
-
self.log.warning("Azure AD OAuth authentication failed: No
|
|
281
|
+
self.log.warning("Azure AD OAuth authentication failed: No upn in user data")
|
|
276
282
|
return None
|
|
277
|
-
|
|
283
|
+
|
|
278
284
|
db_user = (
|
|
279
285
|
self.db.query(m_user.User)
|
|
280
286
|
.filter(m_user.User.email == email)
|
|
281
287
|
.first()
|
|
282
288
|
)
|
|
283
|
-
|
|
289
|
+
|
|
284
290
|
if not db_user:
|
|
285
291
|
self.log.info(f"User with email {email} not found in Dataflow database, creating new user")
|
|
286
|
-
# Extract additional info from user data if available
|
|
287
|
-
auth_state = user.get("auth_state", {})
|
|
288
|
-
user_info = auth_state.get("user", {}) if auth_state else {}
|
|
289
292
|
|
|
290
|
-
first_name = user_info.get("
|
|
291
|
-
last_name = user_info.get("family_name") or user.get("family_name")
|
|
293
|
+
first_name = user_info.get("name") or user.get("name")
|
|
292
294
|
|
|
293
|
-
db_user = self.create_new_user(email, first_name, last_name)
|
|
295
|
+
db_user = self.create_new_user(email, first_name, last_name=None)
|
|
294
296
|
if not db_user:
|
|
295
297
|
self.log.error(f"Failed to create new user for email: {email}")
|
|
296
298
|
return None
|
|
297
|
-
|
|
299
|
+
|
|
298
300
|
username = db_user.user_name
|
|
299
301
|
session_id = self.get_or_create_session(db_user.user_id)
|
|
300
302
|
self.set_session_cookie(handler, session_id)
|
|
301
303
|
self.log.info(f"Azure AD OAuth completed for user: {username}, session_id={session_id}")
|
|
302
304
|
return {
|
|
303
|
-
"name":
|
|
305
|
+
"name": db_user.first_name,
|
|
304
306
|
"session_id": session_id,
|
|
305
307
|
"auth_state": user.get("auth_state", {})
|
|
306
308
|
}
|
|
309
|
+
|
|
307
310
|
except Exception as e:
|
|
308
311
|
self.log.error(f"Azure AD OAuth authentication error: {str(e)}", exc_info=True)
|
|
309
312
|
return None
|
|
310
313
|
finally:
|
|
311
314
|
self.db.close()
|
|
312
315
|
|
|
313
|
-
auth_type = os.environ.get("
|
|
316
|
+
auth_type = os.environ.get("DATAFLOW_OAUTH_TYPE", "google")
|
|
314
317
|
|
|
315
318
|
if auth_type == "google":
|
|
316
319
|
BaseAuthenticator = DataflowGoogleAuthenticator
|
dataflow/dataflow.py
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import os, requests
|
|
2
2
|
from .database_manager import DatabaseManager
|
|
3
|
-
from .utils.aws_secrets_manager import SecretsManagerClient
|
|
4
3
|
import json
|
|
5
4
|
from .configuration import ConfigurationManager
|
|
6
5
|
|
|
7
6
|
|
|
8
7
|
class Dataflow:
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
"""
|
|
9
|
+
Dataflow class to interact with Dataflow services.
|
|
10
|
+
"""
|
|
12
11
|
def auth(self, session_id: str):
|
|
13
12
|
"""
|
|
14
13
|
Retrieve and return user information using their session ID.
|
|
@@ -55,19 +54,26 @@ class Dataflow:
|
|
|
55
54
|
"""
|
|
56
55
|
try:
|
|
57
56
|
host_name = os.environ.get("HOSTNAME", "")
|
|
58
|
-
user_name = host_name.replace("jupyter-", "") if host_name.startswith("jupyter-") else host_name
|
|
59
57
|
runtime = os.environ.get("RUNTIME")
|
|
60
58
|
slug = os.environ.get("SLUG")
|
|
61
59
|
|
|
62
60
|
dataflow_config = ConfigurationManager('/dataflow/app/auth_config/dataflow_auth.cfg')
|
|
63
|
-
|
|
61
|
+
|
|
62
|
+
variable_api = None
|
|
63
|
+
if runtime and slug:
|
|
64
|
+
variable_api = dataflow_config.get_config_value("auth", "variable_ui_api")
|
|
65
|
+
elif host_name:
|
|
66
|
+
variable_api = dataflow_config.get_config_value("auth", "variable_manager_api")
|
|
67
|
+
else:
|
|
68
|
+
raise Exception("Cannot run dataflow methods here!")
|
|
69
|
+
|
|
64
70
|
if not variable_api:
|
|
65
71
|
print("[Dataflow.variable] Variable Unreachable")
|
|
66
72
|
return None
|
|
67
73
|
|
|
68
74
|
if runtime:
|
|
69
75
|
query_params = {
|
|
70
|
-
"
|
|
76
|
+
"key": variable_name,
|
|
71
77
|
"runtime": runtime,
|
|
72
78
|
"slug": slug
|
|
73
79
|
}
|
|
@@ -85,17 +91,27 @@ class Dataflow:
|
|
|
85
91
|
return None
|
|
86
92
|
|
|
87
93
|
query_params = {
|
|
88
|
-
"
|
|
89
|
-
"runtime": None,
|
|
90
|
-
"slug": None,
|
|
91
|
-
"created_by": user_name
|
|
94
|
+
"key": variable_name,
|
|
92
95
|
}
|
|
93
96
|
response = requests.get(variable_api, params=query_params)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
|
|
98
|
+
# Handle different HTTP status codes gracefully
|
|
99
|
+
if response.status_code == 404:
|
|
100
|
+
return None # Variable not found
|
|
101
|
+
elif response.status_code >= 500:
|
|
102
|
+
response.raise_for_status() # Let server errors propagate
|
|
103
|
+
elif response.status_code >= 400:
|
|
104
|
+
print(f"[Dataflow.variable] Client error {response.status_code} for variable '{variable_name}'")
|
|
98
105
|
return None
|
|
106
|
+
elif response.status_code != 200:
|
|
107
|
+
print(f"[Dataflow.variable] Unexpected status {response.status_code} for variable '{variable_name}'")
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
return response.text.strip().strip('"')
|
|
111
|
+
|
|
112
|
+
except requests.exceptions.RequestException as e:
|
|
113
|
+
raise RuntimeError(f"[Dataflow.variable] Failed to fetch variable '{variable_name}'") from e
|
|
114
|
+
|
|
99
115
|
except Exception as e:
|
|
100
116
|
print(f"[Dataflow.variable] Exception occurred: {e}")
|
|
101
117
|
return None
|
|
@@ -112,19 +128,20 @@ class Dataflow:
|
|
|
112
128
|
"""
|
|
113
129
|
try:
|
|
114
130
|
host_name = os.environ.get("HOSTNAME", "")
|
|
115
|
-
user_name = host_name.replace("jupyter-", "") if host_name.startswith("jupyter-") else host_name
|
|
116
131
|
runtime = os.environ.get("RUNTIME")
|
|
117
132
|
slug = os.environ.get("SLUG")
|
|
118
133
|
|
|
119
134
|
dataflow_config = ConfigurationManager('/dataflow/app/auth_config/dataflow_auth.cfg')
|
|
120
|
-
|
|
135
|
+
if runtime:
|
|
136
|
+
secret_api = dataflow_config.get_config_value("auth", "secret_ui_api")
|
|
137
|
+
else:
|
|
138
|
+
secret_api = dataflow_config.get_config_value("auth", "secret_manager_api")
|
|
121
139
|
if not secret_api:
|
|
122
140
|
print("[Dataflow.secret] Secret API Unreachable")
|
|
123
141
|
return None
|
|
124
142
|
|
|
125
143
|
query_params = {
|
|
126
|
-
"
|
|
127
|
-
"created_by": user_name
|
|
144
|
+
"key": secret_name
|
|
128
145
|
}
|
|
129
146
|
|
|
130
147
|
if runtime:
|
|
@@ -134,11 +151,22 @@ class Dataflow:
|
|
|
134
151
|
|
|
135
152
|
response = requests.get(secret_api, params=query_params)
|
|
136
153
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return
|
|
140
|
-
|
|
154
|
+
# Handle different HTTP status codes gracefully
|
|
155
|
+
if response.status_code == 404:
|
|
156
|
+
return None # Secret not found
|
|
157
|
+
elif response.status_code >= 500:
|
|
158
|
+
response.raise_for_status() # Let server errors propagate
|
|
159
|
+
elif response.status_code >= 400:
|
|
160
|
+
print(f"[Dataflow.secret] Client error {response.status_code} for secret '{secret_name}'")
|
|
161
|
+
return None
|
|
162
|
+
elif response.status_code != 200:
|
|
163
|
+
print(f"[Dataflow.secret] Unexpected status {response.status_code} for secret '{secret_name}'")
|
|
141
164
|
return None
|
|
165
|
+
|
|
166
|
+
return response.text.strip().strip('"')
|
|
167
|
+
|
|
168
|
+
except requests.exceptions.RequestException as e:
|
|
169
|
+
raise RuntimeError(f"[Dataflow.secret] Failed to fetch secret '{secret_name}'") from e
|
|
142
170
|
except Exception as e:
|
|
143
171
|
print(f"[Dataflow.secret] Exception occurred: {e}")
|
|
144
172
|
return None
|
|
@@ -156,26 +184,62 @@ class Dataflow:
|
|
|
156
184
|
"""
|
|
157
185
|
try:
|
|
158
186
|
host_name = os.environ["HOSTNAME"]
|
|
159
|
-
user_name=host_name.replace("jupyter-","")
|
|
160
187
|
runtime = os.environ.get("RUNTIME")
|
|
161
188
|
slug = os.environ.get("SLUG")
|
|
162
189
|
|
|
163
|
-
|
|
164
|
-
|
|
190
|
+
dataflow_config = ConfigurationManager('/dataflow/app/auth_config/dataflow_auth.cfg')
|
|
191
|
+
if runtime:
|
|
192
|
+
connection_api = dataflow_config.get_config_value("auth", "connection_ui_api")
|
|
193
|
+
elif host_name:
|
|
194
|
+
connection_api = dataflow_config.get_config_value("auth", "connection_manager_api")
|
|
195
|
+
else:
|
|
196
|
+
raise Exception("Cannot run dataflow methods here! HOSTNAME or RUNTIME env variable not set.")
|
|
197
|
+
|
|
198
|
+
query_params = {
|
|
199
|
+
"conn_id": conn_id
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if runtime:
|
|
203
|
+
query_params["runtime"] = runtime
|
|
204
|
+
if slug:
|
|
205
|
+
query_params["slug"] = slug
|
|
206
|
+
|
|
207
|
+
response = requests.get(connection_api, params=query_params)
|
|
208
|
+
|
|
209
|
+
# Handle different HTTP status codes gracefully
|
|
210
|
+
if response.status_code == 404:
|
|
211
|
+
raise RuntimeError(f"[Dataflow.connection] Connection '{conn_id}' not found!")
|
|
212
|
+
elif response.status_code >= 500:
|
|
213
|
+
response.raise_for_status() # Let server errors propagate
|
|
214
|
+
elif response.status_code >= 400:
|
|
215
|
+
raise RuntimeError(f"[Dataflow.connection] Client error {response.status_code} for connection '{conn_id}'")
|
|
216
|
+
elif response.status_code != 200:
|
|
217
|
+
raise RuntimeError(f"[Dataflow.connection] Unexpected status {response.status_code} for connection '{conn_id}'")
|
|
218
|
+
|
|
219
|
+
connection_details = response.json()
|
|
220
|
+
|
|
221
|
+
if not connection_details:
|
|
222
|
+
raise RuntimeError(f"[Dataflow.connection] Connection '{conn_id}' not found!")
|
|
223
|
+
|
|
224
|
+
if mode == "dict":
|
|
225
|
+
with open('/home/jovyan/log.txt', 'w') as log_file:
|
|
226
|
+
log_file.write(f"Connection details for {conn_id}: {connection_details}\n")
|
|
227
|
+
print(f"connection_details: {connection_details}")
|
|
228
|
+
return dict(connection_details)
|
|
165
229
|
|
|
166
|
-
conn_type =
|
|
167
|
-
username =
|
|
168
|
-
password =
|
|
169
|
-
host =
|
|
170
|
-
port =
|
|
171
|
-
database =
|
|
230
|
+
conn_type = connection_details['conn_type'].lower()
|
|
231
|
+
username = connection_details['login']
|
|
232
|
+
password = connection_details.get('password', '')
|
|
233
|
+
host = connection_details['host']
|
|
234
|
+
port = connection_details['port']
|
|
235
|
+
database = connection_details.get('schemas', '')
|
|
172
236
|
|
|
173
237
|
user_info = f"{username}:{password}@" if password else f"{username}@"
|
|
174
238
|
db_info = f"/{database}" if database else ""
|
|
175
239
|
|
|
176
240
|
connection_string = f"{conn_type}://{user_info}{host}:{port}{db_info}"
|
|
177
241
|
|
|
178
|
-
extra =
|
|
242
|
+
extra = connection_details.get('extra', '')
|
|
179
243
|
if extra:
|
|
180
244
|
try:
|
|
181
245
|
extra_params = json.loads(extra)
|
|
@@ -194,6 +258,66 @@ class Dataflow:
|
|
|
194
258
|
return connection_instance.get_engine()
|
|
195
259
|
elif mode == "session":
|
|
196
260
|
return next(connection_instance.get_session())
|
|
261
|
+
else:
|
|
262
|
+
raise ValueError(f"Unsupported mode: {mode}. Use 'session', 'engine', 'url'.")
|
|
197
263
|
|
|
264
|
+
except requests.exceptions.RequestException as e:
|
|
265
|
+
raise RuntimeError(f"[Dataflow.connection] Failed to fetch connection '{conn_id}'") from e
|
|
266
|
+
|
|
198
267
|
except Exception as e:
|
|
199
|
-
|
|
268
|
+
raise RuntimeError(f"[Dataflow.connection] Error connecting to '{conn_id}': {str(e)}") from e
|
|
269
|
+
|
|
270
|
+
def variable_or_secret(self, key: str):
|
|
271
|
+
"""
|
|
272
|
+
Retrieve a variable or secret by key.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
key (str): Key of the variable or secret
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
str or None: Value if found, None otherwise
|
|
279
|
+
"""
|
|
280
|
+
try:
|
|
281
|
+
host_name = os.environ.get("HOSTNAME", "")
|
|
282
|
+
runtime = os.environ.get("RUNTIME")
|
|
283
|
+
slug = os.environ.get("SLUG")
|
|
284
|
+
|
|
285
|
+
dataflow_config = ConfigurationManager('/dataflow/app/auth_config/dataflow_auth.cfg')
|
|
286
|
+
if runtime and slug:
|
|
287
|
+
variableorsecret_api = dataflow_config.get_config_value("auth", "variableorsecret_ui_api")
|
|
288
|
+
query_params = {
|
|
289
|
+
"key": key,
|
|
290
|
+
"runtime": runtime,
|
|
291
|
+
"slug": slug
|
|
292
|
+
}
|
|
293
|
+
elif host_name:
|
|
294
|
+
variableorsecret_api = dataflow_config.get_config_value("auth", "variableorsecret_manager_api")
|
|
295
|
+
query_params = {
|
|
296
|
+
"key": key
|
|
297
|
+
}
|
|
298
|
+
else:
|
|
299
|
+
raise Exception("Cannot run dataflow methods here!")
|
|
300
|
+
|
|
301
|
+
if not variableorsecret_api:
|
|
302
|
+
print("[Dataflow.variable_or_secret] Variable/Secret Unreachable")
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
response = requests.get(variableorsecret_api, params=query_params)
|
|
306
|
+
|
|
307
|
+
# Handle different HTTP status codes gracefully
|
|
308
|
+
if response.status_code == 404:
|
|
309
|
+
return None # Variable/secret not found
|
|
310
|
+
elif response.status_code >= 500:
|
|
311
|
+
response.raise_for_status() # Let server errors propagate
|
|
312
|
+
elif response.status_code >= 400:
|
|
313
|
+
print(f"[Dataflow.variable_or_secret] Client error {response.status_code} for key '{key}'")
|
|
314
|
+
return None
|
|
315
|
+
elif response.status_code != 200:
|
|
316
|
+
print(f"[Dataflow.variable_or_secret] Unexpected status {response.status_code} for key '{key}'")
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
response_text = response.text.strip().strip('"')
|
|
320
|
+
return response_text
|
|
321
|
+
|
|
322
|
+
except requests.exceptions.RequestException as e:
|
|
323
|
+
raise RuntimeError(f"[Dataflow.variable_or_secret] Failed to fetch '{key}'") from e
|
|
File without changes
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""schemas/connection.py"""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, field_validator
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ConnectionType(str, Enum):
|
|
10
|
+
"""Enum for supported connection types."""
|
|
11
|
+
POSTGRESQL = "PostgreSQL"
|
|
12
|
+
MYSQL = "MySQL"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConnectionBase(BaseModel):
|
|
16
|
+
"""Base connection model with common fields."""
|
|
17
|
+
conn_id: str
|
|
18
|
+
conn_type: ConnectionType
|
|
19
|
+
description: Optional[str] = None
|
|
20
|
+
host: str
|
|
21
|
+
schemas: Optional[str] = None
|
|
22
|
+
password: str
|
|
23
|
+
login: str
|
|
24
|
+
port: int
|
|
25
|
+
extra: Optional[str] = None
|
|
26
|
+
|
|
27
|
+
@field_validator("conn_id")
|
|
28
|
+
def validate_conn_id(cls, v) -> str:
|
|
29
|
+
import re
|
|
30
|
+
if not isinstance(v, str):
|
|
31
|
+
raise ValueError("Connection ID must be a string.")
|
|
32
|
+
if len(v) > 20:
|
|
33
|
+
raise ValueError("Connection ID must be at most 20 characters long.")
|
|
34
|
+
if not re.fullmatch(r"[A-Za-z0-9-]+", v):
|
|
35
|
+
raise ValueError(
|
|
36
|
+
"Connection ID can only contain letters, numbers, and hyphens (-)!"
|
|
37
|
+
)
|
|
38
|
+
return v
|
|
39
|
+
|
|
40
|
+
@field_validator("conn_type")
|
|
41
|
+
def validate_conn_type(cls, v) -> ConnectionType:
|
|
42
|
+
if isinstance(v, str):
|
|
43
|
+
try:
|
|
44
|
+
return ConnectionType(v)
|
|
45
|
+
except ValueError:
|
|
46
|
+
raise ValueError(f'conn_type must be one of {[e.value for e in ConnectionType]}')
|
|
47
|
+
return v
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ConnectionSave(ConnectionBase):
|
|
51
|
+
"""Model for creating a new connection."""
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ConnectionUpdate(BaseModel):
|
|
56
|
+
"""Model for updating an existing connection."""
|
|
57
|
+
conn_type: Optional[ConnectionType] = None
|
|
58
|
+
description: Optional[str] = None
|
|
59
|
+
host: Optional[str] = None
|
|
60
|
+
schemas: Optional[str] = None
|
|
61
|
+
login: Optional[str] = None
|
|
62
|
+
password: Optional[str] = None
|
|
63
|
+
port: Optional[int] = None
|
|
64
|
+
extra: Optional[str] = None
|
|
65
|
+
|
|
66
|
+
@field_validator("conn_type")
|
|
67
|
+
def validate_conn_type(cls, v) -> Optional[ConnectionType]:
|
|
68
|
+
if v is None:
|
|
69
|
+
return v
|
|
70
|
+
if isinstance(v, str):
|
|
71
|
+
# Convert string to enum if needed
|
|
72
|
+
try:
|
|
73
|
+
return ConnectionType(v)
|
|
74
|
+
except ValueError:
|
|
75
|
+
raise ValueError(f'conn_type must be one of {[e.value for e in ConnectionType]}')
|
|
76
|
+
return v
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ConnectionRead(ConnectionBase):
|
|
80
|
+
"""Model for reading/displaying connection data."""
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
class Config:
|
|
84
|
+
from_attributes = True
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""schemas/git_ssh.py"""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, field_validator
|
|
4
|
+
from typing import Optional, Literal
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SSHBase(BaseModel):
|
|
9
|
+
"""Base SSH key model with common fields."""
|
|
10
|
+
key_name: str
|
|
11
|
+
description: Optional[str] = None
|
|
12
|
+
|
|
13
|
+
@field_validator("key_name")
|
|
14
|
+
def validate_key_name(cls, v) -> str:
|
|
15
|
+
import re
|
|
16
|
+
if not isinstance(v, str):
|
|
17
|
+
raise ValueError("SSH key name must be a string.")
|
|
18
|
+
if len(v) > 20:
|
|
19
|
+
raise ValueError("SSH key name must be at most 20 characters long.")
|
|
20
|
+
if not re.fullmatch(r"[A-Za-z0-9-]+", v):
|
|
21
|
+
raise ValueError(
|
|
22
|
+
"SSH key name can only contain letters, numbers, and hyphens (-)!"
|
|
23
|
+
)
|
|
24
|
+
return v
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SSHSave(SSHBase):
|
|
28
|
+
"""Model for creating a new SSH key."""
|
|
29
|
+
public_key: str
|
|
30
|
+
private_key: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SSHUpdate(BaseModel):
|
|
34
|
+
"""Model for updating an existing SSH key."""
|
|
35
|
+
description: Optional[str] = None
|
|
36
|
+
public_key: Optional[str] = None
|
|
37
|
+
private_key: Optional[str] = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SSHRead(SSHBase):
|
|
41
|
+
"""Model for reading/displaying SSH key data."""
|
|
42
|
+
public_key: str
|
|
43
|
+
private_key: str
|
|
44
|
+
created_date: Optional[datetime] = None
|
|
45
|
+
|
|
46
|
+
class Config:
|
|
47
|
+
from_attributes = True
|
|
48
|
+
|
|
49
|
+
class Config:
|
|
50
|
+
from_attributes = True
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""schemas/secret.py"""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, field_validator
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SecretBase(BaseModel):
|
|
9
|
+
"""Base secret model with common fields."""
|
|
10
|
+
key: str
|
|
11
|
+
value: str
|
|
12
|
+
description: Optional[str] = None
|
|
13
|
+
|
|
14
|
+
@field_validator("key")
|
|
15
|
+
def validate_key(cls, v) -> str:
|
|
16
|
+
import re
|
|
17
|
+
if not isinstance(v, str):
|
|
18
|
+
raise ValueError("Secret key must be a string.")
|
|
19
|
+
if len(v) > 20:
|
|
20
|
+
raise ValueError("Secret key must be at most 20 characters long.")
|
|
21
|
+
if not re.fullmatch(r"[A-Za-z0-9-]+", v):
|
|
22
|
+
raise ValueError(
|
|
23
|
+
"Secret key can only contain letters, numbers, and hyphens (-)!"
|
|
24
|
+
)
|
|
25
|
+
return v
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SecretSave(SecretBase):
|
|
29
|
+
"""Model for creating a new secret."""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SecretUpdate(BaseModel):
|
|
34
|
+
"""Model for updating an existing secret."""
|
|
35
|
+
value: Optional[str] = None
|
|
36
|
+
description: Optional[str] = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SecretRead(SecretBase):
|
|
40
|
+
"""Model for reading/displaying secret data."""
|
|
41
|
+
created_date: Optional[datetime] = None
|
|
42
|
+
|
|
43
|
+
class Config:
|
|
44
|
+
from_attributes = True
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# secrets_manager/__init__.py
|
|
2
|
+
|
|
3
|
+
from .factory import get_secret_manager
|
|
4
|
+
from .service import SecretsService
|
|
5
|
+
|
|
6
|
+
# 1. Call the factory to get the configured low-level secret manager
|
|
7
|
+
# (e.g., an instance of AWSSecretsManager or AzureKeyVault).
|
|
8
|
+
# This happens only once when the package is first imported.
|
|
9
|
+
secret_manager_instance = get_secret_manager()
|
|
10
|
+
|
|
11
|
+
# 2. Create the single, high-level service instance that the rest of
|
|
12
|
+
# your application will use. It wraps the low-level instance.
|
|
13
|
+
secrets_service = SecretsService(secret_manager=secret_manager_instance)
|
|
@@ -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
|