ethyca-fides 2.66.1b0__py2.py3-none-any.whl → 2.66.1b1__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.66.1b0.dist-info → ethyca_fides-2.66.1b1.dist-info}/METADATA +1 -1
- {ethyca_fides-2.66.1b0.dist-info → ethyca_fides-2.66.1b1.dist-info}/RECORD +122 -122
- fides/_version.py +3 -3
- fides/api/api/v1/endpoints/dataset_config_endpoints.py +13 -5
- fides/api/api/v1/endpoints/user_endpoints.py +83 -7
- fides/api/graph/execution.py +30 -0
- fides/api/oauth/roles.py +2 -0
- fides/api/schemas/application_config.py +11 -1
- fides/api/service/connectors/base_connector.py +1 -0
- fides/api/service/connectors/bigquery_connector.py +67 -19
- fides/api/service/connectors/dynamodb_connector.py +2 -1
- fides/api/service/connectors/fides_connector.py +1 -0
- fides/api/service/connectors/http_connector.py +1 -0
- fides/api/service/connectors/manual_task_connector.py +1 -0
- fides/api/service/connectors/manual_webhook_connector.py +2 -1
- fides/api/service/connectors/mongodb_connector.py +1 -0
- fides/api/service/connectors/okta_connector.py +1 -0
- fides/api/service/connectors/query_configs/bigquery_query_config.py +45 -20
- fides/api/service/connectors/rds_mysql_connector.py +1 -0
- fides/api/service/connectors/rds_postgres_connector.py +1 -0
- fides/api/service/connectors/s3_connector.py +1 -0
- fides/api/service/connectors/saas_connector.py +1 -0
- fides/api/service/connectors/scylla_connector.py +1 -0
- fides/api/service/connectors/sql_connector.py +36 -4
- fides/api/service/connectors/website_connector.py +1 -0
- fides/api/task/deprecated_graph_task.py +24 -6
- fides/api/task/execute_request_tasks.py +88 -11
- fides/api/task/graph_task.py +11 -0
- fides/api/task/manual/manual_task_graph_task.py +1 -0
- fides/common/api/scope_registry.py +3 -0
- fides/config/utils.py +1 -0
- fides/ui-build/static/admin/404.html +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/{_app-f66151e613766714.js → _app-a152f52351c84ef4.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/lib/fides-preview.js +1 -1
- fides/ui-build/static/admin/lib/fides-tcf.js +3 -3
- fides/ui-build/static/admin/lib/fides.js +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/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
- {ethyca_fides-2.66.1b0.dist-info → ethyca_fides-2.66.1b1.dist-info}/WHEEL +0 -0
- {ethyca_fides-2.66.1b0.dist-info → ethyca_fides-2.66.1b1.dist-info}/entry_points.txt +0 -0
- {ethyca_fides-2.66.1b0.dist-info → ethyca_fides-2.66.1b1.dist-info}/licenses/LICENSE +0 -0
- {ethyca_fides-2.66.1b0.dist-info → ethyca_fides-2.66.1b1.dist-info}/top_level.txt +0 -0
- /fides/ui-build/static/admin/_next/static/{jeMhTiDcCKPk_H4S0nSQq → cUz9aQNEfv77_K6F0m_Ja}/_buildManifest.js +0 -0
- /fides/ui-build/static/admin/_next/static/{jeMhTiDcCKPk_H4S0nSQq → cUz9aQNEfv77_K6F0m_Ja}/_ssgManifest.js +0 -0
|
@@ -6,6 +6,7 @@ from typing import List, Optional
|
|
|
6
6
|
|
|
7
7
|
import jose.exceptions
|
|
8
8
|
from fastapi import Depends, HTTPException, Security
|
|
9
|
+
from fastapi.security import SecurityScopes
|
|
9
10
|
from fastapi_pagination import Page, Params
|
|
10
11
|
from fastapi_pagination.bases import AbstractPage
|
|
11
12
|
from fastapi_pagination.ext.sqlalchemy import paginate
|
|
@@ -39,7 +40,9 @@ from fides.api.oauth.roles import APPROVER, VIEWER
|
|
|
39
40
|
from fides.api.oauth.utils import (
|
|
40
41
|
create_temporary_user_for_login_flow,
|
|
41
42
|
extract_payload,
|
|
43
|
+
extract_token_and_load_client,
|
|
42
44
|
get_current_user,
|
|
45
|
+
has_permissions,
|
|
43
46
|
oauth2_scheme,
|
|
44
47
|
verify_oauth_client,
|
|
45
48
|
)
|
|
@@ -65,6 +68,7 @@ from fides.common.api.scope_registry import (
|
|
|
65
68
|
USER_DELETE,
|
|
66
69
|
USER_PASSWORD_RESET,
|
|
67
70
|
USER_READ,
|
|
71
|
+
USER_READ_OWN,
|
|
68
72
|
USER_UPDATE,
|
|
69
73
|
)
|
|
70
74
|
from fides.common.api.v1 import urn_registry as urls
|
|
@@ -107,6 +111,37 @@ def _validate_current_user(user_id: str, user_from_token: FidesUser) -> None:
|
|
|
107
111
|
)
|
|
108
112
|
|
|
109
113
|
|
|
114
|
+
def verify_user_read_scopes(
|
|
115
|
+
authorization: str = Security(oauth2_scheme),
|
|
116
|
+
db: Session = Depends(get_db),
|
|
117
|
+
) -> ClientDetail:
|
|
118
|
+
"""
|
|
119
|
+
Custom dependency that verifies the user has either USER_READ or USER_READ_OWN scope.
|
|
120
|
+
Returns the client if authorized.
|
|
121
|
+
"""
|
|
122
|
+
token_data, client = extract_token_and_load_client(authorization, db)
|
|
123
|
+
|
|
124
|
+
# Try USER_READ first
|
|
125
|
+
if has_permissions(
|
|
126
|
+
token_data=token_data,
|
|
127
|
+
client=client,
|
|
128
|
+
endpoint_scopes=SecurityScopes([USER_READ]),
|
|
129
|
+
):
|
|
130
|
+
return client
|
|
131
|
+
|
|
132
|
+
if has_permissions(
|
|
133
|
+
token_data=token_data,
|
|
134
|
+
client=client,
|
|
135
|
+
endpoint_scopes=SecurityScopes([USER_READ_OWN]),
|
|
136
|
+
):
|
|
137
|
+
return client
|
|
138
|
+
|
|
139
|
+
raise HTTPException(
|
|
140
|
+
status_code=HTTP_403_FORBIDDEN,
|
|
141
|
+
detail="Not authorized.",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
110
145
|
@router.put(
|
|
111
146
|
urls.USER_DETAIL,
|
|
112
147
|
dependencies=[Security(verify_oauth_client)],
|
|
@@ -498,14 +533,38 @@ def delete_user(
|
|
|
498
533
|
|
|
499
534
|
@router.get(
|
|
500
535
|
urls.USER_DETAIL,
|
|
501
|
-
dependencies=[Security(
|
|
536
|
+
dependencies=[Security(verify_user_read_scopes)],
|
|
502
537
|
response_model=UserResponse,
|
|
503
538
|
)
|
|
504
|
-
def get_user(
|
|
505
|
-
|
|
539
|
+
def get_user(
|
|
540
|
+
*,
|
|
541
|
+
db: Session = Depends(get_db),
|
|
542
|
+
user_id: str,
|
|
543
|
+
client: ClientDetail = Security(verify_user_read_scopes),
|
|
544
|
+
authorization: str = Security(oauth2_scheme),
|
|
545
|
+
) -> FidesUser:
|
|
546
|
+
"""Returns a User based on an Id. Users with USER_READ_OWN scope can only access their own data."""
|
|
506
547
|
user: Optional[FidesUser] = FidesUser.get_by_key_or_id(db, data={"id": user_id})
|
|
507
548
|
if user is None:
|
|
508
549
|
raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail="User not found")
|
|
550
|
+
token_data, _ = extract_token_and_load_client(authorization, db)
|
|
551
|
+
# Check if user has USER_READ_OWN scope and is trying to access someone else's data
|
|
552
|
+
# The verify_user_read_scopes dependency already verified the user has either USER_READ or USER_READ_OWN
|
|
553
|
+
# We need to check if they have USER_READ_OWN and are accessing their own data
|
|
554
|
+
if has_permissions(
|
|
555
|
+
token_data=token_data,
|
|
556
|
+
client=client,
|
|
557
|
+
endpoint_scopes=SecurityScopes([USER_READ]),
|
|
558
|
+
):
|
|
559
|
+
logger.debug("Returning user with id: '{}'.", user_id)
|
|
560
|
+
return user
|
|
561
|
+
|
|
562
|
+
# User has USER_READ_OWN scope, check if they're accessing their own data
|
|
563
|
+
if user.id != client.user_id:
|
|
564
|
+
raise HTTPException(
|
|
565
|
+
status_code=HTTP_403_FORBIDDEN,
|
|
566
|
+
detail="You can only access your own user data with USER_READ_OWN scope.",
|
|
567
|
+
)
|
|
509
568
|
|
|
510
569
|
logger.debug("Returning user with id: '{}'.", user_id)
|
|
511
570
|
return user
|
|
@@ -513,7 +572,7 @@ def get_user(*, db: Session = Depends(get_db), user_id: str) -> FidesUser:
|
|
|
513
572
|
|
|
514
573
|
@router.get(
|
|
515
574
|
urls.USERS,
|
|
516
|
-
dependencies=[Security(
|
|
575
|
+
dependencies=[Security(verify_user_read_scopes)],
|
|
517
576
|
response_model=Page[UserResponse],
|
|
518
577
|
)
|
|
519
578
|
def get_users(
|
|
@@ -521,11 +580,28 @@ def get_users(
|
|
|
521
580
|
db: Session = Depends(get_db),
|
|
522
581
|
params: Params = Depends(),
|
|
523
582
|
username: Optional[str] = None,
|
|
583
|
+
client: ClientDetail = Security(verify_user_read_scopes),
|
|
584
|
+
authorization: str = Security(oauth2_scheme),
|
|
524
585
|
) -> AbstractPage[FidesUser]:
|
|
525
|
-
"""Returns a paginated list of
|
|
586
|
+
"""Returns a paginated list of users. Users with USER_READ_OWN scope only see their own data."""
|
|
526
587
|
query = FidesUser.query(db)
|
|
527
|
-
|
|
528
|
-
|
|
588
|
+
|
|
589
|
+
# Check if user has USER_READ_OWN scope and filter accordingly
|
|
590
|
+
# The verify_user_read_scopes dependency already verified the user has either USER_READ or USER_READ_OWN
|
|
591
|
+
token_data, _ = extract_token_and_load_client(authorization, db)
|
|
592
|
+
if has_permissions(
|
|
593
|
+
token_data=token_data,
|
|
594
|
+
client=client,
|
|
595
|
+
endpoint_scopes=SecurityScopes([USER_READ]),
|
|
596
|
+
):
|
|
597
|
+
# User has USER_READ scope, can see all users
|
|
598
|
+
if username:
|
|
599
|
+
query = query.filter(FidesUser.username.ilike(f"%{escape_like(username)}%"))
|
|
600
|
+
else:
|
|
601
|
+
# User has USER_READ_OWN scope, only show their own data
|
|
602
|
+
query = query.filter(FidesUser.id == client.user_id)
|
|
603
|
+
if username:
|
|
604
|
+
query = query.filter(FidesUser.username.ilike(f"%{escape_like(username)}%"))
|
|
529
605
|
|
|
530
606
|
logger.debug("Returning a paginated list of users.")
|
|
531
607
|
|
fides/api/graph/execution.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from typing import Any, Callable, Dict, List, Optional, Set, Tuple
|
|
2
2
|
|
|
3
3
|
from fideslang.validation import FidesKey
|
|
4
|
+
from loguru import logger
|
|
4
5
|
|
|
5
6
|
from fides.api.graph.config import (
|
|
6
7
|
Collection,
|
|
@@ -117,6 +118,8 @@ class ExecutionNode(Contextualizable): # pylint: disable=too-many-instance-attr
|
|
|
117
118
|
The values are cast based on field types, if those types are specified.
|
|
118
119
|
"""
|
|
119
120
|
out = {}
|
|
121
|
+
failed_conversions: Dict[str, Dict[str, Any]] = {}
|
|
122
|
+
|
|
120
123
|
for key, values in input_data.items():
|
|
121
124
|
path: FieldPath = FieldPath.parse(key)
|
|
122
125
|
field: Optional[Field] = self.collection.field(path)
|
|
@@ -126,4 +129,31 @@ class ExecutionNode(Contextualizable): # pylint: disable=too-many-instance-attr
|
|
|
126
129
|
filtered = list(filter(lambda x: x is not None, cast_values))
|
|
127
130
|
if filtered:
|
|
128
131
|
out[key] = filtered
|
|
132
|
+
elif values: # Had input values but all failed conversion
|
|
133
|
+
# Track conversion failures for logging
|
|
134
|
+
failed_conversions[key] = {"field": field, "input_values": values}
|
|
135
|
+
|
|
136
|
+
# Log conversion failures if any occurred
|
|
137
|
+
if failed_conversions:
|
|
138
|
+
field_details = []
|
|
139
|
+
for field_name, failure_info in failed_conversions.items():
|
|
140
|
+
field = failure_info["field"]
|
|
141
|
+
values = failure_info["input_values"]
|
|
142
|
+
|
|
143
|
+
expected_type = field.data_type() if field else "unknown"
|
|
144
|
+
actual_types = {type(v).__name__ for v in values if v is not None}
|
|
145
|
+
actual_type_str = (
|
|
146
|
+
", ".join(sorted(actual_types)) if actual_types else "NoneType"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
field_details.append(
|
|
150
|
+
f"{field_name} (expected: {expected_type}, got: {actual_type_str})"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
logger.warning(
|
|
154
|
+
"Type conversion failures for {}: {}",
|
|
155
|
+
self.address,
|
|
156
|
+
"; ".join(field_details),
|
|
157
|
+
)
|
|
158
|
+
|
|
129
159
|
return out
|
fides/api/oauth/roles.py
CHANGED
|
@@ -47,6 +47,7 @@ from fides.common.api.scope_registry import (
|
|
|
47
47
|
SYSTEM_READ,
|
|
48
48
|
USER_PERMISSION_ASSIGN_OWNERS,
|
|
49
49
|
USER_READ,
|
|
50
|
+
USER_READ_OWN,
|
|
50
51
|
WEBHOOK_READ,
|
|
51
52
|
)
|
|
52
53
|
|
|
@@ -126,6 +127,7 @@ viewer_scopes = [ # Intentionally omitted USER_PERMISSION_READ and PRIVACY_REQU
|
|
|
126
127
|
|
|
127
128
|
respondent_scopes = [
|
|
128
129
|
PRIVACY_REQUEST_MANUAL_STEPS_RESPOND, # allows respondents to respond to assigned manual steps
|
|
130
|
+
USER_READ_OWN,
|
|
129
131
|
]
|
|
130
132
|
|
|
131
133
|
external_respondent_scopes = [
|
|
@@ -11,6 +11,14 @@ from fides.api.schemas.messaging.messaging import MessagingServiceType
|
|
|
11
11
|
from fides.config.admin_ui_settings import ErrorNotificationMode
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
class SqlDryRunMode(str, Enum):
|
|
15
|
+
"""SQL dry run mode for controlling execution of SQL statements in privacy requests"""
|
|
16
|
+
|
|
17
|
+
none = "none"
|
|
18
|
+
access = "access"
|
|
19
|
+
erasure = "erasure"
|
|
20
|
+
|
|
21
|
+
|
|
14
22
|
class StorageTypeApiAccepted(Enum):
|
|
15
23
|
"""Enum for storage destination types accepted in API updates"""
|
|
16
24
|
|
|
@@ -62,7 +70,9 @@ class ExecutionApplicationConfig(FidesSchema):
|
|
|
62
70
|
subject_identity_verification_required: Optional[bool] = None
|
|
63
71
|
disable_consent_identity_verification: Optional[bool] = None
|
|
64
72
|
require_manual_request_approval: Optional[bool] = None
|
|
65
|
-
|
|
73
|
+
sql_dry_run: Optional[SqlDryRunMode] = None
|
|
74
|
+
|
|
75
|
+
model_config = ConfigDict(use_enum_values=True, extra="forbid")
|
|
66
76
|
|
|
67
77
|
|
|
68
78
|
class AdminUIConfig(FidesSchema):
|
|
@@ -82,6 +82,7 @@ class BaseConnector(Generic[DB_CONNECTOR_TYPE], ABC):
|
|
|
82
82
|
privacy_request: PrivacyRequest,
|
|
83
83
|
request_task: RequestTask,
|
|
84
84
|
rows: List[Row],
|
|
85
|
+
input_data: Optional[Dict[str, List[Any]]] = None,
|
|
85
86
|
) -> int:
|
|
86
87
|
"""Execute a masking request. Return the number of rows that have been updated
|
|
87
88
|
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
from typing import List, Optional
|
|
1
|
+
from typing import Any, Dict, List, Optional
|
|
2
2
|
|
|
3
3
|
from loguru import logger
|
|
4
4
|
from sqlalchemy import text
|
|
5
|
-
from sqlalchemy.engine import
|
|
6
|
-
Connection,
|
|
7
|
-
Engine,
|
|
8
|
-
LegacyCursorResult,
|
|
9
|
-
create_engine,
|
|
10
|
-
)
|
|
5
|
+
from sqlalchemy.engine import Connection, Engine, create_engine # type: ignore
|
|
11
6
|
from sqlalchemy.orm import Session
|
|
12
7
|
from sqlalchemy.sql import Executable # type: ignore
|
|
13
8
|
from sqlalchemy.sql.elements import TextClause
|
|
@@ -17,6 +12,7 @@ from fides.api.graph.execution import ExecutionNode
|
|
|
17
12
|
from fides.api.models.connectionconfig import ConnectionTestStatus
|
|
18
13
|
from fides.api.models.policy import Policy
|
|
19
14
|
from fides.api.models.privacy_request import PrivacyRequest, RequestTask
|
|
15
|
+
from fides.api.schemas.application_config import SqlDryRunMode
|
|
20
16
|
from fides.api.schemas.connection_configuration.connection_secrets_bigquery import (
|
|
21
17
|
BigQuerySchema,
|
|
22
18
|
)
|
|
@@ -99,6 +95,16 @@ class BigQueryConnector(SQLConnector):
|
|
|
99
95
|
logger.info(
|
|
100
96
|
f"Executing {len(partition_clauses)} partition queries for node '{query_config.node.address}' in DSR execution"
|
|
101
97
|
)
|
|
98
|
+
|
|
99
|
+
if self.should_dry_run(SqlDryRunMode.access):
|
|
100
|
+
for partition_clause in partition_clauses:
|
|
101
|
+
existing_bind_params = stmt.compile().params
|
|
102
|
+
partitioned_stmt = text(
|
|
103
|
+
f"{stmt} AND ({text(partition_clause)})"
|
|
104
|
+
).params(existing_bind_params)
|
|
105
|
+
logger.warning(f"SQL DRY RUN - Would execute SQL: {partitioned_stmt}")
|
|
106
|
+
return []
|
|
107
|
+
|
|
102
108
|
rows = []
|
|
103
109
|
for partition_clause in partition_clauses:
|
|
104
110
|
logger.debug(
|
|
@@ -135,6 +141,39 @@ class BigQueryConnector(SQLConnector):
|
|
|
135
141
|
logger.exception(f"Error testing connection to remote BigQuery {str(e)}")
|
|
136
142
|
raise ConnectionException(f"Connection error: {e}")
|
|
137
143
|
|
|
144
|
+
def _execute_statements_with_sql_dry_run(
|
|
145
|
+
self,
|
|
146
|
+
statements: List[Executable],
|
|
147
|
+
sql_dry_run_enabled: bool,
|
|
148
|
+
client: Engine,
|
|
149
|
+
) -> int:
|
|
150
|
+
"""
|
|
151
|
+
Execute SQL statements with sql_dry_run support.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
statements: List of SQL statements to execute
|
|
155
|
+
sql_dry_run_enabled: Whether sql_dry_run mode is enabled
|
|
156
|
+
client: Database engine
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
int: Number of affected rows (0 in sql_dry_run mode)
|
|
160
|
+
"""
|
|
161
|
+
if not statements:
|
|
162
|
+
return 0
|
|
163
|
+
|
|
164
|
+
if sql_dry_run_enabled:
|
|
165
|
+
for stmt in statements:
|
|
166
|
+
logger.warning(f"SQL DRY RUN - Would execute SQL: {stmt}")
|
|
167
|
+
return 0
|
|
168
|
+
|
|
169
|
+
row_count = 0
|
|
170
|
+
with client.connect() as connection:
|
|
171
|
+
for stmt in statements:
|
|
172
|
+
results = connection.execute(stmt)
|
|
173
|
+
logger.debug(f"Affected {results.rowcount} rows")
|
|
174
|
+
row_count += results.rowcount
|
|
175
|
+
return row_count
|
|
176
|
+
|
|
138
177
|
def mask_data(
|
|
139
178
|
self,
|
|
140
179
|
node: ExecutionNode,
|
|
@@ -142,22 +181,31 @@ class BigQueryConnector(SQLConnector):
|
|
|
142
181
|
privacy_request: PrivacyRequest,
|
|
143
182
|
request_task: RequestTask,
|
|
144
183
|
rows: List[Row],
|
|
184
|
+
input_data: Optional[Dict[str, List[Any]]] = None,
|
|
145
185
|
) -> int:
|
|
146
186
|
"""Execute a masking request. Returns the number of records updated or deleted"""
|
|
187
|
+
|
|
147
188
|
query_config = self.query_config(node)
|
|
148
189
|
update_or_delete_ct = 0
|
|
149
190
|
client = self.client()
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
191
|
+
|
|
192
|
+
if query_config.uses_delete_masking_strategy():
|
|
193
|
+
delete_stmts = query_config.generate_delete(client, input_data or {})
|
|
194
|
+
logger.debug(f"Generated {len(delete_stmts)} DELETE statements")
|
|
195
|
+
update_or_delete_ct += self._execute_statements_with_sql_dry_run(
|
|
196
|
+
delete_stmts, self.should_dry_run(SqlDryRunMode.erasure), client
|
|
155
197
|
)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
198
|
+
else:
|
|
199
|
+
for row in rows:
|
|
200
|
+
update_or_delete_stmts: List[Executable] = query_config.generate_update(
|
|
201
|
+
row, policy, privacy_request, client
|
|
202
|
+
)
|
|
203
|
+
logger.debug(
|
|
204
|
+
f"Generated {len(update_or_delete_stmts)} UPDATE statements"
|
|
205
|
+
)
|
|
206
|
+
update_or_delete_ct += self._execute_statements_with_sql_dry_run(
|
|
207
|
+
update_or_delete_stmts,
|
|
208
|
+
self.should_dry_run(SqlDryRunMode.erasure),
|
|
209
|
+
client,
|
|
210
|
+
)
|
|
163
211
|
return update_or_delete_ct
|
|
@@ -140,8 +140,9 @@ class DynamoDBConnector(BaseConnector[Any]): # type: ignore
|
|
|
140
140
|
privacy_request: PrivacyRequest,
|
|
141
141
|
request_task: RequestTask,
|
|
142
142
|
rows: List[Row],
|
|
143
|
+
input_data: Optional[Dict[str, List[Any]]] = None,
|
|
143
144
|
) -> int:
|
|
144
|
-
"""Execute a masking
|
|
145
|
+
"""Execute a masking request for DynamoDB"""
|
|
145
146
|
|
|
146
147
|
query_config = self.query_config(node)
|
|
147
148
|
collection_name = node.address.collection
|
|
@@ -142,6 +142,7 @@ class FidesConnector(BaseConnector[FidesClient]):
|
|
|
142
142
|
privacy_request: PrivacyRequest,
|
|
143
143
|
request_task: RequestTask,
|
|
144
144
|
rows: List[Row],
|
|
145
|
+
input_data: Optional[Dict[str, List[Any]]] = None,
|
|
145
146
|
) -> int:
|
|
146
147
|
"""Execute an erasure request on remote fides"""
|
|
147
148
|
identity_data = {
|
|
@@ -90,6 +90,7 @@ class HTTPSConnector(BaseConnector[None]):
|
|
|
90
90
|
privacy_request: PrivacyRequest,
|
|
91
91
|
request_task: RequestTask,
|
|
92
92
|
rows: List[Row],
|
|
93
|
+
input_data: Optional[Dict[str, List[Any]]] = None,
|
|
93
94
|
) -> int:
|
|
94
95
|
"""Currently not supported as webhooks are not called at the collection level"""
|
|
95
96
|
raise NotImplementedError(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Any, Dict, List
|
|
1
|
+
from typing import Any, Dict, List, Optional
|
|
2
2
|
|
|
3
3
|
from fides.api.graph.execution import ExecutionNode
|
|
4
4
|
from fides.api.models.connectionconfig import ConnectionConfig, ConnectionTestStatus
|
|
@@ -53,6 +53,7 @@ class ManualWebhookConnector(BaseConnector[None]):
|
|
|
53
53
|
privacy_request: PrivacyRequest,
|
|
54
54
|
request_task: RequestTask,
|
|
55
55
|
rows: List[Row],
|
|
56
|
+
input_data: Optional[List[List[Row]]] = None,
|
|
56
57
|
) -> None:
|
|
57
58
|
"""
|
|
58
59
|
Not applicable for a manual webhook. Manual webhooks are not called as part of the traversal.
|
|
@@ -126,6 +126,7 @@ class MongoDBConnector(BaseConnector[MongoClient]):
|
|
|
126
126
|
privacy_request: PrivacyRequest,
|
|
127
127
|
request_task: RequestTask,
|
|
128
128
|
rows: List[Row],
|
|
129
|
+
input_data: Optional[Dict[str, List[Any]]] = None,
|
|
129
130
|
) -> int:
|
|
130
131
|
"""Execute a masking request"""
|
|
131
132
|
query_config = self.query_config(node)
|
|
@@ -3,7 +3,7 @@ from typing import Any, Dict, List, Optional, Union, cast
|
|
|
3
3
|
import pydash
|
|
4
4
|
from fideslang.models import MaskingStrategies
|
|
5
5
|
from loguru import logger
|
|
6
|
-
from sqlalchemy import MetaData, Table, text
|
|
6
|
+
from sqlalchemy import MetaData, Table, or_, text
|
|
7
7
|
from sqlalchemy.engine import Engine
|
|
8
8
|
from sqlalchemy.sql import Delete, Update
|
|
9
9
|
from sqlalchemy.sql.elements import ColumnElement, TextClause
|
|
@@ -125,6 +125,7 @@ class BigQueryQueryConfig(QueryStringWithoutTuplesOverrideQueryConfig):
|
|
|
125
125
|
policy: Policy,
|
|
126
126
|
request: PrivacyRequest,
|
|
127
127
|
client: Engine,
|
|
128
|
+
input_data: Optional[Dict[str, List[Any]]] = None,
|
|
128
129
|
) -> Union[List[Update], List[Delete]]:
|
|
129
130
|
"""
|
|
130
131
|
Generate a masking statement for BigQuery.
|
|
@@ -137,7 +138,7 @@ class BigQueryQueryConfig(QueryStringWithoutTuplesOverrideQueryConfig):
|
|
|
137
138
|
logger.info(
|
|
138
139
|
f"Masking override detected for collection {node.address.value}: {masking_override.strategy.value}"
|
|
139
140
|
)
|
|
140
|
-
return self.generate_delete(
|
|
141
|
+
return self.generate_delete(client, input_data or {})
|
|
141
142
|
return self.generate_update(row, policy, request, client)
|
|
142
143
|
|
|
143
144
|
def generate_update(
|
|
@@ -218,27 +219,30 @@ class BigQueryQueryConfig(QueryStringWithoutTuplesOverrideQueryConfig):
|
|
|
218
219
|
|
|
219
220
|
return [table.update().where(*where_clauses).values(**final_update_map)]
|
|
220
221
|
|
|
221
|
-
def generate_delete(
|
|
222
|
-
|
|
222
|
+
def generate_delete(
|
|
223
|
+
self,
|
|
224
|
+
client: Engine,
|
|
225
|
+
input_data: Optional[Dict[str, List[Any]]] = None,
|
|
226
|
+
) -> List[Delete]:
|
|
227
|
+
"""
|
|
228
|
+
Returns a List of SQLAlchemy DELETE statements for BigQuery. Does not actually execute the delete statement.
|
|
223
229
|
|
|
224
230
|
Used when a collection-level masking override is present and the masking strategy is DELETE.
|
|
225
231
|
|
|
226
232
|
A List of multiple DELETE statements are returned for partitioned tables; for a non-partitioned table,
|
|
227
233
|
a single DELETE statement is returned in a List for consistent typing.
|
|
228
|
-
|
|
229
|
-
TODO: DRY up this method and `generate_update` a bit
|
|
230
234
|
"""
|
|
231
235
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
)
|
|
236
|
+
if not input_data:
|
|
237
|
+
logger.warning(
|
|
238
|
+
"No input data provided for node '{}', skipping DELETE statement generation",
|
|
239
|
+
self.node.address,
|
|
240
|
+
)
|
|
241
|
+
return []
|
|
239
242
|
|
|
240
|
-
|
|
241
|
-
|
|
243
|
+
filtered_data = self.node.typed_filtered_values(input_data)
|
|
244
|
+
|
|
245
|
+
if not filtered_data:
|
|
242
246
|
logger.warning(
|
|
243
247
|
"There is not enough data to generate a valid DELETE statement for {}",
|
|
244
248
|
self.node.address,
|
|
@@ -246,9 +250,17 @@ class BigQueryQueryConfig(QueryStringWithoutTuplesOverrideQueryConfig):
|
|
|
246
250
|
return []
|
|
247
251
|
|
|
248
252
|
table = Table(self._generate_table_name(), MetaData(bind=client), autoload=True)
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
]
|
|
253
|
+
|
|
254
|
+
# Build individual reference clauses
|
|
255
|
+
where_clauses: List[ColumnElement] = []
|
|
256
|
+
for column_name, values in filtered_data.items():
|
|
257
|
+
if len(values) == 1:
|
|
258
|
+
where_clauses.append(getattr(table.c, column_name) == values[0])
|
|
259
|
+
else:
|
|
260
|
+
where_clauses.append(getattr(table.c, column_name).in_(values))
|
|
261
|
+
|
|
262
|
+
# Combine reference clauses with OR instead of AND
|
|
263
|
+
combined_reference_clause = or_(*where_clauses)
|
|
252
264
|
|
|
253
265
|
if self.partitioning:
|
|
254
266
|
partition_clauses = self.get_partition_clauses()
|
|
@@ -259,12 +271,25 @@ class BigQueryQueryConfig(QueryStringWithoutTuplesOverrideQueryConfig):
|
|
|
259
271
|
|
|
260
272
|
for partition_clause in partition_clauses:
|
|
261
273
|
partitioned_queries.append(
|
|
262
|
-
table.delete()
|
|
274
|
+
table.delete()
|
|
275
|
+
.where(combined_reference_clause)
|
|
276
|
+
.where(text(partition_clause))
|
|
263
277
|
)
|
|
264
278
|
|
|
265
279
|
return partitioned_queries
|
|
266
280
|
|
|
267
|
-
return [table.delete().where(
|
|
281
|
+
return [table.delete().where(combined_reference_clause)]
|
|
282
|
+
|
|
283
|
+
def uses_delete_masking_strategy(self) -> bool:
|
|
284
|
+
"""Check if this collection uses DELETE masking strategy.
|
|
285
|
+
|
|
286
|
+
Returns True if masking override is present and strategy is DELETE.
|
|
287
|
+
"""
|
|
288
|
+
masking_override = self.node.collection.masking_strategy_override
|
|
289
|
+
return (
|
|
290
|
+
masking_override is not None
|
|
291
|
+
and masking_override.strategy == MaskingStrategies.DELETE
|
|
292
|
+
)
|
|
268
293
|
|
|
269
294
|
def format_fields_for_query(
|
|
270
295
|
self,
|
|
@@ -158,6 +158,7 @@ class RDSMySQLConnector(RDSConnectorMixin, SQLConnector):
|
|
|
158
158
|
privacy_request: PrivacyRequest,
|
|
159
159
|
request_task: RequestTask,
|
|
160
160
|
rows: List[Row],
|
|
161
|
+
input_data: Optional[Dict[str, List[Any]]] = None,
|
|
161
162
|
) -> int:
|
|
162
163
|
"""DSR execution not yet supported for RDS MySQL"""
|
|
163
164
|
return 0
|
|
@@ -147,6 +147,7 @@ class RDSPostgresConnector(RDSConnectorMixin, SQLConnector):
|
|
|
147
147
|
privacy_request: PrivacyRequest,
|
|
148
148
|
request_task: RequestTask,
|
|
149
149
|
rows: List[Row],
|
|
150
|
+
input_data: Optional[Dict[str, List[Any]]] = None,
|
|
150
151
|
) -> int:
|
|
151
152
|
"""DSR execution not yet supported for RDS Postgres"""
|
|
152
153
|
return 0
|
|
@@ -517,6 +517,7 @@ class SaaSConnector(BaseConnector[AuthenticatedClient], Contextualizable):
|
|
|
517
517
|
privacy_request: PrivacyRequest,
|
|
518
518
|
request_task: RequestTask,
|
|
519
519
|
rows: List[Row],
|
|
520
|
+
input_data: Optional[Dict[str, List[Any]]] = None,
|
|
520
521
|
) -> int:
|
|
521
522
|
"""Execute a masking request. Return the number of rows that have been updated."""
|
|
522
523
|
self.set_privacy_request_state(privacy_request, node, request_task)
|
|
@@ -150,6 +150,7 @@ class ScyllaConnector(BaseConnector[Cluster]):
|
|
|
150
150
|
privacy_request: PrivacyRequest,
|
|
151
151
|
request_task: RequestTask,
|
|
152
152
|
rows: List[Row],
|
|
153
|
+
input_data: Optional[Dict[str, List[Any]]] = None,
|
|
153
154
|
) -> int:
|
|
154
155
|
"""Execute a masking request"""
|
|
155
156
|
query_config = self.query_config(node)
|