ethyca-fides 2.67.2rc0__py2.py3-none-any.whl → 2.67.3b1__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.2rc0.dist-info → ethyca_fides-2.67.3b1.dist-info}/METADATA +2 -2
- {ethyca_fides-2.67.2rc0.dist-info → ethyca_fides-2.67.3b1.dist-info}/RECORD +112 -110
- fides/_version.py +3 -3
- fides/api/db/crud.py +24 -41
- fides/api/graph/traversal.py +1 -1
- fides/api/main.py +2 -1
- fides/api/models/detection_discovery/core.py +59 -24
- 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/conditional_dependencies/evaluator.py +12 -19
- fides/api/task/conditional_dependencies/operators.py +150 -0
- fides/api/task/conditional_dependencies/schemas.py +51 -0
- fides/api/task/create_request_tasks.py +5 -3
- fides/api/task/filter_results.py +5 -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 -201
- fides/api/util/storage_util.py +19 -0
- fides/config/__init__.py +4 -0
- fides/config/config_proxy.py +7 -0
- fides/config/privacy_center_settings.py +17 -0
- fides/config/utils.py +1 -0
- fides/ui-build/static/admin/404.html +1 -1
- fides/ui-build/static/admin/_next/static/{Y7C7V1jdXK7rWXDIdtD13 → KAV7vwBbSuOInfCyClBs2}/_buildManifest.js +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/integrations/{[id]-e0a755c69081fffa.js → [id]-330475705adbd36f.js} +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/lib/fides-preview.js +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.2rc0.dist-info → ethyca_fides-2.67.3b1.dist-info}/WHEEL +0 -0
- {ethyca_fides-2.67.2rc0.dist-info → ethyca_fides-2.67.3b1.dist-info}/entry_points.txt +0 -0
- {ethyca_fides-2.67.2rc0.dist-info → ethyca_fides-2.67.3b1.dist-info}/licenses/LICENSE +0 -0
- {ethyca_fides-2.67.2rc0.dist-info → ethyca_fides-2.67.3b1.dist-info}/top_level.txt +0 -0
- /fides/ui-build/static/admin/_next/static/{Y7C7V1jdXK7rWXDIdtD13 → KAV7vwBbSuOInfCyClBs2}/_ssgManifest.js +0 -0
|
@@ -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,40 +1,43 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from loguru import logger
|
|
1
4
|
from sqlalchemy.orm import Session
|
|
2
5
|
|
|
3
6
|
from fides.api.graph.config import (
|
|
4
7
|
Collection,
|
|
5
8
|
CollectionAddress,
|
|
6
|
-
|
|
9
|
+
FieldAddress,
|
|
7
10
|
GraphDataset,
|
|
8
11
|
ScalarField,
|
|
9
12
|
)
|
|
10
|
-
from fides.api.graph.graph import Node
|
|
11
|
-
from fides.api.graph.traversal import TraversalNode
|
|
12
13
|
from fides.api.models.connectionconfig import ConnectionConfig
|
|
13
14
|
|
|
14
15
|
# Import application models
|
|
15
16
|
from fides.api.models.manual_task import (
|
|
16
17
|
ManualTask,
|
|
17
|
-
|
|
18
|
+
ManualTaskConditionalDependencyType,
|
|
18
19
|
ManualTaskConfigurationType,
|
|
19
|
-
ManualTaskEntityType,
|
|
20
|
-
ManualTaskInstance,
|
|
21
20
|
)
|
|
22
|
-
from fides.api.models.privacy_request import PrivacyRequest
|
|
23
|
-
from fides.api.schemas.policy import ActionType
|
|
24
21
|
from fides.api.task.manual.manual_task_address import ManualTaskAddress
|
|
25
22
|
|
|
23
|
+
PRIVACY_REQUEST_CONFIG_TYPES = {
|
|
24
|
+
ManualTaskConfigurationType.access_privacy_request,
|
|
25
|
+
ManualTaskConfigurationType.erasure_privacy_request,
|
|
26
|
+
}
|
|
27
|
+
|
|
26
28
|
|
|
27
29
|
def get_connection_configs_with_manual_tasks(db: Session) -> list[ConnectionConfig]:
|
|
28
30
|
"""
|
|
29
31
|
Get all connection configs that have manual tasks.
|
|
30
32
|
"""
|
|
31
|
-
|
|
33
|
+
connection_configs = (
|
|
32
34
|
db.query(ConnectionConfig)
|
|
33
35
|
.join(ManualTask, ConnectionConfig.id == ManualTask.parent_entity_id)
|
|
34
36
|
.filter(ManualTask.parent_entity_type == "connection_config")
|
|
35
37
|
.filter(ConnectionConfig.disabled.is_(False))
|
|
36
38
|
.all()
|
|
37
39
|
)
|
|
40
|
+
return connection_configs
|
|
38
41
|
|
|
39
42
|
|
|
40
43
|
def get_manual_task_addresses(db: Session) -> list[CollectionAddress]:
|
|
@@ -48,12 +51,11 @@ def get_manual_task_addresses(db: Session) -> list[CollectionAddress]:
|
|
|
48
51
|
# Get all connection configs that have manual tasks (excluding disabled ones)
|
|
49
52
|
connection_configs_with_manual_tasks = get_connection_configs_with_manual_tasks(db)
|
|
50
53
|
|
|
51
|
-
#
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return manual_task_addresses
|
|
54
|
+
# Return addresses for all connections that have manual tasks
|
|
55
|
+
return [
|
|
56
|
+
ManualTaskAddress.create(config.key)
|
|
57
|
+
for config in connection_configs_with_manual_tasks
|
|
58
|
+
]
|
|
57
59
|
|
|
58
60
|
|
|
59
61
|
def get_manual_task_for_connection_config(
|
|
@@ -73,20 +75,18 @@ def get_manual_task_for_connection_config(
|
|
|
73
75
|
)
|
|
74
76
|
|
|
75
77
|
|
|
76
|
-
def
|
|
77
|
-
db: Session, address: CollectionAddress
|
|
78
|
-
) -> "TraversalNode":
|
|
78
|
+
def create_data_category_scalar_fields(manual_task: ManualTask) -> list[ScalarField]:
|
|
79
79
|
"""
|
|
80
|
-
Create
|
|
80
|
+
Create scalar fields for each field in the given manual task configs.
|
|
81
81
|
"""
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
for config in
|
|
82
|
+
fields = []
|
|
83
|
+
# Get current privacy request configs for this manual task
|
|
84
|
+
current_configs = [
|
|
85
|
+
config
|
|
86
|
+
for config in manual_task.configs
|
|
87
|
+
if config.is_current and config.config_type in PRIVACY_REQUEST_CONFIG_TYPES
|
|
88
|
+
]
|
|
89
|
+
for config in current_configs:
|
|
90
90
|
for field in config.field_definitions:
|
|
91
91
|
# Create a scalar field for each manual task field
|
|
92
92
|
# Extract data categories from field metadata if available
|
|
@@ -99,211 +99,94 @@ def create_manual_data_traversal_node(
|
|
|
99
99
|
# Manual task fields don't have complex relationships
|
|
100
100
|
)
|
|
101
101
|
fields.append(scalar_field)
|
|
102
|
+
return fields
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def create_conditional_dependency_scalar_fields(
|
|
106
|
+
field_addresses: set[str],
|
|
107
|
+
) -> list[ScalarField]:
|
|
108
|
+
fields: list[ScalarField] = []
|
|
109
|
+
for field_address in field_addresses:
|
|
110
|
+
# Use the full field address as the field name to preserve collection context
|
|
111
|
+
# This allows the manual task to receive data from specific collections
|
|
112
|
+
# e.g., "user.name" or "customer.profile.email" instead of just "name" or "email"
|
|
113
|
+
logger.info(
|
|
114
|
+
f"Creating conditional dependency scalar field for field address: {field_address}"
|
|
115
|
+
)
|
|
116
|
+
field_address_obj = FieldAddress.from_string(field_address)
|
|
102
117
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
# Create a synthetic GraphDataset
|
|
112
|
-
dataset = GraphDataset(
|
|
113
|
-
name=connection_key,
|
|
114
|
-
collections=[collection],
|
|
115
|
-
connection_key=connection_key,
|
|
116
|
-
after=set(),
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
node = Node(dataset, collection)
|
|
120
|
-
traversal_node = TraversalNode(node)
|
|
121
|
-
|
|
122
|
-
return traversal_node
|
|
118
|
+
scalar_field = ScalarField(
|
|
119
|
+
name=field_address_obj.value,
|
|
120
|
+
# Conditional dependency fields don't have predefined data categories
|
|
121
|
+
data_categories=[],
|
|
122
|
+
references=[(field_address_obj, "from")],
|
|
123
|
+
)
|
|
124
|
+
fields.append(scalar_field)
|
|
123
125
|
|
|
126
|
+
return fields
|
|
124
127
|
|
|
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
128
|
|
|
131
|
-
|
|
132
|
-
|
|
129
|
+
def create_collection_for_connection_key(
|
|
130
|
+
db: Session, connection_key: str
|
|
131
|
+
) -> Optional[Collection]:
|
|
132
|
+
# Get the manual task for this connection config
|
|
133
|
+
manual_task = get_manual_task_for_connection_config(db, connection_key)
|
|
133
134
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
135
|
+
if not manual_task:
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
# Get conditional dependency field addresses - raw field data
|
|
139
|
+
conditional_field_addresses: set[str] = {
|
|
140
|
+
dependency.field_address
|
|
141
|
+
for dependency in manual_task.conditional_dependencies
|
|
142
|
+
if dependency.condition_type == ManualTaskConditionalDependencyType.leaf
|
|
143
|
+
and dependency.field_address is not None
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
# Create scalar fields for data category fields and conditional dependency field addresses
|
|
147
|
+
fields: list[ScalarField] = []
|
|
148
|
+
fields.extend(create_data_category_scalar_fields(manual_task))
|
|
149
|
+
fields.extend(
|
|
150
|
+
create_conditional_dependency_scalar_fields(conditional_field_addresses)
|
|
140
151
|
)
|
|
141
152
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
.filter(
|
|
146
|
-
ManualTask.parent_entity_id == connection_config.id,
|
|
147
|
-
ManualTask.parent_entity_type == "connection_config",
|
|
148
|
-
)
|
|
149
|
-
.all()
|
|
150
|
-
)
|
|
153
|
+
# Only create collection if there are fields
|
|
154
|
+
if not fields:
|
|
155
|
+
return None
|
|
151
156
|
|
|
152
|
-
|
|
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
|
-
)
|
|
157
|
+
return Collection(name=ManualTaskAddress.MANUAL_DATA_COLLECTION, fields=fields)
|
|
233
158
|
|
|
234
159
|
|
|
235
|
-
def create_manual_task_artificial_graphs(
|
|
236
|
-
db: Session,
|
|
237
|
-
) -> list:
|
|
160
|
+
def create_manual_task_artificial_graphs(db: Session) -> list[GraphDataset]:
|
|
238
161
|
"""
|
|
239
162
|
Create artificial GraphDataset objects for manual tasks that can be included
|
|
240
163
|
in the main dataset graph during the dataset configuration phase.
|
|
241
164
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
Manual task collections are designed as root nodes that execute immediately when
|
|
246
|
-
the privacy request starts, in parallel with identity processing. They don't depend
|
|
247
|
-
on identity data since they provide manually-entered data rather than consuming it.
|
|
165
|
+
Each manual task gets its own collection with its own dependencies based on
|
|
166
|
+
its specific conditional dependencies. This allows individual manual tasks
|
|
167
|
+
to receive only the data they need from regular tasks.
|
|
248
168
|
|
|
249
169
|
Args:
|
|
250
170
|
db: Database session
|
|
251
|
-
policy: The policy being executed (optional, for filtering manual task configs)
|
|
252
171
|
|
|
253
172
|
Returns:
|
|
254
|
-
List of GraphDataset objects representing manual tasks as
|
|
173
|
+
List of GraphDataset objects representing manual tasks as individual collections
|
|
255
174
|
"""
|
|
256
|
-
|
|
257
175
|
manual_task_graphs = []
|
|
258
176
|
manual_addresses = get_manual_task_addresses(db)
|
|
259
177
|
|
|
260
178
|
for address in manual_addresses:
|
|
261
179
|
connection_key = address.dataset
|
|
262
180
|
|
|
263
|
-
# Get
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
# Create fields based only on ManualTaskConfigFields
|
|
267
|
-
fields: list = []
|
|
268
|
-
|
|
269
|
-
# Manual task collections act as root nodes - they don't need identity dependencies
|
|
270
|
-
# since they provide manually-entered data rather than consuming identity data.
|
|
271
|
-
current_configs = [
|
|
272
|
-
config for config in manual_task.configs if config.is_current
|
|
273
|
-
]
|
|
274
|
-
for config in current_configs:
|
|
275
|
-
if config.config_type not in [
|
|
276
|
-
ManualTaskConfigurationType.access_privacy_request,
|
|
277
|
-
ManualTaskConfigurationType.erasure_privacy_request,
|
|
278
|
-
]:
|
|
279
|
-
continue
|
|
280
|
-
|
|
281
|
-
for field in config.field_definitions:
|
|
282
|
-
# Create a scalar field for each manual task field
|
|
283
|
-
field_metadata = field.field_metadata or {}
|
|
284
|
-
data_categories = field_metadata.get("data_categories", [])
|
|
285
|
-
|
|
286
|
-
scalar_field = ScalarField(
|
|
287
|
-
name=field.field_key,
|
|
288
|
-
data_categories=data_categories,
|
|
289
|
-
)
|
|
290
|
-
fields.append(scalar_field)
|
|
291
|
-
|
|
292
|
-
if fields: # Only create graph if there are fields
|
|
293
|
-
# Create a synthetic Collection
|
|
294
|
-
collection = Collection(
|
|
295
|
-
name=ManualTaskAddress.MANUAL_DATA_COLLECTION,
|
|
296
|
-
fields=fields,
|
|
297
|
-
# Manual tasks have no dependencies - they're root nodes
|
|
298
|
-
after=set(),
|
|
299
|
-
)
|
|
181
|
+
# Get the collection for this connection config using the reusable function
|
|
182
|
+
collection = create_collection_for_connection_key(db, connection_key)
|
|
300
183
|
|
|
301
|
-
|
|
184
|
+
if collection: # Only create graph if there are collections
|
|
185
|
+
# Create a synthetic GraphDataset with all manual task collections
|
|
302
186
|
graph_dataset = GraphDataset(
|
|
303
187
|
name=connection_key,
|
|
304
188
|
collections=[collection],
|
|
305
189
|
connection_key=connection_key,
|
|
306
|
-
after=set(),
|
|
307
190
|
)
|
|
308
191
|
|
|
309
192
|
manual_task_graphs.append(graph_dataset)
|
fides/api/util/storage_util.py
CHANGED
|
@@ -14,6 +14,25 @@ from fides.api.schemas.storage.storage_secrets_docs_only import possible_storage
|
|
|
14
14
|
from fides.api.util.custom_json_encoder import CustomJSONEncoder
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
def format_size(size_bytes: float) -> str:
|
|
18
|
+
"""
|
|
19
|
+
Format size in bytes to human readable format.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
size_bytes: Size in bytes
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Formatted string with appropriate unit (B, KB, MB, GB, TB, PB)
|
|
26
|
+
"""
|
|
27
|
+
units = ["B", "KB", "MB", "GB", "TB"]
|
|
28
|
+
size = float(size_bytes)
|
|
29
|
+
for unit in units:
|
|
30
|
+
if size < 1024.0:
|
|
31
|
+
return f"{size:.1f} {unit}"
|
|
32
|
+
size /= 1024.0
|
|
33
|
+
return f"{size:.1f} PB"
|
|
34
|
+
|
|
35
|
+
|
|
17
36
|
def get_schema_for_secrets(
|
|
18
37
|
storage_type: Union[StorageType, str],
|
|
19
38
|
secrets: possible_storage_secrets,
|
fides/config/__init__.py
CHANGED
|
@@ -29,6 +29,7 @@ from .fides_settings import FidesSettings
|
|
|
29
29
|
from .helpers import handle_deprecated_env_variables, handle_deprecated_fields
|
|
30
30
|
from .logging_settings import LoggingSettings
|
|
31
31
|
from .notification_settings import NotificationSettings
|
|
32
|
+
from .privacy_center_settings import PrivacyCenterSettings
|
|
32
33
|
from .redis_settings import RedisSettings
|
|
33
34
|
from .security_settings import SecuritySettings
|
|
34
35
|
from .user_settings import UserSettings
|
|
@@ -82,6 +83,7 @@ class FidesConfig(FidesSettings):
|
|
|
82
83
|
logging: LoggingSettings
|
|
83
84
|
notifications: NotificationSettings
|
|
84
85
|
redis: RedisSettings
|
|
86
|
+
privacy_center: PrivacyCenterSettings
|
|
85
87
|
security: SecuritySettings
|
|
86
88
|
user: UserSettings
|
|
87
89
|
|
|
@@ -111,6 +113,7 @@ class FidesConfig(FidesSettings):
|
|
|
111
113
|
self.security,
|
|
112
114
|
self.execution,
|
|
113
115
|
self.admin_ui,
|
|
116
|
+
self.privacy_center,
|
|
114
117
|
]:
|
|
115
118
|
for key, value in settings.model_dump(mode="json").items(): # type: ignore
|
|
116
119
|
log.debug(
|
|
@@ -161,6 +164,7 @@ def build_config(config_dict: Dict[str, Any]) -> FidesConfig:
|
|
|
161
164
|
"execution": ExecutionSettings,
|
|
162
165
|
"logging": LoggingSettings,
|
|
163
166
|
"notifications": NotificationSettings,
|
|
167
|
+
"privacy_center": PrivacyCenterSettings,
|
|
164
168
|
"redis": RedisSettings,
|
|
165
169
|
"security": SecuritySettings,
|
|
166
170
|
"user": UserSettings,
|