ethyca-fides 2.67.1rc1__py2.py3-none-any.whl → 2.67.2b0__py2.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 ethyca-fides might be problematic. Click here for more details.
- {ethyca_fides-2.67.1rc1.dist-info → ethyca_fides-2.67.2b0.dist-info}/METADATA +2 -2
- {ethyca_fides-2.67.1rc1.dist-info → ethyca_fides-2.67.2b0.dist-info}/RECORD +101 -101
- fides/_version.py +3 -3
- fides/api/db/crud.py +24 -41
- fides/api/main.py +2 -1
- fides/api/models/manual_task/manual_task.py +3 -6
- fides/api/models/privacy_request/privacy_request.py +78 -0
- fides/api/service/privacy_request/dsr_package/dsr_report_builder.py +3 -19
- fides/api/task/create_request_tasks.py +5 -3
- fides/api/task/filter_results.py +1 -1
- fides/api/task/manual/manual_task_address.py +4 -2
- fides/api/task/manual/manual_task_graph_task.py +87 -83
- fides/api/task/manual/manual_task_utils.py +84 -140
- fides/api/util/storage_util.py +19 -0
- fides/ui-build/static/admin/404.html +1 -1
- fides/ui-build/static/admin/add-systems/manual.html +1 -1
- fides/ui-build/static/admin/add-systems/multiple.html +1 -1
- fides/ui-build/static/admin/add-systems.html +1 -1
- fides/ui-build/static/admin/consent/configure/add-vendors.html +1 -1
- fides/ui-build/static/admin/consent/configure.html +1 -1
- fides/ui-build/static/admin/consent/privacy-experience/[id].html +1 -1
- fides/ui-build/static/admin/consent/privacy-experience/new.html +1 -1
- fides/ui-build/static/admin/consent/privacy-experience.html +1 -1
- fides/ui-build/static/admin/consent/privacy-notices/[id].html +1 -1
- fides/ui-build/static/admin/consent/privacy-notices/new.html +1 -1
- fides/ui-build/static/admin/consent/privacy-notices.html +1 -1
- fides/ui-build/static/admin/consent/properties.html +1 -1
- fides/ui-build/static/admin/consent/reporting.html +1 -1
- fides/ui-build/static/admin/consent.html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn]/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn].html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/projects.html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/resources/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/resources.html +1 -1
- fides/ui-build/static/admin/data-catalog.html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center/[monitorId]/[systemId].html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center/[monitorId].html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center.html +1 -1
- fides/ui-build/static/admin/data-discovery/activity.html +1 -1
- fides/ui-build/static/admin/data-discovery/detection/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-discovery/detection.html +1 -1
- fides/ui-build/static/admin/data-discovery/discovery/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-discovery/discovery.html +1 -1
- fides/ui-build/static/admin/datamap.html +1 -1
- fides/ui-build/static/admin/dataset/[datasetId]/[collectionName]/[...subfieldNames].html +1 -1
- fides/ui-build/static/admin/dataset/[datasetId]/[collectionName].html +1 -1
- fides/ui-build/static/admin/dataset/[datasetId].html +1 -1
- fides/ui-build/static/admin/dataset/new.html +1 -1
- fides/ui-build/static/admin/dataset.html +1 -1
- fides/ui-build/static/admin/datastore-connection/[id].html +1 -1
- fides/ui-build/static/admin/datastore-connection/new.html +1 -1
- fides/ui-build/static/admin/datastore-connection.html +1 -1
- fides/ui-build/static/admin/index.html +1 -1
- fides/ui-build/static/admin/integrations/[id].html +1 -1
- fides/ui-build/static/admin/integrations.html +1 -1
- fides/ui-build/static/admin/login/[provider].html +1 -1
- fides/ui-build/static/admin/login.html +1 -1
- fides/ui-build/static/admin/messaging/[id].html +1 -1
- fides/ui-build/static/admin/messaging/add-template.html +1 -1
- fides/ui-build/static/admin/messaging.html +1 -1
- fides/ui-build/static/admin/poc/ant-components.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/AntForm.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikAntFormItem.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikControlled.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikField.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikSpreadField.html +1 -1
- fides/ui-build/static/admin/poc/forms.html +1 -1
- fides/ui-build/static/admin/poc/table-migration.html +1 -1
- fides/ui-build/static/admin/privacy-requests/[id].html +1 -1
- fides/ui-build/static/admin/privacy-requests/configure/messaging.html +1 -1
- fides/ui-build/static/admin/privacy-requests/configure/storage.html +1 -1
- fides/ui-build/static/admin/privacy-requests/configure.html +1 -1
- fides/ui-build/static/admin/privacy-requests.html +1 -1
- fides/ui-build/static/admin/properties/[id].html +1 -1
- fides/ui-build/static/admin/properties/add-property.html +1 -1
- fides/ui-build/static/admin/properties.html +1 -1
- fides/ui-build/static/admin/reporting/datamap.html +1 -1
- fides/ui-build/static/admin/settings/about/alpha.html +1 -1
- fides/ui-build/static/admin/settings/about.html +1 -1
- fides/ui-build/static/admin/settings/consent/[configuration_id]/[purpose_id].html +1 -1
- fides/ui-build/static/admin/settings/consent.html +1 -1
- fides/ui-build/static/admin/settings/custom-fields.html +1 -1
- fides/ui-build/static/admin/settings/domain-records.html +1 -1
- fides/ui-build/static/admin/settings/domains.html +1 -1
- fides/ui-build/static/admin/settings/email-templates.html +1 -1
- fides/ui-build/static/admin/settings/locations.html +1 -1
- fides/ui-build/static/admin/settings/organization.html +1 -1
- fides/ui-build/static/admin/settings/regulations.html +1 -1
- fides/ui-build/static/admin/systems/configure/[id]/test-datasets.html +1 -1
- fides/ui-build/static/admin/systems/configure/[id].html +1 -1
- fides/ui-build/static/admin/systems.html +1 -1
- fides/ui-build/static/admin/taxonomy.html +1 -1
- fides/ui-build/static/admin/user-management/new.html +1 -1
- fides/ui-build/static/admin/user-management/profile/[id].html +1 -1
- fides/ui-build/static/admin/user-management.html +1 -1
- {ethyca_fides-2.67.1rc1.dist-info → ethyca_fides-2.67.2b0.dist-info}/WHEEL +0 -0
- {ethyca_fides-2.67.1rc1.dist-info → ethyca_fides-2.67.2b0.dist-info}/entry_points.txt +0 -0
- {ethyca_fides-2.67.1rc1.dist-info → ethyca_fides-2.67.2b0.dist-info}/licenses/LICENSE +0 -0
- {ethyca_fides-2.67.1rc1.dist-info → ethyca_fides-2.67.2b0.dist-info}/top_level.txt +0 -0
- /fides/ui-build/static/admin/_next/static/{-pDvglWsgqfWeQixOy5zJ → fnhwfkI-rGZMUPYfQ5ACP}/_buildManifest.js +0 -0
- /fides/ui-build/static/admin/_next/static/{-pDvglWsgqfWeQixOy5zJ → fnhwfkI-rGZMUPYfQ5ACP}/_ssgManifest.js +0 -0
|
@@ -13,7 +13,7 @@ from loguru import logger
|
|
|
13
13
|
|
|
14
14
|
from fides.api.models.privacy_request import PrivacyRequest
|
|
15
15
|
from fides.api.schemas.policy import ActionType
|
|
16
|
-
from fides.api.util.storage_util import StorageJSONEncoder
|
|
16
|
+
from fides.api.util.storage_util import StorageJSONEncoder, format_size
|
|
17
17
|
|
|
18
18
|
DSR_DIRECTORY = Path(__file__).parent.resolve()
|
|
19
19
|
|
|
@@ -204,7 +204,7 @@ class DsrReportBuilder:
|
|
|
204
204
|
|
|
205
205
|
file_size = attachment.get("file_size")
|
|
206
206
|
if isinstance(file_size, (int, float)):
|
|
207
|
-
file_size =
|
|
207
|
+
file_size = format_size(float(file_size))
|
|
208
208
|
else:
|
|
209
209
|
file_size = "Unknown"
|
|
210
210
|
|
|
@@ -321,22 +321,6 @@ class DsrReportBuilder:
|
|
|
321
321
|
|
|
322
322
|
return datasets
|
|
323
323
|
|
|
324
|
-
def _format_size(self, size_bytes: float) -> str:
|
|
325
|
-
"""
|
|
326
|
-
Format size in bytes to human readable format.
|
|
327
|
-
|
|
328
|
-
Args:
|
|
329
|
-
size_bytes: Size in bytes
|
|
330
|
-
|
|
331
|
-
Returns:
|
|
332
|
-
Formatted string with appropriate unit (B, KB, MB, GB)
|
|
333
|
-
"""
|
|
334
|
-
for unit in ["B", "KB", "MB", "GB"]:
|
|
335
|
-
if size_bytes < 1024.0:
|
|
336
|
-
return f"{size_bytes:.1f} {unit}"
|
|
337
|
-
size_bytes /= 1024.0
|
|
338
|
-
return f"{size_bytes:.1f} TB"
|
|
339
|
-
|
|
340
324
|
def generate(self) -> BytesIO:
|
|
341
325
|
"""
|
|
342
326
|
Processes the request and DSR data to build zip file containing the DSR report.
|
|
@@ -395,7 +379,7 @@ class DsrReportBuilder:
|
|
|
395
379
|
|
|
396
380
|
# Calculate time taken and file size
|
|
397
381
|
time_taken = time_module.time() - start_time
|
|
398
|
-
file_size =
|
|
382
|
+
file_size = format_size(float(len(self.baos.getvalue())))
|
|
399
383
|
|
|
400
384
|
logger.bind(time_to_generate=time_taken, dsr_package_size=file_size).info(
|
|
401
385
|
"DSR report generation complete."
|
|
@@ -35,7 +35,7 @@ from fides.api.task.deprecated_graph_task import format_data_use_map_for_caching
|
|
|
35
35
|
from fides.api.task.execute_request_tasks import log_task_queued, queue_request_task
|
|
36
36
|
from fides.api.task.manual.manual_task_address import ManualTaskAddress
|
|
37
37
|
from fides.api.task.manual.manual_task_utils import (
|
|
38
|
-
|
|
38
|
+
get_connection_configs_with_manual_tasks,
|
|
39
39
|
)
|
|
40
40
|
from fides.api.util.logger_context_utils import log_context
|
|
41
41
|
|
|
@@ -92,7 +92,7 @@ def build_access_networkx_digraph(
|
|
|
92
92
|
manual_nodes = [
|
|
93
93
|
addr
|
|
94
94
|
for addr in traversal_nodes.keys()
|
|
95
|
-
if
|
|
95
|
+
if ManualTaskAddress.is_manual_task_address(addr)
|
|
96
96
|
]
|
|
97
97
|
for manual_node in manual_nodes:
|
|
98
98
|
networkx_graph.add_edge(ROOT_COLLECTION_ADDRESS, manual_node)
|
|
@@ -472,7 +472,9 @@ def run_access_request(
|
|
|
472
472
|
)
|
|
473
473
|
|
|
474
474
|
# Snapshot manual task field instances for this privacy request
|
|
475
|
-
|
|
475
|
+
privacy_request.create_manual_task_instances(
|
|
476
|
+
session, get_connection_configs_with_manual_tasks(session)
|
|
477
|
+
)
|
|
476
478
|
|
|
477
479
|
# Save Access Request Tasks to the database
|
|
478
480
|
ready_tasks = persist_new_access_request_tasks(
|
fides/api/task/filter_results.py
CHANGED
|
@@ -39,7 +39,7 @@ def filter_data_categories(
|
|
|
39
39
|
continue
|
|
40
40
|
|
|
41
41
|
# Skip manual task data - it doesn't need filtering since it's controlled by field definitions
|
|
42
|
-
if
|
|
42
|
+
if ManualTaskAddress.is_manual_task_address(node_address):
|
|
43
43
|
filtered_access_results[node_address].extend(results)
|
|
44
44
|
continue
|
|
45
45
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
|
|
1
3
|
from fides.api.graph.config import CollectionAddress
|
|
2
4
|
|
|
3
5
|
|
|
@@ -20,7 +22,7 @@ class ManualTaskAddress:
|
|
|
20
22
|
return collection_name == ManualTaskAddress.MANUAL_DATA_COLLECTION
|
|
21
23
|
|
|
22
24
|
@staticmethod
|
|
23
|
-
def is_manual_task_address(address: CollectionAddress) -> bool:
|
|
25
|
+
def is_manual_task_address(address: Union[str, CollectionAddress]) -> bool:
|
|
24
26
|
"""Check if address represents manual task data"""
|
|
25
27
|
if isinstance(address, str):
|
|
26
28
|
# Handle string format "connection_key:collection_name"
|
|
@@ -33,7 +35,7 @@ class ManualTaskAddress:
|
|
|
33
35
|
return ManualTaskAddress._is_manual_data_collection(address.collection)
|
|
34
36
|
|
|
35
37
|
@staticmethod
|
|
36
|
-
def get_connection_key(address: CollectionAddress) -> str:
|
|
38
|
+
def get_connection_key(address: Union[str, CollectionAddress]) -> str:
|
|
37
39
|
"""Extract connection config key from manual task address"""
|
|
38
40
|
if not ManualTaskAddress.is_manual_task_address(address):
|
|
39
41
|
raise ValueError(f"Not a manual task address: {address}")
|
|
@@ -7,11 +7,11 @@ from fides.api.common_exceptions import AwaitingAsyncTaskCallback
|
|
|
7
7
|
from fides.api.models.attachment import AttachmentType
|
|
8
8
|
from fides.api.models.manual_task import (
|
|
9
9
|
ManualTask,
|
|
10
|
-
ManualTaskConfig,
|
|
11
10
|
ManualTaskConfigurationType,
|
|
12
11
|
ManualTaskEntityType,
|
|
13
12
|
ManualTaskFieldType,
|
|
14
13
|
ManualTaskInstance,
|
|
14
|
+
ManualTaskSubmission,
|
|
15
15
|
StatusType,
|
|
16
16
|
)
|
|
17
17
|
from fides.api.models.privacy_request import PrivacyRequest
|
|
@@ -23,6 +23,7 @@ from fides.api.task.manual.manual_task_utils import (
|
|
|
23
23
|
get_manual_task_for_connection_config,
|
|
24
24
|
)
|
|
25
25
|
from fides.api.util.collection_util import Row
|
|
26
|
+
from fides.api.util.storage_util import format_size
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
class ManualTaskGraphTask(GraphTask):
|
|
@@ -122,29 +123,36 @@ class ManualTaskGraphTask(GraphTask):
|
|
|
122
123
|
# request has started, while allowing different config types (access vs erasure)
|
|
123
124
|
# to have separate instances.
|
|
124
125
|
# ------------------------------------------------------------------
|
|
125
|
-
existing_task_instance = (
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
ManualTaskConfig.config_type == allowed_config_type,
|
|
134
|
-
)
|
|
135
|
-
.first()
|
|
126
|
+
existing_task_instance = next(
|
|
127
|
+
(
|
|
128
|
+
instance
|
|
129
|
+
for instance in privacy_request.manual_task_instances
|
|
130
|
+
if instance.task_id == manual_task.id
|
|
131
|
+
and instance.config.config_type == allowed_config_type
|
|
132
|
+
),
|
|
133
|
+
None,
|
|
136
134
|
)
|
|
137
135
|
if existing_task_instance:
|
|
138
136
|
# An instance already exists for this privacy request and config type – no need
|
|
139
137
|
# to create another one tied to a newer config version.
|
|
140
138
|
return
|
|
141
139
|
|
|
142
|
-
#
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
140
|
+
# If no existing instances, create a new one for the current config
|
|
141
|
+
# There will only be one config of each type per manual task
|
|
142
|
+
config = next(
|
|
143
|
+
(
|
|
144
|
+
config
|
|
145
|
+
for config in sorted(
|
|
146
|
+
manual_task.configs,
|
|
147
|
+
key=lambda c: c.version if hasattr(c, "version") else 0,
|
|
148
|
+
reverse=True,
|
|
149
|
+
)
|
|
150
|
+
if config.is_current and config.config_type == allowed_config_type
|
|
151
|
+
),
|
|
152
|
+
None,
|
|
153
|
+
)
|
|
147
154
|
|
|
155
|
+
if config:
|
|
148
156
|
ManualTaskInstance.create(
|
|
149
157
|
db=db,
|
|
150
158
|
data={
|
|
@@ -156,7 +164,6 @@ class ManualTaskGraphTask(GraphTask):
|
|
|
156
164
|
},
|
|
157
165
|
)
|
|
158
166
|
|
|
159
|
-
# pylint: disable=too-many-branches,too-many-nested-blocks
|
|
160
167
|
def _get_submitted_data(
|
|
161
168
|
self,
|
|
162
169
|
db: Session,
|
|
@@ -168,93 +175,90 @@ class ManualTaskGraphTask(GraphTask):
|
|
|
168
175
|
Check if all manual task instances have submissions for ALL fields and return aggregated data
|
|
169
176
|
Returns None if any field submissions are missing (all fields must be completed or skipped)
|
|
170
177
|
"""
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if size < 1024.0:
|
|
178
|
-
return f"{size:.1f} {unit}"
|
|
179
|
-
size /= 1024.0
|
|
180
|
-
return f"{size:.1f} PB"
|
|
181
|
-
|
|
182
|
-
candidate_instances: list[ManualTaskInstance] = (
|
|
183
|
-
db.query(ManualTaskInstance)
|
|
184
|
-
.filter(
|
|
185
|
-
ManualTaskInstance.task_id == manual_task.id,
|
|
186
|
-
ManualTaskInstance.entity_id == privacy_request.id,
|
|
187
|
-
ManualTaskInstance.entity_type == ManualTaskEntityType.privacy_request,
|
|
188
|
-
)
|
|
189
|
-
.all()
|
|
190
|
-
)
|
|
178
|
+
candidate_instances: list[ManualTaskInstance] = [
|
|
179
|
+
instance
|
|
180
|
+
for instance in privacy_request.manual_task_instances
|
|
181
|
+
if instance.task_id == manual_task.id
|
|
182
|
+
and instance.config.config_type == allowed_config_type
|
|
183
|
+
]
|
|
191
184
|
|
|
192
185
|
if not candidate_instances:
|
|
193
186
|
return None # No instance yet for this manual task
|
|
194
187
|
|
|
188
|
+
# Check for incomplete fields and update status in single pass
|
|
195
189
|
for inst in candidate_instances:
|
|
196
|
-
|
|
197
|
-
if not inst.config or inst.config.config_type != allowed_config_type:
|
|
198
|
-
continue
|
|
199
|
-
|
|
200
|
-
all_fields = inst.config.field_definitions or []
|
|
201
|
-
|
|
202
|
-
# Every field must have a submission
|
|
203
|
-
if not all(inst.get_submission_for_field(f.id) for f in all_fields):
|
|
190
|
+
if inst.incomplete_fields:
|
|
204
191
|
return None # At least one instance still incomplete
|
|
205
192
|
|
|
206
|
-
#
|
|
193
|
+
# Update status if needed
|
|
207
194
|
if inst.status != StatusType.completed:
|
|
208
195
|
inst.status = StatusType.completed
|
|
209
196
|
inst.save(db)
|
|
210
197
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
198
|
+
# Aggregate submission data from all instances
|
|
199
|
+
aggregated_data = self._aggregate_submission_data(candidate_instances)
|
|
200
|
+
return aggregated_data or None
|
|
201
|
+
|
|
202
|
+
def _aggregate_submission_data(
|
|
203
|
+
self, instances: list[ManualTaskInstance]
|
|
204
|
+
) -> dict[str, Any]:
|
|
205
|
+
"""Aggregate submission data from all instances into a single dictionary."""
|
|
206
|
+
aggregated_data: dict[str, Any] = {}
|
|
207
|
+
|
|
208
|
+
for inst in instances:
|
|
209
|
+
# Filter valid submissions and process them
|
|
210
|
+
valid_submissions = (
|
|
211
|
+
submission
|
|
212
|
+
for submission in inst.submissions
|
|
213
|
+
if (
|
|
214
|
+
submission.field
|
|
215
|
+
and submission.field.field_key
|
|
216
|
+
and isinstance(submission.data, dict)
|
|
217
|
+
)
|
|
218
|
+
)
|
|
215
219
|
|
|
220
|
+
for submission in valid_submissions:
|
|
216
221
|
field_key = submission.field.field_key
|
|
222
|
+
# We already checked isinstance(submission.data, dict) in valid_submissions
|
|
223
|
+
data_dict: dict[str, Any] = submission.data # type: ignore[assignment]
|
|
224
|
+
field_type = data_dict.get("field_type")
|
|
217
225
|
|
|
218
|
-
|
|
219
|
-
|
|
226
|
+
# Process field data based on type
|
|
227
|
+
aggregated_data[field_key] = (
|
|
228
|
+
self._process_attachment_field(submission)
|
|
229
|
+
if field_type == ManualTaskFieldType.attachment.value
|
|
230
|
+
else data_dict.get("value")
|
|
231
|
+
)
|
|
220
232
|
|
|
221
|
-
|
|
233
|
+
return aggregated_data
|
|
222
234
|
|
|
223
|
-
|
|
235
|
+
def _process_attachment_field(
|
|
236
|
+
self, submission: ManualTaskSubmission
|
|
237
|
+
) -> Optional[dict[str, dict[str, Any]]]:
|
|
238
|
+
"""Process attachment field and return attachment map or None."""
|
|
239
|
+
attachment_map: dict[str, dict[str, Any]] = {}
|
|
224
240
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
) as exc: # pylint: disable=broad-exception-caught
|
|
241
|
-
logger.warning(
|
|
242
|
-
"Error retrieving attachment {}: {}",
|
|
243
|
-
attachment.file_name,
|
|
244
|
-
str(exc),
|
|
245
|
-
)
|
|
246
|
-
|
|
247
|
-
aggregated_data[field_key] = attachment_map or None
|
|
248
|
-
else:
|
|
249
|
-
aggregated_data[field_key] = data_dict.get("value")
|
|
250
|
-
|
|
251
|
-
return aggregated_data if aggregated_data else None
|
|
241
|
+
for attachment in filter(
|
|
242
|
+
lambda a: a.attachment_type == AttachmentType.include_with_access_package,
|
|
243
|
+
submission.attachments,
|
|
244
|
+
):
|
|
245
|
+
try:
|
|
246
|
+
size, url = attachment.retrieve_attachment()
|
|
247
|
+
attachment_map[attachment.file_name] = {
|
|
248
|
+
"url": str(url) if url else None,
|
|
249
|
+
"size": (format_size(size) if size else "Unknown"),
|
|
250
|
+
}
|
|
251
|
+
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
252
|
+
logger.warning(
|
|
253
|
+
f"Error retrieving attachment {attachment.file_name}: {str(exc)}"
|
|
254
|
+
)
|
|
255
|
+
return attachment_map or None
|
|
252
256
|
|
|
253
257
|
def dry_run_task(self) -> int:
|
|
254
258
|
"""Return estimated row count for dry run - manual tasks don't have predictable counts"""
|
|
255
259
|
return 1 # Placeholder - manual tasks generate variable data
|
|
256
260
|
|
|
257
|
-
#
|
|
261
|
+
# Provide erasure support for manual tasks
|
|
258
262
|
@retry(action_type=ActionType.erasure, default_return=0)
|
|
259
263
|
def erasure_request(
|
|
260
264
|
self,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from loguru import logger
|
|
1
2
|
from sqlalchemy.orm import Session
|
|
2
3
|
|
|
3
4
|
from fides.api.graph.config import (
|
|
@@ -12,15 +13,7 @@ from fides.api.graph.traversal import TraversalNode
|
|
|
12
13
|
from fides.api.models.connectionconfig import ConnectionConfig
|
|
13
14
|
|
|
14
15
|
# Import application models
|
|
15
|
-
from fides.api.models.manual_task import
|
|
16
|
-
ManualTask,
|
|
17
|
-
ManualTaskConfig,
|
|
18
|
-
ManualTaskConfigurationType,
|
|
19
|
-
ManualTaskEntityType,
|
|
20
|
-
ManualTaskInstance,
|
|
21
|
-
)
|
|
22
|
-
from fides.api.models.privacy_request import PrivacyRequest
|
|
23
|
-
from fides.api.schemas.policy import ActionType
|
|
16
|
+
from fides.api.models.manual_task import ManualTask, ManualTaskConfigurationType
|
|
24
17
|
from fides.api.task.manual.manual_task_address import ManualTaskAddress
|
|
25
18
|
|
|
26
19
|
|
|
@@ -28,13 +21,18 @@ def get_connection_configs_with_manual_tasks(db: Session) -> list[ConnectionConf
|
|
|
28
21
|
"""
|
|
29
22
|
Get all connection configs that have manual tasks.
|
|
30
23
|
"""
|
|
31
|
-
|
|
24
|
+
logger.info("Querying for connection configs with manual tasks")
|
|
25
|
+
connection_configs = (
|
|
32
26
|
db.query(ConnectionConfig)
|
|
33
27
|
.join(ManualTask, ConnectionConfig.id == ManualTask.parent_entity_id)
|
|
34
28
|
.filter(ManualTask.parent_entity_type == "connection_config")
|
|
35
29
|
.filter(ConnectionConfig.disabled.is_(False))
|
|
36
30
|
.all()
|
|
37
31
|
)
|
|
32
|
+
logger.info(
|
|
33
|
+
f"Found {len(connection_configs)} connection configs with manual tasks: {[cc.key for cc in connection_configs]}"
|
|
34
|
+
)
|
|
35
|
+
return connection_configs
|
|
38
36
|
|
|
39
37
|
|
|
40
38
|
def get_manual_task_addresses(db: Session) -> list[CollectionAddress]:
|
|
@@ -47,12 +45,19 @@ def get_manual_task_addresses(db: Session) -> list[CollectionAddress]:
|
|
|
47
45
|
"""
|
|
48
46
|
# Get all connection configs that have manual tasks (excluding disabled ones)
|
|
49
47
|
connection_configs_with_manual_tasks = get_connection_configs_with_manual_tasks(db)
|
|
48
|
+
logger.debug(
|
|
49
|
+
f"Found {len(connection_configs_with_manual_tasks)} connection configs with manual tasks"
|
|
50
|
+
)
|
|
50
51
|
|
|
51
52
|
# Create addresses for all connections that have manual tasks
|
|
52
53
|
manual_task_addresses = []
|
|
53
54
|
for config in connection_configs_with_manual_tasks:
|
|
55
|
+
logger.info(f"Creating manual task address for connection config: {config.key}")
|
|
54
56
|
manual_task_addresses.append(ManualTaskAddress.create(config.key))
|
|
55
57
|
|
|
58
|
+
logger.info(
|
|
59
|
+
f"Created {len(manual_task_addresses)} manual task addresses: {manual_task_addresses}"
|
|
60
|
+
)
|
|
56
61
|
return manual_task_addresses
|
|
57
62
|
|
|
58
63
|
|
|
@@ -62,7 +67,11 @@ def get_manual_task_for_connection_config(
|
|
|
62
67
|
"""Get the ManualTask for a specific connection config,
|
|
63
68
|
the manual task/connection config relationship is 1:1.
|
|
64
69
|
"""
|
|
65
|
-
|
|
70
|
+
logger.info(
|
|
71
|
+
f"Looking for manual task for connection config: {connection_config_key}"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
manual_task = (
|
|
66
75
|
db.query(ManualTask)
|
|
67
76
|
.join(ConnectionConfig, ManualTask.parent_entity_id == ConnectionConfig.id)
|
|
68
77
|
.filter(
|
|
@@ -72,6 +81,17 @@ def get_manual_task_for_connection_config(
|
|
|
72
81
|
.one_or_none()
|
|
73
82
|
)
|
|
74
83
|
|
|
84
|
+
if manual_task:
|
|
85
|
+
logger.info(
|
|
86
|
+
f"Found manual task {manual_task.id} for connection {connection_config_key}"
|
|
87
|
+
)
|
|
88
|
+
else:
|
|
89
|
+
logger.warning(
|
|
90
|
+
f"No manual task found for connection config: {connection_config_key}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return manual_task
|
|
94
|
+
|
|
75
95
|
|
|
76
96
|
def create_manual_data_traversal_node(
|
|
77
97
|
db: Session, address: CollectionAddress
|
|
@@ -122,116 +142,6 @@ def create_manual_data_traversal_node(
|
|
|
122
142
|
return traversal_node
|
|
123
143
|
|
|
124
144
|
|
|
125
|
-
def create_manual_task_instances_for_privacy_request(
|
|
126
|
-
db: Session, privacy_request: PrivacyRequest
|
|
127
|
-
) -> list[ManualTaskInstance]:
|
|
128
|
-
"""Create ManualTaskInstance entries for all active manual tasks relevant to a privacy request."""
|
|
129
|
-
instances = []
|
|
130
|
-
|
|
131
|
-
# Get all connection configs that have manual tasks (excluding disabled ones)
|
|
132
|
-
connection_configs_with_manual_tasks = get_connection_configs_with_manual_tasks(db)
|
|
133
|
-
|
|
134
|
-
# Determine the privacy request type based on policy rules
|
|
135
|
-
has_access_rules = bool(
|
|
136
|
-
privacy_request.policy.get_rules_for_action(action_type=ActionType.access)
|
|
137
|
-
)
|
|
138
|
-
has_erasure_rules = bool(
|
|
139
|
-
privacy_request.policy.get_rules_for_action(action_type=ActionType.erasure)
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
for connection_config in connection_configs_with_manual_tasks:
|
|
143
|
-
manual_tasks = (
|
|
144
|
-
db.query(ManualTask)
|
|
145
|
-
.filter(
|
|
146
|
-
ManualTask.parent_entity_id == connection_config.id,
|
|
147
|
-
ManualTask.parent_entity_type == "connection_config",
|
|
148
|
-
)
|
|
149
|
-
.all()
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
for manual_task in manual_tasks:
|
|
153
|
-
# Get the active config for this manual task, filtered by request type
|
|
154
|
-
active_config_query = db.query(ManualTaskConfig).filter(
|
|
155
|
-
ManualTaskConfig.task_id == manual_task.id,
|
|
156
|
-
ManualTaskConfig.is_current.is_(True),
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
# Filter by configuration type based on privacy request type
|
|
160
|
-
if has_access_rules and has_erasure_rules:
|
|
161
|
-
# If both access and erasure rules exist, include both types
|
|
162
|
-
active_config_query = active_config_query.filter(
|
|
163
|
-
ManualTaskConfig.config_type.in_(
|
|
164
|
-
[
|
|
165
|
-
ManualTaskConfigurationType.access_privacy_request,
|
|
166
|
-
ManualTaskConfigurationType.erasure_privacy_request,
|
|
167
|
-
]
|
|
168
|
-
)
|
|
169
|
-
)
|
|
170
|
-
elif has_access_rules:
|
|
171
|
-
# Only access rules - only include access configurations
|
|
172
|
-
active_config_query = active_config_query.filter(
|
|
173
|
-
ManualTaskConfig.config_type
|
|
174
|
-
== ManualTaskConfigurationType.access_privacy_request
|
|
175
|
-
)
|
|
176
|
-
elif has_erasure_rules:
|
|
177
|
-
# Only erasure rules - only include erasure configurations
|
|
178
|
-
active_config_query = active_config_query.filter(
|
|
179
|
-
ManualTaskConfig.config_type
|
|
180
|
-
== ManualTaskConfigurationType.erasure_privacy_request
|
|
181
|
-
)
|
|
182
|
-
else:
|
|
183
|
-
# No relevant rules - skip this manual task
|
|
184
|
-
continue
|
|
185
|
-
|
|
186
|
-
active_configs = active_config_query.all()
|
|
187
|
-
|
|
188
|
-
if not active_configs:
|
|
189
|
-
continue # Skip if no active configs
|
|
190
|
-
|
|
191
|
-
# Create instances for each active config
|
|
192
|
-
for active_config in active_configs:
|
|
193
|
-
# Check if instance already exists for this config
|
|
194
|
-
existing_instance = (
|
|
195
|
-
db.query(ManualTaskInstance)
|
|
196
|
-
.filter(
|
|
197
|
-
ManualTaskInstance.entity_id == privacy_request.id,
|
|
198
|
-
ManualTaskInstance.entity_type == "privacy_request",
|
|
199
|
-
ManualTaskInstance.task_id == manual_task.id,
|
|
200
|
-
ManualTaskInstance.config_id == active_config.id,
|
|
201
|
-
)
|
|
202
|
-
.first()
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
if not existing_instance:
|
|
206
|
-
instance = ManualTaskInstance(
|
|
207
|
-
entity_id=privacy_request.id,
|
|
208
|
-
entity_type=ManualTaskEntityType.privacy_request,
|
|
209
|
-
task_id=manual_task.id,
|
|
210
|
-
config_id=active_config.id,
|
|
211
|
-
)
|
|
212
|
-
db.add(instance)
|
|
213
|
-
instances.append(instance)
|
|
214
|
-
|
|
215
|
-
if instances:
|
|
216
|
-
db.commit()
|
|
217
|
-
|
|
218
|
-
return instances
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
def get_manual_task_instances_for_privacy_request(
|
|
222
|
-
db: Session, privacy_request: PrivacyRequest
|
|
223
|
-
) -> list[ManualTaskInstance]:
|
|
224
|
-
"""Get all manual task instances for a privacy request."""
|
|
225
|
-
return (
|
|
226
|
-
db.query(ManualTaskInstance)
|
|
227
|
-
.filter(
|
|
228
|
-
ManualTaskInstance.entity_id == privacy_request.id,
|
|
229
|
-
ManualTaskInstance.entity_type == "privacy_request",
|
|
230
|
-
)
|
|
231
|
-
.all()
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
|
|
235
145
|
def create_manual_task_artificial_graphs(
|
|
236
146
|
db: Session,
|
|
237
147
|
) -> list:
|
|
@@ -254,11 +164,18 @@ def create_manual_task_artificial_graphs(
|
|
|
254
164
|
List of GraphDataset objects representing manual tasks as root nodes
|
|
255
165
|
"""
|
|
256
166
|
|
|
167
|
+
logger.debug("Creating manual task artificial graphs")
|
|
257
168
|
manual_task_graphs = []
|
|
258
169
|
manual_addresses = get_manual_task_addresses(db)
|
|
170
|
+
logger.debug(
|
|
171
|
+
f"Found {len(manual_addresses)} manual task addresses: {manual_addresses}"
|
|
172
|
+
)
|
|
259
173
|
|
|
260
174
|
for address in manual_addresses:
|
|
261
175
|
connection_key = address.dataset
|
|
176
|
+
logger.debug(
|
|
177
|
+
f"Processing manual task address: {address} for connection: {connection_key}"
|
|
178
|
+
)
|
|
262
179
|
|
|
263
180
|
# Get manual tasks for this connection to determine fields
|
|
264
181
|
manual_task = get_manual_task_for_connection_config(db, connection_key)
|
|
@@ -268,28 +185,47 @@ def create_manual_task_artificial_graphs(
|
|
|
268
185
|
|
|
269
186
|
# Manual task collections act as root nodes - they don't need identity dependencies
|
|
270
187
|
# since they provide manually-entered data rather than consuming identity data.
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
188
|
+
if manual_task:
|
|
189
|
+
logger.debug(
|
|
190
|
+
f"Processing manual task {manual_task.id} with {len(manual_task.configs)} configs"
|
|
191
|
+
)
|
|
192
|
+
current_configs = [
|
|
193
|
+
config
|
|
194
|
+
for config in manual_task.configs
|
|
195
|
+
if config.is_current
|
|
196
|
+
and config.config_type
|
|
197
|
+
in [
|
|
198
|
+
ManualTaskConfigurationType.access_privacy_request,
|
|
199
|
+
ManualTaskConfigurationType.erasure_privacy_request,
|
|
200
|
+
]
|
|
201
|
+
]
|
|
202
|
+
logger.debug(
|
|
203
|
+
f"Found {len(current_configs)} current configs for manual task {manual_task.id}"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
for config in current_configs:
|
|
207
|
+
logger.debug(
|
|
208
|
+
f"Processing config {config.id} with {len(config.field_definitions)} fields"
|
|
289
209
|
)
|
|
290
|
-
|
|
210
|
+
for field in config.field_definitions:
|
|
211
|
+
# Create a scalar field for each manual task field
|
|
212
|
+
field_metadata = field.field_metadata or {}
|
|
213
|
+
data_categories = field_metadata.get("data_categories", [])
|
|
214
|
+
|
|
215
|
+
scalar_field = ScalarField(
|
|
216
|
+
name=field.field_key,
|
|
217
|
+
data_categories=data_categories,
|
|
218
|
+
)
|
|
219
|
+
fields.append(scalar_field)
|
|
220
|
+
else:
|
|
221
|
+
logger.warning(
|
|
222
|
+
f"No manual task found for connection {connection_key}, skipping"
|
|
223
|
+
)
|
|
291
224
|
|
|
292
225
|
if fields: # Only create graph if there are fields
|
|
226
|
+
logger.debug(
|
|
227
|
+
f"Creating graph for connection {connection_key} with {len(fields)} fields"
|
|
228
|
+
)
|
|
293
229
|
# Create a synthetic Collection
|
|
294
230
|
collection = Collection(
|
|
295
231
|
name=ManualTaskAddress.MANUAL_DATA_COLLECTION,
|
|
@@ -307,5 +243,13 @@ def create_manual_task_artificial_graphs(
|
|
|
307
243
|
)
|
|
308
244
|
|
|
309
245
|
manual_task_graphs.append(graph_dataset)
|
|
246
|
+
logger.debug(
|
|
247
|
+
f"Successfully created manual task graph for connection {connection_key}"
|
|
248
|
+
)
|
|
249
|
+
else:
|
|
250
|
+
logger.warning(
|
|
251
|
+
f"No fields found for connection {connection_key}, skipping graph creation"
|
|
252
|
+
)
|
|
310
253
|
|
|
254
|
+
logger.debug(f"Created {len(manual_task_graphs)} manual task graphs")
|
|
311
255
|
return manual_task_graphs
|