Skip to content

Commit

Permalink
Adding privacy_request_id placeholder (#911)
Browse files Browse the repository at this point in the history
  • Loading branch information
galvana authored Jul 20, 2022
1 parent abb319f commit 8068597
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 24 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ The types of changes are:
* Erasure support for Zendesk [#775](https://github.com/ethyca/fidesops/pull/775)
* Adds endpoint to get the secrets required for different connectors [#795](https://github.com/ethyca/fidesops/pull/795)
* Adds Vault for secrets management [#688](https://github.com/ethyca/fidesops/pull/869)
* Adds privacy_request_id placeholder to use in SaaS configs [#911](https://github.com/ethyca/fidesops/pull/911)

### Changed

Expand Down
18 changes: 18 additions & 0 deletions data/saas/config/saas_example_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,21 @@ saas_config:
{
<all_object_fields>
}
- name: data_management
requests:
read:
method: GET
path: /v1/privacy_request/<privacy_request_id>
param_values:
- name: placeholder
identity: email
update:
method: POST
path: /v1/privacy_request/
param_values:
- name: placeholder
identity: email
body: |
{
"unique_id": "<privacy_request_id>"
}
35 changes: 22 additions & 13 deletions data/saas/dataset/saas_example_dataset.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,15 +90,18 @@ dataset:
fidesops_meta:
data_type: string
- name: zip
data_categories: [user.provided.identifiable.contact.postal_code]
data_categories:
[user.provided.identifiable.contact.postal_code]
fidesops_meta:
data_type: string
- name: country
data_categories: [user.provided.identifiable.contact.country]
data_categories:
[user.provided.identifiable.contact.country]
fidesops_meta:
data_type: string
- name: PHONE
data_categories: [user.provided.identifiable.contact.phone_number]
data_categories:
[user.provided.identifiable.contact.phone_number]
fidesops_meta:
data_type: string
- name: BIRTHDAY
Expand Down Expand Up @@ -154,9 +157,9 @@ dataset:
- name: projects
fields:
- name: id
data_categories: [ system.operations ]
data_categories: [system.operations]
- name: slug
data_categories: [ system.operations ]
data_categories: [system.operations]
- name: organization
fields:
- name: slug
Expand All @@ -165,21 +168,21 @@ dataset:
- name: users
fields:
- name: id
data_categories: [ system.operations ]
data_categories: [system.operations]
- name: name
data_categories: [ user.provided.identifiable.name ]
data_categories: [user.provided.identifiable.name]
- name: user
fields:
- name: username
data_categories: [ user.provided.identifiable.name ]
data_categories: [user.provided.identifiable.name]
- name: email
data_categories: [ user.provided.identifiable.contact.email ]
data_categories: [user.provided.identifiable.contact.email]
- name: name
data_categories: [ user.provided.identifiable.name ]
data_categories: [user.provided.identifiable.name]
- name: ipAddress
data_categories: [ user.derived.identifiable.device.ip_address ]
data_categories: [user.derived.identifiable.device.ip_address]
- name: email
data_categories: [ user.provided.identifiable.contact.email ]
data_categories: [user.provided.identifiable.contact.email]
- name: customer
fields:
- name: id
Expand All @@ -200,4 +203,10 @@ dataset:
- name: created
data_categories: [system.operations]
fidesops_meta:
read_only: True
read_only: True
- name: data_management
fields:
- name: id
data_categories: [system.operations]
fidesops_meta:
data_type: string
12 changes: 11 additions & 1 deletion src/fidesops/service/connectors/saas_connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def __init__(self, configuration: ConnectionConfig):
self.client_config = self.saas_config.client_config # type: ignore
self.endpoints = self.saas_config.top_level_endpoint_dict # type: ignore
self.collection_name: Optional[str] = None
self.privacy_request: Optional[PrivacyRequest] = None

def query_config(self, node: TraversalNode) -> SaaSQueryConfig:
"""
Expand All @@ -49,7 +50,11 @@ def query_config(self, node: TraversalNode) -> SaaSQueryConfig:
# store collection_name for logging purposes
self.collection_name = node.address.collection
return SaaSQueryConfig(
node, self.endpoints, self.secrets, self.saas_config.data_protection_request # type: ignore
node,
self.endpoints,
self.secrets, # type: ignore
self.saas_config.data_protection_request, # type: ignore
self.privacy_request, # type: ignore
)

def test_connection(self) -> Optional[ConnectionTestStatus]:
Expand Down Expand Up @@ -103,7 +108,9 @@ def retrieve_data(
input_data: Dict[str, List[Any]],
) -> List[Row]:
"""Retrieve data from SaaS APIs"""

# generate initial set of requests if read request is defined, otherwise raise an exception
self.privacy_request = privacy_request
query_config: SaaSQueryConfig = self.query_config(node)
read_request: Optional[SaaSRequest] = query_config.get_request_by_action("read")
if not read_request:
Expand Down Expand Up @@ -139,6 +146,7 @@ def execute_prepared_request(
Executes the prepared request and handles response postprocessing and pagination.
Returns processed data and request_params for next page of data if available.
"""

client: AuthenticatedClient = self.create_client_from_request(saas_request)
response: Response = client.send(prepared_request, saas_request.ignore_errors)
response = self._handle_errored_response(saas_request, response)
Expand Down Expand Up @@ -186,6 +194,7 @@ def process_response_data(
The final result is returned as a list of processed objects.
"""

rows: List[Row] = []
processed_data = response_data
for postprocessor in postprocessors or []:
Expand Down Expand Up @@ -230,6 +239,7 @@ def mask_data(
) -> int:
"""Execute a masking request. Return the number of rows that have been updated."""

self.privacy_request = privacy_request
query_config = self.query_config(node)
masking_request = query_config.get_masking_request()
if not masking_request:
Expand Down
8 changes: 8 additions & 0 deletions src/fidesops/service/connectors/saas_query_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,14 @@ def __init__(
endpoints: Dict[str, Endpoint],
secrets: Dict[str, Any],
data_protection_request: Optional[SaaSRequest] = None,
privacy_request: Optional[PrivacyRequest] = None,
):
super().__init__(node)
self.collection_name = node.address.collection
self.endpoints = endpoints
self.secrets = secrets
self.data_protection_request = data_protection_request
self.privacy_request = privacy_request
self.action: Optional[str] = None

def get_request_by_action(self, action: str) -> Optional[SaaSRequest]:
Expand Down Expand Up @@ -152,6 +154,9 @@ def generate_query(
self.secrets, param_value.connector_param
)

if self.privacy_request:
param_values["privacy_request_id"] = self.privacy_request.id

# map param values to placeholders in path, headers, and query params
saas_request_params: SaaSRequestParams = saas_util.map_param_values(
self.action, self.collection_name, current_request, param_values # type: ignore
Expand Down Expand Up @@ -192,6 +197,9 @@ def generate_update_stmt( # pylint: disable=R0914
self.secrets, param_value.connector_param
)

if self.privacy_request:
param_values["privacy_request_id"] = self.privacy_request.id

# remove any row values for fields marked as read-only, these will be omitted from all update maps
for field_path, field in self.field_map().items():
if field.read_only:
Expand Down
4 changes: 2 additions & 2 deletions tests/api/v1/endpoints/test_saas_config_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ def test_patch_saas_config_update(
)
saas_config = connection_config.saas_config
assert saas_config is not None
assert len(saas_config["endpoints"]) == 6
assert len(saas_config["endpoints"]) == 7


def get_saas_config_url(connection_config: Optional[ConnectionConfig] = None) -> str:
Expand Down Expand Up @@ -318,7 +318,7 @@ def test_get_saas_config(
response_body["fides_key"]
== saas_example_connection_config.get_saas_config().fides_key
)
assert len(response_body["endpoints"]) == 7
assert len(response_body["endpoints"]) == 8
assert response_body["type"] == "custom"


Expand Down
39 changes: 31 additions & 8 deletions tests/service/connectors/test_queryconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,10 @@ def test_generate_query(
CollectionAddress(saas_config.fides_key, "payment_methods")
]

data_management = combined_traversal.traversal_node_dict[
CollectionAddress(saas_config.fides_key, "data_management")
]

# static path with single query param
config = SaaSQueryConfig(member, endpoints, {})
prepared_request: SaaSRequestParams = config.generate_query(
Expand Down Expand Up @@ -700,6 +704,14 @@ def test_generate_query(
"query": "[email protected]",
}

# using privacy_request_id placeholder
config = SaaSQueryConfig(data_management, endpoints, {}, None, privacy_request)
prepared_request = config.generate_query(
{"email": ["[email protected]"]}, policy
)
assert prepared_request.method == HTTPMethod.GET.value
assert prepared_request.path == f"/v1/privacy_request/{privacy_request.id}"

def test_generate_update_stmt(
self,
erasure_policy_string_rewrite,
Expand All @@ -714,6 +726,10 @@ def test_generate_update_stmt(
CollectionAddress(saas_config.fides_key, "member")
]

data_management = combined_traversal.traversal_node_dict[
CollectionAddress(saas_config.fides_key, "data_management")
]

config = SaaSQueryConfig(member, endpoints, {}, update_request)
row = {
"id": "123",
Expand All @@ -730,10 +746,18 @@ def test_generate_update_stmt(
assert prepared_request.path == "/3.0/lists/abc/members/123"
assert prepared_request.headers == {"Content-Type": "application/json"}
assert prepared_request.query_params == {}
assert (
prepared_request.body
== '{\n "merge_fields": {"FNAME": "MASKED", "LNAME": "MASKED"}\n}\n'
assert json.loads(prepared_request.body) == {
"merge_fields": {"FNAME": "MASKED", "LNAME": "MASKED"}
}

# using privacy_request_id placeholder
config = SaaSQueryConfig(data_management, endpoints, {}, None, privacy_request)
prepared_request = config.generate_update_stmt(
row, erasure_policy_string_rewrite, privacy_request
)
assert prepared_request.method == HTTPMethod.POST.value
assert prepared_request.path == "/v1/privacy_request/"
assert json.loads(prepared_request.body) == {"unique_id": privacy_request.id}

def test_generate_update_stmt_custom_http_method(
self,
Expand Down Expand Up @@ -768,10 +792,9 @@ def test_generate_update_stmt_custom_http_method(
assert prepared_request.path == "/3.0/lists/abc/members/123"
assert prepared_request.headers == {"Content-Type": "application/json"}
assert prepared_request.query_params == {}
assert (
prepared_request.body
== '{\n "merge_fields": {"FNAME": "MASKED", "LNAME": "MASKED"}\n}\n'
)
assert json.loads(prepared_request.body) == {
"merge_fields": {"FNAME": "MASKED", "LNAME": "MASKED"}
}

def test_generate_update_stmt_with_request_body(
self,
Expand Down Expand Up @@ -847,7 +870,7 @@ def test_generate_update_stmt_with_request_body(
assert prepared_request.path == "/2.0/payment_methods"
assert prepared_request.headers == {"Content-Type": "application/json"}
assert prepared_request.query_params == {}
assert prepared_request.body == '{\n "customer_name": "MASKED"\n}\n'
assert json.loads(prepared_request.body) == {"customer_name": "MASKED"}

def test_generate_update_stmt_with_url_encoded_body(
self,
Expand Down

0 comments on commit 8068597

Please sign in to comment.