ethyca-fides 2.56.3b1__py2.py3-none-any.whl → 2.57.0__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.
- {ethyca_fides-2.56.3b1.dist-info → ethyca_fides-2.57.0.dist-info}/METADATA +1 -1
- {ethyca_fides-2.56.3b1.dist-info → ethyca_fides-2.57.0.dist-info}/RECORD +120 -116
- fides/_version.py +3 -3
- fides/api/alembic/migrations/versions/69ad6d844e21_add_comments_and_comment_references.py +84 -0
- fides/api/alembic/migrations/versions/6ea2171c544f_change_attachment_storage_key_to_.py +77 -0
- fides/api/models/attachment.py +109 -49
- fides/api/models/comment.py +109 -0
- fides/api/service/connectors/query_configs/bigquery_query_config.py +46 -15
- fides/api/service/connectors/query_configs/query_config.py +118 -21
- fides/api/service/connectors/query_configs/saas_query_config.py +21 -15
- fides/api/service/storage/storage_uploader_service.py +4 -10
- fides/api/tasks/storage.py +106 -15
- fides/api/util/aws_util.py +19 -0
- fides/api/util/collection_util.py +142 -0
- fides/api/util/logger_context_utils.py +36 -1
- fides/api/util/saas_util.py +32 -56
- fides/data/language/languages.yml +2 -0
- fides/service/privacy_request/privacy_request_service.py +14 -5
- fides/ui-build/static/admin/404.html +1 -1
- fides/ui-build/static/admin/_next/static/D8O9QehPDr533GOFed3df/_buildManifest.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/5574-a4047e826a8cd4c3.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/5834-bd9ed01c4ab2ef5c.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/5973-67c7562997f7d5cb.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/6277-515c922445b8edc5.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/9767-4c591bd478c72650.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/pages/{_app-3c1a7742661d3a9e.js → _app-9afd7a4bc9b3eee5.js} +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/consent/privacy-experience/{[id]-fdbafb5a47a6a28c.js → [id]-443e6dd191ce5588.js} +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/consent/privacy-experience/{new-fc4635c6eea7165f.js → new-36162d5bea29dcc2.js} +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/consent/privacy-notices/{[id]-bdc686761a0d2d60.js → [id]-2d651499c07d12d9.js} +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/consent/privacy-notices/{new-8618ab5fa45cd074.js → new-36e57d2f2446be82.js} +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/dataset/new-740824dfa6823e26.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/pages/dataset-5541ecd2f62711a5.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/pages/datastore-connection/{[id]-a6e793e9a8ed00a9.js → [id]-0616437e1a82d3ed.js} +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/datastore-connection/{new-7d314bb7238af067.js → new-f8218440494e8532.js} +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/{index-c9fa68dc0fa42c81.js → index-94e6d589c4edf360.js} +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/integrations/[id]-a6448ce6ba7f0cf5.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/pages/integrations-1a6965d78bfb26b0.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/{[id]-c685d19a131d6960.js → [id]-bbe5854b7d19b7e9.js} +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/systems/configure/{[id]-c6396feeefca887b.js → [id]-82d9af34546adaa2.js} +1 -1
- fides/ui-build/static/admin/_next/static/css/{bd5a72c010fa9c14.css → 957d0e4fea846ff0.css} +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/ant-poc.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/lib/fides-ext-gpp.js +1 -1
- fides/ui-build/static/admin/lib/fides-headless.js +1 -1
- fides/ui-build/static/admin/lib/fides-tcf.js +3 -3
- fides/ui-build/static/admin/lib/fides.js +2 -2
- 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/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.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/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/LOp6RUpN795nyhXOv95wz/_buildManifest.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/2201-abd6092e6df98c26.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/6277-b55810d66362f56e.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/6362-8f79e403fdc2404a.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/7495-a61f0c9fcb458664.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/dataset/new-fc00dbeb18a7b731.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/dataset-295756e1fd640b59.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/integrations/[id]-20489bceb7b068b8.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/integrations-21c9902e7b098b39.js +0 -1
- {ethyca_fides-2.56.3b1.dist-info → ethyca_fides-2.57.0.dist-info}/LICENSE +0 -0
- {ethyca_fides-2.56.3b1.dist-info → ethyca_fides-2.57.0.dist-info}/WHEEL +0 -0
- {ethyca_fides-2.56.3b1.dist-info → ethyca_fides-2.57.0.dist-info}/entry_points.txt +0 -0
- {ethyca_fides-2.56.3b1.dist-info → ethyca_fides-2.57.0.dist-info}/top_level.txt +0 -0
- /fides/ui-build/static/admin/_next/static/{LOp6RUpN795nyhXOv95wz → D8O9QehPDr533GOFed3df}/_ssgManifest.js +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# pylint: disable=too-many-instance-attributes
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
+
from copy import deepcopy
|
|
4
5
|
from datetime import datetime
|
|
5
6
|
from itertools import product
|
|
6
7
|
from typing import Any, Dict, List, Literal, Optional, Tuple, TypeVar
|
|
@@ -12,7 +13,6 @@ from loguru import logger
|
|
|
12
13
|
from sqlalchemy.orm import Session
|
|
13
14
|
|
|
14
15
|
from fides.api.common_exceptions import FidesopsException
|
|
15
|
-
from fides.api.graph.config import ScalarField
|
|
16
16
|
from fides.api.graph.execution import ExecutionNode
|
|
17
17
|
from fides.api.models.policy import Policy
|
|
18
18
|
from fides.api.models.privacy_request import (
|
|
@@ -33,7 +33,12 @@ from fides.api.task.refine_target_path import (
|
|
|
33
33
|
join_detailed_path,
|
|
34
34
|
)
|
|
35
35
|
from fides.api.util import saas_util
|
|
36
|
-
from fides.api.util.collection_util import
|
|
36
|
+
from fides.api.util.collection_util import (
|
|
37
|
+
Row,
|
|
38
|
+
flatten_dict,
|
|
39
|
+
merge_dicts,
|
|
40
|
+
unflatten_dict,
|
|
41
|
+
)
|
|
37
42
|
from fides.api.util.saas_util import (
|
|
38
43
|
ALL_OBJECT_FIELDS,
|
|
39
44
|
CUSTOM_PRIVACY_REQUEST_FIELDS,
|
|
@@ -45,7 +50,6 @@ from fides.api.util.saas_util import (
|
|
|
45
50
|
REPLY_TO_TOKEN,
|
|
46
51
|
UUID,
|
|
47
52
|
get_identities,
|
|
48
|
-
unflatten_dict,
|
|
49
53
|
)
|
|
50
54
|
from fides.common.api.v1.urn_registry import REQUEST_TASK_CALLBACK, V1_URL_PREFIX
|
|
51
55
|
from fides.config.config_proxy import ConfigProxy
|
|
@@ -510,22 +514,24 @@ class SaaSQueryConfig(QueryConfig[SaaSRequestParams]):
|
|
|
510
514
|
|
|
511
515
|
def all_value_map(self, row: Row) -> Dict[str, Any]:
|
|
512
516
|
"""
|
|
513
|
-
Takes a row and preserves only the fields that are defined in the
|
|
517
|
+
Takes a row and preserves only the fields that are defined in the collection.
|
|
514
518
|
Used for scenarios when an update endpoint has required fields other than
|
|
515
519
|
just the fields being updated.
|
|
516
520
|
"""
|
|
521
|
+
flattened_row = flatten_dict(deepcopy(row))
|
|
517
522
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
523
|
+
# Get root field names defined in the collection
|
|
524
|
+
collection_fields = {
|
|
525
|
+
field_path.string_path.split(".")[0]
|
|
526
|
+
for field_path, _ in self.field_map().items()
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
# Only keep the field values defined in the collection
|
|
530
|
+
return {
|
|
531
|
+
path: value
|
|
532
|
+
for path, value in flattened_row.items()
|
|
533
|
+
if path.split(".")[0] in collection_fields
|
|
534
|
+
}
|
|
529
535
|
|
|
530
536
|
def query_to_str(self, t: T, input_data: Dict[str, List[Any]]) -> str:
|
|
531
537
|
"""Convert query to string"""
|
|
@@ -41,9 +41,7 @@ def upload(
|
|
|
41
41
|
logger.warning("Storage type not found: {}", storage_key)
|
|
42
42
|
raise StorageUploadError(f"Storage type not found: {storage_key}")
|
|
43
43
|
uploader: Any = _get_uploader_from_config_type(config.type) # type: ignore
|
|
44
|
-
return uploader(
|
|
45
|
-
db, config, data, privacy_request, data_category_field_mapping, data_use_map
|
|
46
|
-
)
|
|
44
|
+
return uploader(db, config, data, privacy_request)
|
|
47
45
|
|
|
48
46
|
|
|
49
47
|
def get_extension(resp_format: ResponseFormat) -> str:
|
|
@@ -88,14 +86,13 @@ def _s3_uploader(
|
|
|
88
86
|
config: StorageConfig,
|
|
89
87
|
data: Dict,
|
|
90
88
|
privacy_request: PrivacyRequest,
|
|
91
|
-
data_category_field_mapping: Optional[DataCategoryFieldMapping] = None,
|
|
92
|
-
data_use_map: Optional[Dict[str, Set[str]]] = None,
|
|
93
89
|
) -> str:
|
|
94
90
|
"""Constructs necessary info needed for s3 before calling upload"""
|
|
95
91
|
file_key: str = _construct_file_key(privacy_request.id, config)
|
|
96
92
|
|
|
97
93
|
bucket_name = config.details[StorageDetails.BUCKET.value]
|
|
98
94
|
auth_method = config.details[StorageDetails.AUTH_METHOD.value]
|
|
95
|
+
document = None
|
|
99
96
|
|
|
100
97
|
return upload_to_s3(
|
|
101
98
|
config.secrets, # type: ignore
|
|
@@ -104,9 +101,8 @@ def _s3_uploader(
|
|
|
104
101
|
file_key,
|
|
105
102
|
config.format.value, # type: ignore
|
|
106
103
|
privacy_request,
|
|
104
|
+
document,
|
|
107
105
|
auth_method,
|
|
108
|
-
data_category_field_mapping,
|
|
109
|
-
data_use_map,
|
|
110
106
|
)
|
|
111
107
|
|
|
112
108
|
|
|
@@ -115,9 +111,7 @@ def _local_uploader(
|
|
|
115
111
|
config: StorageConfig,
|
|
116
112
|
data: Dict,
|
|
117
113
|
privacy_request: PrivacyRequest,
|
|
118
|
-
data_category_field_mapping: Optional[DataCategoryFieldMapping] = None,
|
|
119
|
-
data_use_map: Optional[Dict[str, Set[str]]] = None,
|
|
120
114
|
) -> str:
|
|
121
115
|
"""Uploads data to local storage, used for quick-start/demo purposes"""
|
|
122
116
|
file_key: str = _construct_file_key(privacy_request.id, config)
|
|
123
|
-
return upload_to_local(data, file_key, privacy_request, config.format.value
|
|
117
|
+
return upload_to_local(data, file_key, privacy_request, config.format.value) # type: ignore
|
fides/api/tasks/storage.py
CHANGED
|
@@ -5,20 +5,20 @@ import os
|
|
|
5
5
|
import secrets
|
|
6
6
|
import zipfile
|
|
7
7
|
from io import BytesIO
|
|
8
|
-
from typing import Any, Dict, Optional,
|
|
8
|
+
from typing import Any, Dict, Optional, Union
|
|
9
9
|
|
|
10
10
|
import pandas as pd
|
|
11
11
|
from botocore.exceptions import ClientError, ParamValidationError
|
|
12
|
+
from fideslang.validation import AnyHttpUrlString
|
|
12
13
|
from loguru import logger
|
|
13
14
|
|
|
14
15
|
from fides.api.cryptography.cryptographic_util import bytes_to_b64_str
|
|
15
|
-
from fides.api.graph.graph import DataCategoryFieldMapping
|
|
16
16
|
from fides.api.models.privacy_request import PrivacyRequest
|
|
17
17
|
from fides.api.schemas.storage.storage import ResponseFormat, StorageSecrets
|
|
18
18
|
from fides.api.service.privacy_request.dsr_package.dsr_report_builder import (
|
|
19
19
|
DsrReportBuilder,
|
|
20
20
|
)
|
|
21
|
-
from fides.api.util.aws_util import
|
|
21
|
+
from fides.api.util.aws_util import get_s3_client
|
|
22
22
|
from fides.api.util.cache import get_cache, get_encryption_cache_key
|
|
23
23
|
from fides.api.util.encryption.aes_gcm_encryption_scheme import (
|
|
24
24
|
encrypt_to_bytes_verify_secrets_length,
|
|
@@ -101,7 +101,9 @@ def write_to_in_memory_buffer(
|
|
|
101
101
|
raise NotImplementedError(f"No handling for response format {resp_format}.")
|
|
102
102
|
|
|
103
103
|
|
|
104
|
-
def create_presigned_url_for_s3(
|
|
104
|
+
def create_presigned_url_for_s3(
|
|
105
|
+
s3_client: Any, bucket_name: str, file_key: str
|
|
106
|
+
) -> AnyHttpUrlString:
|
|
105
107
|
""" "Generate a presigned URL to share an S3 object
|
|
106
108
|
|
|
107
109
|
:param s3_client: s3 base client
|
|
@@ -119,23 +121,108 @@ def create_presigned_url_for_s3(s3_client: Any, bucket_name: str, file_key: str)
|
|
|
119
121
|
return response
|
|
120
122
|
|
|
121
123
|
|
|
124
|
+
def generic_upload_to_s3( # pylint: disable=R0913
|
|
125
|
+
storage_secrets: Dict[StorageSecrets, Any],
|
|
126
|
+
bucket_name: str,
|
|
127
|
+
file_key: str,
|
|
128
|
+
auth_method: str,
|
|
129
|
+
document: bytes,
|
|
130
|
+
) -> Optional[AnyHttpUrlString]:
|
|
131
|
+
"""Uploads arbitrary data to s3 returned from an access request"""
|
|
132
|
+
logger.info("Starting S3 Upload of {}", file_key)
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
s3_client = get_s3_client(auth_method, storage_secrets)
|
|
136
|
+
try:
|
|
137
|
+
s3_client.put_object(Bucket=bucket_name, Key=file_key, Body=document)
|
|
138
|
+
except Exception as e:
|
|
139
|
+
logger.error("Encountered error while uploading s3 object: {}", e)
|
|
140
|
+
raise e
|
|
141
|
+
|
|
142
|
+
presigned_url: AnyHttpUrlString = create_presigned_url_for_s3(
|
|
143
|
+
s3_client, bucket_name, file_key
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return presigned_url
|
|
147
|
+
except ClientError as e:
|
|
148
|
+
logger.error(
|
|
149
|
+
"Encountered error while uploading and generating link for s3 object: {}", e
|
|
150
|
+
)
|
|
151
|
+
raise e
|
|
152
|
+
except ParamValidationError as e:
|
|
153
|
+
raise ValueError(f"The parameters you provided are incorrect: {e}")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def generic_retrieve_from_s3(
|
|
157
|
+
storage_secrets: Dict[StorageSecrets, Any],
|
|
158
|
+
bucket_name: str,
|
|
159
|
+
file_key: str,
|
|
160
|
+
auth_method: str,
|
|
161
|
+
) -> Optional[bytes]:
|
|
162
|
+
"""Retrieves arbitrary data from s3"""
|
|
163
|
+
logger.info("Starting S3 Retrieve of {}", file_key)
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
s3_client = get_s3_client(auth_method, storage_secrets)
|
|
167
|
+
try:
|
|
168
|
+
response = s3_client.get_object(Bucket=bucket_name, Key=file_key)
|
|
169
|
+
return response["Body"].read()
|
|
170
|
+
except Exception as e:
|
|
171
|
+
logger.error("Encountered error while retrieving s3 object: {}", e)
|
|
172
|
+
raise e
|
|
173
|
+
except ClientError as e:
|
|
174
|
+
logger.error("Encountered error while retrieving s3 object: {}", e)
|
|
175
|
+
raise e
|
|
176
|
+
except ParamValidationError as e:
|
|
177
|
+
raise ValueError(f"The parameters you provided are incorrect: {e}")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def generic_delete_from_s3(
|
|
181
|
+
storage_secrets: Dict[StorageSecrets, Any],
|
|
182
|
+
bucket_name: str,
|
|
183
|
+
file_key: str,
|
|
184
|
+
auth_method: str,
|
|
185
|
+
) -> None:
|
|
186
|
+
"""Deletes arbitrary data from s3"""
|
|
187
|
+
logger.info("Starting S3 Delete of {}", file_key)
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
s3_client = get_s3_client(auth_method, storage_secrets)
|
|
191
|
+
try:
|
|
192
|
+
s3_client.delete_object(Bucket=bucket_name, Key=file_key)
|
|
193
|
+
except Exception as e:
|
|
194
|
+
logger.error("Encountered error while deleting s3 object: {}", e)
|
|
195
|
+
raise e
|
|
196
|
+
except ClientError as e:
|
|
197
|
+
logger.error("Encountered error while deleting s3 object: {}", e)
|
|
198
|
+
raise e
|
|
199
|
+
except ParamValidationError as e:
|
|
200
|
+
raise ValueError(f"The parameters you provided are incorrect: {e}")
|
|
201
|
+
|
|
202
|
+
|
|
122
203
|
def upload_to_s3( # pylint: disable=R0913
|
|
123
204
|
storage_secrets: Dict[StorageSecrets, Any],
|
|
124
205
|
data: Dict,
|
|
125
206
|
bucket_name: str,
|
|
126
207
|
file_key: str,
|
|
127
208
|
resp_format: str,
|
|
128
|
-
privacy_request: PrivacyRequest,
|
|
209
|
+
privacy_request: Optional[PrivacyRequest],
|
|
210
|
+
document: Optional[bytes],
|
|
129
211
|
auth_method: str,
|
|
130
|
-
|
|
131
|
-
data_use_map: Optional[Dict[str, Set[str]]] = None,
|
|
132
|
-
) -> str:
|
|
212
|
+
) -> Optional[AnyHttpUrlString]:
|
|
133
213
|
"""Uploads arbitrary data to s3 returned from an access request"""
|
|
134
214
|
logger.info("Starting S3 Upload of {}", file_key)
|
|
135
215
|
|
|
216
|
+
if privacy_request is None and document is not None:
|
|
217
|
+
return generic_upload_to_s3(
|
|
218
|
+
storage_secrets, bucket_name, file_key, auth_method, document
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if privacy_request is None:
|
|
222
|
+
raise ValueError("Privacy request must be provided")
|
|
223
|
+
|
|
136
224
|
try:
|
|
137
|
-
|
|
138
|
-
s3_client = my_session.client("s3")
|
|
225
|
+
s3_client = get_s3_client(auth_method, storage_secrets)
|
|
139
226
|
|
|
140
227
|
# handles file chunking
|
|
141
228
|
try:
|
|
@@ -148,7 +235,7 @@ def upload_to_s3( # pylint: disable=R0913
|
|
|
148
235
|
logger.error("Encountered error while uploading s3 object: {}", e)
|
|
149
236
|
raise e
|
|
150
237
|
|
|
151
|
-
presigned_url:
|
|
238
|
+
presigned_url: AnyHttpUrlString = create_presigned_url_for_s3(
|
|
152
239
|
s3_client, bucket_name, file_key
|
|
153
240
|
)
|
|
154
241
|
|
|
@@ -162,17 +249,21 @@ def upload_to_s3( # pylint: disable=R0913
|
|
|
162
249
|
raise ValueError(f"The parameters you provided are incorrect: {e}")
|
|
163
250
|
|
|
164
251
|
|
|
252
|
+
def get_local_filename(file_key: str) -> str:
|
|
253
|
+
"""Verifies that the local storage directory exists"""
|
|
254
|
+
if not os.path.exists(LOCAL_FIDES_UPLOAD_DIRECTORY):
|
|
255
|
+
os.makedirs(LOCAL_FIDES_UPLOAD_DIRECTORY)
|
|
256
|
+
return f"{LOCAL_FIDES_UPLOAD_DIRECTORY}/{file_key}"
|
|
257
|
+
|
|
258
|
+
|
|
165
259
|
def upload_to_local(
|
|
166
260
|
data: Dict,
|
|
167
261
|
file_key: str,
|
|
168
262
|
privacy_request: PrivacyRequest,
|
|
169
263
|
resp_format: str = ResponseFormat.json.value,
|
|
170
|
-
data_category_field_mapping: Optional[DataCategoryFieldMapping] = None,
|
|
171
|
-
data_use_map: Optional[Dict[str, Set[str]]] = None,
|
|
172
264
|
) -> str:
|
|
173
265
|
"""Uploads access request data to a local folder - for testing/demo purposes only"""
|
|
174
|
-
|
|
175
|
-
os.makedirs(LOCAL_FIDES_UPLOAD_DIRECTORY)
|
|
266
|
+
get_local_filename(file_key)
|
|
176
267
|
|
|
177
268
|
filename = f"{LOCAL_FIDES_UPLOAD_DIRECTORY}/{file_key}"
|
|
178
269
|
in_memory_file = write_to_in_memory_buffer(resp_format, data, privacy_request)
|
fides/api/util/aws_util.py
CHANGED
|
@@ -70,3 +70,22 @@ def get_aws_session(
|
|
|
70
70
|
raise
|
|
71
71
|
else:
|
|
72
72
|
return session
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_s3_client(
|
|
76
|
+
auth_method: str,
|
|
77
|
+
storage_secrets: Optional[Dict[StorageSecrets, Any]],
|
|
78
|
+
assume_role_arn: Optional[str] = None,
|
|
79
|
+
) -> Session:
|
|
80
|
+
"""
|
|
81
|
+
Abstraction to retrieve an AWS S3 client using secrets.
|
|
82
|
+
|
|
83
|
+
If an `assume_role_arn` is provided, the secrets will be used to
|
|
84
|
+
assume that role and return a Session instantiated with that role.
|
|
85
|
+
"""
|
|
86
|
+
session = get_aws_session(
|
|
87
|
+
auth_method=auth_method,
|
|
88
|
+
storage_secrets=storage_secrets,
|
|
89
|
+
assume_role_arn=assume_role_arn,
|
|
90
|
+
)
|
|
91
|
+
return session.client("s3")
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from collections import deque
|
|
1
2
|
from functools import reduce
|
|
2
3
|
from typing import Any, Callable, Dict, Iterable, List, Optional, TypeVar, Union
|
|
3
4
|
|
|
@@ -119,3 +120,144 @@ def extract_key_for_address(
|
|
|
119
120
|
request_id_dataset, collection = full_request_id.split(":")
|
|
120
121
|
dataset = request_id_dataset.split("__", number_of_leading_strings_to_exclude)[-1]
|
|
121
122
|
return f"{dataset}:{collection}"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def unflatten_dict(flat_dict: Dict[str, Any], separator: str = ".") -> Dict[str, Any]:
|
|
126
|
+
"""
|
|
127
|
+
Converts a dictionary of paths/values into a nested dictionary
|
|
128
|
+
|
|
129
|
+
example:
|
|
130
|
+
|
|
131
|
+
{"A.B": "1", "A.C": "2"}
|
|
132
|
+
|
|
133
|
+
becomes
|
|
134
|
+
|
|
135
|
+
{
|
|
136
|
+
"A": {
|
|
137
|
+
"B": "1",
|
|
138
|
+
"C": "2"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
"""
|
|
142
|
+
output: Dict[Any, Any] = {}
|
|
143
|
+
queue = deque(flat_dict.items())
|
|
144
|
+
|
|
145
|
+
while queue:
|
|
146
|
+
path, value = queue.popleft()
|
|
147
|
+
keys = path.split(separator)
|
|
148
|
+
target = output
|
|
149
|
+
for i, current_key in enumerate(keys[:-1]):
|
|
150
|
+
next_key = keys[i + 1]
|
|
151
|
+
if next_key.isdigit():
|
|
152
|
+
target = target.setdefault(current_key, [])
|
|
153
|
+
else:
|
|
154
|
+
if isinstance(target, dict):
|
|
155
|
+
target = target.setdefault(current_key, {})
|
|
156
|
+
elif isinstance(target, list):
|
|
157
|
+
while len(target) <= int(current_key):
|
|
158
|
+
target.append({})
|
|
159
|
+
target = target[int(current_key)]
|
|
160
|
+
try:
|
|
161
|
+
if isinstance(target, list):
|
|
162
|
+
target.append(value)
|
|
163
|
+
else:
|
|
164
|
+
# If the value is a dictionary, add its components to the queue for processing
|
|
165
|
+
if isinstance(value, dict):
|
|
166
|
+
target = target.setdefault(keys[-1], {})
|
|
167
|
+
for inner_key, inner_value in value.items():
|
|
168
|
+
new_key = f"{path}{separator}{inner_key}"
|
|
169
|
+
queue.append((new_key, inner_value))
|
|
170
|
+
else:
|
|
171
|
+
target[keys[-1]] = value
|
|
172
|
+
except TypeError as exc:
|
|
173
|
+
raise ValueError(
|
|
174
|
+
f"Error unflattening dictionary, conflicting levels detected: {exc}"
|
|
175
|
+
)
|
|
176
|
+
return output
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def flatten_dict(data: Any, prefix: str = "", separator: str = ".") -> Dict[str, Any]:
|
|
180
|
+
"""
|
|
181
|
+
Recursively flatten a dictionary or list into a flat dictionary with dot-notation keys.
|
|
182
|
+
Handles nested dictionaries and arrays with proper indices.
|
|
183
|
+
|
|
184
|
+
example:
|
|
185
|
+
|
|
186
|
+
{
|
|
187
|
+
"A": {
|
|
188
|
+
"B": "1",
|
|
189
|
+
"C": "2"
|
|
190
|
+
},
|
|
191
|
+
"D": [
|
|
192
|
+
{"E": "3"},
|
|
193
|
+
{"E": "4"}
|
|
194
|
+
]
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
becomes
|
|
198
|
+
|
|
199
|
+
{
|
|
200
|
+
"A.B": "1",
|
|
201
|
+
"A.C": "2",
|
|
202
|
+
"D.0.E": "3",
|
|
203
|
+
"D.1.E": "4"
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
data: The data to flatten (dict, list, or scalar value)
|
|
208
|
+
prefix: The current key prefix (used in recursion)
|
|
209
|
+
separator: The separator to use between key segments (default: ".")
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
A flattened dictionary with dot-notation keys
|
|
213
|
+
"""
|
|
214
|
+
items = {}
|
|
215
|
+
|
|
216
|
+
if isinstance(data, dict):
|
|
217
|
+
for k, v in data.items():
|
|
218
|
+
new_key = f"{prefix}{separator}{k}" if prefix else k
|
|
219
|
+
if isinstance(v, (dict, list)):
|
|
220
|
+
items.update(flatten_dict(v, new_key, separator))
|
|
221
|
+
else:
|
|
222
|
+
items[new_key] = v
|
|
223
|
+
elif isinstance(data, list):
|
|
224
|
+
for i, v in enumerate(data):
|
|
225
|
+
new_key = f"{prefix}{separator}{i}"
|
|
226
|
+
if isinstance(v, (dict, list)):
|
|
227
|
+
items.update(flatten_dict(v, new_key, separator))
|
|
228
|
+
else:
|
|
229
|
+
items[new_key] = v
|
|
230
|
+
else:
|
|
231
|
+
raise ValueError(
|
|
232
|
+
f"Input to flatten_dict must be a dict or list, got {type(data).__name__}"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
return items
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def replace_none_arrays(
|
|
239
|
+
data: Any,
|
|
240
|
+
) -> Any:
|
|
241
|
+
"""
|
|
242
|
+
Recursively replace any arrays containing only None values with empty arrays.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
data: Any Python object (dict, list, etc.)
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
The transformed data structure with None-only arrays replaced by empty arrays
|
|
249
|
+
"""
|
|
250
|
+
if isinstance(data, dict):
|
|
251
|
+
# Process each key-value pair in dictionaries
|
|
252
|
+
return {k: replace_none_arrays(v) for k, v in data.items()}
|
|
253
|
+
|
|
254
|
+
if isinstance(data, list):
|
|
255
|
+
# Check if list contains only None values
|
|
256
|
+
if all(item is None for item in data):
|
|
257
|
+
return []
|
|
258
|
+
|
|
259
|
+
# Otherwise process each item in the list
|
|
260
|
+
return [replace_none_arrays(item) for item in data]
|
|
261
|
+
|
|
262
|
+
# Return other data types unchanged
|
|
263
|
+
return data
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import inspect
|
|
1
2
|
from abc import abstractmethod
|
|
2
3
|
from enum import Enum
|
|
3
4
|
from functools import wraps
|
|
@@ -81,12 +82,46 @@ def log_context(
|
|
|
81
82
|
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
82
83
|
context = dict(additional_context)
|
|
83
84
|
|
|
84
|
-
# extract specified param values from kwargs
|
|
85
|
+
# extract specified param values from kwargs and args
|
|
85
86
|
if capture_args:
|
|
87
|
+
# First, process kwargs as they're explicitly named
|
|
86
88
|
for arg_name, context_name in capture_args.items():
|
|
87
89
|
if arg_name in kwargs:
|
|
88
90
|
context[context_name.value] = kwargs[arg_name]
|
|
89
91
|
|
|
92
|
+
# Process args using signature binding for more robust parameter mapping
|
|
93
|
+
if args:
|
|
94
|
+
try:
|
|
95
|
+
# Get the signature and bind the arguments
|
|
96
|
+
sig = inspect.signature(func)
|
|
97
|
+
# This will map positional args to their parameter names correctly
|
|
98
|
+
bound_args = sig.bind_partial(*args, **kwargs)
|
|
99
|
+
|
|
100
|
+
# Now we can iterate through the bound arguments
|
|
101
|
+
for param_name, arg_value in bound_args.arguments.items():
|
|
102
|
+
# Only process if this parameter is in capture_args and wasn't already found in kwargs
|
|
103
|
+
if param_name in capture_args and param_name not in kwargs:
|
|
104
|
+
context_name = capture_args[param_name]
|
|
105
|
+
context[context_name.value] = arg_value
|
|
106
|
+
except TypeError:
|
|
107
|
+
# Handle the case where the arguments don't match the signature
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
# Handle default parameters that weren't provided in args or kwargs
|
|
111
|
+
if capture_args:
|
|
112
|
+
sig = inspect.signature(func)
|
|
113
|
+
for param_name, param in sig.parameters.items():
|
|
114
|
+
# Check if parameter has a default value and is in capture_args
|
|
115
|
+
# and hasn't been processed yet (not in context)
|
|
116
|
+
if (
|
|
117
|
+
param.default is not param.empty
|
|
118
|
+
and param_name in capture_args
|
|
119
|
+
and capture_args[param_name].value not in context
|
|
120
|
+
):
|
|
121
|
+
context_name = capture_args[param_name]
|
|
122
|
+
context[context_name.value] = param.default
|
|
123
|
+
|
|
124
|
+
# Process Contextualizable args
|
|
90
125
|
for arg in args:
|
|
91
126
|
if isinstance(arg, Contextualizable):
|
|
92
127
|
arg_context = arg.get_log_context()
|
fides/api/util/saas_util.py
CHANGED
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
import re
|
|
5
5
|
import socket
|
|
6
|
-
from collections import defaultdict
|
|
6
|
+
from collections import defaultdict
|
|
7
7
|
from ipaddress import IPv4Address, IPv6Address, ip_address
|
|
8
8
|
from typing import Any, Dict, List, Optional, Set, Tuple, Union
|
|
9
9
|
|
|
@@ -256,60 +256,6 @@ def merge_datasets(dataset: GraphDataset, config_dataset: GraphDataset) -> Graph
|
|
|
256
256
|
)
|
|
257
257
|
|
|
258
258
|
|
|
259
|
-
def unflatten_dict(flat_dict: Dict[str, Any], separator: str = ".") -> Dict[str, Any]:
|
|
260
|
-
"""
|
|
261
|
-
Converts a dictionary of paths/values into a nested dictionary
|
|
262
|
-
|
|
263
|
-
example:
|
|
264
|
-
|
|
265
|
-
{"A.B": "1", "A.C": "2"}
|
|
266
|
-
|
|
267
|
-
becomes
|
|
268
|
-
|
|
269
|
-
{
|
|
270
|
-
"A": {
|
|
271
|
-
"B": "1",
|
|
272
|
-
"C": "2"
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
"""
|
|
276
|
-
output: Dict[Any, Any] = {}
|
|
277
|
-
queue = deque(flat_dict.items())
|
|
278
|
-
|
|
279
|
-
while queue:
|
|
280
|
-
path, value = queue.popleft()
|
|
281
|
-
keys = path.split(separator)
|
|
282
|
-
target = output
|
|
283
|
-
for i, current_key in enumerate(keys[:-1]):
|
|
284
|
-
next_key = keys[i + 1]
|
|
285
|
-
if next_key.isdigit():
|
|
286
|
-
target = target.setdefault(current_key, [])
|
|
287
|
-
else:
|
|
288
|
-
if isinstance(target, dict):
|
|
289
|
-
target = target.setdefault(current_key, {})
|
|
290
|
-
elif isinstance(target, list):
|
|
291
|
-
while len(target) <= int(current_key):
|
|
292
|
-
target.append({})
|
|
293
|
-
target = target[int(current_key)]
|
|
294
|
-
try:
|
|
295
|
-
if isinstance(target, list):
|
|
296
|
-
target.append(value)
|
|
297
|
-
else:
|
|
298
|
-
# If the value is a dictionary, add its components to the queue for processing
|
|
299
|
-
if isinstance(value, dict):
|
|
300
|
-
target = target.setdefault(keys[-1], {})
|
|
301
|
-
for inner_key, inner_value in value.items():
|
|
302
|
-
new_key = f"{path}{separator}{inner_key}"
|
|
303
|
-
queue.append((new_key, inner_value))
|
|
304
|
-
else:
|
|
305
|
-
target[keys[-1]] = value
|
|
306
|
-
except TypeError as exc:
|
|
307
|
-
raise FidesopsException(
|
|
308
|
-
f"Error unflattening dictionary, conflicting levels detected: {exc}"
|
|
309
|
-
)
|
|
310
|
-
return output
|
|
311
|
-
|
|
312
|
-
|
|
313
259
|
def format_body(
|
|
314
260
|
headers: Dict[str, Any],
|
|
315
261
|
body: Optional[str],
|
|
@@ -339,7 +285,7 @@ def format_body(
|
|
|
339
285
|
if content_type == "application/json":
|
|
340
286
|
output = body
|
|
341
287
|
elif content_type == "application/x-www-form-urlencoded":
|
|
342
|
-
output =
|
|
288
|
+
output = nullsafe_urlencode(json.loads(body))
|
|
343
289
|
elif content_type == "text/plain":
|
|
344
290
|
output = body
|
|
345
291
|
else:
|
|
@@ -470,3 +416,33 @@ def replace_version(saas_config: str, new_version: str) -> str:
|
|
|
470
416
|
version_pattern, f"version: {new_version}", saas_config, count=1
|
|
471
417
|
)
|
|
472
418
|
return updated_config
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def nullsafe_urlencode(data: Any) -> str:
|
|
422
|
+
"""
|
|
423
|
+
Wrapper around multidimensional_urlencode that preserves null values as empty strings.
|
|
424
|
+
|
|
425
|
+
This is useful for APIs that expect keys with empty values (e.g., "name=") to represent
|
|
426
|
+
null values, rather than omitting the field entirely.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
data: The data to encode (can be a dict, list, or other nested structure)
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
URL-encoded string with null values properly handled
|
|
433
|
+
"""
|
|
434
|
+
|
|
435
|
+
def prepare_null_values(data: Any) -> Any:
|
|
436
|
+
"""
|
|
437
|
+
Recursively process data for URL encoding, converting None values to empty strings.
|
|
438
|
+
"""
|
|
439
|
+
if data is None:
|
|
440
|
+
return ""
|
|
441
|
+
if isinstance(data, dict):
|
|
442
|
+
return {k: prepare_null_values(v) for k, v in data.items()}
|
|
443
|
+
if isinstance(data, list):
|
|
444
|
+
return [prepare_null_values(item) for item in data]
|
|
445
|
+
return data
|
|
446
|
+
|
|
447
|
+
processed_data = prepare_null_values(data)
|
|
448
|
+
return multidimensional_urlencode(processed_data)
|
|
@@ -332,11 +332,20 @@ class PrivacyRequestService:
|
|
|
332
332
|
)
|
|
333
333
|
|
|
334
334
|
if _manual_approval_required(self.config_proxy, privacy_request):
|
|
335
|
-
self.
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
335
|
+
has_webhooks = self.db.query(PreApprovalWebhook).count() > 0
|
|
336
|
+
# If there are pre-approval webhooks, then we do nothing since the
|
|
337
|
+
# create_privacy_request method will have already triggered them,
|
|
338
|
+
# and they will be responsible of approving the request.
|
|
339
|
+
|
|
340
|
+
# If there are no pre-approval webhooks, approve the request immediately
|
|
341
|
+
# since it was originally approved by a person and we don't want them to
|
|
342
|
+
# have to manually re-approve
|
|
343
|
+
if not has_webhooks:
|
|
344
|
+
self.approve_privacy_requests(
|
|
345
|
+
[privacy_request_id],
|
|
346
|
+
reviewed_by=reviewed_by,
|
|
347
|
+
suppress_notification=True,
|
|
348
|
+
)
|
|
340
349
|
|
|
341
350
|
return privacy_request
|
|
342
351
|
|