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
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
from typing import Any, List, Optional
|
|
2
|
+
|
|
3
|
+
from fideslang.models import Dataset, DatasetField
|
|
4
|
+
from loguru import logger
|
|
5
|
+
from sqlalchemy import text
|
|
6
|
+
from sqlalchemy.orm import Session
|
|
7
|
+
|
|
8
|
+
from fides.api.models.datasetconfig import DatasetConfig
|
|
9
|
+
from fides.api.models.privacy_request.privacy_request import PrivacyRequest
|
|
10
|
+
from fides.api.schemas.policy import ActionType
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# TODO: keeping this for a bit to help with development and testing
|
|
14
|
+
def get_redaction_entities_map(db: Session) -> set[str]:
|
|
15
|
+
"""
|
|
16
|
+
Create a set of hierarchical entity keys that should be redacted based on fides_meta.redact: name.
|
|
17
|
+
|
|
18
|
+
This utility function reads all enabled dataset configurations from the database
|
|
19
|
+
and builds a set of hierarchical entity keys (dataset_name, dataset_name.collection_name,
|
|
20
|
+
dataset_name.collection_name.field_name) that have fides_meta.redact set to "name".
|
|
21
|
+
|
|
22
|
+
Supports deeply nested field structures with unlimited nesting depth.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
db: Database session
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Set of hierarchical entity keys that should be redacted
|
|
29
|
+
"""
|
|
30
|
+
redaction_entities = set()
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
dataset_configs = DatasetConfig.all(db=db)
|
|
34
|
+
|
|
35
|
+
for dataset_config in dataset_configs:
|
|
36
|
+
ctl_dataset = dataset_config.ctl_dataset
|
|
37
|
+
if not ctl_dataset:
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
dataset = Dataset.model_validate(dataset_config.ctl_dataset)
|
|
41
|
+
# Intentionally using the fides_key instead of name since it's always provided
|
|
42
|
+
dataset_name = dataset.fides_key
|
|
43
|
+
|
|
44
|
+
# Check dataset level
|
|
45
|
+
if dataset.fides_meta and dataset.fides_meta.redact == "name":
|
|
46
|
+
redaction_entities.add(dataset_name)
|
|
47
|
+
|
|
48
|
+
# Check collection level
|
|
49
|
+
for collection_dict in dataset.collections:
|
|
50
|
+
# Collections are stored as dictionaries in the database
|
|
51
|
+
collection_name = collection_dict.name
|
|
52
|
+
if not collection_name:
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
collection_path = f"{dataset_name}.{collection_name}"
|
|
56
|
+
collection_fides_meta = collection_dict.fides_meta
|
|
57
|
+
|
|
58
|
+
if collection_fides_meta and collection_fides_meta.redact == "name":
|
|
59
|
+
redaction_entities.add(collection_path)
|
|
60
|
+
|
|
61
|
+
# Check field level (with recursive nested field support)
|
|
62
|
+
_traverse_fields_for_redaction(
|
|
63
|
+
collection_dict.fields, collection_path, redaction_entities
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
except Exception as exc:
|
|
67
|
+
# Log error but don't fail, just return empty set
|
|
68
|
+
logger.warning(f"Error extracting redaction configurations: {exc}")
|
|
69
|
+
|
|
70
|
+
return redaction_entities
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_redaction_entities_map_db(db: Session) -> set[str]:
|
|
74
|
+
"""
|
|
75
|
+
Create a set of hierarchical entity keys that should be redacted based on fides_meta.redact: name.
|
|
76
|
+
|
|
77
|
+
This function uses a hybrid approach:
|
|
78
|
+
1. First identifies datasets that contain ANY redaction metadata at any level
|
|
79
|
+
2. Then processes only those datasets with redaction metadata
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
db: Database session
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Set of hierarchical entity keys that should be redacted
|
|
87
|
+
"""
|
|
88
|
+
redaction_entities: set[str] = set()
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
# Step 1: Pre-filter to find datasets with ANY redaction metadata
|
|
92
|
+
# Simple existence check - no paths needed, just check if redaction exists anywhere
|
|
93
|
+
pre_filter_query = """
|
|
94
|
+
SELECT DISTINCT dc.ctl_dataset_id
|
|
95
|
+
FROM datasetconfig dc
|
|
96
|
+
JOIN ctl_datasets ds ON dc.ctl_dataset_id = ds.id
|
|
97
|
+
WHERE
|
|
98
|
+
-- Dataset-level redaction
|
|
99
|
+
ds.fides_meta->>'redact' = 'name'
|
|
100
|
+
OR
|
|
101
|
+
-- Collection-level redaction
|
|
102
|
+
EXISTS (
|
|
103
|
+
SELECT 1 FROM jsonb_array_elements(ds.collections::jsonb) AS collection
|
|
104
|
+
WHERE collection->'fides_meta'->>'redact' = 'name'
|
|
105
|
+
LIMIT 1
|
|
106
|
+
)
|
|
107
|
+
OR
|
|
108
|
+
-- Field-level redaction using jsonb_path_query
|
|
109
|
+
EXISTS (
|
|
110
|
+
SELECT 1
|
|
111
|
+
FROM jsonb_path_query(ds.collections::jsonb, '$.**.fides_meta') AS fides_meta
|
|
112
|
+
WHERE fides_meta->>'redact' = 'name'
|
|
113
|
+
LIMIT 1
|
|
114
|
+
)
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
candidate_datasets = db.execute(pre_filter_query).fetchall()
|
|
118
|
+
|
|
119
|
+
if not candidate_datasets:
|
|
120
|
+
logger.debug("No datasets found with redaction metadata")
|
|
121
|
+
return redaction_entities
|
|
122
|
+
|
|
123
|
+
logger.debug(
|
|
124
|
+
f"Pre-filtered to {len(candidate_datasets)} datasets with redaction metadata"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Step 2: Process only the candidate datasets with targeted queries
|
|
128
|
+
# Convert to a format we can use in SQL ANY clause
|
|
129
|
+
dataset_ids = [row[0] for row in candidate_datasets]
|
|
130
|
+
|
|
131
|
+
# Query for dataset-level redactions (only on candidate datasets)
|
|
132
|
+
dataset_query = text(
|
|
133
|
+
"""
|
|
134
|
+
SELECT ds.fides_key as entity_path
|
|
135
|
+
FROM datasetconfig dc
|
|
136
|
+
JOIN ctl_datasets ds ON dc.ctl_dataset_id = ds.id
|
|
137
|
+
WHERE ds.id = ANY(:dataset_ids)
|
|
138
|
+
AND ds.fides_meta->>'redact' = 'name'
|
|
139
|
+
"""
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
dataset_results = db.execute(
|
|
143
|
+
dataset_query, {"dataset_ids": dataset_ids}
|
|
144
|
+
).fetchall()
|
|
145
|
+
for row in dataset_results:
|
|
146
|
+
redaction_entities.add(row[0])
|
|
147
|
+
|
|
148
|
+
# Query for collection-level redactions (only on candidate datasets)
|
|
149
|
+
collection_query = text(
|
|
150
|
+
"""
|
|
151
|
+
SELECT ds.fides_key || '.' || (collection->>'name') as entity_path
|
|
152
|
+
FROM datasetconfig dc
|
|
153
|
+
JOIN ctl_datasets ds ON dc.ctl_dataset_id = ds.id
|
|
154
|
+
CROSS JOIN LATERAL jsonb_array_elements(ds.collections::jsonb) AS collection
|
|
155
|
+
WHERE ds.id = ANY(:dataset_ids)
|
|
156
|
+
AND collection->'fides_meta'->>'redact' = 'name'
|
|
157
|
+
AND collection->>'name' IS NOT NULL
|
|
158
|
+
"""
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
collection_results = db.execute(
|
|
162
|
+
collection_query, {"dataset_ids": dataset_ids}
|
|
163
|
+
).fetchall()
|
|
164
|
+
for row in collection_results:
|
|
165
|
+
redaction_entities.add(row[0])
|
|
166
|
+
|
|
167
|
+
# Query for field-level redactions (including nested fields)
|
|
168
|
+
# This uses a recursive CTE to handle arbitrary nesting levels
|
|
169
|
+
field_query = text(
|
|
170
|
+
"""
|
|
171
|
+
WITH RECURSIVE field_hierarchy AS (
|
|
172
|
+
-- Base case: top-level fields in collections (only candidate datasets)
|
|
173
|
+
SELECT
|
|
174
|
+
ds.fides_key || '.' ||
|
|
175
|
+
(collection->>'name') || '.' ||
|
|
176
|
+
(field->>'name') as entity_path,
|
|
177
|
+
field->'fields' as nested_fields,
|
|
178
|
+
field->'fides_meta'->>'redact' as redact_value
|
|
179
|
+
FROM datasetconfig dc
|
|
180
|
+
JOIN ctl_datasets ds ON dc.ctl_dataset_id = ds.id
|
|
181
|
+
CROSS JOIN LATERAL jsonb_array_elements(ds.collections::jsonb) AS collection
|
|
182
|
+
CROSS JOIN LATERAL jsonb_array_elements(collection->'fields') AS field
|
|
183
|
+
WHERE ds.id = ANY(:dataset_ids)
|
|
184
|
+
AND collection->>'name' IS NOT NULL
|
|
185
|
+
AND field->>'name' IS NOT NULL
|
|
186
|
+
|
|
187
|
+
UNION ALL
|
|
188
|
+
|
|
189
|
+
-- Recursive case: nested fields
|
|
190
|
+
SELECT
|
|
191
|
+
fh.entity_path || '.' || (nested_field->>'name') as entity_path,
|
|
192
|
+
nested_field->'fields' as nested_fields,
|
|
193
|
+
nested_field->'fides_meta'->>'redact' as redact_value
|
|
194
|
+
FROM field_hierarchy fh
|
|
195
|
+
CROSS JOIN LATERAL jsonb_array_elements(fh.nested_fields) AS nested_field
|
|
196
|
+
WHERE jsonb_typeof(fh.nested_fields) = 'array'
|
|
197
|
+
AND nested_field->>'name' IS NOT NULL
|
|
198
|
+
)
|
|
199
|
+
SELECT DISTINCT entity_path
|
|
200
|
+
FROM field_hierarchy
|
|
201
|
+
WHERE redact_value = 'name'
|
|
202
|
+
"""
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
field_results = db.execute(field_query, {"dataset_ids": dataset_ids}).fetchall()
|
|
206
|
+
for row in field_results:
|
|
207
|
+
redaction_entities.add(row[0])
|
|
208
|
+
|
|
209
|
+
logger.debug(f"Found {len(redaction_entities)} entities requiring redaction")
|
|
210
|
+
|
|
211
|
+
except Exception as exc:
|
|
212
|
+
# Log error but don't fail, just return empty set
|
|
213
|
+
logger.warning(
|
|
214
|
+
f"Error extracting redaction configurations from database: {exc}"
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
return redaction_entities
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def map_privacy_request(privacy_request: PrivacyRequest) -> dict[str, Any]:
|
|
221
|
+
"""Creates a map with a subset of values from the privacy request"""
|
|
222
|
+
request_data: dict[str, Any] = {}
|
|
223
|
+
request_data["id"] = privacy_request.id
|
|
224
|
+
|
|
225
|
+
action_type: Optional[ActionType] = privacy_request.policy.get_action_type()
|
|
226
|
+
if action_type:
|
|
227
|
+
request_data["type"] = action_type.value
|
|
228
|
+
|
|
229
|
+
request_data["identity"] = {
|
|
230
|
+
key: value
|
|
231
|
+
for key, value in privacy_request.get_persisted_identity()
|
|
232
|
+
.labeled_dict(include_default_labels=True)
|
|
233
|
+
.items()
|
|
234
|
+
if value["value"] is not None
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if privacy_request.requested_at:
|
|
238
|
+
request_data["requested_at"] = privacy_request.requested_at.strftime(
|
|
239
|
+
"%m/%d/%Y %H:%M %Z"
|
|
240
|
+
)
|
|
241
|
+
return request_data
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _traverse_fields_for_redaction(
|
|
245
|
+
fields: List[DatasetField], current_path: str, redaction_entities: set[str]
|
|
246
|
+
) -> None:
|
|
247
|
+
"""
|
|
248
|
+
Recursively traverse nested fields to find redaction entities.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
fields: List of field dictionaries to traverse
|
|
252
|
+
current_path: Current hierarchical path (e.g., "dataset.collection")
|
|
253
|
+
redaction_entities: Set to add redacted field paths to
|
|
254
|
+
"""
|
|
255
|
+
for field in fields:
|
|
256
|
+
field_name = field.name
|
|
257
|
+
if not field_name:
|
|
258
|
+
continue
|
|
259
|
+
|
|
260
|
+
field_path = f"{current_path}.{field_name}"
|
|
261
|
+
field_fides_meta = field.fides_meta
|
|
262
|
+
|
|
263
|
+
if field_fides_meta and field_fides_meta.redact == "name":
|
|
264
|
+
redaction_entities.add(field_path)
|
|
265
|
+
|
|
266
|
+
# Recursively check nested fields
|
|
267
|
+
if field.fields:
|
|
268
|
+
_traverse_fields_for_redaction(field.fields, field_path, redaction_entities)
|
|
@@ -17,7 +17,7 @@ from fides.api.common_exceptions import StorageUploadError
|
|
|
17
17
|
from fides.api.models.privacy_request import PrivacyRequest
|
|
18
18
|
from fides.api.schemas.storage.storage import ResponseFormat
|
|
19
19
|
from fides.api.service.privacy_request.dsr_package.dsr_report_builder import (
|
|
20
|
-
|
|
20
|
+
DSRReportBuilder,
|
|
21
21
|
)
|
|
22
22
|
from fides.api.service.storage.streaming.dsr_storage import (
|
|
23
23
|
create_dsr_report_files_generator,
|
|
@@ -346,7 +346,7 @@ class SmartOpenStreamingStorage:
|
|
|
346
346
|
)
|
|
347
347
|
|
|
348
348
|
def _collect_and_validate_attachments_from_dsr_builder(
|
|
349
|
-
self, data: dict, dsr_builder: "
|
|
349
|
+
self, data: dict, dsr_builder: "DSRReportBuilder"
|
|
350
350
|
) -> list[AttachmentProcessingInfo]:
|
|
351
351
|
"""Collect and validate attachments using the DSR report builder's processed attachments.
|
|
352
352
|
|
|
@@ -544,7 +544,7 @@ class SmartOpenStreamingStorage:
|
|
|
544
544
|
"""
|
|
545
545
|
# Generate the DSR report first
|
|
546
546
|
try:
|
|
547
|
-
dsr_builder =
|
|
547
|
+
dsr_builder = DSRReportBuilder(
|
|
548
548
|
privacy_request=privacy_request,
|
|
549
549
|
dsr_data=data,
|
|
550
550
|
enable_streaming=True,
|
fides/api/tasks/storage.py
CHANGED
|
@@ -12,7 +12,7 @@ from loguru import logger
|
|
|
12
12
|
from fides.api.common_exceptions import StorageUploadError
|
|
13
13
|
from fides.api.schemas.storage.storage import ResponseFormat, StorageSecrets
|
|
14
14
|
from fides.api.service.privacy_request.dsr_package.dsr_report_builder import (
|
|
15
|
-
|
|
15
|
+
DSRReportBuilder,
|
|
16
16
|
)
|
|
17
17
|
from fides.api.service.storage.gcs import get_gcs_blob
|
|
18
18
|
from fides.api.service.storage.s3 import (
|
|
@@ -47,7 +47,7 @@ def write_to_in_memory_buffer(
|
|
|
47
47
|
logger.debug("Writing data to in-memory buffer")
|
|
48
48
|
try:
|
|
49
49
|
if resp_format == ResponseFormat.html.value:
|
|
50
|
-
return
|
|
50
|
+
return DSRReportBuilder(
|
|
51
51
|
privacy_request=privacy_request,
|
|
52
52
|
dsr_data=data,
|
|
53
53
|
).generate()
|
fides/api/util/endpoint_utils.py
CHANGED
|
@@ -5,8 +5,6 @@ from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
|
5
5
|
|
|
6
6
|
from fastapi import HTTPException
|
|
7
7
|
from fideslang import FidesModelType
|
|
8
|
-
from slowapi import Limiter
|
|
9
|
-
from slowapi.util import get_remote_address # type: ignore
|
|
10
8
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
11
9
|
from starlette.status import HTTP_400_BAD_REQUEST
|
|
12
10
|
|
|
@@ -23,7 +21,6 @@ from fides.common.api.scope_registry import (
|
|
|
23
21
|
ORGANIZATION,
|
|
24
22
|
SYSTEM,
|
|
25
23
|
)
|
|
26
|
-
from fides.config import CONFIG
|
|
27
24
|
|
|
28
25
|
from fides.api.models.sql_models import ( # type: ignore[attr-defined] # isort: skip
|
|
29
26
|
ModelWithDefaultField,
|
|
@@ -44,16 +41,6 @@ CLI_SCOPE_PREFIX_MAPPING: Dict[str, str] = {
|
|
|
44
41
|
"system": SYSTEM,
|
|
45
42
|
}
|
|
46
43
|
|
|
47
|
-
# Used for rate limiting with Slow API
|
|
48
|
-
# Decorate individual routes to deviate from the default rate limits
|
|
49
|
-
fides_limiter = Limiter(
|
|
50
|
-
default_limits=[CONFIG.security.request_rate_limit],
|
|
51
|
-
headers_enabled=True,
|
|
52
|
-
key_prefix=CONFIG.security.rate_limit_prefix,
|
|
53
|
-
key_func=get_remote_address,
|
|
54
|
-
retry_after="http-date",
|
|
55
|
-
)
|
|
56
|
-
|
|
57
44
|
|
|
58
45
|
async def forbid_if_editing_is_default(
|
|
59
46
|
sql_model: Base,
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ipaddress import ip_address
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from fastapi import Request
|
|
7
|
+
from fastapi.responses import JSONResponse
|
|
8
|
+
from loguru import logger
|
|
9
|
+
from slowapi import Limiter
|
|
10
|
+
from slowapi.util import get_remote_address # type: ignore
|
|
11
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
12
|
+
|
|
13
|
+
from fides.config import CONFIG
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class InvalidClientIPError(Exception):
|
|
17
|
+
def __init__(self, detail: str, header_value: str, header_name: str):
|
|
18
|
+
self.detail = detail
|
|
19
|
+
self.header_value = header_value
|
|
20
|
+
self.header_name = header_name
|
|
21
|
+
super().__init__(detail)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def validate_client_ip(ip: Optional[str]) -> bool:
|
|
25
|
+
"""
|
|
26
|
+
Returns true if the provided ip is valid and not from a reserved range.
|
|
27
|
+
Returns false otherwise.
|
|
28
|
+
"""
|
|
29
|
+
if not ip:
|
|
30
|
+
return False
|
|
31
|
+
try:
|
|
32
|
+
ip_obj = ip_address(ip)
|
|
33
|
+
if (
|
|
34
|
+
ip_obj.is_loopback
|
|
35
|
+
or ip_obj.is_link_local
|
|
36
|
+
or ip_obj.is_reserved
|
|
37
|
+
or ip_obj.is_multicast
|
|
38
|
+
or ip_obj.is_private
|
|
39
|
+
):
|
|
40
|
+
return False
|
|
41
|
+
return True
|
|
42
|
+
except ValueError:
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _extract_hostname_from_ip(ip: str) -> Optional[str]:
|
|
47
|
+
"""
|
|
48
|
+
Extract hostname/IP address from header value, stripping port if present.
|
|
49
|
+
|
|
50
|
+
Simple string-based approach following the reference implementation pattern.
|
|
51
|
+
Does not validate whether the result is a valid IP address.
|
|
52
|
+
|
|
53
|
+
Examples:
|
|
54
|
+
# IPv4 cases
|
|
55
|
+
_extract_hostname_from_ip("192.168.1.1") -> "192.168.1.1"
|
|
56
|
+
_extract_hostname_from_ip("192.168.1.1:8080") -> "192.168.1.1"
|
|
57
|
+
|
|
58
|
+
# IPv6 cases
|
|
59
|
+
_extract_hostname_from_ip("2001:db8::1") -> "2001:db8::1"
|
|
60
|
+
_extract_hostname_from_ip("[2001:db8::1]:8080") -> "2001:db8::1"
|
|
61
|
+
|
|
62
|
+
# Edge cases (alidation will later reject)
|
|
63
|
+
_extract_hostname_from_ip("192.168.1.1, 192.168.1.2") -> "192.168.1.1, 192.168.1.2"
|
|
64
|
+
_extract_hostname_from_ip("not-an-ip:8080") -> "not-an-ip"
|
|
65
|
+
|
|
66
|
+
# Error
|
|
67
|
+
_extract_hostname_from_ip("") -> raises ValueError
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
ValueError: If no hostname can be extracted from the input
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
clean_ip = ip.strip()
|
|
74
|
+
|
|
75
|
+
if not clean_ip:
|
|
76
|
+
raise ValueError("Could not parse IP from header value")
|
|
77
|
+
|
|
78
|
+
# Handle IPv6 with port: [IPv6]:port
|
|
79
|
+
if "]:" in clean_ip:
|
|
80
|
+
return clean_ip.split("]:")[0].replace("[", "").strip()
|
|
81
|
+
|
|
82
|
+
# Handle IPv4 with port: IPv4:port
|
|
83
|
+
if ":" in clean_ip and "::" not in clean_ip:
|
|
84
|
+
return clean_ip.split(":")[0].strip()
|
|
85
|
+
|
|
86
|
+
# Return as-is (IPv6 without port, IPv4 without port, or other values)
|
|
87
|
+
return clean_ip
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _resolve_client_ip_from_header(request: Request, strict: bool) -> str:
|
|
91
|
+
"""Shared resolver for client IP from the configured header.
|
|
92
|
+
|
|
93
|
+
- When strict=True: raise InvalidClientIPError on invalid/malformed header values.
|
|
94
|
+
- When strict=False: never raise; fall back to the connection source IP.
|
|
95
|
+
"""
|
|
96
|
+
header_name = CONFIG.security.rate_limit_client_ip_header
|
|
97
|
+
if not header_name:
|
|
98
|
+
# This line should never be reached when rate limiting is enabled
|
|
99
|
+
logger.warning(
|
|
100
|
+
"Rate limit client IP header not configured. Falling back to source IP.",
|
|
101
|
+
header_name,
|
|
102
|
+
)
|
|
103
|
+
return get_remote_address(request)
|
|
104
|
+
|
|
105
|
+
ip_address_from_header = request.headers.get(header_name)
|
|
106
|
+
if not ip_address_from_header:
|
|
107
|
+
logger.debug(
|
|
108
|
+
"Rate limit header '{}' not found. Falling back to source IP.",
|
|
109
|
+
header_name,
|
|
110
|
+
)
|
|
111
|
+
return get_remote_address(request)
|
|
112
|
+
|
|
113
|
+
# Extract and validate IP
|
|
114
|
+
try:
|
|
115
|
+
extracted_ip = _extract_hostname_from_ip(ip_address_from_header)
|
|
116
|
+
if extracted_ip and validate_client_ip(extracted_ip):
|
|
117
|
+
return extracted_ip
|
|
118
|
+
raise ValueError("IP failed validation")
|
|
119
|
+
except ValueError:
|
|
120
|
+
if strict:
|
|
121
|
+
logger.error(
|
|
122
|
+
"Invalid IP '{}' in header '{}'. Rejecting request.",
|
|
123
|
+
ip_address_from_header,
|
|
124
|
+
header_name,
|
|
125
|
+
)
|
|
126
|
+
raise InvalidClientIPError(
|
|
127
|
+
detail="Invalid IP address format",
|
|
128
|
+
header_value=ip_address_from_header,
|
|
129
|
+
header_name=header_name,
|
|
130
|
+
)
|
|
131
|
+
# Non-strict path: fall back silently to source IP
|
|
132
|
+
return get_remote_address(request)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_client_ip_from_header(request: Request) -> str:
|
|
136
|
+
"""
|
|
137
|
+
Extracts the client IP from the configured CDN header.
|
|
138
|
+
|
|
139
|
+
If the header is not configured or is missing, it falls back to the
|
|
140
|
+
source IP on the request.
|
|
141
|
+
|
|
142
|
+
Raises InvalidClientIPError if header contains invalid IP format.
|
|
143
|
+
"""
|
|
144
|
+
return _resolve_client_ip_from_header(request, strict=True)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def safe_rate_limit_key(request: Request) -> str:
|
|
148
|
+
"""
|
|
149
|
+
Safe key function for SlowAPI limiter.
|
|
150
|
+
|
|
151
|
+
Must never raise. If the configured header is missing or malformed,
|
|
152
|
+
fall back to the connection source IP for rate limiting purposes.
|
|
153
|
+
"""
|
|
154
|
+
return _resolve_client_ip_from_header(request, strict=False)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class RateLimitIPValidationMiddleware(BaseHTTPMiddleware):
|
|
158
|
+
"""
|
|
159
|
+
Pre-validate the configured client IP header when rate limiting is enabled.
|
|
160
|
+
|
|
161
|
+
If the header is present but invalid, short-circuit the request with 422.
|
|
162
|
+
This keeps SlowAPI's middleware path free of exceptions from the key function.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
async def dispatch(self, request: Request, call_next): # type: ignore
|
|
166
|
+
if is_rate_limit_enabled:
|
|
167
|
+
try:
|
|
168
|
+
# Triggers parsing/validation; raises on invalid header
|
|
169
|
+
get_client_ip_from_header(request)
|
|
170
|
+
except InvalidClientIPError:
|
|
171
|
+
return JSONResponse(
|
|
172
|
+
status_code=422, content={"detail": "Invalid client IP header"}
|
|
173
|
+
)
|
|
174
|
+
return await call_next(request)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# Used for rate limiting with Slow API
|
|
178
|
+
# Decorate individual routes to deviate from the default rate limits
|
|
179
|
+
is_rate_limit_enabled = (
|
|
180
|
+
CONFIG.security.rate_limit_client_ip_header is not None
|
|
181
|
+
and CONFIG.security.rate_limit_client_ip_header != ""
|
|
182
|
+
)
|
|
183
|
+
fides_limiter = Limiter(
|
|
184
|
+
storage_uri=CONFIG.redis.connection_url_unencoded,
|
|
185
|
+
application_limits=[
|
|
186
|
+
CONFIG.security.request_rate_limit
|
|
187
|
+
], # Creates ONE shared bucket for all endpoints
|
|
188
|
+
headers_enabled=True,
|
|
189
|
+
key_prefix=CONFIG.security.rate_limit_prefix,
|
|
190
|
+
key_func=safe_rate_limit_key,
|
|
191
|
+
retry_after="http-date",
|
|
192
|
+
in_memory_fallback_enabled=False, # Fall back to no rate limiting if Redis unavailable
|
|
193
|
+
enabled=is_rate_limit_enabled,
|
|
194
|
+
)
|
|
@@ -30,6 +30,7 @@ DATA_SUBJECT = "data_subject"
|
|
|
30
30
|
DATA_USE = "data_use"
|
|
31
31
|
DATASET = "dataset"
|
|
32
32
|
DELETE = "delete"
|
|
33
|
+
PRIVACY_REQUEST_REDACTION_PATTERNS = "privacy-request-redaction-patterns"
|
|
33
34
|
ENCRYPTION = "encryption"
|
|
34
35
|
MESSAGING_TEMPLATE = "messaging-template"
|
|
35
36
|
EVALUATION = "evaluation"
|
|
@@ -134,6 +135,11 @@ DATA_USE_UPDATE = f"{DATA_USE}:{UPDATE}"
|
|
|
134
135
|
DATA_USE_DELETE = f"{DATA_USE}:{DELETE}"
|
|
135
136
|
|
|
136
137
|
DATASET_CREATE_OR_UPDATE = f"{DATASET}:{CREATE_OR_UPDATE}"
|
|
138
|
+
|
|
139
|
+
PRIVACY_REQUEST_REDACTION_PATTERNS_READ = f"{PRIVACY_REQUEST_REDACTION_PATTERNS}:{READ}"
|
|
140
|
+
PRIVACY_REQUEST_REDACTION_PATTERNS_UPDATE = (
|
|
141
|
+
f"{PRIVACY_REQUEST_REDACTION_PATTERNS}:{UPDATE}"
|
|
142
|
+
)
|
|
137
143
|
DATASET_DELETE = f"{DATASET}:{DELETE}"
|
|
138
144
|
DATASET_READ = f"{DATASET}:{READ}"
|
|
139
145
|
DATASET_TEST = f"{DATASET}:{TEST}"
|
|
@@ -288,6 +294,8 @@ SCOPE_DOCS = {
|
|
|
288
294
|
DATA_USE_UPDATE: "Update data uses",
|
|
289
295
|
DATASET_CREATE_OR_UPDATE: "Create or modify datasets",
|
|
290
296
|
DATASET_DELETE: "Delete datasets",
|
|
297
|
+
PRIVACY_REQUEST_REDACTION_PATTERNS_READ: "View privacy request redaction patterns",
|
|
298
|
+
PRIVACY_REQUEST_REDACTION_PATTERNS_UPDATE: "Update privacy request redaction patterns",
|
|
291
299
|
DATASET_READ: "View datasets",
|
|
292
300
|
DATASET_TEST: "Run a standalone privacy request test for a dataset",
|
|
293
301
|
ENCRYPTION_EXEC: "Encrypt data",
|
|
@@ -116,6 +116,9 @@ PRIVACY_REQUEST_TRANSFER_TO_PARENT = (
|
|
|
116
116
|
"/privacy-request/transfer/{privacy_request_id}/{rule_key}"
|
|
117
117
|
)
|
|
118
118
|
|
|
119
|
+
# Privacy Request Redaction Patterns URLs
|
|
120
|
+
PRIVACY_REQUEST_REDACTION_PATTERNS = "/privacy-request/redaction-patterns"
|
|
121
|
+
|
|
119
122
|
# Privacy Request pre-approve URLs
|
|
120
123
|
PRIVACY_REQUEST_PRE_APPROVE = "/privacy-request/{privacy_request_id}/pre-approve"
|
|
121
124
|
PRIVACY_REQUEST_PRE_APPROVE_ELIGIBLE = PRIVACY_REQUEST_PRE_APPROVE + "/eligible"
|
fides/config/redis_settings.py
CHANGED
|
@@ -181,8 +181,24 @@ class RedisSettings(FidesSettings):
|
|
|
181
181
|
description="A full connection URL to the read-only Redis cache. If not specified, this URL is automatically assembled from the read_only_host, read_only_port, read_only_password and read_only_db_index specified above.",
|
|
182
182
|
exclude=True,
|
|
183
183
|
)
|
|
184
|
+
connection_url_unencoded: Optional[str] = Field(
|
|
185
|
+
default=None,
|
|
186
|
+
description="A full connection URL to the Redis cache with the password unencoded. If not specified, this URL is automatically assembled from the host, port, password and db_index specified above.",
|
|
187
|
+
exclude=True,
|
|
188
|
+
)
|
|
189
|
+
read_only_connection_url_unencoded: Optional[str] = Field(
|
|
190
|
+
default=None,
|
|
191
|
+
description="A full connection URL to the read-only Redis cache with the password unencoded. If not specified, this URL is automatically assembled from the read_only_host, read_only_port, read_only_password and read_only_db_index specified above.",
|
|
192
|
+
exclude=True,
|
|
193
|
+
)
|
|
184
194
|
|
|
185
|
-
@field_validator(
|
|
195
|
+
@field_validator(
|
|
196
|
+
"connection_url",
|
|
197
|
+
"read_only_connection_url",
|
|
198
|
+
"connection_url_unencoded",
|
|
199
|
+
"read_only_connection_url_unencoded",
|
|
200
|
+
mode="before",
|
|
201
|
+
)
|
|
186
202
|
@classmethod
|
|
187
203
|
def assemble_connection_url(
|
|
188
204
|
cls,
|
|
@@ -195,7 +211,14 @@ class RedisSettings(FidesSettings):
|
|
|
195
211
|
return v
|
|
196
212
|
|
|
197
213
|
# Determine which set of settings to use based on field name
|
|
198
|
-
is_read_only = info.field_name
|
|
214
|
+
is_read_only = info.field_name in (
|
|
215
|
+
"read_only_connection_url",
|
|
216
|
+
"read_only_connection_url_unencoded",
|
|
217
|
+
)
|
|
218
|
+
is_unencoded = info.field_name in (
|
|
219
|
+
"connection_url_unencoded",
|
|
220
|
+
"read_only_connection_url_unencoded",
|
|
221
|
+
)
|
|
199
222
|
|
|
200
223
|
# Extract settings - fallbacks already resolved by field validators for read-only fields
|
|
201
224
|
user = (
|
|
@@ -244,7 +267,8 @@ class RedisSettings(FidesSettings):
|
|
|
244
267
|
# redis://<user>:<password>@<host>
|
|
245
268
|
auth_prefix = ""
|
|
246
269
|
if password or user:
|
|
247
|
-
|
|
270
|
+
encoded_password = password if is_unencoded else quote_plus(password)
|
|
271
|
+
auth_prefix = f"{quote_plus(user)}:{encoded_password}@"
|
|
248
272
|
|
|
249
273
|
# For host, we don't have a fallback - read replica should be a different host
|
|
250
274
|
host = (
|