Skip to content

Commit

Permalink
Add integration for Alchemer Erasure (#4925)
Browse files Browse the repository at this point in the history
Co-authored-by: Adrian Galvan <adrian@ethyca.com>
  • Loading branch information
MarcGEthyca and galvana authored Jul 15, 2024
1 parent e02e6e1 commit 75b7dda
Show file tree
Hide file tree
Showing 7 changed files with 241 additions and 0 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@ The types of changes are:
## [Unreleased](https://github.com/ethyca/fides/compare/2.40.0...main)

### Added
- Added erasure support for Alchemer integration [#4925](https://github.com/ethyca/fides/pull/4925)

### Developer Experience
- Upgrade to React 18 and Chakra 2, including other dependencies [#5036](https://github.com/ethyca/fides/pull/5036)
- Added support for "output templates" in read SaaS requests [#5054](https://github.com/ethyca/fides/pull/5054)


### Changed
- Updated the sample dataset for the Amplitude integration [#5063](https://github.com/ethyca/fides/pull/5063)
- Messaging page now shows a notice if you have properties without any templates [#5077](https://github.com/ethyca/fides/pull/5077)
Expand Down
46 changes: 46 additions & 0 deletions data/saas/config/alchemer_config.yml
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
9 changes: 9 additions & 0 deletions data/saas/dataset/alchemer_dataset.yml
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: []
3 changes: 3 additions & 0 deletions data/saas/icon/alchemer.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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
81 changes: 81 additions & 0 deletions tests/fixtures/saas/alchemer_fixtures.py
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,
)
31 changes: 31 additions & 0 deletions tests/ops/integration_tests/saas/test_alchemer_task.py
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,
}

0 comments on commit 75b7dda

Please sign in to comment.