ethyca-fides 2.69.0rc9__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.0rc9.dist-info → ethyca_fides-2.69.1.dist-info}/METADATA +2 -2
- {ethyca_fides-2.69.0rc9.dist-info → ethyca_fides-2.69.1.dist-info}/RECORD +204 -195
- 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 +286 -120
- fides/api/service/privacy_request/dsr_package/templates/attachments_index.html +4 -2
- fides/api/service/privacy_request/dsr_package/templates/collection_index.html +3 -1
- fides/api/service/privacy_request/dsr_package/templates/dataset_index.html +1 -1
- fides/api/service/privacy_request/dsr_package/utils.py +268 -0
- fides/api/service/privacy_request/request_runner_service.py +8 -2
- fides/api/service/storage/streaming/smart_open_streaming_storage.py +107 -170
- fides/api/service/storage/util.py +579 -0
- fides/api/task/manual/manual_task_graph_task.py +11 -9
- 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/XiHm-6CdVChTC5rbN9GtT/_buildManifest.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/4121-c8d5d717e31899e1.js +0 -1
- {ethyca_fides-2.69.0rc9.dist-info → ethyca_fides-2.69.1.dist-info}/WHEEL +0 -0
- {ethyca_fides-2.69.0rc9.dist-info → ethyca_fides-2.69.1.dist-info}/entry_points.txt +0 -0
- {ethyca_fides-2.69.0rc9.dist-info → ethyca_fides-2.69.1.dist-info}/licenses/LICENSE +0 -0
- {ethyca_fides-2.69.0rc9.dist-info → ethyca_fides-2.69.1.dist-info}/top_level.txt +0 -0
- /fides/ui-build/static/admin/_next/static/{XiHm-6CdVChTC5rbN9GtT → 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
|