diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a4ce1414df..a1767a3be65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ The types of changes are: - Added language switching support to the FidesJS UI based on configured translations [#4737](https://github.com/ethyca/fides/pull/4737) - Added ability to override some experience language and primary color [#4743](https://github.com/ethyca/fides/pull/4743) - Generate FidesJS SDK Reference Docs from tsdoc comments [#4736](https://github.com/ethyca/fides/pull/4736) +- Added erasure support for Adyen [#4735](https://github.com/ethyca/fides/pull/4735) - Added erasure support for Iterable [#4695](https://github.com/ethyca/fides/pull/4695) ### Changed diff --git a/data/saas/config/adyen_config.yml b/data/saas/config/adyen_config.yml new file mode 100644 index 00000000000..2a8cff0148c --- /dev/null +++ b/data/saas/config/adyen_config.yml @@ -0,0 +1,76 @@ +saas_config: + fides_key: + name: Adyen + type: adyen + description: A sample schema representing the Adyen integrations for Fides + user_guide: https://docs.ethyca.com/user-guides/integrations/saas-integrations/adyen + version: 0.1.0 + + connector_params: + - name: domain_management + label: Management domain + description: Please note that Adyen has both TEST and LIVE environments. E.g. management-test.adyen.com or management-live.adyen.com, and that API keys for live and test will not be the same. + - name: domain_ca + label: CA domain + description: Please note that Adyen has both TEST and LIVE environments. E.g. ca-test.adyen.com or ca-live.adyen.com, and that API keys for live and test will not be the same. + - name: merchant_account + label: Merchant account + description: Your merchant account. + - name: api_key + label: API key + description: See Adyen's documentation for me details about creating API keys, and how to assign roles for the key here (https://docs.adyen.com/development-resources/api-credentials/). + sensitive: True + + external_references: + - name: adyen_user_id + label: PSP reference + description: The dataset reference to the PSP reference of the original payment authorization which maps to the desired user. + + client_config: + protocol: https + host: + authentication: + strategy: api_key + configuration: + headers: + - name: x-API-key + value: + test_request: + method: GET + path: /v3/me + + endpoints: + - name: user + requests: + read: + request_override: adyen_user_read + param_values: + - name: user_id + references: + - adyen_user_id + delete: + method: POST + path: /ca/services/DataProtectionService/v1/requestSubjectErasure + client_config: + protocol: https + host: + authentication: + strategy: api_key + configuration: + headers: + - name: x-API-key + value: + body: | + { + "merchantAccount": "", + "pspReference": "", + "forceErasure": "True" + } + param_values: + - name: merchant_account + connector_param: merchant_account + - name: psp_reference + references: + - dataset: + field: user.psp_reference + direction: from diff --git a/data/saas/dataset/adyen_dataset.yml b/data/saas/dataset/adyen_dataset.yml new file mode 100644 index 00000000000..50d742d08ba --- /dev/null +++ b/data/saas/dataset/adyen_dataset.yml @@ -0,0 +1,12 @@ +dataset: + - fides_key: + name: adyen + description: A sample dataset representing the Adyen integration for Fides + collections: + - name: user + fields: + - name: id + data_categories: [system.operations] + fidesops_meta: + primary_key: True + data_type: string diff --git a/data/saas/icon/adyen.svg b/data/saas/icon/adyen.svg new file mode 100644 index 00000000000..afba33ed230 --- /dev/null +++ b/data/saas/icon/adyen.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/fides/api/service/saas_request/override_implementations/adyen_request_overrides.py b/src/fides/api/service/saas_request/override_implementations/adyen_request_overrides.py new file mode 100644 index 00000000000..78d9f8642c0 --- /dev/null +++ b/src/fides/api/service/saas_request/override_implementations/adyen_request_overrides.py @@ -0,0 +1,32 @@ +"""Notes for this particular override +For more details consult the Adyen documentation for their Data Protection API +The gist is that like some other vendors we do not get an email directly for this integration. The data protection endpoint in this case has two main requirements, called merchantAccount and pspReference. +""" + +from typing import Any, Dict, List + +from fides.api.graph.traversal import TraversalNode +from fides.api.models.policy import Policy +from fides.api.models.privacy_request import PrivacyRequest +from fides.api.service.connectors.saas.authenticated_client import AuthenticatedClient +from fides.api.service.saas_request.saas_request_override_factory import ( + SaaSRequestType, + register, +) +from fides.api.util.collection_util import Row + + +@register("adyen_user_read", [SaaSRequestType.READ]) +def adyen_user_read( + client: AuthenticatedClient, + node: TraversalNode, + policy: Policy, + privacy_request: PrivacyRequest, + input_data: Dict[str, List[Any]], + secrets: Dict[str, Any], +) -> List[Row]: + adyen_user_ids = input_data.get("user_id", []) + results = [] + for adyen_user_id in adyen_user_ids: + results.append({"psp_reference": adyen_user_id}) + return results diff --git a/tests/fixtures/saas/adyen_fixtures.py b/tests/fixtures/saas/adyen_fixtures.py new file mode 100644 index 00000000000..38c87f7d23d --- /dev/null +++ b/tests/fixtures/saas/adyen_fixtures.py @@ -0,0 +1,71 @@ +"""Notes on the particulars for this Adyen integration +In this case, we aren't passing an identity email as Adyen's data protection endpoint requires a different value, the pspReference value which will be provided to us by our customer. +The pspReference value itself will be provided by the customer +For reference this is the name of the value in the Adyen API data erasure endpoint +While we have hardcoded a value in this instance, it should be fairly straight-forward to make that a random value. +Naming considerations +adyen_external_references() & adyen_erasure_external_references() +both will need to use naming consistent with that in the config.yml +""" +from typing import Any, Dict, Generator + +import pydash +import pytest + +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("adyen") + + +@pytest.fixture(scope="session") +def adyen_secrets(saas_config) -> Dict[str, Any]: + return { + "domain_management": pydash.get(saas_config, "adyen.domain_management") + or secrets["domain_management"], + "domain_ca": pydash.get(saas_config, "adyen.domain_ca") or secrets["domain_ca"], + "api_key": pydash.get(saas_config, "adyen.api_key") or secrets["adyen.api_key"], + "merchant_account": pydash.get(saas_config, "adyen.merchant_account") + or secrets["adyen.merchant_account"], + "psp_reference": pydash.get(saas_config, "adyen.psp_reference") + or secrets["adyen.pspReference"], + "adyen_user_id": pydash.get(saas_config, "adyen.user_id") + or secrets["adyen.user_id"], + # add the rest of your secrets here + } + + +@pytest.fixture +def adyen_erasure_identity_email() -> str: + return generate_random_email() + + +@pytest.fixture +def adyen_external_references() -> Dict[str, Any]: + return {"adyen_user_id": "852617375522786K"} + + +@pytest.fixture +def adyen_erasure_external_references() -> Dict[str, Any]: + return {"adyen_user_id": "852617375522786K"} + + +@pytest.fixture +def adyen_runner( + db, + cache, + adyen_secrets, + adyen_external_references, + adyen_erasure_external_references, +) -> ConnectorRunner: + return ConnectorRunner( + db, + cache, + "adyen", + adyen_secrets, + external_references=adyen_external_references, + erasure_external_references=adyen_erasure_external_references, + ) diff --git a/tests/ops/integration_tests/saas/connector_runner.py b/tests/ops/integration_tests/saas/connector_runner.py index 1d4088b1627..7edb607e519 100644 --- a/tests/ops/integration_tests/saas/connector_runner.py +++ b/tests/ops/integration_tests/saas/connector_runner.py @@ -25,7 +25,9 @@ ProvidedIdentity, ) from fides.api.models.sql_models import Dataset as CtlDataset +from fides.api.schemas.policy import ActionType from fides.api.schemas.redis_cache import Identity +from fides.api.schemas.saas.saas_config import SaaSConfig from fides.api.service.connectors import get_connector from fides.api.service.privacy_request.request_runner_service import ( build_consent_dataset_graph, @@ -291,20 +293,24 @@ async def _base_erasure_request( _process_external_references(self.db, graph_list, connection_config_list) dataset_graph = DatasetGraph(*graph_list) - access_results = await graph_task.run_access_request( - privacy_request, - access_policy, - dataset_graph, - connection_config_list, - identities, - self.db, - ) + if ( + ActionType.access + in SaaSConfig(**self.connection_config.saas_config).supported_actions + ): + access_results = await graph_task.run_access_request( + privacy_request, + access_policy, + dataset_graph, + connection_config_list, + identities, + self.db, + ) - # verify we returned at least one row for each collection in the dataset - for collection in self.dataset["collections"]: - assert len( - access_results[f"{fides_key}:{collection['name']}"] - ), f"No rows returned for collection '{collection['name']}'" + # verify we returned at least one row for each collection in the dataset + for collection in self.dataset["collections"]: + assert len( + access_results[f"{fides_key}:{collection['name']}"] + ), f"No rows returned for collection '{collection['name']}'" erasure_results = await graph_task.run_erasure( privacy_request, @@ -316,7 +322,7 @@ async def _base_erasure_request( self.db, ) - return access_results, erasure_results + return access_results or {}, erasure_results def _config(connector_type: str) -> Dict[str, Any]: diff --git a/tests/ops/integration_tests/saas/test_adyen_task.py b/tests/ops/integration_tests/saas/test_adyen_task.py new file mode 100644 index 00000000000..dd56be9e7d2 --- /dev/null +++ b/tests/ops/integration_tests/saas/test_adyen_task.py @@ -0,0 +1,30 @@ +import pytest + +from fides.api.models.policy import Policy +from tests.ops.integration_tests.saas.connector_runner import ConnectorRunner + + +@pytest.mark.integration_saas +class TestAdyenConnector: + def test_connection(self, adyen_runner: ConnectorRunner): + adyen_runner.test_connection() + + async def test_non_strict_erasure_request( + self, + adyen_runner: ConnectorRunner, + policy: Policy, + erasure_policy_string_rewrite: Policy, + adyen_erasure_identity_email: str, + ): + ( + _, + erasure_results, + ) = await adyen_runner.non_strict_erasure_request( + access_policy=policy, + erasure_policy=erasure_policy_string_rewrite, + identities={"email": adyen_erasure_identity_email}, + ) + assert erasure_results == { + "adyen_external_dataset:adyen_external_collection": 0, + "adyen_instance:user": 1, + }