ethyca-fides 2.67.2rc0__py2.py3-none-any.whl → 2.67.3b0__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.3b0.dist-info}/METADATA +2 -2
- {ethyca_fides-2.67.2rc0.dist-info → ethyca_fides-2.67.3b0.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 +33 -20
- 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/chunks/pages/integrations/{[id]-e0a755c69081fffa.js → [id]-330475705adbd36f.js} +1 -1
- fides/ui-build/static/admin/_next/static/{Y7C7V1jdXK7rWXDIdtD13 → zmR4ZN0rEFYsCKjAR-Jzz}/_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/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.3b0.dist-info}/WHEEL +0 -0
- {ethyca_fides-2.67.2rc0.dist-info → ethyca_fides-2.67.3b0.dist-info}/entry_points.txt +0 -0
- {ethyca_fides-2.67.2rc0.dist-info → ethyca_fides-2.67.3b0.dist-info}/licenses/LICENSE +0 -0
- {ethyca_fides-2.67.2rc0.dist-info → ethyca_fides-2.67.3b0.dist-info}/top_level.txt +0 -0
- /fides/ui-build/static/admin/_next/static/{Y7C7V1jdXK7rWXDIdtD13 → zmR4ZN0rEFYsCKjAR-Jzz}/_ssgManifest.js +0 -0
|
@@ -380,38 +380,51 @@ class StagedResourceAncestor(Base):
|
|
|
380
380
|
)
|
|
381
381
|
|
|
382
382
|
@classmethod
|
|
383
|
-
def
|
|
383
|
+
def create_all_staged_resource_ancestor_links(
|
|
384
384
|
cls,
|
|
385
385
|
db: Session,
|
|
386
|
-
|
|
387
|
-
|
|
386
|
+
ancestor_links: Dict[str, Set[str]],
|
|
387
|
+
batch_size: int = 10000, # Conservative batch size
|
|
388
388
|
) -> None:
|
|
389
389
|
"""
|
|
390
|
-
Bulk inserts entries in the StagedResourceAncestor table
|
|
391
|
-
based on the provided
|
|
390
|
+
Bulk inserts all entries in the StagedResourceAncestor table
|
|
391
|
+
based on the provided mapping of descendant URNs to their ancestor URN sets.
|
|
392
392
|
|
|
393
393
|
We execute the bulk INSERT with the provided (synchronous) db session,
|
|
394
394
|
but the transaction is _not_ committed, so the caller must commit the transaction
|
|
395
395
|
to persist the changes.
|
|
396
|
+
|
|
397
|
+
Uses batching to handle large datasets without hitting PostgreSQL parameter limits.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
db: Database session
|
|
401
|
+
ancestor_links: Dict mapping descendant URNs to sets of ancestor URNs
|
|
396
402
|
"""
|
|
397
|
-
|
|
403
|
+
stmt_text = text(
|
|
404
|
+
"""
|
|
405
|
+
INSERT INTO stagedresourceancestor (id, ancestor_urn, descendant_urn)
|
|
406
|
+
VALUES ('srl_' || gen_random_uuid(), :ancestor_urn, :descendant_urn)
|
|
407
|
+
ON CONFLICT (ancestor_urn, descendant_urn) DO NOTHING;
|
|
408
|
+
"""
|
|
409
|
+
)
|
|
398
410
|
|
|
399
|
-
|
|
400
|
-
links_to_insert.append(
|
|
401
|
-
{"ancestor_urn": ancestor_urn, "descendant_urn": resource_urn}
|
|
402
|
-
)
|
|
411
|
+
current_batch = []
|
|
403
412
|
|
|
404
|
-
|
|
405
|
-
#
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
+
for descendant_urn, ancestor_urns in ancestor_links.items():
|
|
414
|
+
if ancestor_urns: # Only create links if there are ancestors
|
|
415
|
+
for ancestor_urn in ancestor_urns:
|
|
416
|
+
current_batch.append(
|
|
417
|
+
{"ancestor_urn": ancestor_urn, "descendant_urn": descendant_urn}
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
# Execute batch when it reaches the desired size
|
|
421
|
+
if len(current_batch) >= batch_size:
|
|
422
|
+
db.execute(stmt_text, current_batch)
|
|
423
|
+
current_batch = []
|
|
413
424
|
|
|
414
|
-
|
|
425
|
+
# Execute any remaining items in the final batch
|
|
426
|
+
if current_batch:
|
|
427
|
+
db.execute(stmt_text, current_batch)
|
|
415
428
|
|
|
416
429
|
|
|
417
430
|
class StagedResource(Base):
|
|
@@ -383,15 +383,13 @@ class ManualTaskInstance(Base):
|
|
|
383
383
|
|
|
384
384
|
@property
|
|
385
385
|
def incomplete_fields(self) -> list["ManualTaskConfigField"]:
|
|
386
|
-
"""Get all fields that
|
|
387
|
-
A field is considered incomplete if:
|
|
388
|
-
1. It's required and has no submission
|
|
386
|
+
"""Get all fields that have no submission.
|
|
389
387
|
Returns:
|
|
390
388
|
list[ManualTaskConfigField]: List of incomplete fields
|
|
391
389
|
"""
|
|
392
390
|
return [
|
|
393
391
|
field
|
|
394
|
-
for field in self.
|
|
392
|
+
for field in self.config.field_definitions
|
|
395
393
|
if not self.get_submission_for_field(field.id)
|
|
396
394
|
]
|
|
397
395
|
|
|
@@ -401,8 +399,7 @@ class ManualTaskInstance(Base):
|
|
|
401
399
|
return [
|
|
402
400
|
field
|
|
403
401
|
for field in self.config.field_definitions
|
|
404
|
-
if field.
|
|
405
|
-
and self.get_submission_for_field(field.id)
|
|
402
|
+
if self.get_submission_for_field(field.id)
|
|
406
403
|
]
|
|
407
404
|
|
|
408
405
|
def get_submission_for_field(
|
|
@@ -47,8 +47,16 @@ from fides.api.models.attachment import (
|
|
|
47
47
|
from fides.api.models.audit_log import AuditLog
|
|
48
48
|
from fides.api.models.client import ClientDetail
|
|
49
49
|
from fides.api.models.comment import Comment, CommentReference, CommentReferenceType
|
|
50
|
+
from fides.api.models.connectionconfig import ConnectionConfig
|
|
50
51
|
from fides.api.models.fides_user import FidesUser
|
|
51
52
|
from fides.api.models.field_types import EncryptedLargeDataDescriptor
|
|
53
|
+
from fides.api.models.manual_task import (
|
|
54
|
+
ManualTask,
|
|
55
|
+
ManualTaskConfig,
|
|
56
|
+
ManualTaskConfigurationType,
|
|
57
|
+
ManualTaskEntityType,
|
|
58
|
+
ManualTaskInstance,
|
|
59
|
+
)
|
|
52
60
|
from fides.api.models.manual_webhook import AccessManualWebhook
|
|
53
61
|
from fides.api.models.masking_secret import MaskingSecret
|
|
54
62
|
from fides.api.models.policy import (
|
|
@@ -200,6 +208,14 @@ class PrivacyRequest(
|
|
|
200
208
|
viewonly=True,
|
|
201
209
|
uselist=True,
|
|
202
210
|
)
|
|
211
|
+
manual_task_instances = relationship(
|
|
212
|
+
"ManualTaskInstance",
|
|
213
|
+
lazy="select",
|
|
214
|
+
passive_deletes="all",
|
|
215
|
+
primaryjoin="and_(ManualTaskInstance.entity_id==foreign(PrivacyRequest.id), "
|
|
216
|
+
"ManualTaskInstance.entity_type=='privacy_request')",
|
|
217
|
+
uselist=True,
|
|
218
|
+
)
|
|
203
219
|
property_id = Column(String, nullable=True)
|
|
204
220
|
|
|
205
221
|
cancel_reason = Column(String(200))
|
|
@@ -1175,6 +1191,68 @@ class PrivacyRequest(
|
|
|
1175
1191
|
db, manual_webhook_id, "erasure_manual_webhook"
|
|
1176
1192
|
)
|
|
1177
1193
|
|
|
1194
|
+
def create_manual_task_instances(
|
|
1195
|
+
self, db: Session, connection_configs_with_manual_tasks: list[ConnectionConfig]
|
|
1196
|
+
) -> list[ManualTaskInstance]:
|
|
1197
|
+
"""Create ManualTaskInstance entries for all active manual tasks relevant to a privacy request."""
|
|
1198
|
+
# Early return if no relevant policy rules
|
|
1199
|
+
policy_rules = {
|
|
1200
|
+
ActionType.access: bool(
|
|
1201
|
+
self.policy.get_rules_for_action(action_type=ActionType.access)
|
|
1202
|
+
),
|
|
1203
|
+
ActionType.erasure: bool(
|
|
1204
|
+
self.policy.get_rules_for_action(action_type=ActionType.erasure)
|
|
1205
|
+
),
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
if not any(policy_rules.values()):
|
|
1209
|
+
return []
|
|
1210
|
+
|
|
1211
|
+
# Build configuration types using list comprehension
|
|
1212
|
+
config_types = [
|
|
1213
|
+
(
|
|
1214
|
+
ManualTaskConfigurationType.access_privacy_request
|
|
1215
|
+
if action_type == ActionType.access
|
|
1216
|
+
else ManualTaskConfigurationType.erasure_privacy_request
|
|
1217
|
+
)
|
|
1218
|
+
for action_type, has_rules in policy_rules.items()
|
|
1219
|
+
if has_rules
|
|
1220
|
+
]
|
|
1221
|
+
|
|
1222
|
+
# Get all relevant manual tasks and configs in one query
|
|
1223
|
+
connection_config_ids = [cc.id for cc in connection_configs_with_manual_tasks]
|
|
1224
|
+
manual_tasks_with_configs = (
|
|
1225
|
+
db.query(ManualTask, ManualTaskConfig)
|
|
1226
|
+
.join(ManualTaskConfig, ManualTask.id == ManualTaskConfig.task_id)
|
|
1227
|
+
.filter(
|
|
1228
|
+
ManualTask.parent_entity_id.in_(connection_config_ids),
|
|
1229
|
+
ManualTask.parent_entity_type == "connection_config",
|
|
1230
|
+
ManualTaskConfig.is_current.is_(True),
|
|
1231
|
+
ManualTaskConfig.config_type.in_(config_types),
|
|
1232
|
+
)
|
|
1233
|
+
.all()
|
|
1234
|
+
)
|
|
1235
|
+
|
|
1236
|
+
# Get existing config IDs to avoid duplicates
|
|
1237
|
+
existing_config_ids = {
|
|
1238
|
+
instance.config_id for instance in self.manual_task_instances
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
# Create instances using list comprehension and filter out existing ones
|
|
1242
|
+
return [
|
|
1243
|
+
ManualTaskInstance.create(
|
|
1244
|
+
db=db,
|
|
1245
|
+
data={
|
|
1246
|
+
"entity_id": self.id,
|
|
1247
|
+
"entity_type": ManualTaskEntityType.privacy_request,
|
|
1248
|
+
"task_id": manual_task.id,
|
|
1249
|
+
"config_id": config.id,
|
|
1250
|
+
},
|
|
1251
|
+
)
|
|
1252
|
+
for manual_task, config in manual_tasks_with_configs
|
|
1253
|
+
if config.id not in existing_config_ids
|
|
1254
|
+
]
|
|
1255
|
+
|
|
1178
1256
|
def get_existing_request_task(
|
|
1179
1257
|
self,
|
|
1180
1258
|
db: Session,
|
|
@@ -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."
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import operator as py_operator
|
|
2
1
|
from typing import Any, Union
|
|
3
2
|
|
|
4
3
|
from loguru import logger
|
|
5
4
|
from sqlalchemy.orm import Session
|
|
6
5
|
|
|
7
6
|
from fides.api.graph.config import FieldPath
|
|
7
|
+
from fides.api.task.conditional_dependencies.operators import operator_methods
|
|
8
8
|
from fides.api.task.conditional_dependencies.schemas import (
|
|
9
9
|
Condition,
|
|
10
10
|
ConditionGroup,
|
|
@@ -13,18 +13,9 @@ from fides.api.task.conditional_dependencies.schemas import (
|
|
|
13
13
|
Operator,
|
|
14
14
|
)
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
Operator.eq: py_operator.eq,
|
|
20
|
-
Operator.neq: py_operator.ne,
|
|
21
|
-
Operator.lt: lambda a, b: a < b if a is not None else False,
|
|
22
|
-
Operator.lte: lambda a, b: a <= b if a is not None else False,
|
|
23
|
-
Operator.gt: lambda a, b: a > b if a is not None else False,
|
|
24
|
-
Operator.gte: lambda a, b: a >= b if a is not None else False,
|
|
25
|
-
Operator.list_contains: lambda a, b: b in a if isinstance(a, list) else False,
|
|
26
|
-
Operator.not_in_list: lambda a, b: a not in b if isinstance(b, list) else True,
|
|
27
|
-
}
|
|
16
|
+
|
|
17
|
+
class ConditionEvaluationError(Exception):
|
|
18
|
+
"""Error raised when a condition evaluation fails"""
|
|
28
19
|
|
|
29
20
|
|
|
30
21
|
class ConditionEvaluator:
|
|
@@ -44,9 +35,9 @@ class ConditionEvaluator:
|
|
|
44
35
|
self, condition: ConditionLeaf, data: Union[dict, Any]
|
|
45
36
|
) -> bool:
|
|
46
37
|
"""Evaluate a leaf condition against input data"""
|
|
47
|
-
|
|
38
|
+
data_value = self._get_nested_value(data, condition.field_address.split("."))
|
|
48
39
|
# Apply operator and return result
|
|
49
|
-
return self._apply_operator(
|
|
40
|
+
return self._apply_operator(data_value, condition.operator, condition.value)
|
|
50
41
|
|
|
51
42
|
def _evaluate_group_condition(
|
|
52
43
|
self, group: ConditionGroup, data: Union[dict, Any]
|
|
@@ -96,7 +87,7 @@ class ConditionEvaluator:
|
|
|
96
87
|
return current if current != {} else None
|
|
97
88
|
|
|
98
89
|
def _apply_operator(
|
|
99
|
-
self,
|
|
90
|
+
self, data_value: Any, operator: Operator, user_input_value: Any
|
|
100
91
|
) -> bool:
|
|
101
92
|
"""Apply operator to actual and expected values"""
|
|
102
93
|
|
|
@@ -104,6 +95,8 @@ class ConditionEvaluator:
|
|
|
104
95
|
operator_method = operator_methods.get(operator)
|
|
105
96
|
if operator_method is None:
|
|
106
97
|
logger.warning(f"Unknown operator: {operator}")
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
98
|
+
raise ConditionEvaluationError(f"Unknown operator: {operator}")
|
|
99
|
+
try:
|
|
100
|
+
return operator_method(data_value, user_input_value)
|
|
101
|
+
except (TypeError, ValueError) as e:
|
|
102
|
+
raise ConditionEvaluationError(f"Error evaluating condition: {e}") from e
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import numbers
|
|
2
|
+
import operator as py_operator
|
|
3
|
+
|
|
4
|
+
from fides.api.task.conditional_dependencies.schemas import Operator
|
|
5
|
+
|
|
6
|
+
# Define operator methods for validation
|
|
7
|
+
#
|
|
8
|
+
# None Value Handling:
|
|
9
|
+
# - Basic operators (eq, neq, exists, not_exists) handle None naturally
|
|
10
|
+
# * Note: eq and neq use Python's built-in behavior, so True == 1 returns True
|
|
11
|
+
# - Numeric operators return False for None values (can't compare None with numbers)
|
|
12
|
+
# * Note: Boolean values are excluded from numeric comparisons (True < 10 returns False)
|
|
13
|
+
# - String operators return False for None values (can't perform string operations on None)
|
|
14
|
+
# - List operators handle None naturally using Python's built-in behavior:
|
|
15
|
+
# * None in [None] returns True
|
|
16
|
+
# * None not in [None] returns False
|
|
17
|
+
# * None in [] returns False
|
|
18
|
+
# * None not in [] returns True
|
|
19
|
+
# * This allows None to be a valid list element for membership testing
|
|
20
|
+
operator_methods = {
|
|
21
|
+
# Basic operators - handle None naturally using Python's built-in behavior
|
|
22
|
+
Operator.exists: lambda a, _: a is not None,
|
|
23
|
+
Operator.not_exists: lambda a, _: a is None,
|
|
24
|
+
Operator.eq: py_operator.eq,
|
|
25
|
+
# None == None returns True, None == "anything" returns False
|
|
26
|
+
# None != None returns False, None != "anything" returns True
|
|
27
|
+
Operator.neq: py_operator.ne,
|
|
28
|
+
# Numeric comparison operators - return False for None or non-numeric types
|
|
29
|
+
# Note: Boolean values are excluded as they are not considered numeric for comparisons
|
|
30
|
+
# This differs from basic comparison operators (eq, neq) which use Python's built-in behavior
|
|
31
|
+
Operator.lt: lambda a, b: (
|
|
32
|
+
a < b
|
|
33
|
+
if a is not None and isinstance(a, numbers.Number) and not isinstance(a, bool)
|
|
34
|
+
else False
|
|
35
|
+
),
|
|
36
|
+
Operator.lte: lambda a, b: (
|
|
37
|
+
a <= b
|
|
38
|
+
if a is not None and isinstance(a, numbers.Number) and not isinstance(a, bool)
|
|
39
|
+
else False
|
|
40
|
+
),
|
|
41
|
+
Operator.gt: lambda a, b: (
|
|
42
|
+
a > b
|
|
43
|
+
if a is not None and isinstance(a, numbers.Number) and not isinstance(a, bool)
|
|
44
|
+
else False
|
|
45
|
+
),
|
|
46
|
+
Operator.gte: lambda a, b: (
|
|
47
|
+
a >= b
|
|
48
|
+
if a is not None and isinstance(a, numbers.Number) and not isinstance(a, bool)
|
|
49
|
+
else False
|
|
50
|
+
),
|
|
51
|
+
Operator.list_contains: lambda a, b: (
|
|
52
|
+
# If user provides a list, check if column value is in it
|
|
53
|
+
# Note: Handles None values naturally using Python's built-in behavior
|
|
54
|
+
a in b
|
|
55
|
+
if isinstance(b, list)
|
|
56
|
+
# If column value is a list, check if user value is in it
|
|
57
|
+
else b in a if isinstance(a, list) else False
|
|
58
|
+
),
|
|
59
|
+
Operator.not_in_list: lambda a, b: (
|
|
60
|
+
# If user provides a list, check if column value is NOT in it
|
|
61
|
+
# Note: Handles None values naturally using Python's built-in behavior
|
|
62
|
+
a not in b
|
|
63
|
+
if isinstance(b, list)
|
|
64
|
+
# If column value is a list, check if user value is NOT in it
|
|
65
|
+
else b not in a if isinstance(a, list) else False
|
|
66
|
+
),
|
|
67
|
+
Operator.list_intersects: lambda a, b: (
|
|
68
|
+
# Check if there are any common elements between the lists
|
|
69
|
+
# Note: Returns False for None values because both values must be lists
|
|
70
|
+
bool(set(a) & set(b))
|
|
71
|
+
if isinstance(a, list) and isinstance(b, list)
|
|
72
|
+
else False
|
|
73
|
+
),
|
|
74
|
+
Operator.list_subset: lambda a, b: (
|
|
75
|
+
# Check if column list is a subset of user's list
|
|
76
|
+
# Note: Returns False for None values because both values must be lists
|
|
77
|
+
set(a).issubset(set(b))
|
|
78
|
+
if isinstance(a, list) and isinstance(b, list)
|
|
79
|
+
else False
|
|
80
|
+
),
|
|
81
|
+
Operator.list_superset: lambda a, b: (
|
|
82
|
+
# Check if column list is a superset of user's list
|
|
83
|
+
# Note: Returns False for None values because both values must be lists
|
|
84
|
+
set(a).issuperset(set(b))
|
|
85
|
+
if isinstance(a, list) and isinstance(b, list)
|
|
86
|
+
else False
|
|
87
|
+
),
|
|
88
|
+
Operator.list_disjoint: lambda a, b: (
|
|
89
|
+
# Check if lists have no common elements
|
|
90
|
+
# Note: Returns False for None values because both values must be lists
|
|
91
|
+
set(a).isdisjoint(set(b))
|
|
92
|
+
if isinstance(a, list) and isinstance(b, list)
|
|
93
|
+
else False
|
|
94
|
+
),
|
|
95
|
+
Operator.starts_with: lambda a, b: (
|
|
96
|
+
a.startswith(b) if isinstance(a, str) and isinstance(b, str) else False
|
|
97
|
+
),
|
|
98
|
+
Operator.ends_with: lambda a, b: (
|
|
99
|
+
a.endswith(b) if isinstance(a, str) and isinstance(b, str) else False
|
|
100
|
+
),
|
|
101
|
+
Operator.contains: lambda a, b: (
|
|
102
|
+
b in a if isinstance(a, str) and isinstance(b, str) else False
|
|
103
|
+
),
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# Common operators that work with most data types
|
|
107
|
+
COMMON_OPERATORS = {
|
|
108
|
+
Operator.eq,
|
|
109
|
+
Operator.neq,
|
|
110
|
+
Operator.exists,
|
|
111
|
+
Operator.not_exists,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# Numeric comparison operators
|
|
115
|
+
NUMERIC_OPERATORS = {Operator.lt, Operator.lte, Operator.gt, Operator.gte}
|
|
116
|
+
|
|
117
|
+
# String-specific operators
|
|
118
|
+
STRING_OPERATORS = {
|
|
119
|
+
Operator.contains,
|
|
120
|
+
Operator.starts_with,
|
|
121
|
+
Operator.ends_with,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# List operations that work with all data types
|
|
125
|
+
LIST_OPERATORS = {
|
|
126
|
+
Operator.list_contains, # Element in list
|
|
127
|
+
Operator.not_in_list, # Element not in list
|
|
128
|
+
Operator.list_intersects, # Any common elements
|
|
129
|
+
Operator.list_subset, # Column list ⊆ user list
|
|
130
|
+
Operator.list_superset, # Column list ⊇ user list
|
|
131
|
+
Operator.list_disjoint, # No common elements
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# Define data type compatibility with operators
|
|
135
|
+
data_type_operator_compatibility = {
|
|
136
|
+
"integer": {*COMMON_OPERATORS, *NUMERIC_OPERATORS, *LIST_OPERATORS},
|
|
137
|
+
"float": {*COMMON_OPERATORS, *NUMERIC_OPERATORS, *LIST_OPERATORS},
|
|
138
|
+
"double": {*COMMON_OPERATORS, *NUMERIC_OPERATORS, *LIST_OPERATORS},
|
|
139
|
+
"long": {*COMMON_OPERATORS, *NUMERIC_OPERATORS, *LIST_OPERATORS},
|
|
140
|
+
"boolean": {*COMMON_OPERATORS, *LIST_OPERATORS},
|
|
141
|
+
"string": {*COMMON_OPERATORS, *STRING_OPERATORS, *LIST_OPERATORS},
|
|
142
|
+
"text": {
|
|
143
|
+
*COMMON_OPERATORS,
|
|
144
|
+
*LIST_OPERATORS,
|
|
145
|
+
}, # Form input - no string search operations
|
|
146
|
+
"array": {*COMMON_OPERATORS, *LIST_OPERATORS},
|
|
147
|
+
"object": {*COMMON_OPERATORS, *LIST_OPERATORS},
|
|
148
|
+
"object_id": {*COMMON_OPERATORS, *LIST_OPERATORS},
|
|
149
|
+
"no_op": {*COMMON_OPERATORS, *LIST_OPERATORS},
|
|
150
|
+
}
|
|
@@ -5,16 +5,67 @@ from pydantic import BaseModel, Field, model_validator
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class Operator(str, Enum):
|
|
8
|
+
# Basic comparison operators
|
|
9
|
+
# Column value equals user input (e.g., user.role eq "admin")
|
|
8
10
|
eq = "eq"
|
|
11
|
+
# Column value not equal to user input (e.g., user.status neq "inactive")
|
|
9
12
|
neq = "neq"
|
|
13
|
+
|
|
14
|
+
# Numeric comparison operators
|
|
15
|
+
# Column value less than user input (e.g., user.age lt 18)
|
|
10
16
|
lt = "lt"
|
|
17
|
+
# Column value less than or equal to user input (e.g., user.score lte 100)
|
|
11
18
|
lte = "lte"
|
|
19
|
+
# Column value greater than user input (e.g., user.balance gt 1000)
|
|
12
20
|
gt = "gt"
|
|
21
|
+
# Column value greater than or equal to user input (e.g., user.rating gte 4.0)
|
|
13
22
|
gte = "gte"
|
|
23
|
+
|
|
24
|
+
# Existence operators
|
|
25
|
+
# Field exists and is not None (e.g., user.email exists)
|
|
14
26
|
exists = "exists"
|
|
27
|
+
# Field does not exist or is None (e.g., user.middle_name not_exists)
|
|
15
28
|
not_exists = "not_exists"
|
|
29
|
+
|
|
30
|
+
# List membership operators (work with both single values and lists)
|
|
31
|
+
#
|
|
32
|
+
# Column value is in user's list OR user's value is in column's list
|
|
16
33
|
list_contains = "list_contains"
|
|
34
|
+
# Examples: user.role list_contains ["admin", "moderator"] (role in list)
|
|
35
|
+
# user.permissions list_contains "write" (value in permissions)
|
|
36
|
+
|
|
37
|
+
# Column value is NOT in user's list OR user's value is NOT in column's list
|
|
17
38
|
not_in_list = "not_in_list"
|
|
39
|
+
# Examples: user.role not_in_list ["banned", "suspended"] (role not blocked)
|
|
40
|
+
# user.permissions not_in_list "delete" (value not in permissions)
|
|
41
|
+
|
|
42
|
+
# List-to-list comparison operators (both values must be lists)
|
|
43
|
+
# Lists have at least one common element
|
|
44
|
+
list_intersects = "list_intersects"
|
|
45
|
+
# Example: user.roles list_intersects ["admin", "moderator"] (any common role)
|
|
46
|
+
|
|
47
|
+
# Column list is completely contained within user's list
|
|
48
|
+
list_subset = "list_subset"
|
|
49
|
+
# Example: user.permissions list_subset ["read", "write", "delete", "manage"]
|
|
50
|
+
# (all user permissions are allowed)
|
|
51
|
+
|
|
52
|
+
# Column list completely contains user's list
|
|
53
|
+
list_superset = "list_superset"
|
|
54
|
+
# Example: user.tags list_superset ["premium", "verified"]
|
|
55
|
+
# (user has all required tags plus extras)
|
|
56
|
+
|
|
57
|
+
# Lists have no common elements
|
|
58
|
+
list_disjoint = "list_disjoint"
|
|
59
|
+
# Example: user.roles list_disjoint ["banned", "suspended"]
|
|
60
|
+
# (user has no restricted roles)
|
|
61
|
+
|
|
62
|
+
# String operators
|
|
63
|
+
# String starts with user input (e.g., user.email starts_with "admin@")
|
|
64
|
+
starts_with = "starts_with"
|
|
65
|
+
# String ends with user input (e.g., user.domain ends_with ".com")
|
|
66
|
+
ends_with = "ends_with"
|
|
67
|
+
# String contains user input (e.g., user.description contains "verified")
|
|
68
|
+
contains = "contains"
|
|
18
69
|
|
|
19
70
|
|
|
20
71
|
class GroupOperator(str, Enum):
|
|
@@ -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
|
|
|
@@ -122,6 +122,10 @@ def select_and_save_field(saved: Any, row: Row, target_path: FieldPath) -> Dict:
|
|
|
122
122
|
"""Helper for building new nested resource - can return an empty dict, empty array or resource itself"""
|
|
123
123
|
return type(resource)() if isinstance(resource, (list, dict)) else resource
|
|
124
124
|
|
|
125
|
+
# If we've reached the end of the field path, return the entire current object/array
|
|
126
|
+
if not target_path.levels:
|
|
127
|
+
return row
|
|
128
|
+
|
|
125
129
|
if isinstance(row, list):
|
|
126
130
|
for i, elem in enumerate(row):
|
|
127
131
|
try:
|
|
@@ -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}")
|