ethyca-fides 2.73.2b0__py2.py3-none-any.whl → 2.73.2b1__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ethyca-fides might be problematic. Click here for more details.
- {ethyca_fides-2.73.2b0.dist-info → ethyca_fides-2.73.2b1.dist-info}/METADATA +1 -1
- {ethyca_fides-2.73.2b0.dist-info → ethyca_fides-2.73.2b1.dist-info}/RECORD +233 -230
- fides/_version.py +3 -3
- fides/api/alembic/migrations/versions/xx_2025_10_29_1659_80d28dea3b6b_added_duplicate_group_table.py +79 -0
- fides/api/models/privacy_request/duplicate_group.py +84 -0
- fides/api/models/privacy_request/privacy_request.py +12 -2
- fides/api/schemas/privacy_request.py +2 -1
- fides/api/service/async_dsr/strategies/async_dsr_strategy_polling.py +13 -6
- fides/api/service/privacy_request/duplication_detection.py +357 -0
- fides/api/service/saas_request/saas_request_override_factory.py +7 -2
- fides/ui-build/static/admin/404.html +1 -1
- fides/ui-build/static/admin/_next/static/chunks/5909-56b37040ff0e6933.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/{7245-c9bc628d078c2170.js → 7245-c41aa2528645a47d.js} +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/{_app-ee588a308812715d.js → _app-2986534ccbf5e4a5.js} +2 -2
- fides/ui-build/static/admin/_next/static/chunks/pages/new-privacy-requests-cc9e47f347b657bf.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests-9692057d7ba62e04.js +1 -0
- fides/ui-build/static/admin/_next/static/wfI6N9ecEPXinU3OlxHLy/_buildManifest.js +1 -0
- fides/ui-build/static/admin/add-systems/manual.html +1 -1
- fides/ui-build/static/admin/add-systems/multiple.html +1 -1
- fides/ui-build/static/admin/add-systems.html +1 -1
- fides/ui-build/static/admin/consent/configure/add-vendors.html +1 -1
- fides/ui-build/static/admin/consent/configure.html +1 -1
- fides/ui-build/static/admin/consent/privacy-experience/[id].html +1 -1
- fides/ui-build/static/admin/consent/privacy-experience/new.html +1 -1
- fides/ui-build/static/admin/consent/privacy-experience.html +1 -1
- fides/ui-build/static/admin/consent/privacy-notices/[id].html +1 -1
- fides/ui-build/static/admin/consent/privacy-notices/new.html +1 -1
- fides/ui-build/static/admin/consent/privacy-notices.html +1 -1
- fides/ui-build/static/admin/consent/properties.html +1 -1
- fides/ui-build/static/admin/consent/reporting.html +1 -1
- fides/ui-build/static/admin/consent.html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn]/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn].html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/projects.html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/resources/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/resources.html +1 -1
- fides/ui-build/static/admin/data-catalog.html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center/datastore/[monitorId].html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center/datastore.html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center/website/[monitorId]/[systemId].html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center/website/[monitorId].html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center/website.html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center.html +1 -1
- fides/ui-build/static/admin/data-discovery/activity.html +1 -1
- fides/ui-build/static/admin/data-discovery/detection/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-discovery/detection.html +1 -1
- fides/ui-build/static/admin/data-discovery/discovery/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-discovery/discovery.html +1 -1
- fides/ui-build/static/admin/datamap.html +1 -1
- fides/ui-build/static/admin/dataset/[datasetId]/[collectionName]/[...subfieldNames].html +1 -1
- fides/ui-build/static/admin/dataset/[datasetId]/[collectionName].html +1 -1
- fides/ui-build/static/admin/dataset/[datasetId].html +1 -1
- fides/ui-build/static/admin/dataset/new.html +1 -1
- fides/ui-build/static/admin/dataset.html +1 -1
- fides/ui-build/static/admin/datastore-connection/[id].html +1 -1
- fides/ui-build/static/admin/datastore-connection/new.html +1 -1
- fides/ui-build/static/admin/datastore-connection.html +1 -1
- fides/ui-build/static/admin/index.html +1 -1
- fides/ui-build/static/admin/integrations/[id].html +1 -1
- fides/ui-build/static/admin/integrations.html +1 -1
- fides/ui-build/static/admin/login/[provider].html +1 -1
- fides/ui-build/static/admin/login.html +1 -1
- fides/ui-build/static/admin/new-privacy-requests.html +1 -1
- fides/ui-build/static/admin/notifications/digests/[id].html +1 -1
- fides/ui-build/static/admin/notifications/digests/new.html +1 -1
- fides/ui-build/static/admin/notifications/digests.html +1 -1
- fides/ui-build/static/admin/notifications/providers/[key].html +1 -1
- fides/ui-build/static/admin/notifications/providers/new.html +1 -1
- fides/ui-build/static/admin/notifications/providers.html +1 -1
- fides/ui-build/static/admin/notifications/templates/[id].html +1 -1
- fides/ui-build/static/admin/notifications/templates/add-template.html +1 -1
- fides/ui-build/static/admin/notifications/templates.html +1 -1
- fides/ui-build/static/admin/notifications.html +1 -1
- fides/ui-build/static/admin/poc/ant-components.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/AntForm.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikAntFormItem.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikControlled.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikField.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikSpreadField.html +1 -1
- fides/ui-build/static/admin/poc/forms.html +1 -1
- fides/ui-build/static/admin/poc/table-migration.html +1 -1
- fides/ui-build/static/admin/privacy-requests/[id].html +1 -1
- fides/ui-build/static/admin/privacy-requests/configure/storage.html +1 -1
- fides/ui-build/static/admin/privacy-requests/configure.html +1 -1
- fides/ui-build/static/admin/privacy-requests.html +1 -1
- fides/ui-build/static/admin/properties/[id].html +1 -1
- fides/ui-build/static/admin/properties/add-property.html +1 -1
- fides/ui-build/static/admin/properties.html +1 -1
- fides/ui-build/static/admin/reporting/datamap.html +1 -1
- fides/ui-build/static/admin/sandbox/privacy-notices.html +1 -1
- fides/ui-build/static/admin/settings/about/alpha.html +1 -1
- fides/ui-build/static/admin/settings/about.html +1 -1
- fides/ui-build/static/admin/settings/consent/[configuration_id]/[purpose_id].html +1 -1
- fides/ui-build/static/admin/settings/consent.html +1 -1
- fides/ui-build/static/admin/settings/custom-fields/[id].html +1 -1
- fides/ui-build/static/admin/settings/custom-fields/new.html +1 -1
- fides/ui-build/static/admin/settings/custom-fields.html +1 -1
- fides/ui-build/static/admin/settings/domain-records.html +1 -1
- fides/ui-build/static/admin/settings/domains.html +1 -1
- fides/ui-build/static/admin/settings/email-templates.html +1 -1
- fides/ui-build/static/admin/settings/locations.html +1 -1
- fides/ui-build/static/admin/settings/organization.html +1 -1
- fides/ui-build/static/admin/settings/privacy-requests.html +1 -1
- fides/ui-build/static/admin/settings/regulations.html +1 -1
- fides/ui-build/static/admin/systems/configure/[id]/test-datasets.html +1 -1
- fides/ui-build/static/admin/systems/configure/[id].html +1 -1
- fides/ui-build/static/admin/systems.html +1 -1
- fides/ui-build/static/admin/taxonomy.html +1 -1
- fides/ui-build/static/admin/user-management/new.html +1 -1
- fides/ui-build/static/admin/user-management/profile/[id].html +1 -1
- fides/ui-build/static/admin/user-management.html +1 -1
- fides/ui-build/static/admin/_next/static/chunks/3873-d18e47b327445db5.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/new-privacy-requests-df0c95e408c54c7e.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests-6e4c535b6d614596.js +0 -1
- fides/ui-build/static/admin/_next/static/wCNFtmYQhEDMaMPeBB4BM/_buildManifest.js +0 -1
- {ethyca_fides-2.73.2b0.dist-info → ethyca_fides-2.73.2b1.dist-info}/WHEEL +0 -0
- {ethyca_fides-2.73.2b0.dist-info → ethyca_fides-2.73.2b1.dist-info}/entry_points.txt +0 -0
- {ethyca_fides-2.73.2b0.dist-info → ethyca_fides-2.73.2b1.dist-info}/licenses/LICENSE +0 -0
- {ethyca_fides-2.73.2b0.dist-info → ethyca_fides-2.73.2b1.dist-info}/top_level.txt +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{1099-31f9d973bc287df8.js → 1099-53dc1dd16a1e615b.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{1115-26393b586775ea29.js → 1115-5f133b52b4c719f2.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{1437-8b1f6c8797c68bfd.js → 1437-9485f6a257000689.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{155-f6302d32cba4cab6.js → 155-c4b86b97419eee1b.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{1817-2d5cf537a2992c79.js → 1817-a317936d371df0d0.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{2040-7eed8491ca7276ed.js → 2040-041247847a1d23c9.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{2397-083fc511acb6105d.js → 2397-2426b7f2f3c61c4b.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{2921-34a43f2f8f5e5e69.js → 2921-4ee55c84908dcb6e.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{3377-988ac2f3a2e8d5d4.js → 3377-421918a9708a85fa.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{3696-6f90e41a53d22920.js → 3696-a22951845ef33489.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{3700-f695f2f6b8251971.js → 3700-5cd6f6a219952e84.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{3772-9f1713f9d5f97a10.js → 3772-240d907365cfd536.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{3923-f0a85dc5c3684fa0.js → 3923-c0d5a344c1bcb75f.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{401-7c345d019bb9bcbd.js → 401-2ae64c1931df4bbe.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{4496-4ff19366c597ec16.js → 4496-94d9ef9a7b77bcd8.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{4817-1f3e6ea38625d8d5.js → 4817-119bf7124a66b06d.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{5185-e7f8b81dd3dfbe0b.js → 5185-6220718ffe0578af.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{5258-62d6bc19add60aa6.js → 5258-566264abf15c818e.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{5279-bd6cccabdd6ca447.js → 5279-d702b465ff72c384.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{549-28537a6de666944b.js → 549-9ea1f2819cfa2b47.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{5643-3459282d296a3c59.js → 5643-2c3308ff2cee965d.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{5783-21775c232dce7af7.js → 5783-50e5735cf9cb0f2e.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{6277-3759894435cb8569.js → 6277-7c765deedc54e9a6.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{6315-e2fb5ea77179a871.js → 6315-9f22209b868f61a5.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{6853-de9905d28e5b19b3.js → 6853-748e92c6414f313e.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{6954-84789a4e4fb04eb9.js → 6954-2a52d012b8c44598.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{7476-a02d970ea4d3f7d0.js → 7476-c1e9b034b4d3b1b6.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{7488-cf92601852e3c509.js → 7488-4145699c604c9554.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{7630-a11610c2b31ab2ca.js → 7630-a6631a3a78029573.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{7773-9ae233109bc64ec2.js → 7773-dd824bff406d3057.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{796-e36d610066135f8c.js → 796-8f56cd4d8dfa663e.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{8011-75af8b480fa114e6.js → 8011-3d6cc0af757b41de.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{8212-393420e5a9751791.js → 8212-10bf793014dafd8b.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{8373-22b4d20e8cc06b3a.js → 8373-9b5db3cceef9add4.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{9046-d9c6498368b993d1.js → 9046-48bd5b591ff9cadc.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{9330-f753636a31c4ea04.js → 9330-4a29ef6a17e61b3f.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{9682-d1a3afa1394f8304.js → 9682-1cfc98b906cf0d97.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{9826-a737a9956c1d0905.js → 9826-517b8ba731a6642e.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{9899-d6437facac926264.js → 9899-5eea93b50aee65f7.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{9965-25621dd507e0cfd6.js → 9965-e3a7ac5848e62b5f.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/{404-eb019192ce498f32.js → 404-e30ec05084303d6b.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/add-systems/{manual-7081e0e49f67716c.js → manual-6466b8cdac3674ea.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/add-systems/{multiple-a188f84239f4b2a8.js → multiple-bc06457d27216fad.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/{add-systems-ee9df33ebd471099.js → add-systems-eeba4d74db895be7.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/consent/configure/{add-vendors-4f9cf087fcee87e6.js → add-vendors-ff54e031f93924ad.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/consent/{configure-9af75caefc74eaca.js → configure-13651347dee871d0.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/consent/privacy-experience/{[id]-1b02a4991201b7e4.js → [id]-3645a8f2244cdcf8.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/consent/{privacy-experience-b08abefec298ccf1.js → privacy-experience-c33ebeb4a7288596.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/consent/privacy-notices/{[id]-8c164c4b8310214e.js → [id]-d1868093b96d5dd0.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/consent/privacy-notices/{new-db789002d84c8829.js → new-a3de236a9ae52934.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/consent/{privacy-notices-6528eb24165aceb6.js → privacy-notices-e530117d048ce019.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/consent/{properties-5cba30eba1e97e56.js → properties-29b62430692da924.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/consent/{reporting-11f1683aa15e1a62.js → reporting-4d1f50fbe0ed1562.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/{consent-614af0a2c2ba966c.js → consent-5bebe58701b192c8.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/data-catalog/[systemId]/projects/{[projectUrn]-d60761c20382b259.js → [projectUrn]-cb92b6b0bda753df.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/data-catalog/[systemId]/{projects-07e7d38ce34e1e6c.js → projects-52a3660671c248ed.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/data-catalog/[systemId]/{resources-aed94957009eb3fd.js → resources-26155daa740bcc3a.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/{data-catalog-127c114dd8f102ed.js → data-catalog-487fcb8f2caed2da.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center/datastore/{[monitorId]-5aa7a9fa96160de8.js → [monitorId]-b74862be16b8e2ae.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center/website/[monitorId]/{[systemId]-899bf30dde8b3292.js → [systemId]-3d9fe61dd9e6f664.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center/website/{[monitorId]-73085f50abb775c0.js → [monitorId]-958927ffa1fd8645.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/{action-center-823d0dd77e66585b.js → action-center-037f91cf033616bc.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/{activity-24a82e07a0008516.js → activity-a1cfa65b549a8612.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/{datamap-3b100c44ea9e3988.js → datamap-2f28227308bcddd3.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/dataset/[datasetId]/[collectionName]/{[...subfieldNames]-5f88280db168083e.js → [...subfieldNames]-6d342de24c1a91ad.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/dataset/[datasetId]/{[collectionName]-bfbcf19c28c794ae.js → [collectionName]-8f78f42dafb99e59.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/dataset/{[datasetId]-6fbe2b584a509226.js → [datasetId]-2b3cdd68b333aadf.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/dataset/{new-3d1e48f4b95d7f6b.js → new-1072d90df5d75ba1.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/{dataset-d3c6ecf7f29bea6e.js → dataset-bf9e1cd7b8a91e81.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/datastore-connection/{[id]-0a4aa42be2da0255.js → [id]-e2a756a05906ce5b.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/datastore-connection/{new-14313e441a13192c.js → new-7435d78fcf749e65.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/{datastore-connection-f139d1ce26404f30.js → datastore-connection-dc50c7e9717c31ce.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/{index-1343fa525a206571.js → index-91fb482245b14d86.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/integrations/{[id]-b2d3d28b10a758e6.js → [id]-6166dab37d311c92.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/{integrations-a733e5d7c3ce9bb8.js → integrations-82ffe15ce196c688.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/notifications/digests/{[id]-63b3be660fb12c0f.js → [id]-0c7b41a17cd81544.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/notifications/digests/{new-d3b577962dd33266.js → new-7e02a48cc86f7835.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/notifications/{digests-aed9afd988a48acf.js → digests-b0c864e6411cb8b9.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/notifications/providers/{[key]-720cde29f81db47f.js → [key]-c5e272e1f0c9b097.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/notifications/providers/{new-3668866076b53016.js → new-3cce4a6b31154b54.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/notifications/{providers-36a0ac36062abd02.js → providers-4136309c03cd69b9.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/notifications/templates/{[id]-8063dceb32310c85.js → [id]-95aa9b844d3f19d0.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/notifications/templates/{add-template-4931c70bee62232f.js → add-template-dbdfa8cd7fbb5e3c.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/notifications/{templates-a14c876b49422597.js → templates-a4ce2e4c547ab85a.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/{notifications-93af719dab3bd053.js → notifications-7d2e09a261df0655.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/poc/{ant-components-9cfb469de7b4aeab.js → ant-components-8ab33b70c0087799.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/poc/form-experiments/{AntForm-e715cc654fb6a5cd.js → AntForm-c0f0b4344f2d120a.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/poc/form-experiments/{FormikAntFormItem-72ae299bcb6adae6.js → FormikAntFormItem-d6567b5b37000e85.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/poc/form-experiments/{FormikControlled-2337f8c81a766eb0.js → FormikControlled-4f632928b6dfd0d0.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/poc/form-experiments/{FormikField-0af454f55494f6fa.js → FormikField-ec194d81e022bc47.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/poc/{forms-d1b90ffa996fbd89.js → forms-dad16174a6bfd99c.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/poc/{table-migration-19724b9e0581b96d.js → table-migration-55db71ad40292372.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/{[id]-cdd3754289a28317.js → [id]-4dd70793c7edf0a8.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/configure/{storage-cc41ae605f2b55ae.js → storage-63588b8a340002fd.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/{configure-fa82cffba448ccd8.js → configure-415700a23b1d3c11.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/properties/{[id]-bc1c289647e52c48.js → [id]-61469c255ff06e69.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/properties/{add-property-cbfaa23d96f5ed0b.js → add-property-4be683f2820027cd.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/{properties-a15a3fd0ed88f39c.js → properties-b32d615283f0c31b.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/reporting/{datamap-632b3ee563d070f2.js → datamap-1b4bdd9277062e60.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/sandbox/{privacy-notices-afe921f6e2a526fb.js → privacy-notices-f28535ed0a0080ad.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/settings/consent/[configuration_id]/{[purpose_id]-19de0024418a4924.js → [purpose_id]-b1500de5cd1fe441.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{consent-5b6807dced8d03c5.js → consent-f136f4204c5d6fc5.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{custom-fields-7dce52bfc1b2652c.js → custom-fields-bf9b2ac61418b508.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{domain-records-3b87002753b23ca5.js → domain-records-318bcdad75571ca2.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{domains-aacd9d0ad47082d4.js → domains-1d0b2c16a3565091.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{email-templates-3cdd0b39901190ba.js → email-templates-33a5f7e22a7f8b71.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{locations-61076eedbfd137b9.js → locations-e2d25e574b5b46ee.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{organization-b07b11d6002f8c8c.js → organization-0055cabba6190e47.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{privacy-requests-084a2b4431d35322.js → privacy-requests-f521263a3beaac9b.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{regulations-b7c0d3b1b754e70f.js → regulations-8329b9cd8cd89579.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/systems/configure/[id]/{test-datasets-b72d36243a0a545c.js → test-datasets-81530b3fc0daa137.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/systems/configure/{[id]-59c89489fa32a4cb.js → [id]-4d94d20d4f7089f7.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/{systems-cfaa37a0df83674b.js → systems-22e7979c69e25bda.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/{taxonomy-56a5434969cbe9ba.js → taxonomy-652edb2ad165a845.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/user-management/profile/{[id]-5d5a68e555d18693.js → [id]-1d7d51b194f9dcc6.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/{user-management-e6a211d8a0401086.js → user-management-b23b698c019a9560.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{webpack-c2c11809187b9f84.js → webpack-d8c0ff59e7dbc75d.js} +0 -0
- /fides/ui-build/static/admin/_next/static/{wCNFtmYQhEDMaMPeBB4BM → wfI6N9ecEPXinU3OlxHLy}/_ssgManifest.js +0 -0
fides/_version.py
CHANGED
|
@@ -8,11 +8,11 @@ import json
|
|
|
8
8
|
|
|
9
9
|
version_json = '''
|
|
10
10
|
{
|
|
11
|
-
"date": "2025-10-
|
|
11
|
+
"date": "2025-10-31T14:51:49+0000",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "2.73.
|
|
14
|
+
"full-revisionid": "3a382cb972e2ea964f078259f93ed9b6ea0b1d02",
|
|
15
|
+
"version": "2.73.2b1"
|
|
16
16
|
}
|
|
17
17
|
''' # END VERSION_JSON
|
|
18
18
|
|
fides/api/alembic/migrations/versions/xx_2025_10_29_1659_80d28dea3b6b_added_duplicate_group_table.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""added duplicate group table
|
|
2
|
+
|
|
3
|
+
Revision ID: 80d28dea3b6b
|
|
4
|
+
Revises: c09e76282dd1
|
|
5
|
+
Create Date: 2025-10-29 16:59:09.551676
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sqlalchemy as sa
|
|
10
|
+
from alembic import op
|
|
11
|
+
from sqlalchemy.dialects import postgresql
|
|
12
|
+
|
|
13
|
+
# revision identifiers, used by Alembic.
|
|
14
|
+
revision = "80d28dea3b6b"
|
|
15
|
+
down_revision = "c09e76282dd1"
|
|
16
|
+
branch_labels = None
|
|
17
|
+
depends_on = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def upgrade():
|
|
21
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
22
|
+
op.create_table(
|
|
23
|
+
"duplicate_group",
|
|
24
|
+
sa.Column(
|
|
25
|
+
"created_at",
|
|
26
|
+
sa.DateTime(timezone=True),
|
|
27
|
+
server_default=sa.text("now()"),
|
|
28
|
+
nullable=True,
|
|
29
|
+
),
|
|
30
|
+
sa.Column(
|
|
31
|
+
"updated_at",
|
|
32
|
+
sa.DateTime(timezone=True),
|
|
33
|
+
server_default=sa.text("now()"),
|
|
34
|
+
nullable=True,
|
|
35
|
+
),
|
|
36
|
+
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
37
|
+
sa.Column("rule_version", sa.Text(), nullable=False),
|
|
38
|
+
sa.Column("is_active", sa.Boolean(), server_default="t", nullable=False),
|
|
39
|
+
sa.Column("dedup_key", sa.Text(), nullable=False),
|
|
40
|
+
sa.PrimaryKeyConstraint("id"),
|
|
41
|
+
)
|
|
42
|
+
op.drop_index(
|
|
43
|
+
"ix_privacyrequest_duplicate_request_group_id", table_name="privacyrequest"
|
|
44
|
+
)
|
|
45
|
+
op.drop_column("privacyrequest", "duplicate_request_group_id")
|
|
46
|
+
op.add_column(
|
|
47
|
+
"privacyrequest",
|
|
48
|
+
sa.Column(
|
|
49
|
+
"duplicate_request_group_id", postgresql.UUID(as_uuid=True), nullable=True
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
op.create_foreign_key(
|
|
53
|
+
"fk_privacyrequest_duplicate_group",
|
|
54
|
+
"privacyrequest",
|
|
55
|
+
"duplicate_group",
|
|
56
|
+
["duplicate_request_group_id"],
|
|
57
|
+
["id"],
|
|
58
|
+
)
|
|
59
|
+
# ### end Alembic commands ###
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def downgrade():
|
|
63
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
64
|
+
op.drop_constraint(
|
|
65
|
+
"fk_privacyrequest_duplicate_group", "privacyrequest", type_="foreignkey"
|
|
66
|
+
)
|
|
67
|
+
op.drop_column("privacyrequest", "duplicate_request_group_id")
|
|
68
|
+
op.add_column(
|
|
69
|
+
"privacyrequest",
|
|
70
|
+
sa.Column("duplicate_request_group_id", sa.String(), nullable=True),
|
|
71
|
+
)
|
|
72
|
+
op.create_index(
|
|
73
|
+
"ix_privacyrequest_duplicate_request_group_id",
|
|
74
|
+
"privacyrequest",
|
|
75
|
+
["duplicate_request_group_id"],
|
|
76
|
+
unique=False,
|
|
77
|
+
)
|
|
78
|
+
op.drop_table("duplicate_group")
|
|
79
|
+
# ### end Alembic commands ###
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from sqlalchemy import Boolean, Column, Text
|
|
7
|
+
from sqlalchemy.dialects.postgresql import UUID
|
|
8
|
+
from sqlalchemy.ext.declarative import declared_attr
|
|
9
|
+
from sqlalchemy.orm import Session, relationship
|
|
10
|
+
|
|
11
|
+
from fides.api.db.base_class import Base # type: ignore[attr-defined]
|
|
12
|
+
from fides.config.duplicate_detection_settings import DuplicateDetectionSettings
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from fides.api.models.privacy_request.privacy_request import PrivacyRequest
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def generate_deterministic_uuid(rule_version: str, dedup_key: str) -> uuid.UUID:
|
|
19
|
+
"""Overrides the base class method to generate a deterministic group id.
|
|
20
|
+
|
|
21
|
+
The actual rule version is a hash of the duplicate detection settings config,
|
|
22
|
+
using simple examples here for illustration:
|
|
23
|
+
- under rule "email", duplicate@example.com → group A,
|
|
24
|
+
- under rule "email|phone", duplicate@example.com|1234567890 → group B.
|
|
25
|
+
- ... and so on.
|
|
26
|
+
|
|
27
|
+
No collisions, no overlap.
|
|
28
|
+
"""
|
|
29
|
+
# Combine the rule version and the key into a stable hash
|
|
30
|
+
hash_input = f"{rule_version}:{dedup_key}".encode("utf-8")
|
|
31
|
+
return uuid.UUID(hashlib.md5(hash_input).hexdigest())
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def generate_rule_version(config: DuplicateDetectionSettings) -> str:
|
|
35
|
+
"""Generate a stable short hash for the dedup rule config."""
|
|
36
|
+
normalized = json.dumps(config.model_dump(), sort_keys=True)
|
|
37
|
+
return hashlib.sha256(normalized.encode("utf-8")).hexdigest()[:8]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class DuplicateGroup(Base):
|
|
41
|
+
"""
|
|
42
|
+
A table for storing duplicate request group information.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
@declared_attr
|
|
46
|
+
def __tablename__(self) -> str:
|
|
47
|
+
return "duplicate_group"
|
|
48
|
+
|
|
49
|
+
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
50
|
+
rule_version = Column(Text, nullable=False)
|
|
51
|
+
# TODO: The is_active column is not used yet since we are defaulting to just email for
|
|
52
|
+
# duplicate detection. When we allow users to configure the duplicate detection settings,
|
|
53
|
+
# we will need to add a way to activate/deactivate duplicate groups.
|
|
54
|
+
# This should be done by getting the previous rule version and deactivating all groups with
|
|
55
|
+
# that rule version. New groups will be created with the new rule version.
|
|
56
|
+
is_active = Column(Boolean, default=True, nullable=False)
|
|
57
|
+
dedup_key = Column(Text, nullable=False)
|
|
58
|
+
privacy_requests = relationship(
|
|
59
|
+
"PrivacyRequest",
|
|
60
|
+
back_populates="duplicate_group",
|
|
61
|
+
lazy="dynamic",
|
|
62
|
+
primaryjoin="and_(DuplicateGroup.id == PrivacyRequest.duplicate_request_group_id)",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def create(
|
|
67
|
+
cls, db: Session, *, data: dict[str, Any], check_name: bool = False
|
|
68
|
+
) -> "DuplicateGroup":
|
|
69
|
+
"""Create a new duplicate group.
|
|
70
|
+
Override the base class method to generate a deterministic group id.
|
|
71
|
+
If the group already exists, update the is_active column to True and return the group.
|
|
72
|
+
"""
|
|
73
|
+
group_id = generate_deterministic_uuid(data["rule_version"], data["dedup_key"])
|
|
74
|
+
# If the group already exists, update the is_active column to True and return the group
|
|
75
|
+
duplicate_group = (
|
|
76
|
+
db.query(DuplicateGroup).filter(DuplicateGroup.id == group_id).one_or_none()
|
|
77
|
+
)
|
|
78
|
+
if duplicate_group:
|
|
79
|
+
duplicate_group.update(db, data={"is_active": True})
|
|
80
|
+
return duplicate_group
|
|
81
|
+
|
|
82
|
+
# If the group does not exist, create a new one
|
|
83
|
+
data["id"] = group_id
|
|
84
|
+
return super().create(db, data=data, check_name=check_name)
|
|
@@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Union
|
|
|
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(
|
|
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)
|
|
@@ -1,6 +1,7 @@
|
|
|
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
7
|
from pydantic import ConfigDict, Field, field_serializer, field_validator
|
|
@@ -343,7 +344,7 @@ class PrivacyRequestResponse(FidesSchema):
|
|
|
343
344
|
deleted_by: Optional[str] = None
|
|
344
345
|
finalized_at: Optional[datetime] = None
|
|
345
346
|
finalized_by: Optional[str] = None
|
|
346
|
-
duplicate_request_group_id: Optional[
|
|
347
|
+
duplicate_request_group_id: Optional[UUID] = None
|
|
347
348
|
|
|
348
349
|
model_config = ConfigDict(from_attributes=True, use_enum_values=True)
|
|
349
350
|
|
|
@@ -551,13 +551,20 @@ class AsyncPollingStrategy(AsyncDSRStrategy):
|
|
|
551
551
|
self.result_request.result_path,
|
|
552
552
|
)
|
|
553
553
|
|
|
554
|
-
#
|
|
555
|
-
if
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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,357 @@
|
|
|
1
|
+
from datetime import datetime, timedelta, timezone
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from loguru import logger
|
|
5
|
+
from sqlalchemy.orm import Session
|
|
6
|
+
|
|
7
|
+
from fides.api.models.privacy_request.duplicate_group import (
|
|
8
|
+
DuplicateGroup,
|
|
9
|
+
generate_rule_version,
|
|
10
|
+
)
|
|
11
|
+
from fides.api.models.privacy_request.privacy_request import PrivacyRequest
|
|
12
|
+
from fides.api.schemas.policy import ActionType
|
|
13
|
+
from fides.api.schemas.privacy_request import PrivacyRequestStatus
|
|
14
|
+
from fides.api.task.conditional_dependencies.schemas import (
|
|
15
|
+
Condition,
|
|
16
|
+
ConditionGroup,
|
|
17
|
+
ConditionLeaf,
|
|
18
|
+
GroupOperator,
|
|
19
|
+
Operator,
|
|
20
|
+
)
|
|
21
|
+
from fides.api.task.conditional_dependencies.sql_translator import (
|
|
22
|
+
SQLConditionTranslator,
|
|
23
|
+
)
|
|
24
|
+
from fides.config.duplicate_detection_settings import DuplicateDetectionSettings
|
|
25
|
+
|
|
26
|
+
ACTIONED_REQUEST_STATUSES = [
|
|
27
|
+
PrivacyRequestStatus.approved,
|
|
28
|
+
PrivacyRequestStatus.in_processing,
|
|
29
|
+
PrivacyRequestStatus.complete,
|
|
30
|
+
PrivacyRequestStatus.requires_manual_finalization,
|
|
31
|
+
PrivacyRequestStatus.requires_input,
|
|
32
|
+
PrivacyRequestStatus.paused,
|
|
33
|
+
PrivacyRequestStatus.awaiting_email_send,
|
|
34
|
+
PrivacyRequestStatus.error,
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class DuplicateDetectionService:
|
|
39
|
+
def __init__(self, db: Session):
|
|
40
|
+
self.db = db
|
|
41
|
+
|
|
42
|
+
def _create_identity_conditions(
|
|
43
|
+
self, current_request: PrivacyRequest, config: DuplicateDetectionSettings
|
|
44
|
+
) -> list[Condition]:
|
|
45
|
+
"""Creates conditions for matching identity fields.
|
|
46
|
+
|
|
47
|
+
For identity field matching using the EAV pattern in ProvidedIdentity, we need to match both field_name
|
|
48
|
+
and hashed_value. This function creates the required nested conditions for each identity field.
|
|
49
|
+
Also adds a condition for the policy_id to ensure that we are only matching requests for the same policy.
|
|
50
|
+
"""
|
|
51
|
+
conditions: list[Condition] = []
|
|
52
|
+
current_identities: dict[str, str] = {
|
|
53
|
+
pi.field_name: pi.hashed_value
|
|
54
|
+
for pi in current_request.provided_identities # type: ignore [attr-defined]
|
|
55
|
+
if pi.field_name in config.match_identity_fields
|
|
56
|
+
}
|
|
57
|
+
if len(current_identities) != len(config.match_identity_fields):
|
|
58
|
+
missing_fields = [
|
|
59
|
+
field
|
|
60
|
+
for field in config.match_identity_fields
|
|
61
|
+
if field not in current_identities.keys()
|
|
62
|
+
]
|
|
63
|
+
logger.debug(
|
|
64
|
+
f"Some identity fields were not found in the current request: {missing_fields}"
|
|
65
|
+
)
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
for field_name, hashed_value in current_identities.items():
|
|
69
|
+
identity_condition = ConditionGroup(
|
|
70
|
+
logical_operator=GroupOperator.and_,
|
|
71
|
+
conditions=[
|
|
72
|
+
ConditionLeaf(
|
|
73
|
+
field_address="privacyrequest.provided_identities.field_name",
|
|
74
|
+
operator=Operator.eq,
|
|
75
|
+
value=field_name,
|
|
76
|
+
),
|
|
77
|
+
ConditionLeaf(
|
|
78
|
+
field_address="privacyrequest.provided_identities.hashed_value",
|
|
79
|
+
operator=Operator.eq,
|
|
80
|
+
value=hashed_value,
|
|
81
|
+
),
|
|
82
|
+
],
|
|
83
|
+
)
|
|
84
|
+
conditions.append(identity_condition)
|
|
85
|
+
policy_condition = ConditionLeaf(
|
|
86
|
+
field_address="privacyrequest.policy_id",
|
|
87
|
+
operator=Operator.eq,
|
|
88
|
+
value=current_request.policy_id,
|
|
89
|
+
)
|
|
90
|
+
conditions.append(policy_condition)
|
|
91
|
+
return conditions
|
|
92
|
+
|
|
93
|
+
def _create_time_window_condition(self, time_window_days: int) -> Condition:
|
|
94
|
+
"""Creates a condition for matching requests within a time window."""
|
|
95
|
+
cutoff_date = datetime.now(timezone.utc) - timedelta(days=time_window_days)
|
|
96
|
+
condition = ConditionLeaf(
|
|
97
|
+
field_address="privacyrequest.created_at",
|
|
98
|
+
operator=Operator.gte,
|
|
99
|
+
value=cutoff_date.isoformat(),
|
|
100
|
+
)
|
|
101
|
+
return condition
|
|
102
|
+
|
|
103
|
+
def create_duplicate_detection_conditions(
|
|
104
|
+
self,
|
|
105
|
+
current_request: PrivacyRequest,
|
|
106
|
+
config: DuplicateDetectionSettings,
|
|
107
|
+
) -> Optional[ConditionGroup]:
|
|
108
|
+
"""
|
|
109
|
+
Create conditions for duplicate detection based on configuration.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
current_request: The current privacy request to find duplicates for
|
|
113
|
+
config: Duplicate detection configuration settings
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
A ConditionGroup with AND operator, or None if no conditions can be created
|
|
117
|
+
"""
|
|
118
|
+
if len(config.match_identity_fields) == 0:
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
identity_conditions = self._create_identity_conditions(current_request, config)
|
|
122
|
+
if not identity_conditions:
|
|
123
|
+
return None # Only proceed if we have identity conditions
|
|
124
|
+
|
|
125
|
+
time_window_condition = self._create_time_window_condition(
|
|
126
|
+
config.time_window_days
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Combine all conditions with AND operator
|
|
130
|
+
all_conditions: list[Condition] = [*identity_conditions, time_window_condition]
|
|
131
|
+
return ConditionGroup(
|
|
132
|
+
logical_operator=GroupOperator.and_, conditions=all_conditions
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def find_duplicate_privacy_requests(
|
|
136
|
+
self,
|
|
137
|
+
current_request: PrivacyRequest,
|
|
138
|
+
config: DuplicateDetectionSettings,
|
|
139
|
+
) -> list[PrivacyRequest]:
|
|
140
|
+
"""
|
|
141
|
+
Find potential duplicate privacy requests based on duplicate detection configuration.
|
|
142
|
+
|
|
143
|
+
Uses the SQLConditionTranslator to build queries from conditions, which handles
|
|
144
|
+
the ProvidedIdentity relationship using SQLAlchemy's .any() method.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
current_request: The privacy request to check for duplicates
|
|
148
|
+
config: Duplicate detection configuration settings
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
List of PrivacyRequest objects that match the duplicate criteria,
|
|
152
|
+
does not include the current request
|
|
153
|
+
"""
|
|
154
|
+
condition = self.create_duplicate_detection_conditions(current_request, config)
|
|
155
|
+
|
|
156
|
+
if condition is None:
|
|
157
|
+
return []
|
|
158
|
+
|
|
159
|
+
translator = SQLConditionTranslator(self.db)
|
|
160
|
+
query = translator.generate_query_from_condition(condition)
|
|
161
|
+
|
|
162
|
+
query = query.filter(PrivacyRequest.id != current_request.id)
|
|
163
|
+
return query.all()
|
|
164
|
+
|
|
165
|
+
def generate_dedup_key(
|
|
166
|
+
self, request: PrivacyRequest, config: DuplicateDetectionSettings
|
|
167
|
+
) -> str:
|
|
168
|
+
"""
|
|
169
|
+
Generate a dedup key for a request based on the duplicate detection settings.
|
|
170
|
+
"""
|
|
171
|
+
current_identities: dict[str, str] = {
|
|
172
|
+
pi.field_name: pi.hashed_value
|
|
173
|
+
for pi in request.provided_identities # type: ignore [attr-defined]
|
|
174
|
+
if pi.field_name in config.match_identity_fields
|
|
175
|
+
}
|
|
176
|
+
if len(current_identities) != len(config.match_identity_fields):
|
|
177
|
+
raise ValueError(
|
|
178
|
+
"This request does not contain the required identity fields for duplicate detection."
|
|
179
|
+
)
|
|
180
|
+
return "|".join(
|
|
181
|
+
[
|
|
182
|
+
current_identities[field]
|
|
183
|
+
for field in sorted(config.match_identity_fields)
|
|
184
|
+
]
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def verified_identity_cases(
|
|
188
|
+
self, request: PrivacyRequest, duplicates: list[PrivacyRequest]
|
|
189
|
+
) -> bool:
|
|
190
|
+
"""
|
|
191
|
+
Apply verified identity rules to determine if a request is a duplicate request.
|
|
192
|
+
- If this request does not have a verified identity, it may be a duplicate if another request in the group is verified.
|
|
193
|
+
- If this is the first request to be verified, it is not a duplicate request
|
|
194
|
+
- If other requests identities were verified before this request, it is a duplicate request
|
|
195
|
+
Args:
|
|
196
|
+
request: The privacy request to check
|
|
197
|
+
duplicates: The list of duplicate requests
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
True if the request is a duplicate request, False otherwise
|
|
201
|
+
"""
|
|
202
|
+
verified_in_group = [
|
|
203
|
+
duplicate for duplicate in duplicates if duplicate.identity_verified_at
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
# The request identity is not verified.
|
|
207
|
+
if not request.identity_verified_at:
|
|
208
|
+
if len(verified_in_group) > 0:
|
|
209
|
+
logger.debug(
|
|
210
|
+
f"Request {request.id} is a duplicate: it is not verified and duplicating verified request(s) {verified_in_group}."
|
|
211
|
+
)
|
|
212
|
+
return True
|
|
213
|
+
|
|
214
|
+
min_created_at = min(
|
|
215
|
+
(d.created_at for d in duplicates if d.created_at), default=None
|
|
216
|
+
) or datetime.now(timezone.utc)
|
|
217
|
+
request_created_at = (
|
|
218
|
+
request.created_at
|
|
219
|
+
if request.created_at is not None
|
|
220
|
+
else datetime.now(timezone.utc)
|
|
221
|
+
)
|
|
222
|
+
if request_created_at < min_created_at:
|
|
223
|
+
logger.debug(
|
|
224
|
+
f"Request {request.id} is not a duplicate: it is the first request to be created in the group."
|
|
225
|
+
)
|
|
226
|
+
return False
|
|
227
|
+
logger.debug(
|
|
228
|
+
f"Request {request.id} is a duplicate: it is not verified and is not the first request to be created in the group."
|
|
229
|
+
)
|
|
230
|
+
return True
|
|
231
|
+
|
|
232
|
+
# The request identity is verified.
|
|
233
|
+
if not verified_in_group:
|
|
234
|
+
logger.debug(
|
|
235
|
+
f"Request {request.id} is not a duplicate: it is verified and no other requests in the group are verified."
|
|
236
|
+
)
|
|
237
|
+
return False
|
|
238
|
+
|
|
239
|
+
# If this request is the first with a verified identity, it is not a duplicate.
|
|
240
|
+
min_verified_at = min(
|
|
241
|
+
(d.identity_verified_at for d in duplicates if d.identity_verified_at),
|
|
242
|
+
default=None,
|
|
243
|
+
) or datetime.now(timezone.utc)
|
|
244
|
+
request_verified_at = (
|
|
245
|
+
request.identity_verified_at
|
|
246
|
+
if request.identity_verified_at is not None
|
|
247
|
+
else datetime.now(timezone.utc)
|
|
248
|
+
)
|
|
249
|
+
if request_verified_at < min_verified_at:
|
|
250
|
+
logger.debug(
|
|
251
|
+
f"Request {request.id} is not a duplicate: it is the first request to be verified in the group."
|
|
252
|
+
)
|
|
253
|
+
return False
|
|
254
|
+
logger.debug(
|
|
255
|
+
f"Request {request.id} is a duplicate: it is verified but not the first request to be verified in the group."
|
|
256
|
+
)
|
|
257
|
+
return True
|
|
258
|
+
|
|
259
|
+
# pylint: disable=too-many-return-statements
|
|
260
|
+
def is_duplicate_request(
|
|
261
|
+
self, request: PrivacyRequest, config: DuplicateDetectionSettings
|
|
262
|
+
) -> bool:
|
|
263
|
+
"""
|
|
264
|
+
Determine if a request is a duplicate request and assigns a duplicate request group id.
|
|
265
|
+
|
|
266
|
+
The hierarchy is:
|
|
267
|
+
1. Actioned requests: if this request duplicates an actioned request, it is a duplicate.
|
|
268
|
+
2. Verified identity requests:
|
|
269
|
+
a. if this request has a verified identity:
|
|
270
|
+
- If none of the duplicates have a verified identity, it is not a duplicate.
|
|
271
|
+
- If duplicates have verified identities, but this request is the first with a verified identity, it is not a duplicate.
|
|
272
|
+
b. if this request does not have a verified identity:
|
|
273
|
+
- If no duplicates have a verified identity, and this was the first created request, it is not a duplicate.
|
|
274
|
+
3. First created request: if this is the first created request in the group, it is not a duplicate.
|
|
275
|
+
4. If no canonical requests are found (meaning all requests are marked as duplicates), this request is not a duplicate.
|
|
276
|
+
- Could occur if configuration changes and previous requests were already marked as duplicates.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
request: The privacy request to check
|
|
280
|
+
config: Duplicate detection configuration settings
|
|
281
|
+
Returns:
|
|
282
|
+
True if the request is a duplicate request, False otherwise
|
|
283
|
+
"""
|
|
284
|
+
duplicates = self.find_duplicate_privacy_requests(request, config)
|
|
285
|
+
rule_version = generate_rule_version(config)
|
|
286
|
+
try:
|
|
287
|
+
dedup_key = self.generate_dedup_key(request, config)
|
|
288
|
+
except ValueError as e:
|
|
289
|
+
logger.debug(f"Request {request.id} is not a duplicate: {e}")
|
|
290
|
+
return False
|
|
291
|
+
|
|
292
|
+
_, duplicate_group = DuplicateGroup.get_or_create(
|
|
293
|
+
db=self.db, data={"rule_version": rule_version, "dedup_key": dedup_key}
|
|
294
|
+
)
|
|
295
|
+
if duplicate_group is None:
|
|
296
|
+
logger.error(
|
|
297
|
+
f"Failed to create duplicate group for request {request.id} with dedup key {dedup_key}"
|
|
298
|
+
)
|
|
299
|
+
return False
|
|
300
|
+
logger.info(
|
|
301
|
+
f"Duplicate group {duplicate_group.id} created for request {request.id} with dedup key {dedup_key}"
|
|
302
|
+
)
|
|
303
|
+
request.update(
|
|
304
|
+
db=self.db, data={"duplicate_request_group_id": duplicate_group.id}
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# if this is the only request in the group, it is not a duplicate
|
|
308
|
+
if len(duplicates) == 0:
|
|
309
|
+
logger.debug(
|
|
310
|
+
f"Request {request.id} is not a duplicate: no matching requests were found."
|
|
311
|
+
)
|
|
312
|
+
return False
|
|
313
|
+
|
|
314
|
+
if request.status == PrivacyRequestStatus.duplicate:
|
|
315
|
+
logger.warning(
|
|
316
|
+
f"Request {request.id} is a duplicate request that was requeued. This should not happen."
|
|
317
|
+
)
|
|
318
|
+
request.add_error_execution_log(
|
|
319
|
+
db=self.db,
|
|
320
|
+
connection_key=None,
|
|
321
|
+
dataset_name="Duplicate Request Detection",
|
|
322
|
+
collection_name=None,
|
|
323
|
+
message=f"Request {request.id} is a duplicate request that was requeued. This should not happen.",
|
|
324
|
+
action_type=(
|
|
325
|
+
request.policy.get_action_type() # type: ignore [arg-type]
|
|
326
|
+
if request.policy
|
|
327
|
+
else ActionType.access
|
|
328
|
+
),
|
|
329
|
+
)
|
|
330
|
+
return True
|
|
331
|
+
|
|
332
|
+
# only compare to non-duplicate requests for the following cases
|
|
333
|
+
canonical_requests = [
|
|
334
|
+
duplicate
|
|
335
|
+
for duplicate in duplicates
|
|
336
|
+
if duplicate.status != PrivacyRequestStatus.duplicate
|
|
337
|
+
]
|
|
338
|
+
# If no non-duplicate requests are found, this request is not a duplicate.
|
|
339
|
+
if len(canonical_requests) == 0:
|
|
340
|
+
logger.debug(
|
|
341
|
+
f"Request {request.id} is not a duplicate: all matching requests have been marked as duplicate requests."
|
|
342
|
+
)
|
|
343
|
+
return False
|
|
344
|
+
|
|
345
|
+
# If any requests in group are actioned, this request is a duplicate.
|
|
346
|
+
actioned_in_group = [
|
|
347
|
+
duplicate
|
|
348
|
+
for duplicate in canonical_requests
|
|
349
|
+
if duplicate.status in ACTIONED_REQUEST_STATUSES
|
|
350
|
+
]
|
|
351
|
+
if len(actioned_in_group) > 0:
|
|
352
|
+
logger.debug(
|
|
353
|
+
f"Request {request.id} is a duplicate: it is duplicating actioned request(s) {actioned_in_group}."
|
|
354
|
+
)
|
|
355
|
+
return True
|
|
356
|
+
# Check against verified identity rules.
|
|
357
|
+
return self.verified_identity_cases(request, canonical_requests)
|
|
@@ -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 (
|
|
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
|
)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width"/><meta name="next-head-count" content="2"/><link data-next-font="" rel="preconnect" href="/" crossorigin="anonymous"/><link rel="preload" href="/_next/static/css/d78390d6134d8328.css" as="style"/><link rel="stylesheet" href="/_next/static/css/d78390d6134d8328.css" data-n-g=""/><noscript data-n-css=""></noscript><script defer="" nomodule="" src="/_next/static/chunks/polyfills-42372ed130431b0a.js"></script><script src="/_next/static/chunks/webpack-
|
|
1
|
+
<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width"/><meta name="next-head-count" content="2"/><link data-next-font="" rel="preconnect" href="/" crossorigin="anonymous"/><link rel="preload" href="/_next/static/css/d78390d6134d8328.css" as="style"/><link rel="stylesheet" href="/_next/static/css/d78390d6134d8328.css" data-n-g=""/><noscript data-n-css=""></noscript><script defer="" nomodule="" src="/_next/static/chunks/polyfills-42372ed130431b0a.js"></script><script src="/_next/static/chunks/webpack-d8c0ff59e7dbc75d.js" defer=""></script><script src="/_next/static/chunks/framework-c92fc3344e6fd165.js" defer=""></script><script src="/_next/static/chunks/main-090643377c8254e6.js" defer=""></script><script src="/_next/static/chunks/pages/_app-2986534ccbf5e4a5.js" defer=""></script><script src="/_next/static/chunks/pages/404-e30ec05084303d6b.js" defer=""></script><script src="/_next/static/wfI6N9ecEPXinU3OlxHLy/_buildManifest.js" defer=""></script><script src="/_next/static/wfI6N9ecEPXinU3OlxHLy/_ssgManifest.js" defer=""></script><style>.data-ant-cssinjs-cache-path{content:"";}</style></head><body><div id="__next"><div style="height:100%;display:flex"></div></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{}},"page":"/404","query":{},"buildId":"wfI6N9ecEPXinU3OlxHLy","nextExport":true,"autoExport":true,"isFallback":false,"scriptLoader":[]}</script></body></html>
|