Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consent SaaS Config: Allow definition of multiple opt-in or opt-out requests #2315

Merged
merged 3 commits into from
Jan 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions data/saas/config/mailchimp_transactional_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,17 @@ saas_config:
}

opt_out:
method: POST
path: /allowlists/delete
body: |
{
"email": "<email>"
}
- method: POST
path: /allowlists/delete
body: |
{
"email": "<email>"
}
- method: POST
path: /rejects/add
body: |
{
"email": "<email>"
}

endpoints: []
18 changes: 16 additions & 2 deletions src/fides/api/ops/schemas/saas/saas_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,22 @@ class SaaSRequestMap(BaseModel):
class ConsentRequestMap(BaseModel):
"""A map of actions to Consent requests"""

opt_in: Optional[SaaSRequest] = None
opt_out: Optional[SaaSRequest] = None
opt_in: Union[SaaSRequest, List[SaaSRequest]] = []
opt_out: Union[SaaSRequest, List[SaaSRequest]] = []

@validator("opt_in", "opt_out")
def validate_list_field(
cls,
field_value: Union[SaaSRequest, List[SaaSRequest]],
) -> List[SaaSRequest]:
"""Convert all opt_in/opt_out request formats to a list of requests.

We allow either a single request or a list of requests to be defined, but this makes
sure that everything is a list once that data has been read in.
"""
if isinstance(field_value, SaaSRequest):
return [field_value]
return field_value


class Endpoint(BaseModel):
Expand Down
39 changes: 19 additions & 20 deletions src/fides/api/ops/service/connectors/saas_connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,32 +429,33 @@ def run_consent_request(

for consent in consent_preferences:
request_action: str = "opt_in" if consent.opt_in else "opt_out"
consent_request: Optional[
consent_requests: List[
SaaSRequest
] = self._get_consent_requests_by_preference(consent.opt_in)

if not consent_request:
if not consent_requests:
pattisdr marked this conversation as resolved.
Show resolved Hide resolved
logger.info(
"Skipping consent requests on node {}: No '{}' request defined",
"Skipping consent requests on node {}: No '{}' requests defined",
node.address.value,
request_action,
)
continue

self.set_saas_request_state(consent_request)
for consent_request in consent_requests:
self.set_saas_request_state(consent_request)

param_values: Dict[str, Any] = self.secrets
param_values.update(identity_data)
param_values: Dict[str, Any] = self.secrets
param_values.update(identity_data)

prepared_request: SaaSRequestParams = map_param_values(
request_action,
f"{self.configuration.name}",
consent_request,
self.secrets,
)
client: AuthenticatedClient = self.create_client()
client.send(prepared_request)
self.unset_connector_state()
prepared_request: SaaSRequestParams = map_param_values(
request_action,
f"{self.configuration.name}",
consent_request,
self.secrets,
)
client: AuthenticatedClient = self.create_client()
client.send(prepared_request)
self.unset_connector_state()
return True

def close(self) -> None:
Expand Down Expand Up @@ -580,15 +581,13 @@ def _invoke_masking_request_override(
f"Error executing override mask function '{override_function_name}'"
)

def _get_consent_requests_by_preference(
self, opt_in: bool
) -> Optional[SaaSRequest]:
def _get_consent_requests_by_preference(self, opt_in: bool) -> List[SaaSRequest]:
"""Helper to either pull out the opt-in requests or the opt out requests that were defined."""
consent_requests: Optional[
ConsentRequestMap
] = self.saas_config.consent_requests

if not consent_requests:
return None
return []

return consent_requests.opt_in if opt_in else consent_requests.opt_out
return consent_requests.opt_in if opt_in else consent_requests.opt_out # type: ignore
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,18 @@ async def test_mailchimp_transactional_consent_request_task(
body = response.json()
assert body == [], "Verify email has been removed from allowlist"

request: SaaSRequestParams = SaaSRequestParams(
method=HTTPMethod.POST,
path="/rejects/list",
body=json.dumps({"email": mailchimp_transactional_identity_email}),
)
response = connector.create_client().send(request)
body = response.json()[0]
assert (
body["email"] == mailchimp_transactional_identity_email
), "Verify email has been added to denylist"
assert body["detail"] == "Added manually via the the API"


@pytest.mark.integration_saas
@pytest.mark.integration_mailchimp_transactional
Expand Down Expand Up @@ -133,7 +145,7 @@ async def test_mailchimp_transactional_consent_prepared_requests(

assert mocked_client_send.called
saas_request_params: SaaSRequestParams = mocked_client_send.call_args[0][0]
assert saas_request_params.path == "/allowlists/delete"
assert saas_request_params.path == "/rejects/add"
assert (
mailchimp_transactional_identity_email
in mocked_client_send.call_args[0][0].body
Expand Down
18 changes: 10 additions & 8 deletions tests/ops/service/connectors/test_saas_connector.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
from typing import List
from unittest import mock
from unittest.mock import Mock

Expand Down Expand Up @@ -338,13 +339,14 @@ def test_get_consent_requests_by_preference(
mailchimp_transactional_connection_config
)

opt_in_request: SaaSRequest = connector._get_consent_requests_by_preference(
opt_in=True
)
assert opt_in_request.path == "/allowlists/add"
opt_in_request: List[
SaaSRequest
] = connector._get_consent_requests_by_preference(opt_in=True)
assert opt_in_request[0].path == "/allowlists/add"

opt_out_request: SaaSRequest = connector._get_consent_requests_by_preference(
opt_in=False
)
opt_out_request: List[
SaaSRequest
] = connector._get_consent_requests_by_preference(opt_in=False)

assert opt_out_request.path == "/allowlists/delete"
assert opt_out_request[0].path == "/allowlists/delete"
assert opt_out_request[1].path == "/rejects/add"