ethyca-fides 2.68.1b0__py2.py3-none-any.whl → 2.68.1b2__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.68.1b0.dist-info → ethyca_fides-2.68.1b2.dist-info}/METADATA +1 -1
- {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b2.dist-info}/RECORD +128 -118
- fides/_version.py +3 -3
- fides/api/alembic/migrations/versions/3baf42d251a6_add_generic_taxonomy_models.py +239 -0
- fides/api/api/deps.py +2 -0
- fides/api/api/v1/endpoints/generic_overrides.py +64 -167
- fides/api/db/base.py +6 -0
- fides/api/db/ctl_session.py +3 -0
- fides/api/db/session.py +2 -1
- fides/api/models/privacy_request/privacy_request.py +15 -0
- fides/api/models/taxonomy.py +275 -0
- fides/api/schemas/application_config.py +2 -1
- fides/api/schemas/privacy_center_config.py +15 -0
- fides/api/service/deps.py +5 -0
- fides/api/service/privacy_request/request_service.py +6 -1
- fides/api/task/conditional_dependencies/evaluator.py +192 -45
- fides/api/task/conditional_dependencies/logging_utils.py +196 -0
- fides/api/task/conditional_dependencies/operators.py +8 -2
- fides/api/task/conditional_dependencies/schemas.py +25 -1
- fides/api/task/graph_task.py +9 -2
- fides/api/task/manual/manual_task_conditional_evaluation.py +193 -0
- fides/api/task/manual/manual_task_graph_task.py +224 -119
- fides/api/task/manual/manual_task_utils.py +0 -4
- fides/api/tasks/__init__.py +1 -0
- fides/api/util/connection_type.py +68 -33
- fides/config/database_settings.py +10 -1
- fides/data/sample_project/docker-compose.yml +3 -3
- fides/service/taxonomy/__init__.py +0 -0
- fides/service/taxonomy/handlers/__init__.py +11 -0
- fides/service/taxonomy/handlers/base.py +42 -0
- fides/service/taxonomy/handlers/legacy_handler.py +95 -0
- fides/service/taxonomy/taxonomy_service.py +261 -0
- fides/service/taxonomy/utils.py +160 -0
- fides/ui-build/static/admin/404.html +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/{_app-65723cd4b8fc36ac.js → _app-2c10f6b217b7978b.js} +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center-58827eb86516931f.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/pages/integrations/{[id]-766e57bcf38b5b1e.js → [id]-4e286a1e501a0c73.js} +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests-709bcb0bc6a5382d.js +1 -0
- fides/ui-build/static/admin/_next/static/css/a72179b1754aadd3.css +1 -0
- fides/ui-build/static/admin/_next/static/{JLiYN-Wiw1kNc_8IVythJ → qvk5eMANVfwYkdURE7fgG}/_buildManifest.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/lib/fides-tcf.js +2 -2
- fides/ui-build/static/admin/lib/fides.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
- fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center-53a763e49ce34a74.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests-f43a988542813110.js +0 -1
- fides/ui-build/static/admin/_next/static/css/e1628f15dd5f019b.css +0 -1
- {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b2.dist-info}/WHEEL +0 -0
- {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b2.dist-info}/entry_points.txt +0 -0
- {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b2.dist-info}/licenses/LICENSE +0 -0
- {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b2.dist-info}/top_level.txt +0 -0
- /fides/ui-build/static/admin/_next/static/{JLiYN-Wiw1kNc_8IVythJ → qvk5eMANVfwYkdURE7fgG}/_ssgManifest.js +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from typing import Any, Optional
|
|
2
2
|
|
|
3
3
|
from loguru import logger
|
|
4
|
-
from
|
|
4
|
+
from pydantic.v1.utils import deep_update
|
|
5
5
|
|
|
6
6
|
from fides.api.common_exceptions import AwaitingAsyncTaskCallback
|
|
7
7
|
from fides.api.models.attachment import AttachmentType
|
|
@@ -15,13 +15,23 @@ from fides.api.models.manual_task import (
|
|
|
15
15
|
StatusType,
|
|
16
16
|
)
|
|
17
17
|
from fides.api.models.privacy_request import PrivacyRequest
|
|
18
|
+
from fides.api.models.worker_task import ExecutionLogStatus
|
|
18
19
|
from fides.api.schemas.policy import ActionType
|
|
19
20
|
from fides.api.schemas.privacy_request import PrivacyRequestStatus
|
|
21
|
+
from fides.api.task.conditional_dependencies.logging_utils import (
|
|
22
|
+
format_evaluation_failure_message,
|
|
23
|
+
format_evaluation_success_message,
|
|
24
|
+
)
|
|
20
25
|
from fides.api.task.graph_task import GraphTask, retry
|
|
21
26
|
from fides.api.task.manual.manual_task_address import ManualTaskAddress
|
|
27
|
+
from fides.api.task.manual.manual_task_conditional_evaluation import (
|
|
28
|
+
evaluate_conditional_dependencies,
|
|
29
|
+
extract_conditional_dependency_data_from_inputs,
|
|
30
|
+
)
|
|
22
31
|
from fides.api.task.manual.manual_task_utils import (
|
|
23
32
|
get_manual_task_for_connection_config,
|
|
24
33
|
)
|
|
34
|
+
from fides.api.task.task_resources import TaskResources
|
|
25
35
|
from fides.api.util.collection_util import Row
|
|
26
36
|
from fides.api.util.storage_util import format_size
|
|
27
37
|
|
|
@@ -29,88 +39,236 @@ from fides.api.util.storage_util import format_size
|
|
|
29
39
|
class ManualTaskGraphTask(GraphTask):
|
|
30
40
|
"""GraphTask implementation for ManualTask execution"""
|
|
31
41
|
|
|
42
|
+
# class level constants
|
|
43
|
+
DRY_RUN_PLACEHOLDER_VALUE = 1
|
|
44
|
+
|
|
45
|
+
def __init__(self, resources: TaskResources) -> None:
|
|
46
|
+
super().__init__(resources)
|
|
47
|
+
self.connection_key = ManualTaskAddress.get_connection_key(
|
|
48
|
+
self.execution_node.address
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# ------------------------------------------------------------------------------------------------
|
|
52
|
+
# Public methods
|
|
53
|
+
# ------------------------------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
def dry_run_task(self) -> int:
|
|
56
|
+
"""Return estimated row count for dry run - manual tasks don't have predictable counts"""
|
|
57
|
+
return self.DRY_RUN_PLACEHOLDER_VALUE
|
|
58
|
+
|
|
32
59
|
@retry(action_type=ActionType.access, default_return=[])
|
|
33
60
|
def access_request(self, *inputs: list[Row]) -> list[Row]:
|
|
61
|
+
"""
|
|
62
|
+
Execute manual task logic following the standard GraphTask pattern.
|
|
63
|
+
Calls _run_request with ACCESS configs.
|
|
64
|
+
Returns data if submitted, raise AwaitingAsyncTaskCallback if not
|
|
65
|
+
"""
|
|
66
|
+
if self.resources.request.policy.get_action_type() == ActionType.erasure:
|
|
67
|
+
# We're in an erasure privacy request's access phase - complete access task immediately
|
|
68
|
+
# since access is just for data collection to support erasure, not for user data access
|
|
69
|
+
self.update_status(
|
|
70
|
+
"Access task completed immediately for erasure privacy request (data collection only)",
|
|
71
|
+
[],
|
|
72
|
+
ActionType.access,
|
|
73
|
+
ExecutionLogStatus.complete,
|
|
74
|
+
)
|
|
75
|
+
return []
|
|
76
|
+
|
|
77
|
+
result = self._run_request(
|
|
78
|
+
ManualTaskConfigurationType.access_privacy_request,
|
|
79
|
+
ActionType.access,
|
|
80
|
+
*inputs,
|
|
81
|
+
)
|
|
82
|
+
if result is None:
|
|
83
|
+
# Conditional skip or not applicable already logged upstream; do not mark complete here
|
|
84
|
+
return []
|
|
85
|
+
|
|
86
|
+
# We are picking up after awaiting input and have provided data – mark complete with record count
|
|
87
|
+
self.log_end(ActionType.access, record_count=len(result))
|
|
88
|
+
return result
|
|
89
|
+
|
|
90
|
+
# Provide erasure support for manual tasks
|
|
91
|
+
@retry(action_type=ActionType.erasure, default_return=0)
|
|
92
|
+
def erasure_request(
|
|
93
|
+
self,
|
|
94
|
+
retrieved_data: list[Row], # This is not used for manual tasks.
|
|
95
|
+
*erasure_prereqs: int, # noqa: D401, pylint: disable=unused-argument # TODO Remove when we stop support for DSR 2.0
|
|
96
|
+
inputs: Optional[list[list[Row]]] = None,
|
|
97
|
+
) -> int:
|
|
98
|
+
"""Execute manual-task-driven erasure logic.
|
|
99
|
+
Calls _run_request with ERASURE configs.
|
|
100
|
+
|
|
101
|
+
Mirrors access_request behaviour but returns the number of rows masked (always 0)
|
|
102
|
+
once all required manual task submissions are present. If submissions are
|
|
103
|
+
incomplete the privacy request is paused awaiting user input.
|
|
104
|
+
Returns the number of rows masked (always 0)
|
|
105
|
+
Raises AwaitingAsyncTaskCallback if data is not submitted
|
|
106
|
+
"""
|
|
107
|
+
if not inputs:
|
|
108
|
+
inputs = []
|
|
109
|
+
result = self._run_request(
|
|
110
|
+
ManualTaskConfigurationType.erasure_privacy_request,
|
|
111
|
+
ActionType.erasure,
|
|
112
|
+
*inputs,
|
|
113
|
+
)
|
|
114
|
+
if result is None:
|
|
115
|
+
# Conditional skip or not applicable already logged upstream; do not mark complete here
|
|
116
|
+
return 0
|
|
117
|
+
|
|
118
|
+
# Mark rows_masked = 0 (manual tasks do not mask data directly)
|
|
119
|
+
if self.request_task.id:
|
|
120
|
+
# Storing result for DSR 3.0; SQLAlchemy column typing triggers mypy warning
|
|
121
|
+
self.request_task.rows_masked = 0 # type: ignore[assignment]
|
|
122
|
+
|
|
123
|
+
# Picking up after awaiting input, mark erasure node complete with rows masked count (always 0)
|
|
124
|
+
self.log_end(ActionType.erasure, record_count=0)
|
|
125
|
+
return 0
|
|
126
|
+
|
|
127
|
+
# ------------------------------------------------------------------------------------------------
|
|
128
|
+
# Private methods
|
|
129
|
+
# ------------------------------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
def _run_request(
|
|
132
|
+
self,
|
|
133
|
+
config_type: ManualTaskConfigurationType,
|
|
134
|
+
action_type: ActionType,
|
|
135
|
+
*inputs: list[Row],
|
|
136
|
+
) -> Optional[list[Row]]:
|
|
34
137
|
"""
|
|
35
138
|
Execute manual task logic following the standard GraphTask pattern:
|
|
36
139
|
1. Create ManualTaskInstances if they don't exist
|
|
37
|
-
2. Check
|
|
140
|
+
2. Check if all required submissions are present
|
|
38
141
|
3. Return data if submitted, raise AwaitingAsyncTaskCallback if not
|
|
39
142
|
"""
|
|
40
|
-
|
|
41
|
-
|
|
143
|
+
manual_task = self._get_manual_task_or_none()
|
|
144
|
+
if manual_task is None:
|
|
145
|
+
return None
|
|
42
146
|
|
|
43
|
-
#
|
|
44
|
-
|
|
45
|
-
raise ValueError(f"Invalid manual task address: {collection_address}")
|
|
147
|
+
# Complete a series of checks to determine if the manual task should be executed
|
|
148
|
+
# If any of these checks fail, complete immediately or mark as skipped
|
|
46
149
|
|
|
47
|
-
|
|
150
|
+
# Check if any eligible manual tasks have applicable configs
|
|
151
|
+
if not self._check_manual_task_configs(manual_task, config_type, action_type):
|
|
152
|
+
return None
|
|
48
153
|
|
|
49
|
-
#
|
|
50
|
-
|
|
154
|
+
# Check if there are any rules for this action type
|
|
155
|
+
if not self.resources.request.policy.get_rules_for_action(
|
|
156
|
+
action_type=action_type
|
|
157
|
+
):
|
|
158
|
+
return None
|
|
51
159
|
|
|
52
|
-
|
|
53
|
-
|
|
160
|
+
# Extract conditional dependency data from inputs
|
|
161
|
+
conditional_data = extract_conditional_dependency_data_from_inputs(
|
|
162
|
+
*inputs, manual_task=manual_task, input_keys=self.execution_node.input_keys
|
|
163
|
+
)
|
|
164
|
+
# Evaluate conditional dependencies
|
|
165
|
+
evaluation_result = evaluate_conditional_dependencies(
|
|
166
|
+
self.resources.session, manual_task, conditional_data=conditional_data
|
|
167
|
+
)
|
|
168
|
+
detailed_message: Optional[str] = None
|
|
169
|
+
# if there were conditional dependencies and they were not met,
|
|
170
|
+
# clean up any existing ManualTaskInstances and return None to cause a skip
|
|
171
|
+
if evaluation_result is not None and not evaluation_result.result:
|
|
172
|
+
self._cleanup_manual_task_instances(manual_task, self.resources.request)
|
|
173
|
+
detailed_message = format_evaluation_failure_message(evaluation_result)
|
|
174
|
+
self.update_status(
|
|
175
|
+
f"Manual task conditional dependencies not met. {detailed_message}",
|
|
176
|
+
[],
|
|
177
|
+
ActionType(self.resources.privacy_request_task.action_type),
|
|
178
|
+
ExecutionLogStatus.skipped,
|
|
179
|
+
)
|
|
180
|
+
return None
|
|
54
181
|
|
|
55
|
-
# Check
|
|
56
|
-
|
|
182
|
+
# Check/Create manual task instances for applicable configs only
|
|
183
|
+
self._ensure_manual_task_instances(
|
|
184
|
+
manual_task,
|
|
185
|
+
self.resources.request,
|
|
186
|
+
config_type,
|
|
187
|
+
)
|
|
57
188
|
|
|
189
|
+
# Check if all manual task instances have submissions for applicable configs only
|
|
190
|
+
# No separate pending log; include details in the awaiting-processing log
|
|
191
|
+
if evaluation_result:
|
|
192
|
+
detailed_message = format_evaluation_success_message(evaluation_result)
|
|
193
|
+
result = self._set_submitted_data_or_raise_awaiting_async_task_callback(
|
|
194
|
+
manual_task,
|
|
195
|
+
config_type,
|
|
196
|
+
action_type,
|
|
197
|
+
conditional_data=conditional_data,
|
|
198
|
+
awaiting_detail_message=detailed_message,
|
|
199
|
+
)
|
|
200
|
+
return result
|
|
201
|
+
|
|
202
|
+
def _check_manual_task_configs(
|
|
203
|
+
self,
|
|
204
|
+
manual_task: ManualTask,
|
|
205
|
+
config_type: ManualTaskConfigurationType,
|
|
206
|
+
action_type: ActionType,
|
|
207
|
+
) -> bool:
|
|
58
208
|
has_access_configs = [
|
|
59
209
|
config
|
|
60
210
|
for config in manual_task.configs
|
|
61
|
-
if config.is_current
|
|
62
|
-
and config.config_type == ManualTaskConfigurationType.access_privacy_request
|
|
211
|
+
if config.is_current and config.config_type == config_type
|
|
63
212
|
]
|
|
64
213
|
|
|
65
214
|
if not has_access_configs:
|
|
66
215
|
# No access configs - complete immediately
|
|
67
|
-
self.log_end(
|
|
68
|
-
return
|
|
216
|
+
self.log_end(action_type)
|
|
217
|
+
return False
|
|
69
218
|
|
|
70
|
-
|
|
71
|
-
action_type=ActionType.access
|
|
72
|
-
):
|
|
73
|
-
# TODO: This will be changed with Manual Task Dependencies Implementation.
|
|
74
|
-
self.log_end(ActionType.access)
|
|
75
|
-
return []
|
|
219
|
+
return True
|
|
76
220
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
221
|
+
def _get_manual_task_or_none(self) -> Optional[ManualTask]:
|
|
222
|
+
# Verify this is a manual task address
|
|
223
|
+
if not ManualTaskAddress.is_manual_task_address(self.execution_node.address):
|
|
224
|
+
raise ValueError(
|
|
225
|
+
f"Invalid manual task address: {self.execution_node.address}"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# Get the manual task for this connection config (1:1 relationship)
|
|
229
|
+
manual_task = get_manual_task_for_connection_config(
|
|
230
|
+
self.resources.session, self.connection_key
|
|
83
231
|
)
|
|
232
|
+
return manual_task
|
|
84
233
|
|
|
234
|
+
def _set_submitted_data_or_raise_awaiting_async_task_callback(
|
|
235
|
+
self,
|
|
236
|
+
manual_task: ManualTask,
|
|
237
|
+
config_type: ManualTaskConfigurationType,
|
|
238
|
+
action_type: ActionType,
|
|
239
|
+
conditional_data: Optional[dict[str, Any]] = None,
|
|
240
|
+
awaiting_detail_message: Optional[str] = None,
|
|
241
|
+
) -> Optional[list[Row]]:
|
|
242
|
+
"""
|
|
243
|
+
Set submitted data for a manual task and raise AwaitingAsyncTaskCallback if all instances are not completed
|
|
244
|
+
"""
|
|
85
245
|
# Check if all manual task instances have submissions for ACCESS configs only
|
|
86
246
|
submitted_data = self._get_submitted_data(
|
|
87
|
-
db,
|
|
88
247
|
manual_task,
|
|
89
248
|
self.resources.request,
|
|
90
|
-
|
|
249
|
+
config_type,
|
|
250
|
+
conditional_data=conditional_data,
|
|
91
251
|
)
|
|
92
252
|
|
|
93
253
|
if submitted_data is not None:
|
|
94
254
|
result: list[Row] = [submitted_data] if submitted_data else []
|
|
95
255
|
self.request_task.access_data = result
|
|
96
256
|
|
|
97
|
-
# Mark request task as complete and write execution log
|
|
98
|
-
self.log_end(ActionType.access)
|
|
99
257
|
return result
|
|
100
258
|
|
|
101
259
|
# Set privacy request status to requires_input if not already set
|
|
102
260
|
if self.resources.request.status != PrivacyRequestStatus.requires_input:
|
|
103
261
|
self.resources.request.status = PrivacyRequestStatus.requires_input
|
|
104
|
-
self.resources.request.save(
|
|
262
|
+
self.resources.request.save(self.resources.session)
|
|
105
263
|
|
|
106
|
-
# This
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
264
|
+
# This will trigger log_awaiting_processing via the @retry decorator; include conditional details
|
|
265
|
+
base_msg = f"Manual task for {self.connection_key} requires user input"
|
|
266
|
+
if awaiting_detail_message:
|
|
267
|
+
base_msg = f"{base_msg}. {awaiting_detail_message}"
|
|
268
|
+
raise AwaitingAsyncTaskCallback(base_msg)
|
|
110
269
|
|
|
111
270
|
def _ensure_manual_task_instances(
|
|
112
271
|
self,
|
|
113
|
-
db: Session,
|
|
114
272
|
manual_task: ManualTask,
|
|
115
273
|
privacy_request: PrivacyRequest,
|
|
116
274
|
allowed_config_type: "ManualTaskConfigurationType",
|
|
@@ -139,6 +297,7 @@ class ManualTaskGraphTask(GraphTask):
|
|
|
139
297
|
|
|
140
298
|
# If no existing instances, create a new one for the current config
|
|
141
299
|
# There will only be one config of each type per manual task
|
|
300
|
+
# Sort by version descending to get the latest version first
|
|
142
301
|
config = next(
|
|
143
302
|
(
|
|
144
303
|
config
|
|
@@ -154,7 +313,7 @@ class ManualTaskGraphTask(GraphTask):
|
|
|
154
313
|
|
|
155
314
|
if config:
|
|
156
315
|
ManualTaskInstance.create(
|
|
157
|
-
db=
|
|
316
|
+
db=self.resources.session,
|
|
158
317
|
data={
|
|
159
318
|
"task_id": manual_task.id,
|
|
160
319
|
"config_id": config.id,
|
|
@@ -166,10 +325,10 @@ class ManualTaskGraphTask(GraphTask):
|
|
|
166
325
|
|
|
167
326
|
def _get_submitted_data(
|
|
168
327
|
self,
|
|
169
|
-
db: Session,
|
|
170
328
|
manual_task: ManualTask,
|
|
171
329
|
privacy_request: PrivacyRequest,
|
|
172
330
|
allowed_config_type: "ManualTaskConfigurationType",
|
|
331
|
+
conditional_data: Optional[dict[str, Any]] = None,
|
|
173
332
|
) -> Optional[dict[str, Any]]:
|
|
174
333
|
"""
|
|
175
334
|
Check if all manual task instances have submissions for ALL fields and return aggregated data
|
|
@@ -193,10 +352,15 @@ class ManualTaskGraphTask(GraphTask):
|
|
|
193
352
|
# Update status if needed
|
|
194
353
|
if inst.status != StatusType.completed:
|
|
195
354
|
inst.status = StatusType.completed
|
|
196
|
-
inst.save(
|
|
355
|
+
inst.save(self.resources.session)
|
|
197
356
|
|
|
198
357
|
# Aggregate submission data from all instances
|
|
199
358
|
aggregated_data = self._aggregate_submission_data(candidate_instances)
|
|
359
|
+
|
|
360
|
+
# Merge conditional data with aggregated submission data
|
|
361
|
+
if conditional_data:
|
|
362
|
+
aggregated_data = deep_update(aggregated_data, conditional_data)
|
|
363
|
+
|
|
200
364
|
return aggregated_data or None
|
|
201
365
|
|
|
202
366
|
def _aggregate_submission_data(
|
|
@@ -254,84 +418,25 @@ class ManualTaskGraphTask(GraphTask):
|
|
|
254
418
|
)
|
|
255
419
|
return attachment_map or None
|
|
256
420
|
|
|
257
|
-
def
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
# Provide erasure support for manual tasks
|
|
262
|
-
@retry(action_type=ActionType.erasure, default_return=0)
|
|
263
|
-
def erasure_request(
|
|
264
|
-
self,
|
|
265
|
-
retrieved_data: list[Row],
|
|
266
|
-
*erasure_prereqs: int, # noqa: D401, pylint: disable=unused-argument
|
|
267
|
-
inputs: Optional[list[list[Row]]] = None,
|
|
268
|
-
) -> int:
|
|
269
|
-
"""Execute manual-task-driven erasure logic.
|
|
270
|
-
|
|
271
|
-
Mirrors access_request behaviour but returns the number of rows masked (always 0)
|
|
272
|
-
once all required manual task submissions are present. If submissions are
|
|
273
|
-
incomplete the privacy request is paused awaiting user input.
|
|
421
|
+
def _cleanup_manual_task_instances(
|
|
422
|
+
self, manual_task: ManualTask, privacy_request: PrivacyRequest
|
|
423
|
+
) -> None:
|
|
274
424
|
"""
|
|
275
|
-
|
|
276
|
-
collection_address = self.execution_node.address
|
|
277
|
-
|
|
278
|
-
# Validate manual task address
|
|
279
|
-
if not ManualTaskAddress.is_manual_task_address(collection_address):
|
|
280
|
-
raise ValueError(f"Invalid manual task address: {collection_address}")
|
|
281
|
-
|
|
282
|
-
connection_key = ManualTaskAddress.get_connection_key(collection_address)
|
|
283
|
-
|
|
284
|
-
# Fetch relevant manual tasks for this connection
|
|
285
|
-
manual_task = get_manual_task_for_connection_config(db, connection_key)
|
|
286
|
-
if not manual_task:
|
|
287
|
-
# No manual tasks defined – nothing to erase
|
|
288
|
-
self.log_end(ActionType.erasure)
|
|
289
|
-
return 0
|
|
425
|
+
Clean up ManualTaskInstances for a manual task when conditional dependencies are not met.
|
|
290
426
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
427
|
+
This method removes any existing instances that were created before the conditional
|
|
428
|
+
dependency evaluation determined the task should not execute.
|
|
429
|
+
"""
|
|
430
|
+
# Find all instances for this manual task and privacy request
|
|
431
|
+
instances_to_remove = [
|
|
432
|
+
instance
|
|
433
|
+
for instance in privacy_request.manual_task_instances
|
|
434
|
+
if instance.task_id == manual_task.id
|
|
298
435
|
]
|
|
299
436
|
|
|
300
|
-
if
|
|
301
|
-
# No erasure configs - complete immediately
|
|
302
|
-
self.log_end(ActionType.erasure)
|
|
303
|
-
return 0
|
|
437
|
+
if instances_to_remove:
|
|
304
438
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
self.resources.request,
|
|
310
|
-
ManualTaskConfigurationType.erasure_privacy_request,
|
|
311
|
-
)
|
|
312
|
-
|
|
313
|
-
# Check for full submissions – reuse helper used by access flow, filtering ERASURE configs
|
|
314
|
-
submissions_complete = self._get_submitted_data(
|
|
315
|
-
db,
|
|
316
|
-
manual_task,
|
|
317
|
-
self.resources.request,
|
|
318
|
-
ManualTaskConfigurationType.erasure_privacy_request,
|
|
319
|
-
)
|
|
320
|
-
|
|
321
|
-
# If any field submissions are missing, pause processing
|
|
322
|
-
if submissions_complete is None:
|
|
323
|
-
if self.resources.request.status != PrivacyRequestStatus.requires_input:
|
|
324
|
-
self.resources.request.status = PrivacyRequestStatus.requires_input
|
|
325
|
-
self.resources.request.save(db)
|
|
326
|
-
raise AwaitingAsyncTaskCallback(
|
|
327
|
-
f"Manual erasure task for {connection_key} requires user input"
|
|
328
|
-
)
|
|
329
|
-
|
|
330
|
-
# Mark rows_masked = 0 (manual tasks do not mask data directly)
|
|
331
|
-
if self.request_task.id:
|
|
332
|
-
# Storing result for DSR 3.0; SQLAlchemy column typing triggers mypy warning
|
|
333
|
-
self.request_task.rows_masked = 0 # type: ignore[assignment]
|
|
334
|
-
|
|
335
|
-
# Mark successful completion
|
|
336
|
-
self.log_end(ActionType.erasure)
|
|
337
|
-
return 0
|
|
439
|
+
# Remove instances from the database
|
|
440
|
+
for instance in instances_to_remove:
|
|
441
|
+
instance.delete(self.resources.session)
|
|
442
|
+
self.resources.session.commit()
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
2
|
|
|
3
|
-
from loguru import logger
|
|
4
3
|
from sqlalchemy.orm import Session
|
|
5
4
|
|
|
6
5
|
from fides.api.graph.config import (
|
|
@@ -110,9 +109,6 @@ def create_conditional_dependency_scalar_fields(
|
|
|
110
109
|
# Use the full field address as the field name to preserve collection context
|
|
111
110
|
# This allows the manual task to receive data from specific collections
|
|
112
111
|
# 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
112
|
field_address_obj = FieldAddress.from_string(field_address)
|
|
117
113
|
|
|
118
114
|
scalar_field = ScalarField(
|
fides/api/tasks/__init__.py
CHANGED
|
@@ -74,6 +74,7 @@ class DatabaseTask(Task): # pylint: disable=W0223
|
|
|
74
74
|
keepalives_idle=CONFIG.database.task_engine_keepalives_idle,
|
|
75
75
|
keepalives_interval=CONFIG.database.task_engine_keepalives_interval,
|
|
76
76
|
keepalives_count=CONFIG.database.task_engine_keepalives_count,
|
|
77
|
+
pool_pre_ping=CONFIG.database.task_engine_pool_pre_ping,
|
|
77
78
|
)
|
|
78
79
|
|
|
79
80
|
# same for the sessionmaker
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Any, Dict, Set
|
|
3
|
+
from typing import Any, Dict, Optional, Set
|
|
4
4
|
|
|
5
5
|
from fides.api.common_exceptions import NoSuchConnectionTypeSecretSchemaError
|
|
6
6
|
from fides.api.models.connectionconfig import ConnectionType
|
|
@@ -141,7 +141,9 @@ def get_connection_type_secret_schema(*, connection_type: str) -> dict[str, Any]
|
|
|
141
141
|
Note that this does not return actual secrets, instead we return the *types* of
|
|
142
142
|
secret fields needed to authenticate.
|
|
143
143
|
"""
|
|
144
|
-
connection_system_types: list[ConnectionSystemTypeMap] = get_connection_types(
|
|
144
|
+
connection_system_types: list[ConnectionSystemTypeMap] = get_connection_types(
|
|
145
|
+
include_test_connections=True
|
|
146
|
+
)
|
|
145
147
|
if not any(item.identifier == connection_type for item in connection_system_types):
|
|
146
148
|
raise NoSuchConnectionTypeSecretSchemaError(
|
|
147
149
|
f"No connection type found with name '{connection_type}'."
|
|
@@ -189,15 +191,15 @@ def get_connection_type_secret_schema(*, connection_type: str) -> dict[str, Any]
|
|
|
189
191
|
return schema
|
|
190
192
|
|
|
191
193
|
|
|
192
|
-
def
|
|
193
|
-
search
|
|
194
|
-
|
|
194
|
+
def _is_match(elem: str, search: Optional[str] = None) -> bool:
|
|
195
|
+
"""If a search query param was included, is it a substring of an available connector type?"""
|
|
196
|
+
return search.lower() in elem.lower() if search else True
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def get_saas_connection_types(
|
|
195
200
|
action_types: Set[ActionType] = SUPPORTED_ACTION_TYPES,
|
|
201
|
+
search: Optional[str] = None,
|
|
196
202
|
) -> list[ConnectionSystemTypeMap]:
|
|
197
|
-
def is_match(elem: str) -> bool:
|
|
198
|
-
"""If a search query param was included, is it a substring of an available connector type?"""
|
|
199
|
-
return search.lower() in elem.lower() if search else True
|
|
200
|
-
|
|
201
203
|
def saas_request_type_filter(connection_type: str) -> bool:
|
|
202
204
|
"""
|
|
203
205
|
If any of the request type filters are set to true,
|
|
@@ -216,6 +218,45 @@ def get_connection_types(
|
|
|
216
218
|
action_type in template.supported_actions for action_type in action_types
|
|
217
219
|
)
|
|
218
220
|
|
|
221
|
+
saas_connection_types: list[ConnectionSystemTypeMap] = []
|
|
222
|
+
saas_types: list[str] = sorted(
|
|
223
|
+
[
|
|
224
|
+
saas_type
|
|
225
|
+
for saas_type in ConnectorRegistry.connector_types()
|
|
226
|
+
if _is_match(saas_type, search) and saas_request_type_filter(saas_type)
|
|
227
|
+
]
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
for item in saas_types:
|
|
231
|
+
connector_template = ConnectorRegistry.get_connector_template(item)
|
|
232
|
+
if connector_template is not None:
|
|
233
|
+
saas_connection_types.append(
|
|
234
|
+
ConnectionSystemTypeMap(
|
|
235
|
+
identifier=item,
|
|
236
|
+
type=SystemType.saas,
|
|
237
|
+
human_readable=connector_template.human_readable,
|
|
238
|
+
encoded_icon=connector_template.icon,
|
|
239
|
+
authorization_required=connector_template.authorization_required,
|
|
240
|
+
user_guide=connector_template.user_guide,
|
|
241
|
+
supported_actions=connector_template.supported_actions,
|
|
242
|
+
)
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
return saas_connection_types
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# FIXME: this function needs a refactor
|
|
249
|
+
def get_connection_types(
|
|
250
|
+
search: str | None = None,
|
|
251
|
+
system_type: SystemType | None = None,
|
|
252
|
+
action_types: Set[ActionType] = SUPPORTED_ACTION_TYPES,
|
|
253
|
+
include_test_connections: bool = False,
|
|
254
|
+
) -> list[ConnectionSystemTypeMap]:
|
|
255
|
+
"""
|
|
256
|
+
Returns a list of ConnectionSystemTypeMap objects that match the given search and system type.
|
|
257
|
+
|
|
258
|
+
If include_test_connections is True, test connections like test_website will be included in the response.
|
|
259
|
+
"""
|
|
219
260
|
connection_system_types: list[ConnectionSystemTypeMap] = []
|
|
220
261
|
if (system_type == SystemType.database or system_type is None) and (
|
|
221
262
|
ActionType.access in action_types or ActionType.erasure in action_types
|
|
@@ -238,7 +279,7 @@ def get_connection_types(
|
|
|
238
279
|
ConnectionType.sovrn,
|
|
239
280
|
ConnectionType.test_website,
|
|
240
281
|
]
|
|
241
|
-
and
|
|
282
|
+
and _is_match(conn_type.value, search)
|
|
242
283
|
],
|
|
243
284
|
key=lambda x: x.value,
|
|
244
285
|
)
|
|
@@ -254,28 +295,10 @@ def get_connection_types(
|
|
|
254
295
|
]
|
|
255
296
|
)
|
|
256
297
|
if system_type == SystemType.saas or system_type is None:
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
saas_type
|
|
260
|
-
for saas_type in ConnectorRegistry.connector_types()
|
|
261
|
-
if is_match(saas_type) and saas_request_type_filter(saas_type)
|
|
262
|
-
]
|
|
298
|
+
saas_connection_types = get_saas_connection_types(
|
|
299
|
+
action_types=action_types, search=search
|
|
263
300
|
)
|
|
264
|
-
|
|
265
|
-
for item in saas_types:
|
|
266
|
-
connector_template = ConnectorRegistry.get_connector_template(item)
|
|
267
|
-
if connector_template is not None:
|
|
268
|
-
connection_system_types.append(
|
|
269
|
-
ConnectionSystemTypeMap(
|
|
270
|
-
identifier=item,
|
|
271
|
-
type=SystemType.saas,
|
|
272
|
-
human_readable=connector_template.human_readable,
|
|
273
|
-
encoded_icon=connector_template.icon,
|
|
274
|
-
authorization_required=connector_template.authorization_required,
|
|
275
|
-
user_guide=connector_template.user_guide,
|
|
276
|
-
supported_actions=connector_template.supported_actions,
|
|
277
|
-
)
|
|
278
|
-
)
|
|
301
|
+
connection_system_types.extend(saas_connection_types)
|
|
279
302
|
|
|
280
303
|
if (system_type == SystemType.manual or system_type is None) and (
|
|
281
304
|
ActionType.access in action_types or ActionType.erasure in action_types
|
|
@@ -285,7 +308,7 @@ def get_connection_types(
|
|
|
285
308
|
manual_type.value
|
|
286
309
|
for manual_type in ConnectionType
|
|
287
310
|
if manual_type == ConnectionType.manual_webhook
|
|
288
|
-
and
|
|
311
|
+
and _is_match(manual_type.value, search)
|
|
289
312
|
]
|
|
290
313
|
)
|
|
291
314
|
connection_system_types.extend(
|
|
@@ -307,7 +330,7 @@ def get_connection_types(
|
|
|
307
330
|
for email_type in ConnectionType
|
|
308
331
|
if email_type
|
|
309
332
|
in ERASURE_EMAIL_CONNECTOR_TYPES + CONSENT_EMAIL_CONNECTOR_TYPES
|
|
310
|
-
and
|
|
333
|
+
and _is_match(email_type.value, search)
|
|
311
334
|
and ( # include consent or erasure connectors if requested, respectively
|
|
312
335
|
(
|
|
313
336
|
ActionType.consent in action_types
|
|
@@ -339,4 +362,16 @@ def get_connection_types(
|
|
|
339
362
|
]
|
|
340
363
|
)
|
|
341
364
|
|
|
365
|
+
if include_test_connections and (
|
|
366
|
+
system_type == SystemType.website or system_type is None
|
|
367
|
+
):
|
|
368
|
+
connection_system_types.append(
|
|
369
|
+
ConnectionSystemTypeMap(
|
|
370
|
+
identifier=ConnectionType.test_website.value,
|
|
371
|
+
type=SystemType.website,
|
|
372
|
+
human_readable=ConnectionType.test_website.human_readable,
|
|
373
|
+
supported_actions=[],
|
|
374
|
+
)
|
|
375
|
+
)
|
|
376
|
+
|
|
342
377
|
return connection_system_types
|
|
@@ -44,6 +44,11 @@ class DatabaseSettings(FidesSettings):
|
|
|
44
44
|
default=5,
|
|
45
45
|
description="Maximum number of TCP keepalive retries before the client considers the connection dead and closes it.",
|
|
46
46
|
)
|
|
47
|
+
api_engine_pool_pre_ping: bool = Field(
|
|
48
|
+
default=True,
|
|
49
|
+
description="If true, the engine will pre-ping connections to ensure they are still valid before using them.",
|
|
50
|
+
)
|
|
51
|
+
|
|
47
52
|
# Async Engine Settings
|
|
48
53
|
api_async_engine_keepalives_idle: Optional[int] = Field(
|
|
49
54
|
default=None,
|
|
@@ -58,7 +63,7 @@ class DatabaseSettings(FidesSettings):
|
|
|
58
63
|
description="Maximum number of TCP keepalive retries before the client considers the connection dead and closes it.",
|
|
59
64
|
)
|
|
60
65
|
api_async_engine_pool_pre_ping: bool = Field(
|
|
61
|
-
default=
|
|
66
|
+
default=True,
|
|
62
67
|
description="If true, the async engine will pre-ping connections to ensure they are still valid before using them.",
|
|
63
68
|
)
|
|
64
69
|
|
|
@@ -115,6 +120,10 @@ class DatabaseSettings(FidesSettings):
|
|
|
115
120
|
default=5,
|
|
116
121
|
description="Maximum number of TCP keepalive retries before the client considers the connection dead and closes it.",
|
|
117
122
|
)
|
|
123
|
+
task_engine_pool_pre_ping: bool = Field(
|
|
124
|
+
default=True,
|
|
125
|
+
description="If true, the engine will pre-ping connections to ensure they are still valid before using them.",
|
|
126
|
+
)
|
|
118
127
|
test_db: str = Field(
|
|
119
128
|
default="default_test_db",
|
|
120
129
|
description="Used instead of the 'db' value when the FIDES_TEST_MODE environment variable is set to True. Avoids overwriting production data.",
|