-
Notifications
You must be signed in to change notification settings - Fork 74
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add integration for Alchemer Erasure (#4925)
Co-authored-by: Adrian Galvan <adrian@ethyca.com>
- Loading branch information
1 parent
e02e6e1
commit 75b7dda
Showing
7 changed files
with
241 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
saas_config: | ||
fides_key: <instance_fides_key> | ||
name: Alchemer | ||
type: alchemer | ||
description: A sample schema representing the Alchemer erasure only integration for Fides | ||
user_guide: https://docs.ethyca.com/user-guides/integrations/saas-integrations/alchemer | ||
version: 0.1.0 | ||
|
||
connector_params: | ||
- name: domain | ||
label: Domain | ||
description: The API domain for Alchemer. Default api.alchemer.com | ||
default_value: api.alchemer.com | ||
- name: api_key | ||
label: API key | ||
description: The API key for Alchemer | ||
sensitive: True | ||
- name: api_key_secret | ||
label: API key secret | ||
description: The API key secret for Alchemer | ||
sensitive: True | ||
|
||
client_config: | ||
protocol: https | ||
host: <domain> | ||
authentication: | ||
strategy: api_key | ||
configuration: | ||
query_params: | ||
- name: api_token | ||
value: <api_key> | ||
- name: api_token_secret | ||
value: <api_key_secret> | ||
|
||
test_request: | ||
method: GET | ||
path: /v5/account | ||
|
||
endpoints: | ||
- name: user | ||
requests: | ||
delete: | ||
request_override: alchemer_user_delete | ||
param_values: | ||
- name: email | ||
identity: email |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
dataset: | ||
- fides_key: <instance_fides_key> | ||
name: Alchemer_erasure_only | ||
description: | ||
A sample dataset representing the Alchemer erasure only connector for | ||
Fides | ||
collections: | ||
- name: user | ||
fields: [] |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
66 changes: 66 additions & 0 deletions
66
src/fides/api/service/saas_request/override_implementations/alchemer_request_overrides.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
from typing import Any, Dict, List | ||
|
||
from fides.api.models.policy import Policy | ||
from fides.api.models.privacy_request import PrivacyRequest | ||
from fides.api.schemas.saas.shared_schemas import HTTPMethod, SaaSRequestParams | ||
from fides.api.service.connectors.saas.authenticated_client import AuthenticatedClient | ||
from fides.api.service.saas_request.saas_request_override_factory import ( | ||
SaaSRequestType, | ||
register, | ||
) | ||
|
||
|
||
@register("alchemer_user_delete", [SaaSRequestType.DELETE]) | ||
def alchemer_user_delete( | ||
client: AuthenticatedClient, | ||
param_values_per_row: List[Dict[str, Any]], | ||
policy: Policy, | ||
privacy_request: PrivacyRequest, | ||
secrets: Dict[str, Any], | ||
) -> int: | ||
# The delete endpoint has a structure like this | ||
# https://api.alchemer.com/v5/contactlist/31/contactlistcontact/100012345?_method=DELETE | ||
# where 31 is the contact list id and 100012345 is the contact id | ||
# So we first get all the contact lists and extract their ids | ||
# Then we query for all contacts in each list, filtering on our identity email | ||
# Then we call a delete on the contact | ||
rows_deleted = 0 | ||
params = { | ||
"api_token": secrets["api_key"], | ||
"api_token_secret": secrets["api_key_secret"], | ||
} | ||
get_list_ids = client.send( | ||
SaaSRequestParams( | ||
method=HTTPMethod.GET, | ||
path="/v5/contactlist", | ||
params=params, | ||
) | ||
) | ||
|
||
list_ids_data = get_list_ids.json() | ||
list_results = [] | ||
for list_id in list_ids_data["data"]: | ||
list_results.append(list_id["id"]) | ||
for list_result in list_results: | ||
contacts_data_call = client.send( | ||
SaaSRequestParams( | ||
method=HTTPMethod.GET, | ||
path=f"/v5/contactlist/{list_result}/contactlistcontact", | ||
params=params, | ||
) | ||
) | ||
contacts_data = contacts_data_call.json() | ||
for contact in contacts_data["data"]: | ||
for row_param_values in param_values_per_row: | ||
email = row_param_values["email"] | ||
if contact["email_address"] == email: | ||
client.send( | ||
SaaSRequestParams( | ||
method=HTTPMethod.DELETE, | ||
path=f"/v5/contactlist/{list_result}/contactlistcontact/{contact['id']}", | ||
params=params, | ||
) | ||
) | ||
rows_deleted += 1 | ||
|
||
return rows_deleted |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import random | ||
import string | ||
import time | ||
from typing import Any, Dict, Generator | ||
|
||
import pydash | ||
import pytest | ||
import requests | ||
|
||
from tests.ops.integration_tests.saas.connector_runner import ( | ||
ConnectorRunner, | ||
generate_random_email, | ||
) | ||
from tests.ops.test_helpers.vault_client import get_secrets | ||
|
||
secrets = get_secrets("alchemer") | ||
|
||
|
||
@pytest.fixture(scope="session") | ||
def alchemer_secrets(saas_config) -> Dict[str, Any]: | ||
return { | ||
"domain": pydash.get(saas_config, "alchemer.domain") or secrets["domain"], | ||
"api_key": pydash.get(saas_config, "alchemer.api_key") or secrets["api_key"], | ||
"api_key_secret": pydash.get(saas_config, "alchemer.api_key_secret") | ||
or secrets["api_key_secret"], | ||
} | ||
|
||
|
||
@pytest.fixture(scope="session") | ||
def alchemer_identity_email(saas_config) -> str: | ||
return ( | ||
pydash.get(saas_config, "alchemer.identity_email") or secrets["identity_email"] | ||
) | ||
|
||
|
||
@pytest.fixture | ||
def alchemer_erasure_identity_email() -> str: | ||
return generate_random_email() | ||
|
||
|
||
@pytest.fixture | ||
def alchemer_erasure_data( | ||
alchemer_erasure_identity_email: str, | ||
alchemer_secrets, | ||
) -> Generator: | ||
gen_string = string.ascii_lowercase | ||
test_contactlist = "".join(random.choice(gen_string) for i in range(10)) | ||
x_contactlist_name = f"Ethyca Test {test_contactlist}" | ||
|
||
base_url = f"https://{alchemer_secrets['domain']}/v5" | ||
params = { | ||
"api_token": alchemer_secrets["api_key"], | ||
"api_token_secret": alchemer_secrets["api_key_secret"], | ||
"list_name": x_contactlist_name, | ||
} | ||
contactlist_url = f"{base_url}/contactlist/" | ||
response = requests.put(contactlist_url, params=params) | ||
|
||
contactlist_id = response.json()["data"]["id"] | ||
contactlistcontact_url = f"{contactlist_url}{contactlist_id}/contactlistcontact" | ||
params = { | ||
"api_token": alchemer_secrets["api_key"], | ||
"api_token_secret": alchemer_secrets["api_key_secret"], | ||
"email_address": alchemer_erasure_identity_email, | ||
} | ||
response = requests.put(contactlistcontact_url, params=params) | ||
time.sleep(5) | ||
|
||
|
||
@pytest.fixture | ||
def alchemer_runner( | ||
db, | ||
cache, | ||
alchemer_secrets, | ||
) -> ConnectorRunner: | ||
return ConnectorRunner( | ||
db, | ||
cache, | ||
"alchemer", | ||
alchemer_secrets, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import pytest | ||
|
||
from fides.api.models.policy import Policy | ||
from tests.ops.integration_tests.saas.connector_runner import ConnectorRunner | ||
|
||
|
||
@pytest.mark.skip(reason="No active account") | ||
@pytest.mark.integration_saas | ||
class TestAlchemerConnector: | ||
def test_connection(self, alchemer_runner: ConnectorRunner): | ||
alchemer_runner.test_connection() | ||
|
||
async def test_non_strict_erasure_request( | ||
self, | ||
alchemer_runner: ConnectorRunner, | ||
policy: Policy, | ||
erasure_policy_string_rewrite: Policy, | ||
alchemer_erasure_identity_email: str, | ||
alchemer_erasure_data, | ||
): | ||
( | ||
_, | ||
erasure_results, | ||
) = await alchemer_runner.non_strict_erasure_request( | ||
access_policy=policy, | ||
erasure_policy=erasure_policy_string_rewrite, | ||
identities={"email": alchemer_erasure_identity_email}, | ||
) | ||
assert erasure_results == { | ||
"alchemer_instance:user": 1, | ||
} |