ethyca-fides 2.73.2b0__py2.py3-none-any.whl → 2.74.0rc1__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 (336) hide show
  1. {ethyca_fides-2.73.2b0.dist-info → ethyca_fides-2.74.0rc1.dist-info}/METADATA +2 -1
  2. {ethyca_fides-2.73.2b0.dist-info → ethyca_fides-2.74.0rc1.dist-info}/RECORD +273 -276
  3. fides/_version.py +3 -3
  4. fides/api/alembic/migrations/versions/xx_2025_10_29_1659_80d28dea3b6b_added_duplicate_group_table.py +79 -0
  5. fides/api/alembic/migrations/versions/xx_2025_11_05_0200_f1a2b3c4d5e6_create_staged_resource_error_table.py +82 -0
  6. fides/api/api/v1/endpoints/privacy_request_endpoints.py +33 -6
  7. fides/api/db/database.py +225 -0
  8. fides/api/models/detection_discovery/__init__.py +2 -0
  9. fides/api/models/detection_discovery/core.py +10 -0
  10. fides/api/models/detection_discovery/staged_resource_error.py +25 -0
  11. fides/api/models/privacy_preference.py +1 -0
  12. fides/api/models/privacy_request/duplicate_group.py +84 -0
  13. fides/api/models/privacy_request/privacy_request.py +53 -8
  14. fides/api/schemas/privacy_request.py +16 -4
  15. fides/api/service/async_dsr/strategies/async_dsr_strategy_polling.py +13 -6
  16. fides/api/service/privacy_request/duplication_detection.py +424 -0
  17. fides/api/service/privacy_request/request_runner_service.py +5 -0
  18. fides/api/service/saas_request/saas_request_override_factory.py +7 -2
  19. fides/api/util/logger_context_utils.py +3 -1
  20. fides/api/util/memory_watchdog.py +118 -0
  21. fides/common/api/v1/urn_registry.py +1 -0
  22. fides/config/__init__.py +7 -1
  23. fides/config/celery_settings.py +42 -0
  24. fides/service/privacy_request/privacy_request_service.py +66 -5
  25. fides/ui-build/static/admin/404.html +1 -1
  26. fides/ui-build/static/admin/_next/static/FZTEUgamBvOhgPWce135w/_buildManifest.js +1 -0
  27. fides/ui-build/static/admin/_next/static/chunks/{1115-26393b586775ea29.js → 1115-0da062111df309bf.js} +1 -1
  28. fides/ui-build/static/admin/_next/static/chunks/{6148-59a59d5c5925344f.js → 1533-84e250d1f26e6d7d.js} +1 -1
  29. fides/ui-build/static/admin/_next/static/chunks/1817-508b16628e8eb225.js +1 -0
  30. fides/ui-build/static/admin/_next/static/chunks/1840-5bbe6d878ed73fb4.js +1 -0
  31. fides/ui-build/static/admin/_next/static/chunks/3214-90ce0a366b0f461a.js +1 -0
  32. fides/ui-build/static/admin/_next/static/chunks/3377-eb5cd82b3ee6ab0c.js +1 -0
  33. fides/ui-build/static/admin/_next/static/chunks/3615-5e2d062d684b8fa1.js +1 -0
  34. fides/ui-build/static/admin/_next/static/chunks/3655-93ecd09f1cb9dbef.js +1 -0
  35. fides/ui-build/static/admin/_next/static/chunks/{4259.d1507e0db19cbed7.js → 4259.05038c9b78467244.js} +1 -1
  36. fides/ui-build/static/admin/_next/static/chunks/4277-13bcf4516326d474.js +1 -0
  37. fides/ui-build/static/admin/_next/static/chunks/454-d5c2c84f1a14e4f1.js +1 -0
  38. fides/ui-build/static/admin/_next/static/chunks/5487-8dedd1ca94fbba54.js +1 -0
  39. fides/ui-build/static/admin/_next/static/chunks/549-6e2442db533a711e.js +1 -0
  40. fides/ui-build/static/admin/_next/static/chunks/5643-55d758444a8d7162.js +1 -0
  41. fides/ui-build/static/admin/_next/static/chunks/5724-1e40975cefa405f0.js +1 -0
  42. fides/ui-build/static/admin/_next/static/chunks/6084-82e2df433fe5ba85.js +1 -0
  43. fides/ui-build/static/admin/_next/static/chunks/6344-3e21444374f8059f.js +1 -0
  44. fides/ui-build/static/admin/_next/static/chunks/6362-12e3fd23130ccf15.js +1 -0
  45. fides/ui-build/static/admin/_next/static/chunks/6372-a8d0f08dac1ebd0e.js +1 -0
  46. fides/ui-build/static/admin/_next/static/chunks/6780-3db5133c1f4c6f1e.js +1 -0
  47. fides/ui-build/static/admin/_next/static/chunks/6954-aa0c60ee1092be8e.js +1 -0
  48. fides/ui-build/static/admin/_next/static/chunks/7079-6e6efc3396ff1ebb.js +1 -0
  49. fides/ui-build/static/admin/_next/static/chunks/7218-d297a4a06f924b09.js +1 -0
  50. fides/ui-build/static/admin/_next/static/chunks/{7245-c9bc628d078c2170.js → 7245-686665c197b58e68.js} +1 -1
  51. fides/ui-build/static/admin/_next/static/chunks/7654-716cf37a020b3d11.js +1 -0
  52. fides/ui-build/static/admin/_next/static/chunks/8939-4925751c57c51f87.js +1 -0
  53. fides/ui-build/static/admin/_next/static/chunks/9014-eeae6f581158e645.js +1 -0
  54. fides/ui-build/static/admin/_next/static/chunks/{9046-d9c6498368b993d1.js → 9046-e4daf28840a69fd6.js} +1 -1
  55. fides/ui-build/static/admin/_next/static/chunks/{5596-29a7c8322530b7cf.js → 9195-550bd50d538c5f79.js} +3 -3
  56. fides/ui-build/static/admin/_next/static/chunks/9341-bfc0e59bcc56c604.js +1 -0
  57. fides/ui-build/static/admin/_next/static/chunks/9450-b7b7bb1d755ecf57.js +1 -0
  58. fides/ui-build/static/admin/_next/static/chunks/9682-da69ac5d06f281da.js +1 -0
  59. fides/ui-build/static/admin/_next/static/chunks/9911-ece086f2230e34f0.js +1 -0
  60. fides/ui-build/static/admin/_next/static/chunks/9965-56c5e4fc9cd3b3a5.js +1 -0
  61. fides/ui-build/static/admin/_next/static/chunks/pages/{_app-ee588a308812715d.js → _app-e64fd8510033a27c.js} +63 -63
  62. fides/ui-build/static/admin/_next/static/chunks/pages/add-systems/{manual-7081e0e49f67716c.js → manual-ddd9d7d40847fc28.js} +1 -1
  63. fides/ui-build/static/admin/_next/static/chunks/pages/consent/privacy-experience/{[id]-1b02a4991201b7e4.js → [id]-9b1f2b1c06968166.js} +1 -1
  64. fides/ui-build/static/admin/_next/static/chunks/pages/consent/privacy-experience/{new-a5e738a234dadc7e.js → new-115a085e5d42de45.js} +1 -1
  65. fides/ui-build/static/admin/_next/static/chunks/pages/consent/privacy-notices/{[id]-8c164c4b8310214e.js → [id]-3de34624829cbce8.js} +1 -1
  66. fides/ui-build/static/admin/_next/static/chunks/pages/consent/privacy-notices/{new-db789002d84c8829.js → new-dc95e7ed278d1a29.js} +1 -1
  67. fides/ui-build/static/admin/_next/static/chunks/pages/consent/reporting-e3ad3a55624e302a.js +1 -0
  68. fides/ui-build/static/admin/_next/static/chunks/pages/data-catalog/[systemId]/projects/[projectUrn]/{[resourceUrn]-32bd7a7c990e5bf6.js → [resourceUrn]-dd82729296dee5c5.js} +1 -1
  69. fides/ui-build/static/admin/_next/static/chunks/pages/data-catalog/[systemId]/projects/[projectUrn]-49e5477eb1a11b92.js +1 -0
  70. fides/ui-build/static/admin/_next/static/chunks/pages/data-catalog/[systemId]/{projects-07e7d38ce34e1e6c.js → projects-dfc1ead4a12c9ffa.js} +1 -1
  71. fides/ui-build/static/admin/_next/static/chunks/pages/data-catalog/[systemId]/resources/{[resourceUrn]-b07a0707f8c2ec0d.js → [resourceUrn]-8442eb219958ac7e.js} +1 -1
  72. fides/ui-build/static/admin/_next/static/chunks/pages/data-catalog/[systemId]/resources-feda358d1801c18d.js +1 -0
  73. fides/ui-build/static/admin/_next/static/chunks/pages/data-catalog-d16acb6fc07aad46.js +1 -0
  74. fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center/datastore/[monitorId]-c51a1e98c45d231a.js +1 -0
  75. fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center/website/[monitorId]/[systemId]-bcfe38eebca30f8c.js +1 -0
  76. fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center/website/{[monitorId]-73085f50abb775c0.js → [monitorId]-f66d0655897c4400.js} +1 -1
  77. fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center-6f1e012cd641da19.js +1 -0
  78. fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/{activity-24a82e07a0008516.js → activity-581d6248fcf98d17.js} +1 -1
  79. fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/detection/{[resourceUrn]-b072cf25aefc98f6.js → [resourceUrn]-ddc1c1641e1e9430.js} +1 -1
  80. fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/{detection-fd3e8817d8e6dee4.js → detection-2b48f7e524743b2b.js} +1 -1
  81. fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/discovery/{[resourceUrn]-146624cf59792bf7.js → [resourceUrn]-862b67418600251e.js} +1 -1
  82. fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/{discovery-9695cc9c85592ec5.js → discovery-0ffec855f5df262c.js} +1 -1
  83. fides/ui-build/static/admin/_next/static/chunks/pages/datamap-6a030ab8c2e2b0db.js +1 -0
  84. fides/ui-build/static/admin/_next/static/chunks/pages/dataset/[datasetId]/[collectionName]/{[...subfieldNames]-5f88280db168083e.js → [...subfieldNames]-6cb66f649b8ca4bf.js} +1 -1
  85. fides/ui-build/static/admin/_next/static/chunks/pages/dataset/[datasetId]/{[collectionName]-bfbcf19c28c794ae.js → [collectionName]-0b008dad90b00aaa.js} +1 -1
  86. fides/ui-build/static/admin/_next/static/chunks/pages/dataset/{[datasetId]-6fbe2b584a509226.js → [datasetId]-5566edf9a9d1be2d.js} +1 -1
  87. fides/ui-build/static/admin/_next/static/chunks/pages/dataset/new-d4ca1f485b6e9e02.js +1 -0
  88. fides/ui-build/static/admin/_next/static/chunks/pages/dataset-85dee7e81dc4bafb.js +1 -0
  89. fides/ui-build/static/admin/_next/static/chunks/pages/datastore-connection/[id]-e905e018a2cab35d.js +1 -0
  90. fides/ui-build/static/admin/_next/static/chunks/pages/datastore-connection/new-912723bc86299b1a.js +1 -0
  91. fides/ui-build/static/admin/_next/static/chunks/pages/datastore-connection-f1f0affc18327033.js +1 -0
  92. fides/ui-build/static/admin/_next/static/chunks/pages/{fides-js-docs-1f4335dca5c09860.js → fides-js-docs-5235760b3e508d7d.js} +1 -1
  93. fides/ui-build/static/admin/_next/static/chunks/pages/integrations/[id]-1b94e2d769a182b2.js +1 -0
  94. fides/ui-build/static/admin/_next/static/chunks/pages/{integrations-a733e5d7c3ce9bb8.js → integrations-adfe6c5ac5b703d0.js} +1 -1
  95. fides/ui-build/static/admin/_next/static/chunks/pages/new-privacy-requests-f9be7080ebbb7445.js +1 -0
  96. fides/ui-build/static/admin/_next/static/chunks/pages/notifications/providers/{[key]-720cde29f81db47f.js → [key]-f94e3accf9507ebf.js} +1 -1
  97. fides/ui-build/static/admin/_next/static/chunks/pages/notifications/providers/{new-3668866076b53016.js → new-5e83220ff1f2a250.js} +1 -1
  98. fides/ui-build/static/admin/_next/static/chunks/pages/notifications/templates-1621a4b87c432117.js +1 -0
  99. fides/ui-build/static/admin/_next/static/chunks/pages/poc/{ant-components-9cfb469de7b4aeab.js → ant-components-e02516d9fd314528.js} +1 -1
  100. fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/[id]-1fe486f3af832c80.js +1 -0
  101. fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests-2c0ec8fed16c20ae.js +1 -0
  102. fides/ui-build/static/admin/_next/static/chunks/pages/reporting/datamap-baf77d34a3b3bece.js +1 -0
  103. fides/ui-build/static/admin/_next/static/chunks/pages/settings/consent-ee8820fe0fa14c77.js +1 -0
  104. fides/ui-build/static/admin/_next/static/chunks/pages/settings/custom-fields/{[id]-8634aae3259def37.js → [id]-8eb862182f19a6c2.js} +1 -1
  105. fides/ui-build/static/admin/_next/static/chunks/pages/settings/custom-fields/{new-2d9dcca17965dc57.js → new-37c29ef618e9fe3c.js} +1 -1
  106. fides/ui-build/static/admin/_next/static/chunks/pages/settings/custom-fields-1db425150dcb1b6b.js +1 -0
  107. fides/ui-build/static/admin/_next/static/chunks/pages/systems/configure/[id]/test-datasets-f108bf5015144d2f.js +1 -0
  108. fides/ui-build/static/admin/_next/static/chunks/pages/systems/configure/{[id]-59c89489fa32a4cb.js → [id]-0e7c7228d01290ea.js} +1 -1
  109. fides/ui-build/static/admin/_next/static/chunks/pages/{systems-cfaa37a0df83674b.js → systems-adc13b542e10a37d.js} +1 -1
  110. fides/ui-build/static/admin/_next/static/chunks/pages/taxonomy-23dd250da26511c5.js +1 -0
  111. fides/ui-build/static/admin/_next/static/chunks/webpack-6f97ebe373e7ef6b.js +1 -0
  112. fides/ui-build/static/admin/_next/static/css/65ae906f224cd8ae.css +1 -0
  113. fides/ui-build/static/admin/_next/static/css/d5701118537cbdd2.css +1 -0
  114. fides/ui-build/static/admin/add-systems/manual.html +1 -1
  115. fides/ui-build/static/admin/add-systems/multiple.html +1 -1
  116. fides/ui-build/static/admin/add-systems.html +1 -1
  117. fides/ui-build/static/admin/consent/configure/add-vendors.html +1 -1
  118. fides/ui-build/static/admin/consent/configure.html +1 -1
  119. fides/ui-build/static/admin/consent/privacy-experience/[id].html +1 -1
  120. fides/ui-build/static/admin/consent/privacy-experience/new.html +1 -1
  121. fides/ui-build/static/admin/consent/privacy-experience.html +1 -1
  122. fides/ui-build/static/admin/consent/privacy-notices/[id].html +1 -1
  123. fides/ui-build/static/admin/consent/privacy-notices/new.html +1 -1
  124. fides/ui-build/static/admin/consent/privacy-notices.html +1 -1
  125. fides/ui-build/static/admin/consent/properties.html +1 -1
  126. fides/ui-build/static/admin/consent/reporting.html +1 -1
  127. fides/ui-build/static/admin/consent.html +1 -1
  128. fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn]/[resourceUrn].html +1 -1
  129. fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn].html +1 -1
  130. fides/ui-build/static/admin/data-catalog/[systemId]/projects.html +1 -1
  131. fides/ui-build/static/admin/data-catalog/[systemId]/resources/[resourceUrn].html +1 -1
  132. fides/ui-build/static/admin/data-catalog/[systemId]/resources.html +1 -1
  133. fides/ui-build/static/admin/data-catalog.html +1 -1
  134. fides/ui-build/static/admin/data-discovery/action-center/datastore/[monitorId].html +1 -1
  135. fides/ui-build/static/admin/data-discovery/action-center/datastore.html +1 -1
  136. fides/ui-build/static/admin/data-discovery/action-center/website/[monitorId]/[systemId].html +1 -1
  137. fides/ui-build/static/admin/data-discovery/action-center/website/[monitorId].html +1 -1
  138. fides/ui-build/static/admin/data-discovery/action-center/website.html +1 -1
  139. fides/ui-build/static/admin/data-discovery/action-center.html +1 -1
  140. fides/ui-build/static/admin/data-discovery/activity.html +1 -1
  141. fides/ui-build/static/admin/data-discovery/detection/[resourceUrn].html +1 -1
  142. fides/ui-build/static/admin/data-discovery/detection.html +1 -1
  143. fides/ui-build/static/admin/data-discovery/discovery/[resourceUrn].html +1 -1
  144. fides/ui-build/static/admin/data-discovery/discovery.html +1 -1
  145. fides/ui-build/static/admin/datamap.html +1 -1
  146. fides/ui-build/static/admin/dataset/[datasetId]/[collectionName]/[...subfieldNames].html +1 -1
  147. fides/ui-build/static/admin/dataset/[datasetId]/[collectionName].html +1 -1
  148. fides/ui-build/static/admin/dataset/[datasetId].html +1 -1
  149. fides/ui-build/static/admin/dataset/new.html +1 -1
  150. fides/ui-build/static/admin/dataset.html +1 -1
  151. fides/ui-build/static/admin/datastore-connection/[id].html +1 -1
  152. fides/ui-build/static/admin/datastore-connection/new.html +1 -1
  153. fides/ui-build/static/admin/datastore-connection.html +1 -1
  154. fides/ui-build/static/admin/index.html +1 -1
  155. fides/ui-build/static/admin/integrations/[id].html +1 -1
  156. fides/ui-build/static/admin/integrations.html +1 -1
  157. fides/ui-build/static/admin/lib/fides-ext-gpp.js +1 -1
  158. fides/ui-build/static/admin/lib/fides-preview.js +1 -1
  159. fides/ui-build/static/admin/lib/fides-tcf.js +2 -2
  160. fides/ui-build/static/admin/login/[provider].html +1 -1
  161. fides/ui-build/static/admin/login.html +1 -1
  162. fides/ui-build/static/admin/new-privacy-requests.html +1 -1
  163. fides/ui-build/static/admin/notifications/digests/[id].html +1 -1
  164. fides/ui-build/static/admin/notifications/digests/new.html +1 -1
  165. fides/ui-build/static/admin/notifications/digests.html +1 -1
  166. fides/ui-build/static/admin/notifications/providers/[key].html +1 -1
  167. fides/ui-build/static/admin/notifications/providers/new.html +1 -1
  168. fides/ui-build/static/admin/notifications/providers.html +1 -1
  169. fides/ui-build/static/admin/notifications/templates/[id].html +1 -1
  170. fides/ui-build/static/admin/notifications/templates/add-template.html +1 -1
  171. fides/ui-build/static/admin/notifications/templates.html +1 -1
  172. fides/ui-build/static/admin/notifications.html +1 -1
  173. fides/ui-build/static/admin/poc/ant-components.html +1 -1
  174. fides/ui-build/static/admin/poc/form-experiments/AntForm.html +1 -1
  175. fides/ui-build/static/admin/poc/form-experiments/FormikAntFormItem.html +1 -1
  176. fides/ui-build/static/admin/poc/form-experiments/FormikControlled.html +1 -1
  177. fides/ui-build/static/admin/poc/form-experiments/FormikField.html +1 -1
  178. fides/ui-build/static/admin/poc/form-experiments/FormikSpreadField.html +1 -1
  179. fides/ui-build/static/admin/poc/forms.html +1 -1
  180. fides/ui-build/static/admin/poc/table-migration.html +1 -1
  181. fides/ui-build/static/admin/privacy-requests/[id].html +1 -1
  182. fides/ui-build/static/admin/privacy-requests/configure/storage.html +1 -1
  183. fides/ui-build/static/admin/privacy-requests/configure.html +1 -1
  184. fides/ui-build/static/admin/privacy-requests.html +1 -1
  185. fides/ui-build/static/admin/properties/[id].html +1 -1
  186. fides/ui-build/static/admin/properties/add-property.html +1 -1
  187. fides/ui-build/static/admin/properties.html +1 -1
  188. fides/ui-build/static/admin/reporting/datamap.html +1 -1
  189. fides/ui-build/static/admin/sandbox/privacy-notices.html +1 -1
  190. fides/ui-build/static/admin/settings/about/alpha.html +1 -1
  191. fides/ui-build/static/admin/settings/about.html +1 -1
  192. fides/ui-build/static/admin/settings/consent/[configuration_id]/[purpose_id].html +1 -1
  193. fides/ui-build/static/admin/settings/consent.html +1 -1
  194. fides/ui-build/static/admin/settings/custom-fields/[id].html +1 -1
  195. fides/ui-build/static/admin/settings/custom-fields/new.html +1 -1
  196. fides/ui-build/static/admin/settings/custom-fields.html +1 -1
  197. fides/ui-build/static/admin/settings/domain-records.html +1 -1
  198. fides/ui-build/static/admin/settings/domains.html +1 -1
  199. fides/ui-build/static/admin/settings/email-templates.html +1 -1
  200. fides/ui-build/static/admin/settings/locations.html +1 -1
  201. fides/ui-build/static/admin/settings/organization.html +1 -1
  202. fides/ui-build/static/admin/settings/privacy-requests.html +1 -1
  203. fides/ui-build/static/admin/settings/regulations.html +1 -1
  204. fides/ui-build/static/admin/systems/configure/[id]/test-datasets.html +1 -1
  205. fides/ui-build/static/admin/systems/configure/[id].html +1 -1
  206. fides/ui-build/static/admin/systems.html +1 -1
  207. fides/ui-build/static/admin/taxonomy.html +1 -1
  208. fides/ui-build/static/admin/user-management/new.html +1 -1
  209. fides/ui-build/static/admin/user-management/profile/[id].html +1 -1
  210. fides/ui-build/static/admin/user-management.html +1 -1
  211. fides/ui-build/static/admin/_next/static/chunks/1099-31f9d973bc287df8.js +0 -1
  212. fides/ui-build/static/admin/_next/static/chunks/1316-2606e19807c08aa5.js +0 -1
  213. fides/ui-build/static/admin/_next/static/chunks/1437-8b1f6c8797c68bfd.js +0 -1
  214. fides/ui-build/static/admin/_next/static/chunks/1467-8808ec8836e033f9.js +0 -1
  215. fides/ui-build/static/admin/_next/static/chunks/155-f6302d32cba4cab6.js +0 -1
  216. fides/ui-build/static/admin/_next/static/chunks/1817-2d5cf537a2992c79.js +0 -1
  217. fides/ui-build/static/admin/_next/static/chunks/2962-342ad1b4ab402ded.js +0 -1
  218. fides/ui-build/static/admin/_next/static/chunks/3377-988ac2f3a2e8d5d4.js +0 -1
  219. fides/ui-build/static/admin/_next/static/chunks/3446-f40c352c43ac950c.js +0 -1
  220. fides/ui-build/static/admin/_next/static/chunks/346-aa3b88efb85f2e28.js +0 -1
  221. fides/ui-build/static/admin/_next/static/chunks/3700-f695f2f6b8251971.js +0 -1
  222. fides/ui-build/static/admin/_next/static/chunks/3772-9f1713f9d5f97a10.js +0 -1
  223. fides/ui-build/static/admin/_next/static/chunks/3873-d18e47b327445db5.js +0 -1
  224. fides/ui-build/static/admin/_next/static/chunks/5163-e682273cd76a7d07.js +0 -1
  225. fides/ui-build/static/admin/_next/static/chunks/5279-bd6cccabdd6ca447.js +0 -1
  226. fides/ui-build/static/admin/_next/static/chunks/549-28537a6de666944b.js +0 -1
  227. fides/ui-build/static/admin/_next/static/chunks/5619-9b50cec521203989.js +0 -1
  228. fides/ui-build/static/admin/_next/static/chunks/5643-3459282d296a3c59.js +0 -1
  229. fides/ui-build/static/admin/_next/static/chunks/6277-3759894435cb8569.js +0 -1
  230. fides/ui-build/static/admin/_next/static/chunks/6315-e2fb5ea77179a871.js +0 -1
  231. fides/ui-build/static/admin/_next/static/chunks/6344-ca66a6e10d128179.js +0 -1
  232. fides/ui-build/static/admin/_next/static/chunks/6419-11d67f7fd4e2f247.js +0 -1
  233. fides/ui-build/static/admin/_next/static/chunks/6954-84789a4e4fb04eb9.js +0 -1
  234. fides/ui-build/static/admin/_next/static/chunks/699-8ca44b0de9fa20f0.js +0 -1
  235. fides/ui-build/static/admin/_next/static/chunks/7488-cf92601852e3c509.js +0 -1
  236. fides/ui-build/static/admin/_next/static/chunks/7773-9ae233109bc64ec2.js +0 -1
  237. fides/ui-build/static/admin/_next/static/chunks/8011-75af8b480fa114e6.js +0 -1
  238. fides/ui-build/static/admin/_next/static/chunks/8373-22b4d20e8cc06b3a.js +0 -1
  239. fides/ui-build/static/admin/_next/static/chunks/8765-f622a35b40a7ec63.js +0 -1
  240. fides/ui-build/static/admin/_next/static/chunks/9037-453224ba3ee65b13.js +0 -1
  241. fides/ui-build/static/admin/_next/static/chunks/9187-7438242f0d380bb0.js +0 -1
  242. fides/ui-build/static/admin/_next/static/chunks/9278-08cc704317fe535e.js +0 -1
  243. fides/ui-build/static/admin/_next/static/chunks/9682-d1a3afa1394f8304.js +0 -1
  244. fides/ui-build/static/admin/_next/static/chunks/9729-fcf6ff4e3534e4a8.js +0 -1
  245. fides/ui-build/static/admin/_next/static/chunks/9899-d6437facac926264.js +0 -1
  246. fides/ui-build/static/admin/_next/static/chunks/9965-25621dd507e0cfd6.js +0 -1
  247. fides/ui-build/static/admin/_next/static/chunks/pages/consent/reporting-11f1683aa15e1a62.js +0 -1
  248. fides/ui-build/static/admin/_next/static/chunks/pages/data-catalog/[systemId]/projects/[projectUrn]-d60761c20382b259.js +0 -1
  249. fides/ui-build/static/admin/_next/static/chunks/pages/data-catalog/[systemId]/resources-aed94957009eb3fd.js +0 -1
  250. fides/ui-build/static/admin/_next/static/chunks/pages/data-catalog-127c114dd8f102ed.js +0 -1
  251. fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center/datastore/[monitorId]-5aa7a9fa96160de8.js +0 -1
  252. fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center/website/[monitorId]/[systemId]-899bf30dde8b3292.js +0 -1
  253. fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center-823d0dd77e66585b.js +0 -1
  254. fides/ui-build/static/admin/_next/static/chunks/pages/datamap-3b100c44ea9e3988.js +0 -1
  255. fides/ui-build/static/admin/_next/static/chunks/pages/dataset/new-3d1e48f4b95d7f6b.js +0 -1
  256. fides/ui-build/static/admin/_next/static/chunks/pages/dataset-d3c6ecf7f29bea6e.js +0 -1
  257. fides/ui-build/static/admin/_next/static/chunks/pages/datastore-connection/[id]-0a4aa42be2da0255.js +0 -1
  258. fides/ui-build/static/admin/_next/static/chunks/pages/datastore-connection/new-14313e441a13192c.js +0 -1
  259. fides/ui-build/static/admin/_next/static/chunks/pages/datastore-connection-f139d1ce26404f30.js +0 -1
  260. fides/ui-build/static/admin/_next/static/chunks/pages/integrations/[id]-b2d3d28b10a758e6.js +0 -1
  261. fides/ui-build/static/admin/_next/static/chunks/pages/new-privacy-requests-df0c95e408c54c7e.js +0 -1
  262. fides/ui-build/static/admin/_next/static/chunks/pages/notifications/templates-a14c876b49422597.js +0 -1
  263. fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/[id]-cdd3754289a28317.js +0 -1
  264. fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests-6e4c535b6d614596.js +0 -1
  265. fides/ui-build/static/admin/_next/static/chunks/pages/reporting/datamap-632b3ee563d070f2.js +0 -1
  266. fides/ui-build/static/admin/_next/static/chunks/pages/settings/consent-5b6807dced8d03c5.js +0 -1
  267. fides/ui-build/static/admin/_next/static/chunks/pages/settings/custom-fields-7dce52bfc1b2652c.js +0 -1
  268. fides/ui-build/static/admin/_next/static/chunks/pages/systems/configure/[id]/test-datasets-b72d36243a0a545c.js +0 -1
  269. fides/ui-build/static/admin/_next/static/chunks/pages/taxonomy-56a5434969cbe9ba.js +0 -1
  270. fides/ui-build/static/admin/_next/static/chunks/webpack-c2c11809187b9f84.js +0 -1
  271. fides/ui-build/static/admin/_next/static/css/d41a048a166d50e4.css +0 -1
  272. fides/ui-build/static/admin/_next/static/css/d78390d6134d8328.css +0 -1
  273. fides/ui-build/static/admin/_next/static/wCNFtmYQhEDMaMPeBB4BM/_buildManifest.js +0 -1
  274. {ethyca_fides-2.73.2b0.dist-info → ethyca_fides-2.74.0rc1.dist-info}/WHEEL +0 -0
  275. {ethyca_fides-2.73.2b0.dist-info → ethyca_fides-2.74.0rc1.dist-info}/entry_points.txt +0 -0
  276. {ethyca_fides-2.73.2b0.dist-info → ethyca_fides-2.74.0rc1.dist-info}/licenses/LICENSE +0 -0
  277. {ethyca_fides-2.73.2b0.dist-info → ethyca_fides-2.74.0rc1.dist-info}/top_level.txt +0 -0
  278. /fides/ui-build/static/admin/_next/static/{wCNFtmYQhEDMaMPeBB4BM → FZTEUgamBvOhgPWce135w}/_ssgManifest.js +0 -0
  279. /fides/ui-build/static/admin/_next/static/chunks/{2040-7eed8491ca7276ed.js → 2040-fe1a06d82c0413f1.js} +0 -0
  280. /fides/ui-build/static/admin/_next/static/chunks/{2397-083fc511acb6105d.js → 2397-40b8db1cb2f23e2a.js} +0 -0
  281. /fides/ui-build/static/admin/_next/static/chunks/{2921-34a43f2f8f5e5e69.js → 2921-b10bbc3a9104933b.js} +0 -0
  282. /fides/ui-build/static/admin/_next/static/chunks/{3696-6f90e41a53d22920.js → 3696-6db05a35ae806825.js} +0 -0
  283. /fides/ui-build/static/admin/_next/static/chunks/{3923-f0a85dc5c3684fa0.js → 3923-44255a63d6d80ff5.js} +0 -0
  284. /fides/ui-build/static/admin/_next/static/chunks/{401-7c345d019bb9bcbd.js → 401-fe8db8b5d8f600de.js} +0 -0
  285. /fides/ui-build/static/admin/_next/static/chunks/{4496-4ff19366c597ec16.js → 4496-bed72bd5639075be.js} +0 -0
  286. /fides/ui-build/static/admin/_next/static/chunks/{4817-1f3e6ea38625d8d5.js → 4817-d29f40d4ce729f37.js} +0 -0
  287. /fides/ui-build/static/admin/_next/static/chunks/{5185-e7f8b81dd3dfbe0b.js → 5185-b2ac9fecc00b67e7.js} +0 -0
  288. /fides/ui-build/static/admin/_next/static/chunks/{5258-62d6bc19add60aa6.js → 5258-4672eae0656430f9.js} +0 -0
  289. /fides/ui-build/static/admin/_next/static/chunks/{5783-21775c232dce7af7.js → 5783-6055edba275155ca.js} +0 -0
  290. /fides/ui-build/static/admin/_next/static/chunks/{6853-de9905d28e5b19b3.js → 6853-1adbdf6418ec3d62.js} +0 -0
  291. /fides/ui-build/static/admin/_next/static/chunks/{7059-2bb7c38578549703.js → 7059-12be23a345a94c1e.js} +0 -0
  292. /fides/ui-build/static/admin/_next/static/chunks/{7476-a02d970ea4d3f7d0.js → 7476-a43c046c24de37cc.js} +0 -0
  293. /fides/ui-build/static/admin/_next/static/chunks/{7630-a11610c2b31ab2ca.js → 7630-c654c61ba98d8c74.js} +0 -0
  294. /fides/ui-build/static/admin/_next/static/chunks/{796-e36d610066135f8c.js → 796-e83ace3c6ab99ac7.js} +0 -0
  295. /fides/ui-build/static/admin/_next/static/chunks/{8212-393420e5a9751791.js → 8212-b9e8295ca883c9f8.js} +0 -0
  296. /fides/ui-build/static/admin/_next/static/chunks/{9330-f753636a31c4ea04.js → 9330-e519adec48222d45.js} +0 -0
  297. /fides/ui-build/static/admin/_next/static/chunks/{9826-a737a9956c1d0905.js → 9826-657652d55936a8c6.js} +0 -0
  298. /fides/ui-build/static/admin/_next/static/chunks/pages/{404-eb019192ce498f32.js → 404-d079b8bf35250874.js} +0 -0
  299. /fides/ui-build/static/admin/_next/static/chunks/pages/add-systems/{multiple-a188f84239f4b2a8.js → multiple-b4d18c1f4d414f5f.js} +0 -0
  300. /fides/ui-build/static/admin/_next/static/chunks/pages/{add-systems-ee9df33ebd471099.js → add-systems-d451bc8932330141.js} +0 -0
  301. /fides/ui-build/static/admin/_next/static/chunks/pages/consent/configure/{add-vendors-4f9cf087fcee87e6.js → add-vendors-c24663cd5dec57db.js} +0 -0
  302. /fides/ui-build/static/admin/_next/static/chunks/pages/consent/{configure-9af75caefc74eaca.js → configure-d93418688bd258eb.js} +0 -0
  303. /fides/ui-build/static/admin/_next/static/chunks/pages/consent/{privacy-experience-b08abefec298ccf1.js → privacy-experience-d9b7b311195df29e.js} +0 -0
  304. /fides/ui-build/static/admin/_next/static/chunks/pages/consent/{privacy-notices-6528eb24165aceb6.js → privacy-notices-cdfc9bb19f47c709.js} +0 -0
  305. /fides/ui-build/static/admin/_next/static/chunks/pages/consent/{properties-5cba30eba1e97e56.js → properties-0b995b01dc4dbd1f.js} +0 -0
  306. /fides/ui-build/static/admin/_next/static/chunks/pages/{consent-614af0a2c2ba966c.js → consent-b37ed76849330edd.js} +0 -0
  307. /fides/ui-build/static/admin/_next/static/chunks/pages/{index-1343fa525a206571.js → index-692d27dbe9392c9f.js} +0 -0
  308. /fides/ui-build/static/admin/_next/static/chunks/pages/notifications/digests/{[id]-63b3be660fb12c0f.js → [id]-caaa8602a1d449b1.js} +0 -0
  309. /fides/ui-build/static/admin/_next/static/chunks/pages/notifications/digests/{new-d3b577962dd33266.js → new-9b106b1d2d93985b.js} +0 -0
  310. /fides/ui-build/static/admin/_next/static/chunks/pages/notifications/{digests-aed9afd988a48acf.js → digests-6a1ded8cdde836c4.js} +0 -0
  311. /fides/ui-build/static/admin/_next/static/chunks/pages/notifications/{providers-36a0ac36062abd02.js → providers-a03cbd698a23e5b3.js} +0 -0
  312. /fides/ui-build/static/admin/_next/static/chunks/pages/notifications/templates/{[id]-8063dceb32310c85.js → [id]-3cde574b3c8447c0.js} +0 -0
  313. /fides/ui-build/static/admin/_next/static/chunks/pages/notifications/templates/{add-template-4931c70bee62232f.js → add-template-0448bb4ae8536c58.js} +0 -0
  314. /fides/ui-build/static/admin/_next/static/chunks/pages/{notifications-93af719dab3bd053.js → notifications-4ea28f6b1dd63642.js} +0 -0
  315. /fides/ui-build/static/admin/_next/static/chunks/pages/poc/form-experiments/{AntForm-e715cc654fb6a5cd.js → AntForm-fec08bea801b4918.js} +0 -0
  316. /fides/ui-build/static/admin/_next/static/chunks/pages/poc/form-experiments/{FormikAntFormItem-72ae299bcb6adae6.js → FormikAntFormItem-d911e5fbf5a4a888.js} +0 -0
  317. /fides/ui-build/static/admin/_next/static/chunks/pages/poc/form-experiments/{FormikControlled-2337f8c81a766eb0.js → FormikControlled-91b1adcac6a57b2d.js} +0 -0
  318. /fides/ui-build/static/admin/_next/static/chunks/pages/poc/form-experiments/{FormikField-0af454f55494f6fa.js → FormikField-de309d8813b1ebfb.js} +0 -0
  319. /fides/ui-build/static/admin/_next/static/chunks/pages/poc/{forms-d1b90ffa996fbd89.js → forms-f2943c1309062284.js} +0 -0
  320. /fides/ui-build/static/admin/_next/static/chunks/pages/poc/{table-migration-19724b9e0581b96d.js → table-migration-03eda417711ae909.js} +0 -0
  321. /fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/configure/{storage-cc41ae605f2b55ae.js → storage-ea6f78fa8b2d3f6c.js} +0 -0
  322. /fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/{configure-fa82cffba448ccd8.js → configure-bda7b474493e7128.js} +0 -0
  323. /fides/ui-build/static/admin/_next/static/chunks/pages/properties/{[id]-bc1c289647e52c48.js → [id]-30d298a47e85709f.js} +0 -0
  324. /fides/ui-build/static/admin/_next/static/chunks/pages/properties/{add-property-cbfaa23d96f5ed0b.js → add-property-438084cca0d0f10d.js} +0 -0
  325. /fides/ui-build/static/admin/_next/static/chunks/pages/{properties-a15a3fd0ed88f39c.js → properties-17fd44d98f5bd5b6.js} +0 -0
  326. /fides/ui-build/static/admin/_next/static/chunks/pages/sandbox/{privacy-notices-afe921f6e2a526fb.js → privacy-notices-8c80391025ca7339.js} +0 -0
  327. /fides/ui-build/static/admin/_next/static/chunks/pages/settings/consent/[configuration_id]/{[purpose_id]-19de0024418a4924.js → [purpose_id]-7c19810858b708cc.js} +0 -0
  328. /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{domain-records-3b87002753b23ca5.js → domain-records-e334b43fa5c5b1e6.js} +0 -0
  329. /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{domains-aacd9d0ad47082d4.js → domains-9d18eb5c38d85522.js} +0 -0
  330. /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{email-templates-3cdd0b39901190ba.js → email-templates-cb937ed7c4b1e5a8.js} +0 -0
  331. /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{locations-61076eedbfd137b9.js → locations-835281251f0785cd.js} +0 -0
  332. /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{organization-b07b11d6002f8c8c.js → organization-7fd050c92866938c.js} +0 -0
  333. /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{privacy-requests-084a2b4431d35322.js → privacy-requests-59ea66130fca0d05.js} +0 -0
  334. /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{regulations-b7c0d3b1b754e70f.js → regulations-b0fe1051d908f366.js} +0 -0
  335. /fides/ui-build/static/admin/_next/static/chunks/pages/user-management/profile/{[id]-5d5a68e555d18693.js → [id]-aed30fb22ae7c9ec.js} +0 -0
  336. /fides/ui-build/static/admin/_next/static/chunks/pages/{user-management-e6a211d8a0401086.js → user-management-6b88ca3e02ee67c9.js} +0 -0
@@ -4,12 +4,12 @@ from __future__ import annotations
4
4
 
5
5
  import json
6
6
  from datetime import datetime, timedelta
7
- from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Union
7
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union
8
8
 
9
9
  from celery.result import AsyncResult
10
10
  from loguru import logger
11
11
  from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String
12
- from sqlalchemy.dialects.postgresql import JSONB
12
+ from sqlalchemy.dialects.postgresql import JSONB, UUID
13
13
  from sqlalchemy.ext.declarative import declared_attr
14
14
  from sqlalchemy.ext.mutable import MutableDict, MutableList
15
15
  from sqlalchemy.orm import Query, RelationshipProperty, Session, backref, relationship
@@ -69,6 +69,7 @@ from fides.api.models.pre_approval_webhook import (
69
69
  PreApprovalWebhook,
70
70
  PreApprovalWebhookReply,
71
71
  )
72
+ from fides.api.models.privacy_request.duplicate_group import DuplicateGroup
72
73
  from fides.api.models.privacy_request.execution_log import (
73
74
  COMPLETED_EXECUTION_LOG_STATUSES,
74
75
  EXITED_EXECUTION_LOG_STATUSES,
@@ -225,7 +226,16 @@ class PrivacyRequest(
225
226
  location = Column(String, nullable=True)
226
227
 
227
228
  # Duplicate detection - group ID to link duplicates together
228
- duplicate_request_group_id = Column(String, index=True, nullable=True)
229
+ duplicate_request_group_id = Column(
230
+ UUID(as_uuid=True),
231
+ ForeignKey("duplicate_group.id"),
232
+ nullable=True,
233
+ )
234
+ duplicate_group = relationship(
235
+ DuplicateGroup,
236
+ back_populates="privacy_requests",
237
+ uselist=False,
238
+ )
229
239
 
230
240
  # A PrivacyRequest can be soft deleted, so we store when it was deleted
231
241
  deleted_at = Column(DateTime(timezone=True), nullable=True)
@@ -604,7 +614,10 @@ class PrivacyRequest(
604
614
  """Verify the identification code supplied by the user
605
615
  If verified, change the status of the request to "pending", and set the datetime the identity was verified.
606
616
  """
607
- if not self.status == PrivacyRequestStatus.identity_unverified:
617
+ if not self.status in [
618
+ PrivacyRequestStatus.identity_unverified,
619
+ PrivacyRequestStatus.duplicate,
620
+ ]:
608
621
  raise IdentityVerificationException(
609
622
  f"Invalid identity verification request. Privacy request '{self.id}' status = {self.status.value}." # type: ignore # pylint: disable=no-member
610
623
  )
@@ -682,12 +695,29 @@ class PrivacyRequest(
682
695
  },
683
696
  )
684
697
 
685
- def get_cached_identity_data(self) -> Dict[str, Any]:
686
- """Retrieves any identity data pertaining to this request from the cache"""
698
+ def identity_prefix_cache_and_keys(self) -> Tuple[str, FidesopsRedis, List[str]]:
699
+ """Returns the prefix and cache keys for the identity data for this request"""
687
700
  prefix = f"id-{self.id}-identity-*"
688
701
  cache: FidesopsRedis = get_cache()
689
702
  keys = cache.keys(prefix)
690
- result = {}
703
+ return prefix, cache, keys
704
+
705
+ def verify_cache_for_identity_data(self) -> bool:
706
+ """Verifies if the identity data is cached for this request"""
707
+ _, _, keys = self.identity_prefix_cache_and_keys()
708
+ return len(keys) > 0
709
+
710
+ def get_cached_identity_data(self) -> Dict[str, Any]:
711
+ """Retrieves any identity data pertaining to this request from the cache"""
712
+ result: Dict[str, Any] = {}
713
+ prefix, cache, keys = self.identity_prefix_cache_and_keys()
714
+
715
+ if not keys:
716
+ logger.debug(f"Cache miss for request {self.id}, falling back to DB")
717
+ identity = self.get_persisted_identity()
718
+ self.cache_identity(identity)
719
+ keys = cache.keys(prefix)
720
+
691
721
  for key in keys:
692
722
  value = cache.get(key)
693
723
  if value:
@@ -705,10 +735,25 @@ class PrivacyRequest(
705
735
 
706
736
  def get_cached_custom_privacy_request_fields(self) -> Dict[str, Any]:
707
737
  """Retrieves any custom fields pertaining to this request from the cache"""
738
+ result: Dict[str, Any] = {}
708
739
  prefix = f"id-{self.id}-custom-privacy-request-field-*"
740
+
709
741
  cache: FidesopsRedis = get_cache()
710
742
  keys = cache.keys(prefix)
711
- result = {}
743
+
744
+ if not keys:
745
+ logger.debug(f"Cache miss for request {self.id}, falling back to DB")
746
+ custom_privacy_request_fields = (
747
+ self.get_persisted_custom_privacy_request_fields()
748
+ )
749
+ self.cache_custom_privacy_request_fields(
750
+ {
751
+ key: CustomPrivacyRequestFieldSchema(**value)
752
+ for key, value in custom_privacy_request_fields.items()
753
+ }
754
+ )
755
+ keys = cache.keys(prefix)
756
+
712
757
  for key in keys:
713
758
  value = cache.get(key)
714
759
  if value:
@@ -1,9 +1,10 @@
1
1
  from datetime import datetime
2
2
  from enum import Enum as EnumType
3
3
  from typing import Any, Dict, List, Optional, Type, Union
4
+ from uuid import UUID
4
5
 
5
6
  from fideslang.validation import FidesKey
6
- from pydantic import ConfigDict, Field, field_serializer, field_validator
7
+ from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator
7
8
 
8
9
  from fides.api.custom_types import SafeStr
9
10
  from fides.api.graph.config import CollectionAddress
@@ -309,6 +310,11 @@ class PrivacyRequestStatus(str, EnumType):
309
310
  duplicate = "duplicate" # Request identified as duplicate of another request
310
311
 
311
312
 
313
+ class IdentityValue(BaseModel):
314
+ label: str
315
+ value: Optional[str] = None
316
+
317
+
312
318
  class PrivacyRequestResponse(FidesSchema):
313
319
  """Schema to check the status of a PrivacyRequest"""
314
320
 
@@ -329,7 +335,7 @@ class PrivacyRequestResponse(FidesSchema):
329
335
  # as it is an API response field, and we don't want to reveal any more
330
336
  # about our PII structure than is explicitly stored in the cache on request
331
337
  # creation.
332
- identity: Optional[Dict[str, Union[Optional[str], Dict[str, Any]]]] = None
338
+ identity: Optional[Dict[str, Union[Optional[str], IdentityValue]]] = None
333
339
  custom_privacy_request_fields: Optional[Dict[str, Any]] = None
334
340
  policy: PolicySchema
335
341
  action_required_details: Optional[CheckpointActionRequiredDetails] = None
@@ -343,7 +349,7 @@ class PrivacyRequestResponse(FidesSchema):
343
349
  deleted_by: Optional[str] = None
344
350
  finalized_at: Optional[datetime] = None
345
351
  finalized_by: Optional[str] = None
346
- duplicate_request_group_id: Optional[str] = None
352
+ duplicate_request_group_id: Optional[UUID] = None
347
353
 
348
354
  model_config = ConfigDict(from_attributes=True, use_enum_values=True)
349
355
 
@@ -370,6 +376,12 @@ class DenyPrivacyRequests(ReviewPrivacyRequestIds):
370
376
  reason: Optional[SafeStr] = None
371
377
 
372
378
 
379
+ class CancelPrivacyRequests(ReviewPrivacyRequestIds):
380
+ """Pass in a list of privacy request ids and cancellation reason"""
381
+
382
+ reason: Optional[SafeStr] = None
383
+
384
+
373
385
  class BulkPostPrivacyRequests(BulkResponse):
374
386
  """Schema with mixed success/failure responses for Bulk Create of PrivacyRequest responses."""
375
387
 
@@ -458,7 +470,7 @@ class PrivacyRequestFilter(FidesSchema):
458
470
  errored_gt: Optional[datetime] = None
459
471
  external_id: Optional[str] = None
460
472
  location: Optional[str] = None
461
- action_type: Optional[ActionType] = None
473
+ action_type: Optional[Union[ActionType, List[ActionType]]] = None
462
474
  verbose: Optional[bool] = False
463
475
  include_identities: Optional[bool] = False
464
476
  include_custom_privacy_request_fields: Optional[bool] = False
@@ -551,13 +551,20 @@ class AsyncPollingStrategy(AsyncDSRStrategy):
551
551
  self.result_request.result_path,
552
552
  )
553
553
 
554
- # Ensure we have the expected polling result type
555
- if not isinstance(polling_result, PollingResult):
556
- raise PrivacyRequestError("Polling result must be PollingResult instance")
557
-
558
- # Store results on the sub-request
559
- self._store_sub_request_result(polling_result, sub_request, polling_task)
554
+ # Checks if we have a polling result, response could be empty in case there was no data to access
555
+ if polling_result:
556
+ # Ensure we have the expected polling result type
557
+ if not isinstance(polling_result, PollingResult):
558
+ raise PrivacyRequestError(
559
+ "Polling result must be PollingResult instance"
560
+ )
560
561
 
562
+ # Store results on the sub-request
563
+ self._store_sub_request_result(polling_result, sub_request, polling_task)
564
+ else:
565
+ logger.info(
566
+ f"No result response for sub-request {sub_request.id} for task {polling_task.id}"
567
+ )
561
568
  # Mark as complete using existing method
562
569
  sub_request.update_status(self.session, ExecutionLogStatus.complete.value)
563
570
 
@@ -0,0 +1,424 @@
1
+ from datetime import datetime, timedelta, timezone
2
+ from typing import Optional
3
+ from uuid import UUID
4
+
5
+ from loguru import logger
6
+ from sqlalchemy.orm import Session
7
+
8
+ from fides.api.models.privacy_request.duplicate_group import (
9
+ DuplicateGroup,
10
+ generate_rule_version,
11
+ )
12
+ from fides.api.models.privacy_request.privacy_request import PrivacyRequest
13
+ from fides.api.schemas.policy import ActionType
14
+ from fides.api.schemas.privacy_request import PrivacyRequestStatus
15
+ from fides.api.task.conditional_dependencies.schemas import (
16
+ Condition,
17
+ ConditionGroup,
18
+ ConditionLeaf,
19
+ GroupOperator,
20
+ Operator,
21
+ )
22
+ from fides.api.task.conditional_dependencies.sql_translator import (
23
+ SQLConditionTranslator,
24
+ )
25
+ from fides.config.config_proxy import ConfigProxy
26
+ from fides.config.duplicate_detection_settings import DuplicateDetectionSettings
27
+
28
+ ACTIONED_REQUEST_STATUSES = [
29
+ PrivacyRequestStatus.approved,
30
+ PrivacyRequestStatus.in_processing,
31
+ PrivacyRequestStatus.requires_manual_finalization,
32
+ PrivacyRequestStatus.requires_input,
33
+ PrivacyRequestStatus.paused,
34
+ PrivacyRequestStatus.awaiting_email_send,
35
+ PrivacyRequestStatus.error,
36
+ ]
37
+
38
+
39
+ class DuplicateDetectionService:
40
+ def __init__(self, db: Session):
41
+ self.db = db
42
+ self._config = ConfigProxy(db).privacy_request_duplicate_detection
43
+
44
+ def is_enabled(self) -> bool:
45
+ return self._config.enabled
46
+
47
+ def _create_identity_conditions(
48
+ self, current_request: PrivacyRequest
49
+ ) -> list[Condition]:
50
+ """Creates conditions for matching identity fields.
51
+
52
+ For identity field matching using the EAV pattern in ProvidedIdentity, we need to match both field_name
53
+ and hashed_value. This function creates the required nested conditions for each identity field.
54
+ Also adds a condition for the policy_id to ensure that we are only matching requests for the same policy.
55
+ """
56
+ conditions: list[Condition] = []
57
+ current_identities: dict[str, str] = {
58
+ pi.field_name: pi.hashed_value
59
+ for pi in current_request.provided_identities # type: ignore [attr-defined]
60
+ if pi.field_name in self._config.match_identity_fields
61
+ }
62
+ if len(current_identities) != len(self._config.match_identity_fields):
63
+ missing_fields = [
64
+ field
65
+ for field in self._config.match_identity_fields
66
+ if field not in current_identities.keys()
67
+ ]
68
+ logger.debug(
69
+ f"Some identity fields were not found in the current request: {missing_fields}"
70
+ )
71
+ return []
72
+
73
+ for field_name, hashed_value in current_identities.items():
74
+ identity_condition = ConditionGroup(
75
+ logical_operator=GroupOperator.and_,
76
+ conditions=[
77
+ ConditionLeaf(
78
+ field_address="privacyrequest.provided_identities.field_name",
79
+ operator=Operator.eq,
80
+ value=field_name,
81
+ ),
82
+ ConditionLeaf(
83
+ field_address="privacyrequest.provided_identities.hashed_value",
84
+ operator=Operator.eq,
85
+ value=hashed_value,
86
+ ),
87
+ ],
88
+ )
89
+ conditions.append(identity_condition)
90
+ policy_condition = ConditionLeaf(
91
+ field_address="privacyrequest.policy_id",
92
+ operator=Operator.eq,
93
+ value=current_request.policy_id,
94
+ )
95
+ conditions.append(policy_condition)
96
+ return conditions
97
+
98
+ def _create_time_window_condition(self, time_window_days: int) -> Condition:
99
+ """Creates a condition for matching requests within a time window."""
100
+ cutoff_date = datetime.now(timezone.utc) - timedelta(days=time_window_days)
101
+ condition = ConditionLeaf(
102
+ field_address="privacyrequest.created_at",
103
+ operator=Operator.gte,
104
+ value=cutoff_date.isoformat(),
105
+ )
106
+ return condition
107
+
108
+ def create_duplicate_detection_conditions(
109
+ self,
110
+ current_request: PrivacyRequest,
111
+ ) -> Optional[ConditionGroup]:
112
+ """
113
+ Create conditions for duplicate detection based on configuration.
114
+
115
+ Args:
116
+ current_request: The current privacy request to find duplicates for
117
+ config: Duplicate detection configuration settings
118
+
119
+ Returns:
120
+ A ConditionGroup with AND operator, or None if no conditions can be created
121
+ """
122
+ if len(self._config.match_identity_fields) == 0:
123
+ return None
124
+
125
+ identity_conditions = self._create_identity_conditions(current_request)
126
+ if not identity_conditions:
127
+ return None # Only proceed if we have identity conditions
128
+
129
+ time_window_condition = self._create_time_window_condition(
130
+ self._config.time_window_days
131
+ )
132
+
133
+ # Combine all conditions with AND operator
134
+ all_conditions: list[Condition] = [*identity_conditions, time_window_condition]
135
+ return ConditionGroup(
136
+ logical_operator=GroupOperator.and_, conditions=all_conditions
137
+ )
138
+
139
+ def find_duplicate_privacy_requests(
140
+ self,
141
+ current_request: PrivacyRequest,
142
+ ) -> list[PrivacyRequest]:
143
+ """
144
+ Find potential duplicate privacy requests based on duplicate detection configuration.
145
+
146
+ Uses the SQLConditionTranslator to build queries from conditions, which handles
147
+ the ProvidedIdentity relationship using SQLAlchemy's .any() method.
148
+
149
+ Args:
150
+ current_request: The privacy request to check for duplicates
151
+ config: Duplicate detection configuration settings
152
+
153
+ Returns:
154
+ List of PrivacyRequest objects that match the duplicate criteria,
155
+ does not include the current request
156
+ """
157
+ condition = self.create_duplicate_detection_conditions(current_request)
158
+
159
+ if condition is None:
160
+ return []
161
+
162
+ translator = SQLConditionTranslator(self.db)
163
+ query = translator.generate_query_from_condition(condition)
164
+
165
+ query = query.filter(PrivacyRequest.id != current_request.id).filter(
166
+ PrivacyRequest.deleted_at.is_(None)
167
+ )
168
+ return query.all()
169
+
170
+ def generate_dedup_key(self, request: PrivacyRequest) -> str:
171
+ """
172
+ Generate a dedup key for a request based on the duplicate detection settings.
173
+ """
174
+ current_identities: dict[str, str] = {
175
+ pi.field_name: pi.hashed_value
176
+ for pi in request.provided_identities # type: ignore [attr-defined]
177
+ if pi.field_name in self._config.match_identity_fields
178
+ }
179
+ if len(current_identities) != len(self._config.match_identity_fields):
180
+ raise ValueError(
181
+ "This request does not contain the required identity fields for duplicate detection."
182
+ )
183
+ return "|".join(
184
+ [
185
+ current_identities[field]
186
+ for field in sorted(self._config.match_identity_fields)
187
+ ]
188
+ )
189
+
190
+ def update_duplicate_group_ids(
191
+ self,
192
+ request: PrivacyRequest,
193
+ duplicates: list[PrivacyRequest],
194
+ duplicate_group_id: UUID,
195
+ ) -> None:
196
+ """
197
+ Update the duplicate request group ids for a request and its duplicates.
198
+ Args:
199
+ request: The privacy request to update
200
+ duplicates: The list of duplicate requests to update
201
+ duplicate_group_id: The duplicate request group id to update
202
+ """
203
+ update_all = [request] + duplicates
204
+ try:
205
+ for privacy_request in update_all:
206
+ privacy_request.duplicate_request_group_id = duplicate_group_id # type: ignore [assignment]
207
+ except Exception as e:
208
+ logger.error(f"Failed to update duplicate request group ids: {e}")
209
+ raise e
210
+
211
+ def mark_as_duplicate(self, request: PrivacyRequest, message: str) -> None:
212
+ """
213
+ Mark a request as a duplicate.
214
+ """
215
+ request.update(self.db, data={"status": PrivacyRequestStatus.duplicate})
216
+ logger.debug(message)
217
+ self.add_error_execution_log(request, message)
218
+
219
+ def add_error_execution_log(self, request: PrivacyRequest, message: str) -> None:
220
+ request.add_error_execution_log(
221
+ db=self.db,
222
+ connection_key=None,
223
+ dataset_name="Duplicate Request Detection",
224
+ collection_name=None,
225
+ message=message,
226
+ action_type=(
227
+ request.policy.get_action_type() # type: ignore [arg-type]
228
+ if request.policy
229
+ else ActionType.access
230
+ ),
231
+ )
232
+
233
+ def add_success_execution_log(self, request: PrivacyRequest, message: str) -> None:
234
+ request.add_success_execution_log(
235
+ db=self.db,
236
+ connection_key=None,
237
+ dataset_name="Duplicate Request Detection",
238
+ collection_name=None,
239
+ message=message,
240
+ action_type=(
241
+ request.policy.get_action_type() # type: ignore [arg-type]
242
+ if request.policy
243
+ else ActionType.access
244
+ ),
245
+ )
246
+
247
+ def verified_identity_cases(
248
+ self, request: PrivacyRequest, duplicates: list[PrivacyRequest]
249
+ ) -> bool:
250
+ """
251
+ Apply verified identity rules to determine if a request is a duplicate request.
252
+ - If this request does not have a verified identity, it may be a duplicate if another request in the group is verified.
253
+ - If this is the first request to be verified, it is not a duplicate request
254
+ - If other requests identities were verified before this request, it is a duplicate request
255
+ Args:
256
+ request: The privacy request to check
257
+ duplicates: The list of duplicate requests
258
+
259
+ Returns:
260
+ True if the request is a duplicate request, False otherwise
261
+ """
262
+ verified_in_group = [
263
+ duplicate for duplicate in duplicates if duplicate.identity_verified_at
264
+ ]
265
+
266
+ # The request identity is not verified.
267
+ if not request.identity_verified_at:
268
+ if len(verified_in_group) > 0:
269
+ message = f"Request {request.id} is a duplicate: it is duplicating request(s) {[duplicate.id for duplicate in verified_in_group]}."
270
+ self.mark_as_duplicate(request, message)
271
+ return True
272
+
273
+ canonical_request = min(duplicates, key=lambda x: x.created_at) # type: ignore [arg-type, return-value]
274
+ canonical_request_created_at = canonical_request.created_at or datetime.now(
275
+ timezone.utc
276
+ )
277
+ request_created_at = request.created_at or datetime.now(timezone.utc)
278
+ if request_created_at < canonical_request_created_at:
279
+ message = f"Request {request.id} is not a duplicate: it is the first request to be created in the group."
280
+ logger.debug(message)
281
+ self.add_success_execution_log(request, message)
282
+ return False
283
+
284
+ message = f"Request {request.id} is a duplicate: it is duplicating request(s) ['{canonical_request.id}']."
285
+ self.mark_as_duplicate(request, message)
286
+ return True
287
+
288
+ # The request identity is verified.
289
+ if not verified_in_group:
290
+ message = f"Request {request.id} is not a duplicate: it is the first request to be verified in the group."
291
+ logger.debug(message)
292
+ for duplicate in duplicates:
293
+ dup_message = f"Request {duplicate.id} is a duplicate: it is duplicating request(s) ['{request.id}']."
294
+ self.mark_as_duplicate(duplicate, dup_message)
295
+ self.add_success_execution_log(request, message)
296
+ return False
297
+
298
+ # If this request is the first with a verified identity, it is not a duplicate.
299
+ canonical_request = min(verified_in_group, key=lambda x: x.identity_verified_at) # type: ignore [arg-type, return-value]
300
+ canonical_request_verified_at = (
301
+ canonical_request.identity_verified_at or datetime.now(timezone.utc)
302
+ )
303
+ request_verified_at = request.identity_verified_at or datetime.now(timezone.utc)
304
+ if request_verified_at < canonical_request_verified_at:
305
+ message = f"Request {request.id} is not a duplicate: it is the first request to be verified in the group."
306
+ logger.debug(message)
307
+ self.add_success_execution_log(request, message)
308
+ for duplicate in duplicates:
309
+ dup_message = f"Request {duplicate.id} is a duplicate: it is duplicating request(s) ['{request.id}']."
310
+ self.mark_as_duplicate(duplicate, dup_message)
311
+ return False
312
+ message = f"Request {request.id} is a duplicate: it is duplicating request(s) ['{canonical_request.id}']."
313
+ self.mark_as_duplicate(request, message)
314
+ return True
315
+
316
+ # pylint: disable=too-many-return-statements
317
+ def is_duplicate_request(self, request: PrivacyRequest) -> bool:
318
+ """
319
+ Determine if a request is a duplicate request and assigns a duplicate request group id.
320
+
321
+ The hierarchy is:
322
+ 1. Actioned requests: if this request duplicates an actioned request, it is a duplicate.
323
+ 2. Verified identity requests:
324
+ a. if this request has a verified identity:
325
+ - If none of the duplicates have a verified identity, it is not a duplicate.
326
+ - If duplicates have verified identities, but this request is the first with a verified identity, it is not a duplicate.
327
+ b. if this request does not have a verified identity:
328
+ - If no duplicates have a verified identity, and this was the first created request, it is not a duplicate.
329
+ 3. First created request: if this is the first created request in the group, it is not a duplicate.
330
+ 4. If no canonical requests are found (meaning all requests are marked as duplicates), this request is not a duplicate.
331
+ - Could occur if configuration changes and previous requests were already marked as duplicates.
332
+
333
+ Args:
334
+ request: The privacy request to check
335
+ config: Duplicate detection configuration settings
336
+ Returns:
337
+ True if the request is a duplicate request, False otherwise
338
+ """
339
+ if request.policy.get_action_type() == ActionType.consent:
340
+ message = f"Consent request {request.id} is not a duplicate."
341
+ logger.info(message)
342
+ self.add_success_execution_log(request, message)
343
+ return False
344
+ duplicates = self.find_duplicate_privacy_requests(request)
345
+ rule_version = generate_rule_version(
346
+ DuplicateDetectionSettings(
347
+ enabled=self._config.enabled,
348
+ time_window_days=self._config.time_window_days,
349
+ match_identity_fields=self._config.match_identity_fields,
350
+ )
351
+ )
352
+ try:
353
+ dedup_key = self.generate_dedup_key(request)
354
+ except ValueError as e:
355
+ message = f"Request {request.id} is not a duplicate: {e}"
356
+ logger.warning(message)
357
+ self.add_success_execution_log(request, message)
358
+ return False
359
+
360
+ _, duplicate_group = DuplicateGroup.get_or_create(
361
+ db=self.db, data={"rule_version": rule_version, "dedup_key": dedup_key}
362
+ )
363
+ if duplicate_group is None:
364
+ message = f"Failed to create duplicate group for request {request.id} with dedup key {dedup_key}"
365
+ logger.error(message)
366
+ self.add_error_execution_log(request, message)
367
+ return False
368
+
369
+ self.update_duplicate_group_ids(request, duplicates, duplicate_group.id) # type: ignore [arg-type]
370
+
371
+ if request.status in ACTIONED_REQUEST_STATUSES:
372
+ message = (
373
+ f"Request {request.id} is not a duplicate: it is an actioned request."
374
+ )
375
+ logger.debug(message)
376
+ self.add_success_execution_log(request, message)
377
+ return False
378
+
379
+ # if this is the only request in the group, it is not a duplicate
380
+ if len(duplicates) == 0:
381
+ message = f"Request {request.id} is not a duplicate."
382
+ logger.debug(message)
383
+ self.add_success_execution_log(request, message)
384
+ return False
385
+
386
+ if request.status == PrivacyRequestStatus.duplicate:
387
+ return True
388
+
389
+ # only compare to non-duplicate/complete requests for the following cases
390
+ canonical_requests = [
391
+ duplicate
392
+ for duplicate in duplicates
393
+ if duplicate.status
394
+ not in [PrivacyRequestStatus.duplicate, PrivacyRequestStatus.complete]
395
+ ]
396
+ # If no non-duplicate requests are found, this request is not a duplicate.
397
+ if len(canonical_requests) == 0:
398
+ message = f"Request {request.id} is not a duplicate."
399
+ logger.debug(message)
400
+ self.add_success_execution_log(request, message)
401
+ return False
402
+
403
+ # If any requests in group are actioned, this request is a duplicate.
404
+ actioned_in_group = [
405
+ duplicate
406
+ for duplicate in canonical_requests
407
+ if duplicate.status in ACTIONED_REQUEST_STATUSES
408
+ ]
409
+ if len(actioned_in_group) > 0:
410
+ message = f"Request {request.id} is a duplicate: it is duplicating actioned request(s) {[duplicate.id for duplicate in actioned_in_group]}."
411
+ self.mark_as_duplicate(request, message)
412
+ return True
413
+ # Check against verified identity rules.
414
+ return self.verified_identity_cases(request, canonical_requests)
415
+
416
+
417
+ def check_for_duplicates(db: Session, privacy_request: PrivacyRequest) -> None:
418
+ duplicate_detection_service = DuplicateDetectionService(db)
419
+ if duplicate_detection_service.is_enabled():
420
+ logger.info(
421
+ "Duplicate detection is enabled. Checking if privacy request is a duplicate."
422
+ )
423
+ if duplicate_detection_service.is_duplicate_request(privacy_request):
424
+ logger.info("Terminating privacy request: request is a duplicate.")
@@ -71,6 +71,7 @@ from fides.api.service.privacy_request.attachment_handling import (
71
71
  get_attachments_content,
72
72
  process_attachments_for_upload,
73
73
  )
74
+ from fides.api.service.privacy_request.duplication_detection import check_for_duplicates
74
75
  from fides.api.service.storage.storage_uploader_service import upload
75
76
  from fides.api.task.filter_results import filter_data_categories
76
77
  from fides.api.task.graph_runners import access_runner, consent_runner, erasure_runner
@@ -446,6 +447,10 @@ def run_privacy_request(
446
447
  logger.info("Terminating privacy request: request deleted.")
447
448
  return
448
449
 
450
+ check_for_duplicates(db=session, privacy_request=privacy_request)
451
+ if privacy_request.status == PrivacyRequestStatus.duplicate:
452
+ return
453
+
449
454
  logger.info("Dispatching privacy request")
450
455
  privacy_request.start_processing(session)
451
456
 
@@ -1,6 +1,6 @@
1
1
  from enum import Enum
2
2
  from inspect import Signature, signature
3
- from typing import TYPE_CHECKING, Callable, Dict, List, Union
3
+ from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Union
4
4
 
5
5
  from loguru import logger
6
6
 
@@ -319,7 +319,12 @@ def validate_polling_result_override_function(f: Callable) -> None:
319
319
 
320
320
  # Check return type annotation - handle both direct class and string literals
321
321
  return_annotation = sig.return_annotation
322
- if return_annotation not in (PollingResult, "PollingResult"):
322
+ if return_annotation not in (
323
+ PollingResult,
324
+ "PollingResult",
325
+ Optional[PollingResult],
326
+ "Optional[PollingResult]",
327
+ ):
323
328
  raise InvalidSaaSRequestOverrideException(
324
329
  "Polling result override function must return a PollingResult"
325
330
  )
@@ -165,7 +165,9 @@ def request_details(
165
165
 
166
166
  if response is not None:
167
167
  if CONFIG.dev_mode and response.content:
168
- details[LoggerContextKeys.response.value] = response.content.decode("utf-8")
168
+ details[LoggerContextKeys.response.value] = response.content.decode(
169
+ "utf-8", errors="replace"
170
+ )
169
171
 
170
172
  details[LoggerContextKeys.status_code.value] = response.status_code
171
173