dataflow-core 2.1.14__py3-none-any.whl → 2.1.15__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 +29 -44
- dataflow/dataflow.py +45 -69
- dataflow/environment.py +20 -11
- dataflow/models/__init__.py +5 -3
- dataflow/models/app_types.py +8 -3
- dataflow/models/connection.py +5 -4
- dataflow/models/dataflow_zone.py +2 -3
- dataflow/models/environment.py +64 -21
- dataflow/models/git_ssh.py +2 -1
- dataflow/models/org_associations.py +38 -0
- dataflow/models/organization.py +78 -0
- dataflow/models/pinned_projects.py +2 -2
- dataflow/models/project_details.py +9 -6
- dataflow/models/recent_project_studio.py +1 -1
- dataflow/models/role.py +12 -6
- dataflow/models/role_server.py +2 -5
- dataflow/models/role_zone.py +8 -3
- dataflow/models/server_config.py +9 -5
- dataflow/models/team.py +10 -4
- dataflow/models/user.py +49 -12
- dataflow/models/user_team.py +1 -4
- dataflow/models/variables.py +6 -4
- dataflow/secrets_manager/factory.py +14 -8
- dataflow/secrets_manager/providers/gcp_manager.py +332 -0
- dataflow/secrets_manager/service.py +11 -9
- dataflow/utils/get_current_user.py +51 -28
- {dataflow_core-2.1.14.dist-info → dataflow_core-2.1.15.dist-info}/METADATA +4 -1
- dataflow_core-2.1.15.dist-info/RECORD +63 -0
- {dataflow_core-2.1.14.dist-info → dataflow_core-2.1.15.dist-info}/top_level.txt +1 -0
- dfmigration/__init__.py +0 -0
- dfmigration/env.py +45 -0
- dfmigration/versions/001_initial_baseline_migration.py +20 -0
- dfmigration/versions/__init__.py +0 -0
- dataflow/models/user_environment.py +0 -16
- dataflow_core-2.1.14.dist-info/RECORD +0 -57
- {dataflow_core-2.1.14.dist-info → dataflow_core-2.1.15.dist-info}/WHEEL +0 -0
- {dataflow_core-2.1.14.dist-info → dataflow_core-2.1.15.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import os, base64, json, atexit
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from google.cloud import secretmanager
|
|
4
|
+
from google.cloud.secretmanager_v1 import SecretManagerServiceClient
|
|
5
|
+
from google.cloud.secretmanager_v1.types import Secret, SecretPayload
|
|
6
|
+
from google.api_core.exceptions import (
|
|
7
|
+
AlreadyExists,
|
|
8
|
+
NotFound,
|
|
9
|
+
PermissionDenied,
|
|
10
|
+
Forbidden,
|
|
11
|
+
ResourceExhausted,
|
|
12
|
+
InvalidArgument,
|
|
13
|
+
FailedPrecondition
|
|
14
|
+
)
|
|
15
|
+
import json
|
|
16
|
+
from ..interface import SecretManager
|
|
17
|
+
from ...utils.exceptions import (
|
|
18
|
+
SecretNotFoundException,
|
|
19
|
+
SecretAlreadyExistsException,
|
|
20
|
+
SecretManagerAuthException,
|
|
21
|
+
SecretManagerServiceException
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def _setup_gcp_credentials():
|
|
25
|
+
"""Setup GCP credentials from base64 encoded JSON environment variable"""
|
|
26
|
+
|
|
27
|
+
# Only run if GOOGLE_APPLICATION_CREDENTIALS is not already set
|
|
28
|
+
if os.getenv('GOOGLE_APPLICATION_CREDENTIALS'):
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
# Get base64 encoded JSON credentials
|
|
32
|
+
encoded_json = os.getenv('GOOGLE_APPLICATION_CREDENTIALS_JSON')
|
|
33
|
+
|
|
34
|
+
if encoded_json:
|
|
35
|
+
try:
|
|
36
|
+
# Decode base64 to JSON string
|
|
37
|
+
json_credentials = base64.b64decode(encoded_json).decode('utf-8')
|
|
38
|
+
|
|
39
|
+
# Validate it's valid JSON
|
|
40
|
+
json.loads(json_credentials) # Just to validate
|
|
41
|
+
|
|
42
|
+
# Create credentials file in home directory
|
|
43
|
+
home_dir = Path.home()
|
|
44
|
+
credentials_dir = home_dir / '.gcp'
|
|
45
|
+
credentials_dir.mkdir(exist_ok=True) # Create .gcp directory if it doesn't exist
|
|
46
|
+
|
|
47
|
+
credentials_path = credentials_dir / 'credentials.json'
|
|
48
|
+
|
|
49
|
+
# Write JSON string to credentials file
|
|
50
|
+
with open(credentials_path, 'w') as f:
|
|
51
|
+
f.write(json_credentials)
|
|
52
|
+
|
|
53
|
+
# Set the standard Google environment variable that the SDK looks for
|
|
54
|
+
os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = str(credentials_path)
|
|
55
|
+
|
|
56
|
+
# Clean up file on exit
|
|
57
|
+
atexit.register(lambda: credentials_path.unlink() if credentials_path.exists() else None)
|
|
58
|
+
|
|
59
|
+
print(f"GCP credentials decoded and configured at {credentials_path}")
|
|
60
|
+
|
|
61
|
+
except Exception as e:
|
|
62
|
+
print(f"Error setting up GCP credentials: {e}")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class GCPSecretsManager(SecretManager):
|
|
66
|
+
"""Google Cloud Platform Secrets Manager implementation."""
|
|
67
|
+
|
|
68
|
+
def __init__(self, project_id: str, region: str):
|
|
69
|
+
"""Initialize the GCP Secret Manager client.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
project_id: The GCP project ID where secrets will be stored.
|
|
73
|
+
region: The GCP region where secrets will be stored.
|
|
74
|
+
"""
|
|
75
|
+
self.project_id = project_id
|
|
76
|
+
self.region = region
|
|
77
|
+
try:
|
|
78
|
+
_setup_gcp_credentials()
|
|
79
|
+
self.client = secretmanager.SecretManagerServiceClient()
|
|
80
|
+
except PermissionDenied as e:
|
|
81
|
+
raise SecretManagerAuthException("initialize_gcp_client", original_error=str(e))
|
|
82
|
+
except Exception as e:
|
|
83
|
+
raise SecretManagerServiceException("initialize_gcp_client", original_error=str(e))
|
|
84
|
+
|
|
85
|
+
def _get_secret_path(self, vault_path: str) -> str:
|
|
86
|
+
"""Get the full secret path in GCP format.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
vault_path: The path/name of the secret.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
The full path to the secret in GCP format.
|
|
93
|
+
"""
|
|
94
|
+
return f"projects/{self.project_id}/secrets/{vault_path}"
|
|
95
|
+
|
|
96
|
+
def _get_secret_version_path(self, vault_path: str, version: str = "latest") -> str:
|
|
97
|
+
"""Get the full path to a specific secret version.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
vault_path: The path/name of the secret.
|
|
101
|
+
version: The version of the secret (default is "latest").
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
The full path to the secret version in GCP format.
|
|
105
|
+
"""
|
|
106
|
+
return f"{self._get_secret_path(vault_path)}/versions/{version}"
|
|
107
|
+
|
|
108
|
+
def create_secret(self, vault_path: str, secret_data: dict) -> str:
|
|
109
|
+
"""Create a new secret.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
vault_path: The path/name of the secret.
|
|
113
|
+
region: The region where the secret will be stored.
|
|
114
|
+
secret_data: The data to store in the secret.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
A success message.
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
SecretAlreadyExistsException: If the secret already exists.
|
|
121
|
+
SecretManagerAuthException: If there are permission issues.
|
|
122
|
+
SecretManagerServiceException: For other service errors.
|
|
123
|
+
"""
|
|
124
|
+
try:
|
|
125
|
+
# Convert dictionary to JSON string before saving
|
|
126
|
+
secret_string = json.dumps(secret_data)
|
|
127
|
+
|
|
128
|
+
# First create the secret
|
|
129
|
+
parent = f"projects/{self.project_id}"
|
|
130
|
+
secret = Secret(
|
|
131
|
+
replication={
|
|
132
|
+
"user_managed": {
|
|
133
|
+
"replicas": [
|
|
134
|
+
{"location": self.region }
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
self.client.create_secret(
|
|
141
|
+
request={
|
|
142
|
+
"parent": parent,
|
|
143
|
+
"secret_id": vault_path,
|
|
144
|
+
"secret": secret
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Then add the secret version with the data
|
|
149
|
+
secret_path = self._get_secret_path(vault_path)
|
|
150
|
+
self.client.add_secret_version(
|
|
151
|
+
request={
|
|
152
|
+
"parent": secret_path,
|
|
153
|
+
"payload": {"data": secret_string.encode("UTF-8")}
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
return "Secret created successfully"
|
|
158
|
+
except AlreadyExists as e:
|
|
159
|
+
raise SecretAlreadyExistsException("secret", vault_path, original_error=str(e))
|
|
160
|
+
except PermissionDenied as e:
|
|
161
|
+
raise SecretManagerAuthException("create_secret", original_error=str(e))
|
|
162
|
+
except Forbidden as e:
|
|
163
|
+
raise SecretManagerAuthException("create_secret", original_error=str(e))
|
|
164
|
+
except FailedPrecondition as e:
|
|
165
|
+
# Handle case where secret might be in recovery/deleted state
|
|
166
|
+
if "pending deletion" in str(e).lower() or "scheduled for deletion" in str(e).lower():
|
|
167
|
+
raise SecretAlreadyExistsException("secret", vault_path, original_error=str(e), is_scheduled_for_deletion=True)
|
|
168
|
+
else:
|
|
169
|
+
raise SecretManagerServiceException("create_secret", original_error=str(e))
|
|
170
|
+
except ResourceExhausted as e:
|
|
171
|
+
raise SecretManagerServiceException("create_secret", original_error=str(e))
|
|
172
|
+
except Exception as e:
|
|
173
|
+
raise SecretManagerServiceException("create_secret", original_error=str(e))
|
|
174
|
+
|
|
175
|
+
def get_secret_by_key(self, vault_path: str) -> dict:
|
|
176
|
+
"""Get a secret by its key.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
vault_path: The path/name of the secret.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
The secret data as a dictionary.
|
|
183
|
+
|
|
184
|
+
Raises:
|
|
185
|
+
SecretNotFoundException: If the secret doesn't exist.
|
|
186
|
+
SecretManagerAuthException: If there are permission issues.
|
|
187
|
+
SecretManagerServiceException: For other service errors.
|
|
188
|
+
"""
|
|
189
|
+
try:
|
|
190
|
+
# Get the latest version of the secret
|
|
191
|
+
name = self._get_secret_version_path(vault_path)
|
|
192
|
+
response = self.client.access_secret_version(request={"name": name})
|
|
193
|
+
|
|
194
|
+
# Decode the payload and convert JSON string back to dictionary
|
|
195
|
+
secret_string = response.payload.data.decode("UTF-8")
|
|
196
|
+
secret_data = json.loads(secret_string)
|
|
197
|
+
return secret_data
|
|
198
|
+
except NotFound as e:
|
|
199
|
+
raise SecretNotFoundException("secret", vault_path, original_error=str(e))
|
|
200
|
+
except PermissionDenied as e:
|
|
201
|
+
raise SecretManagerAuthException("get_secret", original_error=str(e))
|
|
202
|
+
except Forbidden as e:
|
|
203
|
+
raise SecretManagerAuthException("get_secret", original_error=str(e))
|
|
204
|
+
except InvalidArgument as e:
|
|
205
|
+
raise SecretManagerServiceException("get_secret", original_error=str(e))
|
|
206
|
+
except json.JSONDecodeError as e:
|
|
207
|
+
raise SecretManagerServiceException("get_secret", original_error=str(e))
|
|
208
|
+
except Exception as e:
|
|
209
|
+
raise SecretManagerServiceException("get_secret", original_error=str(e))
|
|
210
|
+
|
|
211
|
+
def update_secret(self, vault_path: str, update_data: dict) -> str:
|
|
212
|
+
"""Update an existing secret.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
vault_path: The path/name of the secret.
|
|
216
|
+
update_data: The data to update in the secret.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
A success message.
|
|
220
|
+
|
|
221
|
+
Raises:
|
|
222
|
+
SecretNotFoundException: If the secret doesn't exist.
|
|
223
|
+
SecretManagerAuthException: If there are permission issues.
|
|
224
|
+
SecretManagerServiceException: For other service errors.
|
|
225
|
+
"""
|
|
226
|
+
try:
|
|
227
|
+
# Get current secret data
|
|
228
|
+
current_data = self.get_secret_by_key(vault_path)
|
|
229
|
+
|
|
230
|
+
# Update with new data
|
|
231
|
+
current_data.update(update_data)
|
|
232
|
+
|
|
233
|
+
# Convert updated dictionary to JSON string
|
|
234
|
+
updated_string = json.dumps(current_data)
|
|
235
|
+
|
|
236
|
+
# Add a new version of the secret with updated data
|
|
237
|
+
secret_path = self._get_secret_path(vault_path)
|
|
238
|
+
self.client.add_secret_version(
|
|
239
|
+
request={
|
|
240
|
+
"parent": secret_path,
|
|
241
|
+
"payload": {"data": updated_string.encode("UTF-8")}
|
|
242
|
+
}
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
return "Secret updated successfully"
|
|
246
|
+
except SecretNotFoundException:
|
|
247
|
+
raise
|
|
248
|
+
except PermissionDenied as e:
|
|
249
|
+
raise SecretManagerAuthException("update_secret", original_error=str(e))
|
|
250
|
+
except Forbidden as e:
|
|
251
|
+
raise SecretManagerAuthException("update_secret", original_error=str(e))
|
|
252
|
+
except NotFound as e:
|
|
253
|
+
raise SecretNotFoundException("secret", vault_path, original_error=str(e))
|
|
254
|
+
except InvalidArgument as e:
|
|
255
|
+
raise SecretManagerServiceException("update_secret", original_error=str(e))
|
|
256
|
+
except json.JSONDecodeError as e:
|
|
257
|
+
raise SecretManagerServiceException("update_secret", original_error=str(e))
|
|
258
|
+
except Exception as e:
|
|
259
|
+
raise SecretManagerServiceException("update_secret", original_error=str(e))
|
|
260
|
+
|
|
261
|
+
def delete_secret(self, vault_path: str) -> str:
|
|
262
|
+
"""Delete a secret.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
vault_path: The path/name of the secret.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
A success message.
|
|
269
|
+
|
|
270
|
+
Raises:
|
|
271
|
+
SecretNotFoundException: If the secret doesn't exist.
|
|
272
|
+
SecretManagerAuthException: If there are permission issues.
|
|
273
|
+
SecretManagerServiceException: For other service errors.
|
|
274
|
+
"""
|
|
275
|
+
try:
|
|
276
|
+
# Get the full path to the secret
|
|
277
|
+
name = self._get_secret_path(vault_path)
|
|
278
|
+
|
|
279
|
+
# For git-ssh secrets, destroy without recovery
|
|
280
|
+
if "git-ssh" in vault_path:
|
|
281
|
+
# Get all versions to destroy them permanently
|
|
282
|
+
versions = self.client.list_secret_versions(request={"parent": name})
|
|
283
|
+
for version in versions:
|
|
284
|
+
if version.state == secretmanager.SecretVersion.State.ENABLED:
|
|
285
|
+
version_name = f"{name}/versions/{version.name.split('/')[-1]}"
|
|
286
|
+
self.client.destroy_secret_version(request={"name": version_name})
|
|
287
|
+
|
|
288
|
+
# Delete the secret itself
|
|
289
|
+
self.client.delete_secret(request={"name": name})
|
|
290
|
+
else:
|
|
291
|
+
# For regular secrets, use the default 7-day recovery window
|
|
292
|
+
self.client.delete_secret(request={
|
|
293
|
+
"name": name,
|
|
294
|
+
# In GCP, the recovery window is configured at the service level,
|
|
295
|
+
# not per API call, so we don't specify it here
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
return "Secret deleted successfully"
|
|
299
|
+
except NotFound as e:
|
|
300
|
+
raise SecretNotFoundException("secret", vault_path, original_error=str(e))
|
|
301
|
+
except PermissionDenied as e:
|
|
302
|
+
raise SecretManagerAuthException("delete_secret", original_error=str(e))
|
|
303
|
+
except Forbidden as e:
|
|
304
|
+
raise SecretManagerAuthException("delete_secret", original_error=str(e))
|
|
305
|
+
except InvalidArgument as e:
|
|
306
|
+
raise SecretManagerServiceException("delete_secret", original_error=str(e))
|
|
307
|
+
except Exception as e:
|
|
308
|
+
raise SecretManagerServiceException("delete_secret", original_error=str(e))
|
|
309
|
+
|
|
310
|
+
def test_connection(self, vault_path: str) -> str:
|
|
311
|
+
"""Test the connection to the secret manager by attempting to access a secret.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
vault_path: The path/name of the secret to test.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
The status of the secret.
|
|
318
|
+
|
|
319
|
+
Raises:
|
|
320
|
+
SecretNotFoundException: If the secret doesn't exist.
|
|
321
|
+
SecretManagerAuthException: If there are permission issues.
|
|
322
|
+
SecretManagerServiceException: For other service errors.
|
|
323
|
+
"""
|
|
324
|
+
try:
|
|
325
|
+
secret = self.get_secret_by_key(vault_path)
|
|
326
|
+
return secret.get('status', 'Unknown')
|
|
327
|
+
except SecretNotFoundException:
|
|
328
|
+
raise
|
|
329
|
+
except (SecretManagerAuthException, SecretManagerServiceException):
|
|
330
|
+
raise
|
|
331
|
+
except Exception as e:
|
|
332
|
+
raise SecretManagerServiceException("test_connection", original_error=str(e))
|
|
@@ -117,27 +117,29 @@ class _BaseAccessor(ABC):
|
|
|
117
117
|
# These classes implement the logic for building the vault path based on the context.
|
|
118
118
|
# --------------------------------------------------------------------------
|
|
119
119
|
class _RuntimeAccessor(_BaseAccessor):
|
|
120
|
-
def __init__(self, secret_manager: SecretManager, runtime_env: str, slug: str = None):
|
|
120
|
+
def __init__(self, secret_manager: SecretManager, org_id: str, runtime_env: str, slug: str = None):
|
|
121
121
|
super().__init__(secret_manager)
|
|
122
|
+
self.org_id = org_id
|
|
122
123
|
self.runtime_env = runtime_env
|
|
123
124
|
self.slug = slug
|
|
124
125
|
|
|
125
126
|
def _get_vault_path(self, secret_type: str, key: str) -> str:
|
|
126
127
|
# Special case for git-ssh in runtime context
|
|
127
128
|
if secret_type == "git-ssh":
|
|
128
|
-
return f"{self.runtime_env}-{secret_type}-{self.slug}"
|
|
129
|
+
return f"{self.org_id}-{self.runtime_env}-{secret_type}-{self.slug}"
|
|
129
130
|
|
|
130
131
|
# Standard format for all other secret types
|
|
131
132
|
context = self.slug if self.slug else "global"
|
|
132
|
-
return f"{self.runtime_env}-{context}-{secret_type}-{key}"
|
|
133
|
+
return f"{self.org_id}-{self.runtime_env}-{context}-{secret_type}-{key}"
|
|
133
134
|
|
|
134
135
|
class _StudioAccessor(_BaseAccessor):
|
|
135
|
-
def __init__(self, secret_manager: SecretManager, user_name: str):
|
|
136
|
+
def __init__(self, secret_manager: SecretManager, org_id: str, user_name: str):
|
|
136
137
|
super().__init__(secret_manager)
|
|
138
|
+
self.org_id = org_id
|
|
137
139
|
self.user_name = user_name
|
|
138
140
|
|
|
139
141
|
def _get_vault_path(self, secret_type: str, key: str) -> str:
|
|
140
|
-
return f"{self.user_name}-{secret_type}-{key}"
|
|
142
|
+
return f"{self.org_id}-{self.user_name}-{secret_type}-{key}"
|
|
141
143
|
|
|
142
144
|
# --------------------------------------------------------------------------
|
|
143
145
|
# PUBLIC INTERFACE CLASS
|
|
@@ -147,10 +149,10 @@ class SecretsService:
|
|
|
147
149
|
def __init__(self, secret_manager: SecretManager):
|
|
148
150
|
self._secret_manager = secret_manager
|
|
149
151
|
|
|
150
|
-
def runtime(self, env: str, slug: str = None) -> _RuntimeAccessor:
|
|
152
|
+
def runtime(self, org_id: int, env: str, slug: str = None) -> _RuntimeAccessor:
|
|
151
153
|
"""Sets the context to RUNTIME and returns the appropriate accessor."""
|
|
152
|
-
return _RuntimeAccessor(self._secret_manager, env, slug)
|
|
154
|
+
return _RuntimeAccessor(self._secret_manager, str(org_id), env, slug)
|
|
153
155
|
|
|
154
|
-
def studio(self, user: str) -> _StudioAccessor:
|
|
156
|
+
def studio(self, org_id: int, user: str) -> _StudioAccessor:
|
|
155
157
|
"""Sets the context to STUDIO and returns the appropriate accessor."""
|
|
156
|
-
return _StudioAccessor(self._secret_manager, user)
|
|
158
|
+
return _StudioAccessor(self._secret_manager, str(org_id), user)
|
|
@@ -1,37 +1,60 @@
|
|
|
1
|
-
from fastapi import HTTPException
|
|
1
|
+
from fastapi import HTTPException
|
|
2
2
|
from sqlalchemy.orm import Session
|
|
3
|
-
from
|
|
4
|
-
from dataflow.models import
|
|
3
|
+
from sqlalchemy import and_
|
|
4
|
+
from dataflow.models import (
|
|
5
|
+
user as m_user,
|
|
6
|
+
session as m_session,
|
|
7
|
+
org_associations as m_org_associations,
|
|
8
|
+
role as m_role,
|
|
9
|
+
organization as m_organization
|
|
10
|
+
)
|
|
5
11
|
|
|
6
12
|
def get_user_from_session(session_id: str, db: Session):
|
|
7
13
|
"""
|
|
8
|
-
Retrieve
|
|
9
|
-
|
|
10
|
-
Args:
|
|
11
|
-
session_id (str): The unique session identifier
|
|
12
|
-
db (Session): Database session
|
|
13
|
-
|
|
14
|
-
Returns:
|
|
15
|
-
User: User object if found
|
|
16
|
-
|
|
17
|
-
Raises:
|
|
18
|
-
HTTPException: If session is invalid or user not found
|
|
14
|
+
Retrieve user information based on the session ID.
|
|
19
15
|
"""
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
user = (
|
|
17
|
+
db.query(
|
|
18
|
+
m_user.User,
|
|
19
|
+
m_org_associations.OrganizationUser.role_id,
|
|
20
|
+
m_role.Role.name,
|
|
21
|
+
m_role.Role.base_role,
|
|
22
|
+
m_org_associations.OrganizationUser.active_server_id,
|
|
23
|
+
m_org_associations.OrganizationUser.show_server_page,
|
|
24
|
+
m_org_associations.OrganizationUser.active_env_short_name,
|
|
25
|
+
m_org_associations.OrganizationUser.active_env_type,
|
|
26
|
+
m_org_associations.OrganizationUser.monthly_allocation,
|
|
27
|
+
m_organization.Organization.uid
|
|
28
|
+
)
|
|
29
|
+
.join(m_session.Session, m_session.Session.user_id == m_user.User.user_id)
|
|
30
|
+
.outerjoin(
|
|
31
|
+
m_org_associations.OrganizationUser,
|
|
32
|
+
and_(
|
|
33
|
+
m_org_associations.OrganizationUser.user_id == m_user.User.user_id,
|
|
34
|
+
m_org_associations.OrganizationUser.org_id == m_user.User.active_org_id
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
.outerjoin(m_role.Role, m_role.Role.id == m_org_associations.OrganizationUser.role_id)
|
|
38
|
+
.outerjoin(
|
|
39
|
+
m_organization.Organization,
|
|
40
|
+
m_organization.Organization.id == m_user.User.active_org_id # join to Organization
|
|
41
|
+
)
|
|
42
|
+
.filter(m_session.Session.session_id == session_id)
|
|
43
|
+
.first()
|
|
44
|
+
)
|
|
23
45
|
|
|
24
|
-
user = db.query(m_user.User).filter(m_user.User.user_id == session_record.user_id).first()
|
|
25
46
|
if not user:
|
|
26
|
-
raise HTTPException(status_code=
|
|
27
|
-
|
|
28
|
-
base_role = user.role_details.base_role
|
|
29
|
-
role_id = user.role_details.id
|
|
30
|
-
role_name = user.role_details.name
|
|
31
|
-
user.base_role = base_role
|
|
32
|
-
user.role = role_name
|
|
33
|
-
user.role_id = role_id
|
|
34
|
-
|
|
35
|
-
return user
|
|
47
|
+
raise HTTPException(status_code=401, detail="Invalid session")
|
|
36
48
|
|
|
49
|
+
user_obj, role_id, role_name, base_role, active_server_id, show_server_page, active_env_short_name, active_env_type, monthly_allocation, uid = user
|
|
50
|
+
user_obj.role_id = role_id
|
|
51
|
+
user_obj.role = role_name
|
|
52
|
+
user_obj.base_role = base_role
|
|
53
|
+
user_obj.current_server_id = active_server_id
|
|
54
|
+
user_obj.show_server_page = show_server_page
|
|
55
|
+
user_obj.active_env = active_env_short_name
|
|
56
|
+
user_obj.active_env_type = active_env_type
|
|
57
|
+
user_obj.monthly_allocation = monthly_allocation
|
|
58
|
+
user_obj.org_uid = str(uid)
|
|
37
59
|
|
|
60
|
+
return user_obj
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dataflow-core
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.15
|
|
4
4
|
Summary: Dataflow core package
|
|
5
5
|
Author: Dataflow
|
|
6
6
|
Author-email:
|
|
7
7
|
Requires-Dist: sqlalchemy
|
|
8
|
+
Requires-Dist: alembic
|
|
8
9
|
Requires-Dist: boto3
|
|
9
10
|
Requires-Dist: psycopg2-binary
|
|
10
11
|
Requires-Dist: pymysql
|
|
11
12
|
Requires-Dist: requests
|
|
12
13
|
Requires-Dist: azure-identity
|
|
13
14
|
Requires-Dist: azure-keyvault-secrets
|
|
15
|
+
Requires-Dist: google-auth
|
|
16
|
+
Requires-Dist: google-cloud-secret-manager
|
|
14
17
|
Dynamic: author
|
|
15
18
|
Dynamic: requires-dist
|
|
16
19
|
Dynamic: summary
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
authenticator/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
authenticator/dataflowairflowauthenticator.py,sha256=gEdCiL2yJQ7lYvAwbrjcAkccVMfehoMJldw9eU7cc2s,2243
|
|
3
|
+
authenticator/dataflowhubauthenticator.py,sha256=0dt8-yeFtty1kq50qxBHafzwuWy8fG00QVnq6svH12c,12913
|
|
4
|
+
authenticator/dataflowsupersetauthenticator.py,sha256=NkAmDaIc-ui-qEolu4xz_UY7P_2g8111hwNjPvAOW1Q,2839
|
|
5
|
+
dataflow/__init__.py,sha256=WTRg8HMpMWSgxYJ9ZGVldx4k07fAbta3mBmZ1hG9mWE,30
|
|
6
|
+
dataflow/configuration.py,sha256=7To6XwH1eESiYp39eqPcswXWwrdBUdPF6xN6WnazOF0,663
|
|
7
|
+
dataflow/database_manager.py,sha256=tJHMuOZ9Muskrh9t4uLRlTuFU0VkHAzoHlGP5DORIC4,899
|
|
8
|
+
dataflow/dataflow.py,sha256=EzeOMtMCqG0WJ0vld6ArKbKNFAmA9sbjBqVJ8sd2sQw,13026
|
|
9
|
+
dataflow/db.py,sha256=73ojGqpCTRVTlPszD73Ozhjih_BI2KTHmazqxxL6iWk,3780
|
|
10
|
+
dataflow/environment.py,sha256=7XiSojSIOnMOz5YVk-k6w1yQCKbBTTvUxv5cnn6cqJg,28830
|
|
11
|
+
dataflow/models/__init__.py,sha256=FHus-b2oTgLSJ9H7dOCNJ14XpjTzHARqK_Vwh8VhCtM,1178
|
|
12
|
+
dataflow/models/app_types.py,sha256=BNGtjwpQU6MSpBGp0pDzUI4uDZNqvE3_x33itIInasM,689
|
|
13
|
+
dataflow/models/blacklist_library.py,sha256=B2oi3Z8GcR_glhLAyinFk0W8c9txXvm3uOER6dY-q7I,991
|
|
14
|
+
dataflow/models/connection.py,sha256=c1zfwanuZRf6NvtA7lVU8z3taj_QX-bQSO1zbQzcBI0,1174
|
|
15
|
+
dataflow/models/dataflow_zone.py,sha256=PHvZRpINTBN3NTxHYsSuTR-v2G2jZHF0zIIIrQVaBJw,762
|
|
16
|
+
dataflow/models/environment.py,sha256=vkuFBRY9pZNMRXx3F4ZI6P2Nr-UNpqwzZkT-gM4NhyU,5097
|
|
17
|
+
dataflow/models/environment_status.py,sha256=lvPDNUsUoTW9D97B07aKqJQHRKp4LvPM28pQDMPH1ac,536
|
|
18
|
+
dataflow/models/git_ssh.py,sha256=xRkzK8lZ4Fa25zhE3Y9J78BiWv6xtd10YDBNf1XDKXE,797
|
|
19
|
+
dataflow/models/org_associations.py,sha256=iih8xpbcWTsvOdBTs9LjW8kqPtFy7sBhIh7IU2pMGvo,1991
|
|
20
|
+
dataflow/models/organization.py,sha256=hrvwQQuypM20eFX48QPmly1N0SM_pgML38BYA98vMb8,3446
|
|
21
|
+
dataflow/models/pinned_projects.py,sha256=R_Euj98J2LzWrmedQ-A7mAluDyWes5VWt5dgqUs7EWc,639
|
|
22
|
+
dataflow/models/pod_activity.py,sha256=4NQplUtckS4d2Uc-Iyi8PfgciblHiExbXo_VTMaukB8,751
|
|
23
|
+
dataflow/models/pod_session_history.py,sha256=-O8DHtH1AtGLntJ1shI6eiciTJGL_vYvG3gfupvr0zY,778
|
|
24
|
+
dataflow/models/project_details.py,sha256=C7b2VTOiF1BiUfgixIxdyZ2fMncCR7klQdTpF1rB6Wc,1342
|
|
25
|
+
dataflow/models/recent_project_studio.py,sha256=5E5f55FXOq7vC76GNw-Bf8dUKmpFjwkkD-qthPGFKhM,836
|
|
26
|
+
dataflow/models/recent_projects.py,sha256=OFd5MSRXVRHs9UbvUNoJBBnh9rgsJ0lwE23wm5_Hc5w,321
|
|
27
|
+
dataflow/models/role.py,sha256=Kw9zqpG3R5nzwPXzg8MaoJGwDzN9r0rrOAKcVpsdkeY,1307
|
|
28
|
+
dataflow/models/role_server.py,sha256=Qz9VqGSpVGvfLz9_lKITUfkiuR7pmVc8SIIfYrDPDso,547
|
|
29
|
+
dataflow/models/role_zone.py,sha256=kQ9tUxxpwQ_VdbUNKqyacQ87SovsKKP5K6-EFrSTphk,962
|
|
30
|
+
dataflow/models/server_config.py,sha256=FHO4y0Lm7wBcYDQYn1144duOkihIdRTLPIK0sfDEXIo,1768
|
|
31
|
+
dataflow/models/session.py,sha256=c8TI6qXsM8utzp5vSQtAOXJSbgasnyu-a0qSAvA-rWs,459
|
|
32
|
+
dataflow/models/team.py,sha256=u5VKOxi-0wGgxssIhXr1Ydn3X9IEAHNWxz029KaVm5g,780
|
|
33
|
+
dataflow/models/user.py,sha256=ggJftOVLBEs_6fhn1_13Joe9aL32KMnJwHfBMzeMAOA,2959
|
|
34
|
+
dataflow/models/user_team.py,sha256=vD6BsiMfx1iIMQiT8fpwhsQ8AGuPuBO83F22Cy7j-nU,524
|
|
35
|
+
dataflow/models/variables.py,sha256=qCyiVbzC3Wd2onLx-eCY36VWiVytxxWvYld6_3ki5H8,1572
|
|
36
|
+
dataflow/schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
37
|
+
dataflow/schemas/connection.py,sha256=ut2sqz06yOjmFKzHry92FEt7DN09Bj30GYse35__Cuw,2467
|
|
38
|
+
dataflow/schemas/git_ssh.py,sha256=N1O7HM6ZbygIBZn2rKvNR0e7IM3ZJMAH6aJtjaghDr0,1283
|
|
39
|
+
dataflow/schemas/secret.py,sha256=wMSCn6zoBHS-R4NoKwljq2JUad8p9JY542UNJFa86X8,1183
|
|
40
|
+
dataflow/scripts/clone_environment.sh,sha256=Qy0GylsA3kUVUL_L1MirxIWujOFhT1tikKqXNtCTWd4,506
|
|
41
|
+
dataflow/scripts/create_environment.sh,sha256=3FHgNplJuEZvyTsLqlCJNX9oyfXgsfqn80VZk2xtvso,828
|
|
42
|
+
dataflow/scripts/update_environment.sh,sha256=2dtn2xlNi6frpig-sqlGE1_IKRbbkqYOCpf_qyMKKII,992
|
|
43
|
+
dataflow/secrets_manager/__init__.py,sha256=idGqIDtYl0De2WIK9Obl-N7SDPSYtVM0D-wXfZjCiy4,559
|
|
44
|
+
dataflow/secrets_manager/factory.py,sha256=LblshkGG9q2C3RHYp0QykianUtpOOQz7sBdlerutyWY,2479
|
|
45
|
+
dataflow/secrets_manager/interface.py,sha256=HhrKpQrprWIbDsVfU_qc59OXmSIuHXv106OXv6-Epqc,506
|
|
46
|
+
dataflow/secrets_manager/service.py,sha256=A4PgiSS1bzlzXPi_GvsiepChULqAa5yONGkaU-d5MLM,7419
|
|
47
|
+
dataflow/secrets_manager/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
48
|
+
dataflow/secrets_manager/providers/aws_manager.py,sha256=16peXyKeuAjv2RVTMUjrzArPYENK9Zu7jREWVgMfScA,8671
|
|
49
|
+
dataflow/secrets_manager/providers/azure_manager.py,sha256=sWOz-7ALnLt6vyM3lt14GBpzpmDnlH3hkdqtuApqkgU,9430
|
|
50
|
+
dataflow/secrets_manager/providers/gcp_manager.py,sha256=AJgotHZRQraxtmfJX1Z8u2Gcr7KJLRJTN_qbth3A5Xk,13738
|
|
51
|
+
dataflow/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
52
|
+
dataflow/utils/exceptions.py,sha256=8GRFoYZ5dPGQckVm2znaHpPi0ZAs69fK-RGKukEsapk,4432
|
|
53
|
+
dataflow/utils/get_current_user.py,sha256=iPyUpEpuxitdmFwvaPiRWIlII8JaaGoxUhQcmNWi0vI,2319
|
|
54
|
+
dataflow/utils/logger.py,sha256=7BFrOq5Oiqn8P4XZbgJzMP5O07d2fpdECbbfsjrUuHw,1213
|
|
55
|
+
dfmigration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
56
|
+
dfmigration/env.py,sha256=w_3Kzr3LJCU5l1zBGsNcoNGZyR8fZEtMRsxE-cEqfHQ,1110
|
|
57
|
+
dfmigration/versions/001_initial_baseline_migration.py,sha256=lxjs7LZLs9-c7FQj7t7t49EOfLPPHMLI6iNp0PIBMRA,299
|
|
58
|
+
dfmigration/versions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
59
|
+
dataflow_core-2.1.15.dist-info/METADATA,sha256=Urk3FJfV79VaX794Lgc-q1d-2JvOhUQnWGzwmElyUpQ,463
|
|
60
|
+
dataflow_core-2.1.15.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
61
|
+
dataflow_core-2.1.15.dist-info/entry_points.txt,sha256=ppj_EIbYrJJwCPg1kfdsZk5q1N-Ejfis1neYrnjhO8o,117
|
|
62
|
+
dataflow_core-2.1.15.dist-info/top_level.txt,sha256=-gGoIBh-bUMtzOdna2jsDOdkE2CRu3w3aA7yEFCmNAI,35
|
|
63
|
+
dataflow_core-2.1.15.dist-info/RECORD,,
|
dfmigration/__init__.py
ADDED
|
File without changes
|
dfmigration/env.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from sqlalchemy import engine_from_config, pool
|
|
2
|
+
from alembic import context
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
config = context.config
|
|
6
|
+
target_metadata = None
|
|
7
|
+
|
|
8
|
+
def get_url():
|
|
9
|
+
return os.getenv('DATABASE_URL')
|
|
10
|
+
|
|
11
|
+
def run_migrations_offline() -> None:
|
|
12
|
+
url = get_url()
|
|
13
|
+
context.configure(
|
|
14
|
+
url=url,
|
|
15
|
+
target_metadata=target_metadata,
|
|
16
|
+
literal_binds=True,
|
|
17
|
+
dialect_opts={"paramstyle": "named"},
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
with context.begin_transaction():
|
|
21
|
+
context.run_migrations()
|
|
22
|
+
|
|
23
|
+
def run_migrations_online() -> None:
|
|
24
|
+
configuration = config.get_section(config.config_ini_section) or {}
|
|
25
|
+
configuration['sqlalchemy.url'] = get_url()
|
|
26
|
+
|
|
27
|
+
connectable = engine_from_config(
|
|
28
|
+
configuration,
|
|
29
|
+
prefix="sqlalchemy.",
|
|
30
|
+
poolclass=pool.NullPool,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
with connectable.connect() as connection:
|
|
34
|
+
context.configure(
|
|
35
|
+
connection=connection,
|
|
36
|
+
target_metadata=target_metadata
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
with context.begin_transaction():
|
|
40
|
+
context.run_migrations()
|
|
41
|
+
|
|
42
|
+
if context.is_offline_mode():
|
|
43
|
+
run_migrations_offline()
|
|
44
|
+
else:
|
|
45
|
+
run_migrations_online()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Initial baseline migration
|
|
2
|
+
|
|
3
|
+
Revision ID: 001
|
|
4
|
+
Revises: None
|
|
5
|
+
Create Date: 2025-09-10 15:00:00.000000
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from alembic import op
|
|
9
|
+
import sqlalchemy as sa
|
|
10
|
+
|
|
11
|
+
revision = '001'
|
|
12
|
+
down_revision = None
|
|
13
|
+
branch_labels = None
|
|
14
|
+
depends_on = None
|
|
15
|
+
|
|
16
|
+
def upgrade() -> None:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
def downgrade() -> None:
|
|
20
|
+
pass
|
|
File without changes
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
from sqlalchemy import Column, Integer, String, DateTime
|
|
2
|
-
from sqlalchemy.sql.schema import ForeignKey
|
|
3
|
-
from sqlalchemy.sql.expression import func
|
|
4
|
-
from dataflow.db import Base
|
|
5
|
-
|
|
6
|
-
class UserEnvironment(Base):
|
|
7
|
-
"""
|
|
8
|
-
Table USER_ENVIRONMENT
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
__tablename__ = 'USER_ENVIRONMENT'
|
|
12
|
-
|
|
13
|
-
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
14
|
-
user_id = Column(Integer, ForeignKey('USER.user_id', ondelete="CASCADE"), nullable=False)
|
|
15
|
-
env_name = Column(String)
|
|
16
|
-
timestamp = Column(DateTime, server_default=func.now(), onupdate=func.now())
|