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.

Files changed (112) hide show
  1. {ethyca_fides-2.67.2rc0.dist-info → ethyca_fides-2.67.3b1.dist-info}/METADATA +2 -2
  2. {ethyca_fides-2.67.2rc0.dist-info → ethyca_fides-2.67.3b1.dist-info}/RECORD +112 -110
  3. fides/_version.py +3 -3
  4. fides/api/db/crud.py +24 -41
  5. fides/api/graph/traversal.py +1 -1
  6. fides/api/main.py +2 -1
  7. fides/api/models/detection_discovery/core.py +59 -24
  8. fides/api/models/manual_task/manual_task.py +3 -6
  9. fides/api/models/privacy_request/privacy_request.py +78 -0
  10. fides/api/service/privacy_request/dsr_package/dsr_report_builder.py +3 -19
  11. fides/api/task/conditional_dependencies/evaluator.py +12 -19
  12. fides/api/task/conditional_dependencies/operators.py +150 -0
  13. fides/api/task/conditional_dependencies/schemas.py +51 -0
  14. fides/api/task/create_request_tasks.py +5 -3
  15. fides/api/task/filter_results.py +5 -1
  16. fides/api/task/manual/manual_task_address.py +4 -2
  17. fides/api/task/manual/manual_task_graph_task.py +87 -83
  18. fides/api/task/manual/manual_task_utils.py +84 -201
  19. fides/api/util/storage_util.py +19 -0
  20. fides/config/__init__.py +4 -0
  21. fides/config/config_proxy.py +7 -0
  22. fides/config/privacy_center_settings.py +17 -0
  23. fides/config/utils.py +1 -0
  24. fides/ui-build/static/admin/404.html +1 -1
  25. fides/ui-build/static/admin/_next/static/{Y7C7V1jdXK7rWXDIdtD13 → KAV7vwBbSuOInfCyClBs2}/_buildManifest.js +1 -1
  26. fides/ui-build/static/admin/_next/static/chunks/pages/integrations/{[id]-e0a755c69081fffa.js → [id]-330475705adbd36f.js} +1 -1
  27. fides/ui-build/static/admin/add-systems/manual.html +1 -1
  28. fides/ui-build/static/admin/add-systems/multiple.html +1 -1
  29. fides/ui-build/static/admin/add-systems.html +1 -1
  30. fides/ui-build/static/admin/consent/configure/add-vendors.html +1 -1
  31. fides/ui-build/static/admin/consent/configure.html +1 -1
  32. fides/ui-build/static/admin/consent/privacy-experience/[id].html +1 -1
  33. fides/ui-build/static/admin/consent/privacy-experience/new.html +1 -1
  34. fides/ui-build/static/admin/consent/privacy-experience.html +1 -1
  35. fides/ui-build/static/admin/consent/privacy-notices/[id].html +1 -1
  36. fides/ui-build/static/admin/consent/privacy-notices/new.html +1 -1
  37. fides/ui-build/static/admin/consent/privacy-notices.html +1 -1
  38. fides/ui-build/static/admin/consent/properties.html +1 -1
  39. fides/ui-build/static/admin/consent/reporting.html +1 -1
  40. fides/ui-build/static/admin/consent.html +1 -1
  41. fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn]/[resourceUrn].html +1 -1
  42. fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn].html +1 -1
  43. fides/ui-build/static/admin/data-catalog/[systemId]/projects.html +1 -1
  44. fides/ui-build/static/admin/data-catalog/[systemId]/resources/[resourceUrn].html +1 -1
  45. fides/ui-build/static/admin/data-catalog/[systemId]/resources.html +1 -1
  46. fides/ui-build/static/admin/data-catalog.html +1 -1
  47. fides/ui-build/static/admin/data-discovery/action-center/[monitorId]/[systemId].html +1 -1
  48. fides/ui-build/static/admin/data-discovery/action-center/[monitorId].html +1 -1
  49. fides/ui-build/static/admin/data-discovery/action-center.html +1 -1
  50. fides/ui-build/static/admin/data-discovery/activity.html +1 -1
  51. fides/ui-build/static/admin/data-discovery/detection/[resourceUrn].html +1 -1
  52. fides/ui-build/static/admin/data-discovery/detection.html +1 -1
  53. fides/ui-build/static/admin/data-discovery/discovery/[resourceUrn].html +1 -1
  54. fides/ui-build/static/admin/data-discovery/discovery.html +1 -1
  55. fides/ui-build/static/admin/datamap.html +1 -1
  56. fides/ui-build/static/admin/dataset/[datasetId]/[collectionName]/[...subfieldNames].html +1 -1
  57. fides/ui-build/static/admin/dataset/[datasetId]/[collectionName].html +1 -1
  58. fides/ui-build/static/admin/dataset/[datasetId].html +1 -1
  59. fides/ui-build/static/admin/dataset/new.html +1 -1
  60. fides/ui-build/static/admin/dataset.html +1 -1
  61. fides/ui-build/static/admin/datastore-connection/[id].html +1 -1
  62. fides/ui-build/static/admin/datastore-connection/new.html +1 -1
  63. fides/ui-build/static/admin/datastore-connection.html +1 -1
  64. fides/ui-build/static/admin/index.html +1 -1
  65. fides/ui-build/static/admin/integrations/[id].html +1 -1
  66. fides/ui-build/static/admin/integrations.html +1 -1
  67. fides/ui-build/static/admin/lib/fides-preview.js +1 -1
  68. fides/ui-build/static/admin/login/[provider].html +1 -1
  69. fides/ui-build/static/admin/login.html +1 -1
  70. fides/ui-build/static/admin/messaging/[id].html +1 -1
  71. fides/ui-build/static/admin/messaging/add-template.html +1 -1
  72. fides/ui-build/static/admin/messaging.html +1 -1
  73. fides/ui-build/static/admin/poc/ant-components.html +1 -1
  74. fides/ui-build/static/admin/poc/form-experiments/AntForm.html +1 -1
  75. fides/ui-build/static/admin/poc/form-experiments/FormikAntFormItem.html +1 -1
  76. fides/ui-build/static/admin/poc/form-experiments/FormikControlled.html +1 -1
  77. fides/ui-build/static/admin/poc/form-experiments/FormikField.html +1 -1
  78. fides/ui-build/static/admin/poc/form-experiments/FormikSpreadField.html +1 -1
  79. fides/ui-build/static/admin/poc/forms.html +1 -1
  80. fides/ui-build/static/admin/poc/table-migration.html +1 -1
  81. fides/ui-build/static/admin/privacy-requests/[id].html +1 -1
  82. fides/ui-build/static/admin/privacy-requests/configure/messaging.html +1 -1
  83. fides/ui-build/static/admin/privacy-requests/configure/storage.html +1 -1
  84. fides/ui-build/static/admin/privacy-requests/configure.html +1 -1
  85. fides/ui-build/static/admin/privacy-requests.html +1 -1
  86. fides/ui-build/static/admin/properties/[id].html +1 -1
  87. fides/ui-build/static/admin/properties/add-property.html +1 -1
  88. fides/ui-build/static/admin/properties.html +1 -1
  89. fides/ui-build/static/admin/reporting/datamap.html +1 -1
  90. fides/ui-build/static/admin/settings/about/alpha.html +1 -1
  91. fides/ui-build/static/admin/settings/about.html +1 -1
  92. fides/ui-build/static/admin/settings/consent/[configuration_id]/[purpose_id].html +1 -1
  93. fides/ui-build/static/admin/settings/consent.html +1 -1
  94. fides/ui-build/static/admin/settings/custom-fields.html +1 -1
  95. fides/ui-build/static/admin/settings/domain-records.html +1 -1
  96. fides/ui-build/static/admin/settings/domains.html +1 -1
  97. fides/ui-build/static/admin/settings/email-templates.html +1 -1
  98. fides/ui-build/static/admin/settings/locations.html +1 -1
  99. fides/ui-build/static/admin/settings/organization.html +1 -1
  100. fides/ui-build/static/admin/settings/regulations.html +1 -1
  101. fides/ui-build/static/admin/systems/configure/[id]/test-datasets.html +1 -1
  102. fides/ui-build/static/admin/systems/configure/[id].html +1 -1
  103. fides/ui-build/static/admin/systems.html +1 -1
  104. fides/ui-build/static/admin/taxonomy.html +1 -1
  105. fides/ui-build/static/admin/user-management/new.html +1 -1
  106. fides/ui-build/static/admin/user-management/profile/[id].html +1 -1
  107. fides/ui-build/static/admin/user-management.html +1 -1
  108. {ethyca_fides-2.67.2rc0.dist-info → ethyca_fides-2.67.3b1.dist-info}/WHEEL +0 -0
  109. {ethyca_fides-2.67.2rc0.dist-info → ethyca_fides-2.67.3b1.dist-info}/entry_points.txt +0 -0
  110. {ethyca_fides-2.67.2rc0.dist-info → ethyca_fides-2.67.3b1.dist-info}/licenses/LICENSE +0 -0
  111. {ethyca_fides-2.67.2rc0.dist-info → ethyca_fides-2.67.3b1.dist-info}/top_level.txt +0 -0
  112. /fides/ui-build/static/admin/_next/static/{Y7C7V1jdXK7rWXDIdtD13 → KAV7vwBbSuOInfCyClBs2}/_ssgManifest.js +0 -0
@@ -380,38 +380,51 @@ class StagedResourceAncestor(Base):
380
380
  )
381
381
 
382
382
  @classmethod
383
- def create_staged_resource_ancestor_links(
383
+ def create_all_staged_resource_ancestor_links(
384
384
  cls,
385
385
  db: Session,
386
- resource_urn: str,
387
- ancestor_urns: Set[str],
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 resource URN and the set of its ancestor URNs.
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
- links_to_insert = []
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
- for ancestor_urn in ancestor_urns:
400
- links_to_insert.append(
401
- {"ancestor_urn": ancestor_urn, "descendant_urn": resource_urn}
402
- )
411
+ current_batch = []
403
412
 
404
- if links_to_insert:
405
- # Using raw SQL for ON CONFLICT with parameters for safety
406
- stmt_text = text(
407
- """
408
- INSERT INTO stagedresourceancestor (id, ancestor_urn, descendant_urn)
409
- VALUES ('srl_' || gen_random_uuid(), :ancestor_urn, :descendant_urn)
410
- ON CONFLICT (ancestor_urn, descendant_urn) DO NOTHING;
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
- db.execute(stmt_text, links_to_insert)
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):
@@ -503,17 +516,39 @@ class StagedResource(Base):
503
516
  foreign_keys=[StagedResourceAncestor.ancestor_urn],
504
517
  )
505
518
 
506
- def ancestors(self) -> List[StagedResource]:
519
+ def ancestors(self, db: Session) -> List[StagedResource]:
507
520
  """
508
521
  Returns the ancestors of the staged resource
509
522
  """
510
- return [link.ancestor_staged_resource for link in self.ancestor_links]
523
+ # Single query to get all ancestors with their data
524
+ query = (
525
+ select(StagedResource)
526
+ .join(
527
+ StagedResourceAncestor,
528
+ StagedResource.urn == StagedResourceAncestor.ancestor_urn,
529
+ )
530
+ .where(StagedResourceAncestor.descendant_urn == self.urn)
531
+ )
532
+
533
+ result = db.execute(query)
534
+ return list(result.scalars().all())
511
535
 
512
- def descendants(self) -> List[StagedResource]:
536
+ def descendants(self, db: Session) -> List[StagedResource]:
513
537
  """
514
538
  Returns the descendants of the staged resource
515
539
  """
516
- return [link.descendant_staged_resource for link in self.descendant_links]
540
+ # Single query to get all descendants with their data
541
+ query = (
542
+ select(StagedResource)
543
+ .join(
544
+ StagedResourceAncestor,
545
+ StagedResource.urn == StagedResourceAncestor.descendant_urn,
546
+ )
547
+ .where(StagedResourceAncestor.ancestor_urn == self.urn)
548
+ )
549
+
550
+ result = db.execute(query)
551
+ return list(result.scalars().all())
517
552
 
518
553
  # placeholder for additional attributes
519
554
  meta = Column(
@@ -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 haven't been completed yet.
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.required_fields
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.field_metadata.get("required", False)
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 = self._format_size(float(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 = self._format_size(float(len(self.baos.getvalue())))
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
- operator_methods = {
17
- Operator.exists: lambda a, _: a is not None,
18
- Operator.not_exists: lambda a, _: a is None,
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
- actual_value = self._get_nested_value(data, condition.field_address.split("."))
38
+ data_value = self._get_nested_value(data, condition.field_address.split("."))
48
39
  # Apply operator and return result
49
- return self._apply_operator(actual_value, condition.operator, condition.value)
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, actual_value: Any, operator: Operator, expected_value: Any
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
- return False
108
-
109
- return operator_method(actual_value, expected_value)
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
- create_manual_task_instances_for_privacy_request,
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 addr.collection == ManualTaskAddress.MANUAL_DATA_COLLECTION
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
- create_manual_task_instances_for_privacy_request(session, privacy_request)
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(
@@ -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 f":{ManualTaskAddress.MANUAL_DATA_COLLECTION}" in node_address:
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}")