ethyca-fides 2.69.0rc10__py2.py3-none-any.whl → 2.69.1__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.69.0rc10.dist-info → ethyca_fides-2.69.1.dist-info}/METADATA +2 -2
- {ethyca_fides-2.69.0rc10.dist-info → ethyca_fides-2.69.1.dist-info}/RECORD +198 -189
- fides/_version.py +3 -3
- fides/api/alembic/migrations/versions/78dbe23d8204_adding_privacy_request_redaction_patterns.py +52 -0
- fides/api/api/v1/api.py +2 -0
- fides/api/api/v1/endpoints/dsr_package_link.py +2 -2
- fides/api/api/v1/endpoints/oauth_endpoints.py +20 -6
- fides/api/api/v1/endpoints/privacy_request_redaction_patterns_endpoints.py +95 -0
- fides/api/api/v1/endpoints/user_endpoints.py +28 -1
- fides/api/app_setup.py +16 -2
- fides/api/db/base.py +3 -0
- fides/api/main.py +22 -0
- fides/api/models/client.py +1 -0
- fides/api/models/privacy_request_redaction_pattern.py +64 -0
- fides/api/oauth/utils.py +117 -6
- fides/api/schemas/privacy_request_redaction_patterns.py +55 -0
- fides/api/service/privacy_request/dsr_package/dsr_data_preprocessor.py +231 -0
- fides/api/service/privacy_request/dsr_package/dsr_report_builder.py +31 -47
- fides/api/service/privacy_request/dsr_package/utils.py +268 -0
- fides/api/service/storage/streaming/smart_open_streaming_storage.py +3 -3
- fides/api/tasks/storage.py +2 -2
- fides/api/util/endpoint_utils.py +0 -13
- fides/api/util/rate_limit.py +194 -0
- fides/common/api/scope_registry.py +8 -0
- fides/common/api/v1/urn_registry.py +3 -0
- fides/config/redis_settings.py +27 -3
- fides/config/security_settings.py +31 -9
- fides/ui-build/static/admin/404.html +1 -1
- fides/ui-build/static/admin/_next/static/1TigfgzjzHeoVqRLNIMYa/_buildManifest.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/4831-fd99c0b3784de128.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/pages/{_app-ef8e1c986bc5b795.js → _app-fcdad91f6f66292b.js} +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/settings/privacy-requests-2ecc073f41628f62.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/pages/user-management/{new-de8cb3739ab99c09.js → new-92f52c43f522a350.js} +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/user-management/profile/{[id]-05d61c80a556b2d5.js → [id]-64452dfae2c5e614.js} +1 -1
- fides/ui-build/static/admin/add-systems/manual.html +1 -1
- fides/ui-build/static/admin/add-systems/multiple.html +1 -1
- fides/ui-build/static/admin/add-systems.html +1 -1
- fides/ui-build/static/admin/consent/configure/add-vendors.html +1 -1
- fides/ui-build/static/admin/consent/configure.html +1 -1
- fides/ui-build/static/admin/consent/privacy-experience/[id].html +1 -1
- fides/ui-build/static/admin/consent/privacy-experience/new.html +1 -1
- fides/ui-build/static/admin/consent/privacy-experience.html +1 -1
- fides/ui-build/static/admin/consent/privacy-notices/[id].html +1 -1
- fides/ui-build/static/admin/consent/privacy-notices/new.html +1 -1
- fides/ui-build/static/admin/consent/privacy-notices.html +1 -1
- fides/ui-build/static/admin/consent/properties.html +1 -1
- fides/ui-build/static/admin/consent/reporting.html +1 -1
- fides/ui-build/static/admin/consent.html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn]/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn].html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/projects.html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/resources/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/resources.html +1 -1
- fides/ui-build/static/admin/data-catalog.html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center/[monitorId]/[systemId].html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center/[monitorId].html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center.html +1 -1
- fides/ui-build/static/admin/data-discovery/activity.html +1 -1
- fides/ui-build/static/admin/data-discovery/detection/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-discovery/detection.html +1 -1
- fides/ui-build/static/admin/data-discovery/discovery/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-discovery/discovery.html +1 -1
- fides/ui-build/static/admin/datamap.html +1 -1
- fides/ui-build/static/admin/dataset/[datasetId]/[collectionName]/[...subfieldNames].html +1 -1
- fides/ui-build/static/admin/dataset/[datasetId]/[collectionName].html +1 -1
- fides/ui-build/static/admin/dataset/[datasetId].html +1 -1
- fides/ui-build/static/admin/dataset/new.html +1 -1
- fides/ui-build/static/admin/dataset.html +1 -1
- fides/ui-build/static/admin/datastore-connection/[id].html +1 -1
- fides/ui-build/static/admin/datastore-connection/new.html +1 -1
- fides/ui-build/static/admin/datastore-connection.html +1 -1
- fides/ui-build/static/admin/index.html +1 -1
- fides/ui-build/static/admin/integrations/[id].html +1 -1
- fides/ui-build/static/admin/integrations.html +1 -1
- fides/ui-build/static/admin/login/[provider].html +1 -1
- fides/ui-build/static/admin/login.html +1 -1
- fides/ui-build/static/admin/messaging/[id].html +1 -1
- fides/ui-build/static/admin/messaging/add-template.html +1 -1
- fides/ui-build/static/admin/messaging.html +1 -1
- fides/ui-build/static/admin/poc/ant-components.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/AntForm.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikAntFormItem.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikControlled.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikField.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikSpreadField.html +1 -1
- fides/ui-build/static/admin/poc/forms.html +1 -1
- fides/ui-build/static/admin/poc/table-migration.html +1 -1
- fides/ui-build/static/admin/privacy-requests/[id].html +1 -1
- fides/ui-build/static/admin/privacy-requests/configure/messaging.html +1 -1
- fides/ui-build/static/admin/privacy-requests/configure/storage.html +1 -1
- fides/ui-build/static/admin/privacy-requests/configure.html +1 -1
- fides/ui-build/static/admin/privacy-requests.html +1 -1
- fides/ui-build/static/admin/properties/[id].html +1 -1
- fides/ui-build/static/admin/properties/add-property.html +1 -1
- fides/ui-build/static/admin/properties.html +1 -1
- fides/ui-build/static/admin/reporting/datamap.html +1 -1
- fides/ui-build/static/admin/settings/about/alpha.html +1 -1
- fides/ui-build/static/admin/settings/about.html +1 -1
- fides/ui-build/static/admin/settings/consent/[configuration_id]/[purpose_id].html +1 -1
- fides/ui-build/static/admin/settings/consent.html +1 -1
- fides/ui-build/static/admin/settings/custom-fields.html +1 -1
- fides/ui-build/static/admin/settings/domain-records.html +1 -1
- fides/ui-build/static/admin/settings/domains.html +1 -1
- fides/ui-build/static/admin/settings/email-templates.html +1 -1
- fides/ui-build/static/admin/settings/locations.html +1 -1
- fides/ui-build/static/admin/settings/organization.html +1 -1
- fides/ui-build/static/admin/settings/privacy-requests.html +1 -0
- 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/8qfO1Ol3G3QbcXpHAnPlU/_buildManifest.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/4121-c8d5d717e31899e1.js +0 -1
- {ethyca_fides-2.69.0rc10.dist-info → ethyca_fides-2.69.1.dist-info}/WHEEL +0 -0
- {ethyca_fides-2.69.0rc10.dist-info → ethyca_fides-2.69.1.dist-info}/entry_points.txt +0 -0
- {ethyca_fides-2.69.0rc10.dist-info → ethyca_fides-2.69.1.dist-info}/licenses/LICENSE +0 -0
- {ethyca_fides-2.69.0rc10.dist-info → ethyca_fides-2.69.1.dist-info}/top_level.txt +0 -0
- /fides/ui-build/static/admin/_next/static/{8qfO1Ol3G3QbcXpHAnPlU → 1TigfgzjzHeoVqRLNIMYa}/_ssgManifest.js +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{1817-3d9e110e007853f0.js → 1817-0ca16d288fad916d.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{3620-31ebb43dba84cbbd.js → 3620-602eb74dc896d556.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{3729-a1ca1608efc11ac4.js → 3729-c17ac8031a4c4fd1.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{3872-a91143aa35fa8ef8.js → 3872-f78dec02f0d959ae.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{4608-23bbd4c3c4a59f42.js → 4608-be8cba73f5d7c326.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{4786-0827aae7aceadd22.js → 4786-61154adf88e448e1.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{4808-78ca630f2d2503cd.js → 4808-dd4157aa72648068.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{5487-8c635883dcaa9c2a.js → 5487-02d00bad7c6830e0.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{6084-0096d7de64ef8015.js → 6084-c153669d5567e242.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{6954-9d46e2276c461c26.js → 6954-5296188c19d7d0ac.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{7476-d1b0af9ade392e5b.js → 7476-45c5088baa8b66af.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{7630-da0a7ce4e3a0d62c.js → 7630-7ed6c6117775dffe.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{787-3499983fa346b380.js → 787-a8c7eab617e2fceb.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{79-f197fc4db8d530e5.js → 79-65674011d455af4d.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{796-db1e30119ea973c7.js → 796-9e1ca1a4030707c5.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{8002-971e29181f72edd1.js → 8002-24af20d679efc04e.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{9826-b0b3d3cfb13bfbc1.js → 9826-dbae8dee941a7fac.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/add-systems/{manual-9dc7e70ab5b05723.js → manual-ace203dfacacbdc4.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/add-systems/{multiple-4b79a1652297ed9a.js → multiple-920fb469e0dda1d2.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/{add-systems-1632a59203fe8eab.js → add-systems-bd0d82078e67cac3.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/consent/configure/{add-vendors-1ca9df7ca91bd101.js → add-vendors-406170eaae4329c6.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/consent/{configure-07bdbc9ae4137db4.js → configure-7207ab23bdb36ce8.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/consent/{privacy-experience-2795cd4115a77c94.js → privacy-experience-9dda4de5ec580279.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/consent/privacy-notices/{[id]-e02921dc82dccbb1.js → [id]-b378576cba255609.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/consent/privacy-notices/{new-98f9e4ba3610628a.js → new-2ca1de7b88094ab0.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/consent/{privacy-notices-17ed82777810d1c6.js → privacy-notices-0d4844d0b808e6e4.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/{consent-09610b10923d9268.js → consent-3e8bdefe714254ec.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/data-catalog/[systemId]/projects/[projectUrn]/{[resourceUrn]-da1a48336daff6f8.js → [resourceUrn]-2c29ff7a01198f30.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/data-catalog/[systemId]/projects/{[projectUrn]-d8e776f1e64e4ba8.js → [projectUrn]-04cfe2cfba7b7cd8.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/data-catalog/[systemId]/{projects-75b9629b0d9cdf96.js → projects-5f2d7b24804f861f.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/data-catalog/[systemId]/resources/{[resourceUrn]-470da05db63767cd.js → [resourceUrn]-8eb581024bc0172f.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/data-catalog/[systemId]/{resources-6c3714ee97a718c1.js → resources-de704de849960f01.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/{data-catalog-6984c033b8fe3a13.js → data-catalog-30108b00ac769fc3.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center/[monitorId]/{[systemId]-2f0a33ef9ba1f1da.js → [systemId]-e1ba213fb666b3f4.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center/{[monitorId]-e9d4f25b20ff6781.js → [monitorId]-6d133580045abdda.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/{action-center-9c428d3ef0985915.js → action-center-9a81d42a474e1e48.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/detection/{[resourceUrn]-c3a97e6721ca0abe.js → [resourceUrn]-8f736b078e9842da.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/{detection-a0a7de552ef71f5b.js → detection-eb814e3c22807871.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/discovery/{[resourceUrn]-109754fec0755339.js → [resourceUrn]-6875b7783fcfda2f.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/{discovery-88654783b06b3b21.js → discovery-172dbd7740e212ca.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/{datamap-89136e6800dc9369.js → datamap-c7390e046b2e2b7f.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/dataset/[datasetId]/[collectionName]/{[...subfieldNames]-8f58192dcb54883d.js → [...subfieldNames]-dfd71c1e9c458b89.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/dataset/[datasetId]/{[collectionName]-dcb4ab380a77aa1e.js → [collectionName]-7cdc42ec5493b83d.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/dataset/{[datasetId]-6f16d43071fb9c11.js → [datasetId]-e12b11ba15bc3fc1.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/dataset/{new-97f06e21580f1f6a.js → new-e32fccc4ca520d2b.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/{dataset-674bb3940f088ecc.js → dataset-7c59a6abf6ba6207.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/datastore-connection/{[id]-6f77d8647fca71e0.js → [id]-927b7e476c4b47d0.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/datastore-connection/{new-821dd1269834cfa2.js → new-cbe100d50df34285.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/{datastore-connection-23e4caf79faa8106.js → datastore-connection-cce20440b177050b.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/{index-23eb64eed81dcb69.js → index-6cd8708106331b8d.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/integrations/{[id]-3a4cd3fe9094fba3.js → [id]-4c3c413a2668df53.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/{integrations-57e618d7b16ac69a.js → integrations-95402b5001c07ef2.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/messaging/{[id]-c9a323eb6a929476.js → [id]-3c6dc2f6e6bae960.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/messaging/{add-template-b9bb09e46921a590.js → add-template-4a6d4023a7791be8.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/{messaging-82c631a12b5a008c.js → messaging-76b204c9b98d656f.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/poc/{table-migration-38360083348c3d6c.js → table-migration-48500551fd6a7602.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/{[id]-0d0bb9eb004a3336.js → [id]-0f25a76dd18c5e20.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/configure/{messaging-f9320a58f489f5b7.js → messaging-ad6ad3e5bd72765d.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/configure/{storage-d0cfa8aeddd43a40.js → storage-6032d82f0fc2893d.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/{configure-72ca94ec5ed85733.js → configure-d83e5bd52a638234.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/{privacy-requests-5a5edc8a4aa7c30a.js → privacy-requests-baf31c3e4b081046.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/properties/{[id]-5ec775c4904fdbfe.js → [id]-e784c05d056b2371.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/properties/{add-property-a6812c0916f2949e.js → add-property-0a7a2db148a7561a.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/settings/about/{alpha-3e72e9f91991c119.js → alpha-a82f3df840d5c1b5.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{about-6aab092f4871cecb.js → about-d06fb16487705b9d.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{consent-be47008304106395.js → consent-93a978443bf299db.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{custom-fields-ae1b57589da7b175.js → custom-fields-9ecb803099082bf4.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{domain-records-23a6d7a921150188.js → domain-records-16fdd91a81074dd1.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{domains-2a9e8859ab4d9de6.js → domains-4cdd6001e7cb9aee.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{email-templates-4f9f0fdf9925ae90.js → email-templates-1914de830ce5cfc4.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{locations-46f7af35cee4a8bb.js → locations-2e635dcd11b78224.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{organization-a596a96cb8d0aa8e.js → organization-f547f1f33c12faf3.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{regulations-6ed5fc2410e00857.js → regulations-7c02e469d8c5bd74.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/systems/configure/[id]/{test-datasets-86811e3cda277e77.js → test-datasets-20b1193ed76c56b0.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/systems/configure/{[id]-5a43f108d8047d5b.js → [id]-6e15332935f6b538.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/{systems-045a841e22e85ea8.js → systems-fbc8761ef4d55516.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/{taxonomy-1b3f2d4bcb0e164d.js → taxonomy-4d7827fc9c46b6b8.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/{user-management-2cab41659f1ee7da.js → user-management-9cec020f89544426.js} +0 -0
fides/api/oauth/utils.py
CHANGED
|
@@ -4,16 +4,16 @@ import json
|
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
from functools import update_wrapper
|
|
6
6
|
from types import FunctionType
|
|
7
|
-
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
7
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, cast
|
|
8
8
|
|
|
9
|
-
from fastapi import Depends, HTTPException, Security
|
|
9
|
+
from fastapi import Depends, HTTPException, Request, Security
|
|
10
10
|
from fastapi.security import SecurityScopes
|
|
11
11
|
from jose import exceptions, jwe
|
|
12
12
|
from jose.constants import ALGORITHMS
|
|
13
13
|
from loguru import logger
|
|
14
14
|
from pydantic import ValidationError
|
|
15
15
|
from sqlalchemy.orm import Session
|
|
16
|
-
from starlette.status import HTTP_404_NOT_FOUND
|
|
16
|
+
from starlette.status import HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND
|
|
17
17
|
|
|
18
18
|
from fides.api.api.deps import get_db
|
|
19
19
|
from fides.api.common_exceptions import AuthenticationError, AuthorizationError
|
|
@@ -30,7 +30,7 @@ from fides.api.models.fides_user_permissions import FidesUserPermissions
|
|
|
30
30
|
from fides.api.models.policy import PolicyPreWebhook
|
|
31
31
|
from fides.api.models.pre_approval_webhook import PreApprovalWebhook
|
|
32
32
|
from fides.api.models.privacy_request import RequestTask
|
|
33
|
-
from fides.api.oauth.roles import get_scopes_from_roles
|
|
33
|
+
from fides.api.oauth.roles import ROLES_TO_SCOPES_MAPPING, get_scopes_from_roles
|
|
34
34
|
from fides.api.request_context import set_user_id
|
|
35
35
|
from fides.api.schemas.external_https import (
|
|
36
36
|
DownloadTokenJWE,
|
|
@@ -80,6 +80,28 @@ def is_callback_token_expired(issued_at: Optional[datetime]) -> bool:
|
|
|
80
80
|
).total_seconds() / 60.0 > CONFIG.execution.privacy_request_delay_timeout
|
|
81
81
|
|
|
82
82
|
|
|
83
|
+
def is_token_invalidated(issued_at: datetime, client: ClientDetail) -> bool:
|
|
84
|
+
"""
|
|
85
|
+
Return True if the token should be considered invalid due to security events
|
|
86
|
+
(e.g., user password reset) that occurred after the token was issued.
|
|
87
|
+
|
|
88
|
+
Any errors accessing related objects are logged and treated as non-invalidating.
|
|
89
|
+
"""
|
|
90
|
+
try:
|
|
91
|
+
if (
|
|
92
|
+
client.user is not None
|
|
93
|
+
and client.user.password_reset_at is not None
|
|
94
|
+
and issued_at < client.user.password_reset_at
|
|
95
|
+
):
|
|
96
|
+
return True
|
|
97
|
+
return False
|
|
98
|
+
except Exception as exc:
|
|
99
|
+
logger.exception(
|
|
100
|
+
"Unable to evaluate password reset timestamp for client user: {}", exc
|
|
101
|
+
)
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
|
|
83
105
|
def _get_webhook_jwe_or_error(
|
|
84
106
|
security_scopes: SecurityScopes, authorization: str = Security(oauth2_scheme)
|
|
85
107
|
) -> WebhookJWE:
|
|
@@ -225,7 +247,7 @@ async def get_current_user(
|
|
|
225
247
|
created_at=datetime.utcnow(),
|
|
226
248
|
)
|
|
227
249
|
|
|
228
|
-
return client.user
|
|
250
|
+
return cast(FidesUser, client.user)
|
|
229
251
|
|
|
230
252
|
|
|
231
253
|
def verify_callback_oauth_policy_pre_webhook(
|
|
@@ -370,8 +392,10 @@ def extract_token_and_load_client(
|
|
|
370
392
|
logger.debug("Auth token expired.")
|
|
371
393
|
raise AuthorizationError(detail="Not Authorized for this action")
|
|
372
394
|
|
|
395
|
+
issued_at_dt = datetime.fromisoformat(issued_at)
|
|
396
|
+
|
|
373
397
|
if is_token_expired(
|
|
374
|
-
|
|
398
|
+
issued_at_dt,
|
|
375
399
|
token_duration_override or CONFIG.security.oauth_access_token_expire_minutes,
|
|
376
400
|
):
|
|
377
401
|
raise AuthorizationError(detail="Not Authorized for this action")
|
|
@@ -394,6 +418,12 @@ def extract_token_and_load_client(
|
|
|
394
418
|
logger.debug("Auth token belongs to an invalid client_id.")
|
|
395
419
|
raise AuthorizationError(detail="Not Authorized for this action")
|
|
396
420
|
|
|
421
|
+
# Invalidate tokens issued prior to the user's most recent password reset.
|
|
422
|
+
# This ensures any existing sessions are expired immediately after a password change.
|
|
423
|
+
if is_token_invalidated(issued_at_dt, client):
|
|
424
|
+
logger.debug("Auth token issued before latest password reset.")
|
|
425
|
+
raise AuthorizationError(detail="Not Authorized for this action")
|
|
426
|
+
|
|
397
427
|
# Populate request-scoped context with the authenticated user identifier.
|
|
398
428
|
# Prefer the linked user_id; fall back to the client id when this is the
|
|
399
429
|
# special root client (which has no associated FidesUser row).
|
|
@@ -476,6 +506,87 @@ def has_scope_subset(user_scopes: List[str], endpoint_scopes: SecurityScopes) ->
|
|
|
476
506
|
return set(endpoint_scopes.scopes).issubset(user_scopes)
|
|
477
507
|
|
|
478
508
|
|
|
509
|
+
def get_client_effective_scopes(client: "ClientDetail") -> List[str]:
|
|
510
|
+
"""
|
|
511
|
+
Get all scopes available to a client, including both direct scopes and role-derived scopes.
|
|
512
|
+
|
|
513
|
+
Args:
|
|
514
|
+
client: The ClientDetail instance
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
List of scope strings that the client has access to
|
|
518
|
+
"""
|
|
519
|
+
effective_scopes = set()
|
|
520
|
+
|
|
521
|
+
# Add direct scopes
|
|
522
|
+
if client.scopes:
|
|
523
|
+
effective_scopes.update(client.scopes)
|
|
524
|
+
|
|
525
|
+
# Add role-derived scopes
|
|
526
|
+
if client.roles:
|
|
527
|
+
for role in client.roles:
|
|
528
|
+
role_scopes = ROLES_TO_SCOPES_MAPPING.get(role, [])
|
|
529
|
+
effective_scopes.update(role_scopes)
|
|
530
|
+
|
|
531
|
+
# Add user permission scopes if client is associated with a user
|
|
532
|
+
# Note: client.user is available via SQLAlchemy backref from FidesUser.client relationship
|
|
533
|
+
user = getattr(client, "user", None) # Use getattr to avoid mypy attr-defined error
|
|
534
|
+
if user and hasattr(user, "permissions") and user.permissions:
|
|
535
|
+
effective_scopes.update(user.permissions.total_scopes)
|
|
536
|
+
|
|
537
|
+
return sorted(list(effective_scopes))
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def verify_client_can_assign_scopes(
|
|
541
|
+
request: "Request",
|
|
542
|
+
requesting_client: "ClientDetail",
|
|
543
|
+
scopes: List[str],
|
|
544
|
+
db: "Session",
|
|
545
|
+
) -> None:
|
|
546
|
+
"""
|
|
547
|
+
Verify that a requesting client has permission to assign the given scopes.
|
|
548
|
+
|
|
549
|
+
Raises HTTPException if the client lacks permission.
|
|
550
|
+
Root client is exempt from this check.
|
|
551
|
+
|
|
552
|
+
Args:
|
|
553
|
+
request: FastAPI request object containing Authorization header
|
|
554
|
+
requesting_client: The client making the request
|
|
555
|
+
scopes: List of scopes to be assigned
|
|
556
|
+
db: Database session
|
|
557
|
+
|
|
558
|
+
Raises:
|
|
559
|
+
HTTPException: If the client lacks permission to assign the scopes
|
|
560
|
+
"""
|
|
561
|
+
# Root client can assign any scope
|
|
562
|
+
if requesting_client.id == CONFIG.security.oauth_root_client_id:
|
|
563
|
+
return
|
|
564
|
+
|
|
565
|
+
# Get the actual token scopes (not the client's database scopes)
|
|
566
|
+
authorization = request.headers.get("Authorization", "").replace("Bearer ", "")
|
|
567
|
+
token_data, _ = extract_token_and_load_client(authorization, db)
|
|
568
|
+
|
|
569
|
+
# Get token's effective scopes
|
|
570
|
+
token_scopes = token_data.get("scopes", [])
|
|
571
|
+
|
|
572
|
+
# Check if user has the scopes via roles as well
|
|
573
|
+
has_scope_via_role = has_permissions(
|
|
574
|
+
token_data=token_data,
|
|
575
|
+
client=requesting_client,
|
|
576
|
+
endpoint_scopes=SecurityScopes(scopes),
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
# If they don't have all scopes via direct assignment or roles, check individual scopes
|
|
580
|
+
if not has_scope_via_role:
|
|
581
|
+
unauthorized_scopes = set(scopes) - set(token_scopes)
|
|
582
|
+
|
|
583
|
+
if unauthorized_scopes:
|
|
584
|
+
raise HTTPException(
|
|
585
|
+
status_code=HTTP_403_FORBIDDEN,
|
|
586
|
+
detail=f"Cannot assign scopes that you do not have. Missing scopes: {sorted(unauthorized_scopes)}",
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
|
|
479
590
|
def create_temporary_user_for_login_flow(config: FidesConfig) -> FidesUser:
|
|
480
591
|
"""
|
|
481
592
|
Create a temporary FidesUser in-memory with an attached in-memory ClientDetail
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, Field, field_validator
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PrivacyRequestRedactionPatternsRequest(BaseModel):
|
|
8
|
+
"""Request schema for updating privacy request redaction patterns."""
|
|
9
|
+
|
|
10
|
+
patterns: List[str] = Field(
|
|
11
|
+
description="List of regex patterns used to redact dataset, collection, and field names in privacy request package reports",
|
|
12
|
+
max_length=100, # Limit number of patterns
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
@field_validator("patterns")
|
|
16
|
+
@classmethod
|
|
17
|
+
def validate_patterns(cls, patterns: List[str]) -> List[str]:
|
|
18
|
+
"""Validate regex patterns with ReDoS protection via Pydantic's default rust-regex engine."""
|
|
19
|
+
if not patterns:
|
|
20
|
+
return patterns
|
|
21
|
+
|
|
22
|
+
validated_patterns = []
|
|
23
|
+
for i, pattern in enumerate(patterns):
|
|
24
|
+
if not isinstance(pattern, str):
|
|
25
|
+
raise ValueError(f"Pattern at index {i} must be a string")
|
|
26
|
+
|
|
27
|
+
pattern = pattern.strip()
|
|
28
|
+
if not pattern:
|
|
29
|
+
raise ValueError(f"Pattern at index {i} cannot be empty")
|
|
30
|
+
|
|
31
|
+
# Reasonable length limit for regex patterns
|
|
32
|
+
if len(pattern) > 500:
|
|
33
|
+
raise ValueError(
|
|
34
|
+
f"Pattern at index {i} is too long (max 500 characters)"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Pydantic's rust-regex engine provides ReDoS protection
|
|
38
|
+
try:
|
|
39
|
+
re.compile(pattern, re.IGNORECASE)
|
|
40
|
+
except re.error as e:
|
|
41
|
+
raise ValueError(f"Invalid regex pattern at index {i}: {e}")
|
|
42
|
+
|
|
43
|
+
validated_patterns.append(pattern)
|
|
44
|
+
|
|
45
|
+
return validated_patterns
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class PrivacyRequestRedactionPatternsResponse(BaseModel):
|
|
49
|
+
"""Response schema for privacy request redaction patterns."""
|
|
50
|
+
|
|
51
|
+
patterns: List[str] = Field(
|
|
52
|
+
description="List of regex patterns used to redact dataset, collection, and field names in privacy request package reports"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
model_config = {"from_attributes": True}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Any, Dict, List, Literal, Optional, Set, Tuple
|
|
3
|
+
|
|
4
|
+
from loguru import logger
|
|
5
|
+
from sqlalchemy.orm import Session
|
|
6
|
+
|
|
7
|
+
from fides.api.models.privacy_request_redaction_pattern import (
|
|
8
|
+
PrivacyRequestRedactionPattern,
|
|
9
|
+
)
|
|
10
|
+
from fides.api.service.privacy_request.dsr_package.utils import (
|
|
11
|
+
get_redaction_entities_map_db,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DSRDataPreprocessor:
|
|
16
|
+
"""
|
|
17
|
+
Processes DSR data to apply name redaction before report generation.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, db: Session):
|
|
21
|
+
self.db = db
|
|
22
|
+
self.redaction_patterns: List[str] = (
|
|
23
|
+
PrivacyRequestRedactionPattern.get_patterns(db) or []
|
|
24
|
+
)
|
|
25
|
+
self.entities_to_redact: Set[str] = get_redaction_entities_map_db(db)
|
|
26
|
+
|
|
27
|
+
def process_dsr_data(self, dsr_data: dict[str, Any]) -> dict[str, Any]:
|
|
28
|
+
"""Process the DSR data to apply all redaction upfront."""
|
|
29
|
+
if not self.redaction_patterns and not self.entities_to_redact:
|
|
30
|
+
return dsr_data
|
|
31
|
+
|
|
32
|
+
# First pass: collect and map dataset names
|
|
33
|
+
dataset_mapping = self._create_dataset_mapping(dsr_data)
|
|
34
|
+
|
|
35
|
+
# Second pass: process data with redaction
|
|
36
|
+
processed_data = {}
|
|
37
|
+
collection_indices: Dict[str, Dict[str, int]] = (
|
|
38
|
+
{}
|
|
39
|
+
) # Track collection indices within each dataset
|
|
40
|
+
|
|
41
|
+
for key, rows in dsr_data.items():
|
|
42
|
+
# The "attachment" key is used to pass in the privacy request's attachments into `dsr_data`.
|
|
43
|
+
# We don't need to redact these.
|
|
44
|
+
if key == "attachments":
|
|
45
|
+
processed_data[key] = rows
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
dataset_name, collection_name = self._parse_key(key, rows)
|
|
49
|
+
|
|
50
|
+
# Get redacted dataset name
|
|
51
|
+
redacted_dataset = dataset_mapping.get(dataset_name, dataset_name)
|
|
52
|
+
|
|
53
|
+
# Get redacted collection name (index per dataset)
|
|
54
|
+
if dataset_name not in collection_indices:
|
|
55
|
+
collection_indices[dataset_name] = {}
|
|
56
|
+
|
|
57
|
+
if collection_name not in collection_indices[dataset_name]:
|
|
58
|
+
collection_indices[dataset_name][collection_name] = (
|
|
59
|
+
len(collection_indices[dataset_name]) + 1
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
collection_index = collection_indices[dataset_name][collection_name]
|
|
63
|
+
redacted_collection = self._redact_name(
|
|
64
|
+
"collection", collection_name, collection_index, dataset_name
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Process rows
|
|
68
|
+
new_key = f"{redacted_dataset}:{redacted_collection}"
|
|
69
|
+
processed_data[new_key] = [
|
|
70
|
+
self._process_row(row, dataset_name, collection_name) for row in rows
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
return processed_data
|
|
74
|
+
|
|
75
|
+
def _create_dataset_mapping(self, dsr_data: dict[str, Any]) -> Dict[str, str]:
|
|
76
|
+
"""Create dataset name mapping with ordered numbering for redacted datasets."""
|
|
77
|
+
|
|
78
|
+
# Extract unique dataset names in order of appearance
|
|
79
|
+
dataset_names = []
|
|
80
|
+
for key, rows in dsr_data.items():
|
|
81
|
+
if key not in ["attachments", "dataset"]:
|
|
82
|
+
dataset_name, _ = self._parse_key(key, rows)
|
|
83
|
+
dataset_names.append(dataset_name)
|
|
84
|
+
|
|
85
|
+
unique_datasets = list(dict.fromkeys(dataset_names))
|
|
86
|
+
|
|
87
|
+
# Create mapping using position-based numbering for redacted datasets
|
|
88
|
+
mapping = {}
|
|
89
|
+
|
|
90
|
+
# Handle regular datasets
|
|
91
|
+
for index, name in enumerate(unique_datasets, 1):
|
|
92
|
+
hierarchical_key = self._build_hierarchical_key("dataset", name)
|
|
93
|
+
if self._should_redact(name, hierarchical_key):
|
|
94
|
+
mapping[name] = f"dataset_{index}"
|
|
95
|
+
else:
|
|
96
|
+
mapping[name] = name # Keep original name
|
|
97
|
+
|
|
98
|
+
# Special "dataset" and "attachment" cases comes last
|
|
99
|
+
# These keys are reserved for additional data and do not need to be redacted
|
|
100
|
+
if "dataset" in dsr_data.keys():
|
|
101
|
+
mapping["dataset"] = "dataset"
|
|
102
|
+
|
|
103
|
+
if "attachments" in dsr_data.keys():
|
|
104
|
+
mapping["attachments"] = "attachments"
|
|
105
|
+
|
|
106
|
+
return mapping
|
|
107
|
+
|
|
108
|
+
def _parse_key(self, key: str, rows: List[dict]) -> Tuple[str, str]:
|
|
109
|
+
"""Parse a key into dataset and collection names."""
|
|
110
|
+
if ":" in key:
|
|
111
|
+
parts = key.split(":", 1)
|
|
112
|
+
return parts[0], parts[1]
|
|
113
|
+
|
|
114
|
+
# Fallback logic
|
|
115
|
+
for row in rows:
|
|
116
|
+
if "system_name" in row:
|
|
117
|
+
return row["system_name"], key
|
|
118
|
+
return "manual", key
|
|
119
|
+
|
|
120
|
+
def _should_redact(self, name: str, hierarchical_key: str) -> bool:
|
|
121
|
+
"""Check if a name should be redacted."""
|
|
122
|
+
if hierarchical_key in self.entities_to_redact:
|
|
123
|
+
return True
|
|
124
|
+
|
|
125
|
+
for pattern in self.redaction_patterns:
|
|
126
|
+
try:
|
|
127
|
+
if re.search(pattern, name, re.IGNORECASE):
|
|
128
|
+
return True
|
|
129
|
+
except re.error:
|
|
130
|
+
logger.warning(f"Invalid regex pattern: {pattern}")
|
|
131
|
+
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
def _redact_name(
|
|
135
|
+
self,
|
|
136
|
+
name_type: Literal["dataset", "collection", "field"],
|
|
137
|
+
name: str,
|
|
138
|
+
index: int,
|
|
139
|
+
dataset_name: Optional[str] = None,
|
|
140
|
+
collection_name: Optional[str] = None,
|
|
141
|
+
) -> str:
|
|
142
|
+
"""Apply redaction to a name based on patterns and configurations."""
|
|
143
|
+
hierarchical_key = self._build_hierarchical_key(
|
|
144
|
+
name_type, name, dataset_name, collection_name
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if self._should_redact(name, hierarchical_key):
|
|
148
|
+
return f"{name_type}_{index}"
|
|
149
|
+
|
|
150
|
+
return name
|
|
151
|
+
|
|
152
|
+
def _build_hierarchical_key(
|
|
153
|
+
self,
|
|
154
|
+
name_type: Literal["dataset", "collection", "field"],
|
|
155
|
+
name: str,
|
|
156
|
+
dataset_name: Optional[str] = None,
|
|
157
|
+
collection_name: Optional[str] = None,
|
|
158
|
+
) -> str:
|
|
159
|
+
"""Build hierarchical key for entity lookup."""
|
|
160
|
+
if name_type == "dataset":
|
|
161
|
+
return name
|
|
162
|
+
if name_type == "collection" and dataset_name:
|
|
163
|
+
return f"{dataset_name}.{name}"
|
|
164
|
+
if name_type == "field" and dataset_name and collection_name:
|
|
165
|
+
return f"{dataset_name}.{collection_name}.{name}"
|
|
166
|
+
return name
|
|
167
|
+
|
|
168
|
+
def _process_row(
|
|
169
|
+
self, row: dict[str, Any], dataset_name: str, collection_name: str
|
|
170
|
+
) -> dict[str, Any]:
|
|
171
|
+
"""Process a single row with field redaction."""
|
|
172
|
+
processed = {}
|
|
173
|
+
|
|
174
|
+
# Use enumerate for positional indexing (matches original)
|
|
175
|
+
for field_index, (field_name, value) in enumerate(row.items(), start=1):
|
|
176
|
+
redacted_field = self._redact_name(
|
|
177
|
+
"field", field_name, field_index, dataset_name, collection_name
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Process nested values
|
|
181
|
+
base_path = f"{dataset_name}.{collection_name}.{field_name}"
|
|
182
|
+
processed[redacted_field] = self._process_nested_value(value, base_path)
|
|
183
|
+
|
|
184
|
+
return processed
|
|
185
|
+
|
|
186
|
+
def _process_nested_value(
|
|
187
|
+
self,
|
|
188
|
+
value: Any,
|
|
189
|
+
current_path: str,
|
|
190
|
+
field_index_counter: Optional[Dict[str, Dict[str, int]]] = None,
|
|
191
|
+
) -> Any:
|
|
192
|
+
"""Recursively process nested values matching original logic."""
|
|
193
|
+
if field_index_counter is None:
|
|
194
|
+
field_index_counter = {}
|
|
195
|
+
|
|
196
|
+
if isinstance(value, dict):
|
|
197
|
+
processed = {}
|
|
198
|
+
level_key = f"{current_path}_fields"
|
|
199
|
+
|
|
200
|
+
if level_key not in field_index_counter:
|
|
201
|
+
field_index_counter[level_key] = {}
|
|
202
|
+
|
|
203
|
+
for field_name, field_value in value.items():
|
|
204
|
+
full_path = f"{current_path}.{field_name}"
|
|
205
|
+
|
|
206
|
+
# Get or create index for this field at this level
|
|
207
|
+
if field_name not in field_index_counter[level_key]:
|
|
208
|
+
field_index_counter[level_key][field_name] = (
|
|
209
|
+
len(field_index_counter[level_key]) + 1
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
if self._should_redact(field_name, full_path):
|
|
213
|
+
redacted_name = (
|
|
214
|
+
f"field_{field_index_counter[level_key][field_name]}"
|
|
215
|
+
)
|
|
216
|
+
else:
|
|
217
|
+
redacted_name = field_name
|
|
218
|
+
|
|
219
|
+
processed[redacted_name] = self._process_nested_value(
|
|
220
|
+
field_value, full_path, field_index_counter
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
return processed
|
|
224
|
+
|
|
225
|
+
if isinstance(value, list):
|
|
226
|
+
return [
|
|
227
|
+
self._process_nested_value(item, current_path, field_index_counter)
|
|
228
|
+
for item in value
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
return value
|
|
@@ -5,13 +5,18 @@ import time as time_module
|
|
|
5
5
|
import zipfile
|
|
6
6
|
from io import BytesIO
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import
|
|
8
|
+
from typing import Any, Optional
|
|
9
9
|
|
|
10
10
|
import jinja2
|
|
11
11
|
from jinja2 import Environment, FileSystemLoader
|
|
12
12
|
from loguru import logger
|
|
13
|
+
from sqlalchemy.orm import object_session
|
|
13
14
|
|
|
14
|
-
from fides.api.
|
|
15
|
+
from fides.api.models.privacy_request import PrivacyRequest
|
|
16
|
+
from fides.api.service.privacy_request.dsr_package.dsr_data_preprocessor import (
|
|
17
|
+
DSRDataPreprocessor,
|
|
18
|
+
)
|
|
19
|
+
from fides.api.service.privacy_request.dsr_package.utils import map_privacy_request
|
|
15
20
|
from fides.api.service.storage.util import (
|
|
16
21
|
_get_datasets_from_dsr_data,
|
|
17
22
|
create_attachment_info_dict,
|
|
@@ -31,12 +36,9 @@ TEXT_COLOR = "#4A5568"
|
|
|
31
36
|
HEADER_COLOR = "#FAFAFA"
|
|
32
37
|
BORDER_COLOR = "#E2E8F0"
|
|
33
38
|
|
|
34
|
-
if TYPE_CHECKING:
|
|
35
|
-
from fides.api.models.privacy_request import PrivacyRequest # pragma: no cover
|
|
36
|
-
|
|
37
39
|
|
|
38
40
|
# pylint: disable=too-many-instance-attributes
|
|
39
|
-
class
|
|
41
|
+
class DSRReportBuilder:
|
|
40
42
|
"""
|
|
41
43
|
Manages populating HTML templates from the given data and adding the generated
|
|
42
44
|
pages to a zip file in a way that the pages can be navigated between.
|
|
@@ -48,7 +50,7 @@ class DsrReportBuilder:
|
|
|
48
50
|
- data/dataset_name/collection_name/item_index.html: the detail page for the item
|
|
49
51
|
- attachments/index.html: the index page for the attachments
|
|
50
52
|
|
|
51
|
-
|
|
53
|
+
Args:
|
|
52
54
|
privacy_request: the privacy request object
|
|
53
55
|
dsr_data: the DSR data
|
|
54
56
|
"""
|
|
@@ -86,9 +88,18 @@ class DsrReportBuilder:
|
|
|
86
88
|
}
|
|
87
89
|
self.main_links: dict[str, Any] = {} # used to track the generated pages
|
|
88
90
|
|
|
91
|
+
# Process the DSR data for redaction
|
|
92
|
+
db = object_session(privacy_request)
|
|
93
|
+
if db is not None:
|
|
94
|
+
processor = DSRDataPreprocessor(db)
|
|
95
|
+
processed_dsr_data = processor.process_dsr_data(dsr_data)
|
|
96
|
+
else:
|
|
97
|
+
# Fallback if no database session available
|
|
98
|
+
processed_dsr_data = dsr_data
|
|
99
|
+
|
|
89
100
|
# report data to populate the templates
|
|
90
|
-
self.request_data =
|
|
91
|
-
self.dsr_data =
|
|
101
|
+
self.request_data = map_privacy_request(privacy_request)
|
|
102
|
+
self.dsr_data = processed_dsr_data
|
|
92
103
|
self.enable_streaming = enable_streaming
|
|
93
104
|
|
|
94
105
|
# Track used filenames per dataset to prevent conflicts within the same dataset
|
|
@@ -150,10 +161,6 @@ class DsrReportBuilder:
|
|
|
150
161
|
"""
|
|
151
162
|
Generates a page for each collection in the dataset and an index page for the dataset.
|
|
152
163
|
Tracks the generated links to build a root level index after each collection has been processed.
|
|
153
|
-
|
|
154
|
-
Args:
|
|
155
|
-
dataset_name: the name of the dataset to add
|
|
156
|
-
collections: the collections to add to the dataset
|
|
157
164
|
"""
|
|
158
165
|
# track links to collection indexes
|
|
159
166
|
collection_links = {}
|
|
@@ -325,19 +332,17 @@ class DsrReportBuilder:
|
|
|
325
332
|
return corrected_attachment_info
|
|
326
333
|
|
|
327
334
|
def _add_collection(
|
|
328
|
-
self,
|
|
335
|
+
self,
|
|
336
|
+
rows: list[dict[str, Any]],
|
|
337
|
+
dataset_name: str,
|
|
338
|
+
collection_name: str,
|
|
329
339
|
) -> None:
|
|
330
340
|
"""
|
|
331
341
|
Adds a collection to the zip file.
|
|
332
|
-
|
|
333
|
-
Args:
|
|
334
|
-
rows: the rows to add to the collection
|
|
335
|
-
dataset_name: the name of the dataset to add the collection to
|
|
336
|
-
collection_name: the name of the collection to add
|
|
337
342
|
"""
|
|
338
343
|
items_content = []
|
|
339
344
|
|
|
340
|
-
for
|
|
345
|
+
for item_index, collection_item in enumerate(rows, 1):
|
|
341
346
|
# Create a deep copy of the item data to avoid modifying the original DSR data
|
|
342
347
|
# This ensures the comprehensive attachments index can access unmodified attachments
|
|
343
348
|
item_data = copy.deepcopy(collection_item)
|
|
@@ -377,11 +382,13 @@ class DsrReportBuilder:
|
|
|
377
382
|
# Replace the field value with processed attachment links
|
|
378
383
|
item_data[field_name] = attachment_links
|
|
379
384
|
|
|
380
|
-
# Add item content to the list
|
|
385
|
+
# Add item content to the list with item heading
|
|
386
|
+
# Field names are already redacted in the processed data
|
|
387
|
+
item_heading = f"{collection_name} (item #{item_index})"
|
|
381
388
|
items_content.append(
|
|
382
389
|
{
|
|
383
|
-
"index":
|
|
384
|
-
"heading":
|
|
390
|
+
"index": item_index,
|
|
391
|
+
"heading": item_heading,
|
|
385
392
|
"data": item_data,
|
|
386
393
|
}
|
|
387
394
|
)
|
|
@@ -518,6 +525,7 @@ class DsrReportBuilder:
|
|
|
518
525
|
# Add Additional Data if it exists
|
|
519
526
|
if "dataset" in datasets:
|
|
520
527
|
self._add_dataset("dataset", datasets["dataset"])
|
|
528
|
+
# Use a more friendly name for the link but keep the dataset name for the path
|
|
521
529
|
self.main_links["Additional Data"] = "data/dataset/index.html"
|
|
522
530
|
|
|
523
531
|
# Add comprehensive attachments index that includes ALL attachments
|
|
@@ -569,27 +577,3 @@ class DsrReportBuilder:
|
|
|
569
577
|
"DSR report generation complete."
|
|
570
578
|
)
|
|
571
579
|
return self.baos
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
def _map_privacy_request(privacy_request: "PrivacyRequest") -> dict[str, Any]:
|
|
575
|
-
"""Creates a map with a subset of values from the privacy request"""
|
|
576
|
-
request_data: dict[str, Any] = {}
|
|
577
|
-
request_data["id"] = privacy_request.id
|
|
578
|
-
|
|
579
|
-
action_type: Optional[ActionType] = privacy_request.policy.get_action_type()
|
|
580
|
-
if action_type:
|
|
581
|
-
request_data["type"] = action_type.value
|
|
582
|
-
|
|
583
|
-
request_data["identity"] = {
|
|
584
|
-
key: value
|
|
585
|
-
for key, value in privacy_request.get_persisted_identity()
|
|
586
|
-
.labeled_dict(include_default_labels=True)
|
|
587
|
-
.items()
|
|
588
|
-
if value["value"] is not None
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
if privacy_request.requested_at:
|
|
592
|
-
request_data["requested_at"] = privacy_request.requested_at.strftime(
|
|
593
|
-
"%m/%d/%Y %H:%M %Z"
|
|
594
|
-
)
|
|
595
|
-
return request_data
|