diff --git a/CHANGELOG.md b/CHANGELOG.md index 134d6fa763..6c7ff5efce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ The types of changes are: ### Fixed - Fix rendering of subfield names in D&D tables [#5439](https://github.com/ethyca/fides/pull/5439) - Fix "Save" button on system source/destination page not working [#5469](https://github.com/ethyca/fides/pull/5469) + - Updating Salesforce erasure request with overrides so it properly passes validation. Removing Account endpoint since it does not contain user data [#5452](https://github.com/ethyca/fides/pull/5452) ### Developer Experience - Added Carbon Icons to FidesUI [#5416](https://github.com/ethyca/fides/pull/5416) diff --git a/src/fides/api/service/saas_request/override_implementations/salesforce_request_overrides.py b/src/fides/api/service/saas_request/override_implementations/salesforce_request_overrides.py new file mode 100644 index 0000000000..c07772d27b --- /dev/null +++ b/src/fides/api/service/saas_request/override_implementations/salesforce_request_overrides.py @@ -0,0 +1,128 @@ +from json import dumps +from typing import Any, Dict, List + +from loguru import logger + +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, +) + + +def truncate_fields_to_40_characters(masked_object_fields: Dict) -> Dict: + """ + Check if the masked field is over 40 characters long, if so truncate it to 40 characters. + """ + for key in masked_object_fields: + if isinstance(masked_object_fields[key], str) and len(masked_object_fields[key]) > 40: + logger.info("Truncating {key} field to 40 characters") + masked_object_fields[key] = masked_object_fields[key][:40] + return masked_object_fields + + + +def mask_email(masked_object_fields: Dict, email_field: str) -> Dict: + """ + Masking Email properly so it does not breaks validation rules + """ + if email_field in masked_object_fields: + masked_object_fields[email_field] = "masked@company.com" + return masked_object_fields + + +@register("salesforce_contacts_update", [SaaSRequestType.UPDATE]) +def salesforce_contacts_update( + client: AuthenticatedClient, + param_values_per_row: List[Dict[str, Any]], + policy: Policy, + privacy_request: PrivacyRequest, + secrets: Dict[str, Any], +) -> int: + rows_updated = 0 + # each update_params dict correspond to a record that needs to be updated + for row_param_values in param_values_per_row: + + masked_object_fields = row_param_values["masked_object_fields"] + + masked_object_fields = mask_email(masked_object_fields, "Email") + + masked_object_fields = truncate_fields_to_40_characters(masked_object_fields) + + update_body = dumps(masked_object_fields) + contact_id = row_param_values["contact_id"] + client.send( + SaaSRequestParams( + method=HTTPMethod.PATCH, + headers={"Content-Type": "application/json"}, + path=f"/services/data/v54.0/sobjects/Contact/{contact_id}", + body=update_body, + ) + ) + rows_updated += 1 + return rows_updated + + +@register("salesforce_cases_update", [SaaSRequestType.UPDATE]) +def salesforce_cases_update( + client: AuthenticatedClient, + param_values_per_row: List[Dict[str, Any]], + policy: Policy, + privacy_request: PrivacyRequest, + secrets: Dict[str, Any], +) -> int: + rows_updated = 0 + # each update_params dict correspond to a record that needs to be updated + for row_param_values in param_values_per_row: + masked_object_fields = row_param_values["masked_object_fields"] + + masked_object_fields = mask_email(masked_object_fields, "SuppliedEmail") + + masked_object_fields = truncate_fields_to_40_characters(masked_object_fields) + + update_body = dumps(masked_object_fields) + case_id = row_param_values["case_id"] + client.send( + SaaSRequestParams( + method=HTTPMethod.PATCH, + headers={"Content-Type": "application/json"}, + path=f"/services/data/v54.0/sobjects/Case/{case_id}", + body=update_body, + ) + ) + rows_updated += 1 + return rows_updated + + +@register("salesforce_leads_update", [SaaSRequestType.UPDATE]) +def salesforce_leads_update( + client: AuthenticatedClient, + param_values_per_row: List[Dict[str, Any]], + policy: Policy, + privacy_request: PrivacyRequest, + secrets: Dict[str, Any], +) -> int: + rows_updated = 0 + # each update_params dict correspond to a record that needs to be updated + for row_param_values in param_values_per_row: + masked_object_fields = row_param_values["masked_object_fields"] + + masked_object_fields = mask_email(masked_object_fields, "Email") + + masked_object_fields = truncate_fields_to_40_characters(masked_object_fields) + + update_body = dumps(masked_object_fields) + lead_id = row_param_values["lead_id"] + client.send( + SaaSRequestParams( + method=HTTPMethod.PATCH, + headers={"Content-Type": "application/json"}, + path=f"/services/data/v54.0/sobjects/Lead/{lead_id}", + body=update_body, + ) + ) + rows_updated += 1 + return rows_updated diff --git a/tests/fixtures/saas/salesforce_fixtures.py b/tests/fixtures/saas/salesforce_fixtures.py index ba48c39ff2..8e9470bb19 100644 --- a/tests/fixtures/saas/salesforce_fixtures.py +++ b/tests/fixtures/saas/salesforce_fixtures.py @@ -20,6 +20,7 @@ load_dataset_with_replacement, ) from tests.ops.test_helpers.vault_client import get_secrets +from tests.ops.integration_tests.saas.connector_runner import ConnectorRunner secrets = get_secrets("salesforce") @@ -62,72 +63,6 @@ def salesforce_erasure_identity_email(): return f"{cryptographic_util.generate_secure_random_string(13)}@email.com" -@pytest.fixture -def salesforce_config() -> Dict[str, Any]: - return load_config_with_replacement( - "data/saas/config/salesforce_config.yml", - "", - "salesforce_instance", - ) - - -@pytest.fixture -def salesforce_dataset() -> Dict[str, Any]: - return load_dataset_with_replacement( - "data/saas/dataset/salesforce_dataset.yml", - "", - "salesforce_instance", - )[0] - - -@pytest.fixture(scope="function") -def salesforce_connection_config( - db: session, - salesforce_config, - salesforce_secrets, -) -> Generator: - fides_key = salesforce_config["fides_key"] - connection_config = ConnectionConfig.create( - db=db, - data={ - "key": fides_key, - "name": fides_key, - "connection_type": ConnectionType.saas, - "access": AccessLevel.write, - "secrets": salesforce_secrets, - "saas_config": salesforce_config, - }, - ) - yield connection_config - connection_config.delete(db) - - -@pytest.fixture -def salesforce_dataset_config( - db: Session, - salesforce_connection_config: ConnectionConfig, - salesforce_dataset: Dict[str, Any], -) -> Generator: - fides_key = salesforce_dataset["fides_key"] - salesforce_connection_config.name = fides_key - salesforce_connection_config.key = fides_key - salesforce_connection_config.save(db=db) - - ctl_dataset = CtlDataset.create_from_dataset_dict(db, salesforce_dataset) - - dataset = DatasetConfig.create( - db=db, - data={ - "connection_config_id": salesforce_connection_config.id, - "fides_key": fides_key, - "ctl_dataset_id": ctl_dataset.id, - }, - ) - yield dataset - dataset.delete(db=db) - ctl_dataset.delete(db=db) - - @pytest.fixture(scope="function") def salesforce_create_erasure_data( salesforce_erasure_identity_email, salesforce_secrets @@ -251,3 +186,12 @@ def salesforce_create_erasure_data( headers=headers, ) assert account_response.status_code == HTTP_404_NOT_FOUND + + +@pytest.fixture +def salesforce_runner( + db, + cache, + salesforce_secrets, +) -> ConnectorRunner: + return ConnectorRunner(db, cache, "salesforce", salesforce_secrets) diff --git a/tests/ops/integration_tests/saas/test_salesforce_task.py b/tests/ops/integration_tests/saas/test_salesforce_task.py index 84acb47c9f..b51895d28f 100644 --- a/tests/ops/integration_tests/saas/test_salesforce_task.py +++ b/tests/ops/integration_tests/saas/test_salesforce_task.py @@ -1,6 +1,7 @@ import pytest import requests +from fides.api.models.policy import Policy from fides.api.graph.graph import DatasetGraph from fides.api.schemas.redis_cache import Identity from fides.api.service.connectors import get_connector @@ -8,1135 +9,177 @@ from fides.config import CONFIG from tests.conftest import access_runner_tester, erasure_runner_tester from tests.ops.graph.graph_test_util import assert_rows_match +from tests.ops.integration_tests.saas.connector_runner import ConnectorRunner @pytest.mark.skip(reason="Currently unable to test OAuth2 connectors") @pytest.mark.integration_saas -def test_salesforce_connection_test(salesforce_connection_config) -> None: - get_connector(salesforce_connection_config).test_connection() +class TestSalesforceConnector: + def test_connection(self, salesforce_runner: ConnectorRunner) -> None: + salesforce_runner.test_connection() -@pytest.mark.skip(reason="Currently unable to test OAuth2 connectors") -@pytest.mark.integration_saas -@pytest.mark.asyncio -@pytest.mark.parametrize( - "dsr_version", - ["use_dsr_3_0", "use_dsr_2_0"], -) -async def test_salesforce_access_request_task_by_email( - privacy_request, - dsr_version, - request, - policy, - salesforce_identity_email, - salesforce_connection_config, - salesforce_dataset_config, - db, -) -> None: - """Full access request based on the Salesforce SaaS config""" - request.getfixturevalue(dsr_version) # REQUIRED to test both DSR 3.0 and 2.0 - - identity = Identity(**{"email": salesforce_identity_email}) - privacy_request.cache_identity(identity) - - dataset_name = salesforce_connection_config.get_saas_config().fides_key - merged_graph = salesforce_dataset_config.get_graph() - graph = DatasetGraph(merged_graph) - - v = access_runner_tester( - privacy_request, - policy, - graph, - [salesforce_connection_config], - {"email": salesforce_identity_email}, - db, - ) - - assert_rows_match( - v[f"{dataset_name}:contact_list"], min_size=1, keys=["attributes", "Id"] - ) - - assert_rows_match( - v[f"{dataset_name}:contacts"], - min_size=1, - keys=[ - "attributes", - "Id", - "IsDeleted", - "MasterRecordId", - "AccountId", - "LastName", - "FirstName", - "Salutation", - "Name", - "OtherStreet", - "OtherCity", - "OtherState", - "OtherPostalCode", - "OtherCountry", - "OtherLatitude", - "OtherLongitude", - "OtherGeocodeAccuracy", - "OtherAddress", - "MailingStreet", - "MailingCity", - "MailingState", - "MailingPostalCode", - "MailingCountry", - "MailingLatitude", - "MailingLongitude", - "MailingGeocodeAccuracy", - "MailingAddress", - "Phone", - "Fax", - "MobilePhone", - "HomePhone", - "OtherPhone", - "AssistantPhone", - "ReportsToId", - "Email", - "Title", - "Department", - "AssistantName", - "LeadSource", - "Birthdate", - "Description", - "OwnerId", - "CreatedDate", - "CreatedById", - "LastModifiedDate", - "LastModifiedById", - "SystemModstamp", - "LastActivityDate", - "LastCURequestDate", - "LastCUUpdateDate", - "LastViewedDate", - "LastReferencedDate", - "EmailBouncedReason", - "EmailBouncedDate", - "IsEmailBounced", - "PhotoUrl", - "Jigsaw", - "JigsawContactId", - "CleanStatus", - "IndividualId", - ], - ) - - assert_rows_match( - v[f"{dataset_name}:case_list"], min_size=1, keys=["attributes", "Id"] - ) - - assert_rows_match( - v[f"{dataset_name}:cases"], - min_size=1, - keys=[ - "attributes", - "Id", - "IsDeleted", - "MasterRecordId", - "CaseNumber", - "ContactId", - "AccountId", - "AssetId", - "SourceId", - "ParentId", - "SuppliedName", - "SuppliedEmail", - "SuppliedPhone", - "SuppliedCompany", - "Type", - "Status", - "Reason", - "Origin", - "Subject", - "Priority", - "Description", - "IsClosed", - "ClosedDate", - "IsEscalated", - "OwnerId", - "CreatedDate", - "CreatedById", - "LastModifiedDate", - "LastModifiedById", - "SystemModstamp", - "ContactPhone", - "ContactMobile", - "ContactEmail", - "ContactFax", - "Comments", - "LastViewedDate", - "LastReferencedDate", - ], - ) - - assert_rows_match( - v[f"{dataset_name}:campaign_member_list"], min_size=1, keys=["attributes", "Id"] - ) - - assert_rows_match( - v[f"{dataset_name}:campaign_members"], - min_size=1, - keys=[ - "attributes", - "Id", - "IsDeleted", - "CampaignId", - "LeadId", - "ContactId", - "Status", - "HasResponded", - "CreatedDate", - "CreatedById", - "LastModifiedDate", - "LastModifiedById", - "SystemModstamp", - "FirstRespondedDate", - "Salutation", - "Name", - "FirstName", - "LastName", - "Title", - "Street", - "City", - "State", - "PostalCode", - "Country", - "Email", - "Phone", - "Fax", - "MobilePhone", - "Description", - "DoNotCall", - "HasOptedOutOfEmail", - "HasOptedOutOfFax", - "LeadSource", - "CompanyOrAccount", - "Type", - "LeadOrContactId", - "LeadOrContactOwnerId", - ], - ) - - assert_rows_match( - v[f"{dataset_name}:lead_list"], min_size=1, keys=["attributes", "Id"] - ) - - assert_rows_match( - v[f"{dataset_name}:leads"], - min_size=1, - keys=[ - "attributes", - "Id", - "IsDeleted", - "MasterRecordId", - "LastName", - "FirstName", - "Salutation", - "Name", - "Title", - "Company", - "Street", - "City", - "State", - "PostalCode", - "Country", - "Latitude", - "Longitude", - "GeocodeAccuracy", - "Address", - "Phone", - "MobilePhone", - "Fax", - "Email", - "Website", - "PhotoUrl", - "Description", - "LeadSource", - "Status", - "Industry", - "Rating", - "AnnualRevenue", - "NumberOfEmployees", - "OwnerId", - "IsConverted", - "ConvertedDate", - "ConvertedAccountId", - "ConvertedContactId", - "ConvertedOpportunityId", - "IsUnreadByOwner", - "CreatedDate", - "CreatedById", - "LastModifiedDate", - "LastModifiedById", - "SystemModstamp", - "LastActivityDate", - "LastViewedDate", - "LastReferencedDate", - "Jigsaw", - "JigsawContactId", - "CleanStatus", - "CompanyDunsNumber", - "DandbCompanyId", - "EmailBouncedReason", - "EmailBouncedDate", - "IndividualId", - ], - ) - assert_rows_match( - v[f"{dataset_name}:accounts"], - min_size=1, - keys=[ - "attributes", - "Id", - "IsDeleted", - "MasterRecordId", - "Name", - "Type", - "ParentId", - "BillingStreet", - "BillingCity", - "BillingState", - "BillingPostalCode", - "BillingCountry", - "BillingLatitude", - "BillingLongitude", - "BillingGeocodeAccuracy", - "BillingAddress", - "ShippingStreet", - "ShippingCity", - "ShippingState", - "ShippingPostalCode", - "ShippingCountry", - "ShippingLatitude", - "ShippingLongitude", - "ShippingGeocodeAccuracy", - "ShippingAddress", - "Phone", - "Fax", - "AccountNumber", - "Website", - "PhotoUrl", - "Sic", - "Industry", - "AnnualRevenue", - "NumberOfEmployees", - "Ownership", - "TickerSymbol", - "Description", - "Rating", - "Site", - "OwnerId", - "CreatedDate", - "CreatedById", - "LastModifiedDate", - "LastModifiedById", - "SystemModstamp", - "LastActivityDate", - "LastViewedDate", - "LastReferencedDate", - "Jigsaw", - "JigsawCompanyId", - "CleanStatus", - "AccountSource", - "DunsNumber", - "Tradestyle", - "NaicsCode", - "NaicsDesc", - "YearStarted", - "SicDesc", - "DandbCompanyId", - "OperatingHoursId", - ], + @pytest.mark.asyncio + @pytest.mark.parametrize( + "dsr_version", + ["use_dsr_3_0", "use_dsr_2_0"], ) - - # verify we only returned data for our identity - assert v[f"{dataset_name}:contacts"][0]["Email"] == salesforce_identity_email - account_id = v[f"{dataset_name}:contacts"][0]["AccountId"] - - for case in v[f"{dataset_name}:cases"]: - assert case["ContactEmail"] == salesforce_identity_email - - for lead in v[f"{dataset_name}:leads"]: - assert lead["Email"] == salesforce_identity_email - - for campaign_member in v[f"{dataset_name}:campaign_members"]: - assert campaign_member["Email"] == salesforce_identity_email - - for account in v[f"{dataset_name}:accounts"]: - assert account["Id"] == account_id - - -@pytest.mark.skip(reason="Currently unable to test OAuth2 connectors") -@pytest.mark.integration_saas -@pytest.mark.asyncio -@pytest.mark.parametrize( - "dsr_version", - ["use_dsr_3_0", "use_dsr_2_0"], -) -async def test_salesforce_access_request_task_by_phone_number( - policy, - dsr_version, - request, - privacy_request, - salesforce_identity_phone_number, - salesforce_identity_email, - salesforce_connection_config, - salesforce_dataset_config, - db, -) -> None: - """Full access request based on the Salesforce SaaS config""" - request.getfixturevalue(dsr_version) # REQUIRED to test both DSR 3.0 and 2.0 - - identity = Identity(**{"phone_number": salesforce_identity_phone_number}) - privacy_request.cache_identity(identity) - - dataset_name = salesforce_connection_config.get_saas_config().fides_key - merged_graph = salesforce_dataset_config.get_graph() - graph = DatasetGraph(merged_graph) - - v = access_runner_tester( + async def test_salesforce_access_request_task_by_email( privacy_request, - policy, - graph, - [salesforce_connection_config], - {"phone_number": salesforce_identity_phone_number}, - db, - ) + dsr_version, + request, + salesforce_runner: ConnectorRunner, + policy: Policy, + salesforce_identity_email, + ) -> None: + """Full access request based on the Salesforce SaaS config""" + request.getfixturevalue(dsr_version) # REQUIRED to test both DSR 3.0 and 2.0 - assert_rows_match( - v[f"{dataset_name}:contact_list"], min_size=1, keys=["attributes", "Id"] - ) + access_results = await salesforce_runner.access_request( + access_policy=policy, identities={"email": salesforce_identity_email} + ) - assert_rows_match( - v[f"{dataset_name}:contacts"], - min_size=1, - keys=[ - "attributes", - "Id", - "IsDeleted", - "MasterRecordId", - "AccountId", - "LastName", - "FirstName", - "Salutation", - "Name", - "OtherStreet", - "OtherCity", - "OtherState", - "OtherPostalCode", - "OtherCountry", - "OtherLatitude", - "OtherLongitude", - "OtherGeocodeAccuracy", - "OtherAddress", - "MailingStreet", - "MailingCity", - "MailingState", - "MailingPostalCode", - "MailingCountry", - "MailingLatitude", - "MailingLongitude", - "MailingGeocodeAccuracy", - "MailingAddress", - "Phone", - "Fax", - "MobilePhone", - "HomePhone", - "OtherPhone", - "AssistantPhone", - "ReportsToId", - "Email", - "Title", - "Department", - "AssistantName", - "LeadSource", - "Birthdate", - "Description", - "OwnerId", - "CreatedDate", - "CreatedById", - "LastModifiedDate", - "LastModifiedById", - "SystemModstamp", - "LastActivityDate", - "LastCURequestDate", - "LastCUUpdateDate", - "LastViewedDate", - "LastReferencedDate", - "EmailBouncedReason", - "EmailBouncedDate", - "IsEmailBounced", - "PhotoUrl", - "Jigsaw", - "JigsawContactId", - "CleanStatus", - "IndividualId", - ], - ) + dataset_name = "salesforce_instance" - assert_rows_match( - v[f"{dataset_name}:case_list"], min_size=1, keys=["attributes", "Id"] - ) - assert_rows_match( - v[f"{dataset_name}:cases"], - min_size=1, - keys=[ - "attributes", - "Id", - "IsDeleted", - "MasterRecordId", - "CaseNumber", - "ContactId", - "AccountId", - "AssetId", - "SourceId", - "ParentId", - "SuppliedName", - "SuppliedEmail", - "SuppliedPhone", - "SuppliedCompany", - "Type", - "Status", - "Reason", - "Origin", - "Subject", - "Priority", - "Description", - "IsClosed", - "ClosedDate", - "IsEscalated", - "OwnerId", - "CreatedDate", - "CreatedById", - "LastModifiedDate", - "LastModifiedById", - "SystemModstamp", - "ContactPhone", - "ContactMobile", - "ContactEmail", - "ContactFax", - "Comments", - "LastViewedDate", - "LastReferencedDate", - ], - ) + # verify we only returned data for our identity + assert access_results[f"{dataset_name}:contacts"][0]["Email"] == salesforce_identity_email - assert_rows_match( - v[f"{dataset_name}:campaign_member_list"], min_size=1, keys=["attributes", "Id"] - ) + for case in access_results[f"{dataset_name}:cases"]: + assert case["ContactEmail"] == salesforce_identity_email - assert_rows_match( - v[f"{dataset_name}:campaign_members"], - min_size=1, - keys=[ - "attributes", - "Id", - "IsDeleted", - "CampaignId", - "LeadId", - "ContactId", - "Status", - "HasResponded", - "CreatedDate", - "CreatedById", - "LastModifiedDate", - "LastModifiedById", - "SystemModstamp", - "FirstRespondedDate", - "Salutation", - "Name", - "FirstName", - "LastName", - "Title", - "Street", - "City", - "State", - "PostalCode", - "Country", - "Email", - "Phone", - "Fax", - "MobilePhone", - "Description", - "DoNotCall", - "HasOptedOutOfEmail", - "HasOptedOutOfFax", - "LeadSource", - "CompanyOrAccount", - "Type", - "LeadOrContactId", - "LeadOrContactOwnerId", - ], - ) + for lead in access_results[f"{dataset_name}:leads"]: + assert lead["Email"] == salesforce_identity_email - assert_rows_match( - v[f"{dataset_name}:lead_list"], min_size=1, keys=["attributes", "Id"] - ) + for campaign_member in access_results[f"{dataset_name}:campaign_members"]: + assert campaign_member["Email"] == salesforce_identity_email - assert_rows_match( - v[f"{dataset_name}:leads"], - min_size=1, - keys=[ - "attributes", - "Id", - "IsDeleted", - "MasterRecordId", - "LastName", - "FirstName", - "Salutation", - "Name", - "Title", - "Company", - "Street", - "City", - "State", - "PostalCode", - "Country", - "Latitude", - "Longitude", - "GeocodeAccuracy", - "Address", - "Phone", - "MobilePhone", - "Fax", - "Email", - "Website", - "PhotoUrl", - "Description", - "LeadSource", - "Status", - "Industry", - "Rating", - "AnnualRevenue", - "NumberOfEmployees", - "OwnerId", - "IsConverted", - "ConvertedDate", - "ConvertedAccountId", - "ConvertedContactId", - "ConvertedOpportunityId", - "IsUnreadByOwner", - "CreatedDate", - "CreatedById", - "LastModifiedDate", - "LastModifiedById", - "SystemModstamp", - "LastActivityDate", - "LastViewedDate", - "LastReferencedDate", - "Jigsaw", - "JigsawContactId", - "CleanStatus", - "CompanyDunsNumber", - "DandbCompanyId", - "EmailBouncedReason", - "EmailBouncedDate", - "IndividualId", - ], - ) - assert_rows_match( - v[f"{dataset_name}:accounts"], - min_size=1, - keys=[ - "attributes", - "Id", - "IsDeleted", - "MasterRecordId", - "Name", - "Type", - "ParentId", - "BillingStreet", - "BillingCity", - "BillingState", - "BillingPostalCode", - "BillingCountry", - "BillingLatitude", - "BillingLongitude", - "BillingGeocodeAccuracy", - "BillingAddress", - "ShippingStreet", - "ShippingCity", - "ShippingState", - "ShippingPostalCode", - "ShippingCountry", - "ShippingLatitude", - "ShippingLongitude", - "ShippingGeocodeAccuracy", - "ShippingAddress", - "Phone", - "Fax", - "AccountNumber", - "Website", - "PhotoUrl", - "Sic", - "Industry", - "AnnualRevenue", - "NumberOfEmployees", - "Ownership", - "TickerSymbol", - "Description", - "Rating", - "Site", - "OwnerId", - "CreatedDate", - "CreatedById", - "LastModifiedDate", - "LastModifiedById", - "SystemModstamp", - "LastActivityDate", - "LastViewedDate", - "LastReferencedDate", - "Jigsaw", - "JigsawCompanyId", - "CleanStatus", - "AccountSource", - "DunsNumber", - "Tradestyle", - "NaicsCode", - "NaicsDesc", - "YearStarted", - "SicDesc", - "DandbCompanyId", - "OperatingHoursId", - ], + @pytest.mark.asyncio + @pytest.mark.parametrize( + "dsr_version", + ["use_dsr_3_0", "use_dsr_2_0"], ) - - # verify we only returned data for our identity - for contact in v[f"{dataset_name}:contacts"]: - assert contact["Phone"] == salesforce_identity_phone_number - assert contact["Email"] == salesforce_identity_email - - for lead in v[f"{dataset_name}:leads"]: - assert lead["Phone"] == salesforce_identity_phone_number - assert lead["Email"] == salesforce_identity_email - - for campaign_member in v[f"{dataset_name}:campaign_members"]: - assert campaign_member["Phone"] == salesforce_identity_phone_number - assert campaign_member["Email"] == salesforce_identity_email - - -@pytest.mark.skip(reason="Currently unable to test OAuth2 connectors") -@pytest.mark.integration_saas -@pytest.mark.asyncio -@pytest.mark.parametrize( - "dsr_version", - ["use_dsr_3_0", "use_dsr_2_0"], -) -async def test_salesforce_erasure_request_task( - db, - dsr_version, - request, - privacy_request, - erasure_policy_string_rewrite, - salesforce_connection_config, - salesforce_dataset_config, - salesforce_erasure_identity_email, - salesforce_create_erasure_data, -) -> None: - """Full erasure request based on the Salesforce SaaS config""" - privacy_request.policy_id = erasure_policy_string_rewrite.id - privacy_request.save(db) - request.getfixturevalue(dsr_version) # REQUIRED to test both DSR 3.0 and 2.0 - - ( - account_id, - contact_id, - case_id, - lead_id, - campaign_member_id, - ) = salesforce_create_erasure_data - - identity = Identity(**{"email": salesforce_erasure_identity_email}) - privacy_request.cache_identity(identity) - - dataset_name = salesforce_connection_config.get_saas_config().fides_key - merged_graph = salesforce_dataset_config.get_graph() - graph = DatasetGraph(merged_graph) - - v = access_runner_tester( + async def test_salesforce_access_request_task_by_phone_number( + self, + dsr_version, + request, privacy_request, - erasure_policy_string_rewrite, - graph, - [salesforce_connection_config], - {"email": salesforce_erasure_identity_email}, + salesforce_runner: ConnectorRunner, + policy: Policy, + salesforce_identity_phone_number, + salesforce_identity_email, db, - ) + ) -> None: - # verify staged data is available for erasure - assert_rows_match( - v[f"{dataset_name}:contact_list"], min_size=1, keys=["attributes", "Id"] - ) + """Full access request based on the Salesforce SaaS config""" + request.getfixturevalue(dsr_version) # REQUIRED to test both DSR 3.0 and 2.0 - assert_rows_match( - v[f"{dataset_name}:contacts"], - min_size=1, - keys=[ - "attributes", - "Id", - "IsDeleted", - "MasterRecordId", - "AccountId", - "LastName", - "FirstName", - "Salutation", - "Name", - "OtherStreet", - "OtherCity", - "OtherState", - "OtherPostalCode", - "OtherCountry", - "OtherLatitude", - "OtherLongitude", - "OtherGeocodeAccuracy", - "OtherAddress", - "MailingStreet", - "MailingCity", - "MailingState", - "MailingPostalCode", - "MailingCountry", - "MailingLatitude", - "MailingLongitude", - "MailingGeocodeAccuracy", - "MailingAddress", - "Phone", - "Fax", - "MobilePhone", - "HomePhone", - "OtherPhone", - "AssistantPhone", - "ReportsToId", - "Email", - "Title", - "Department", - "AssistantName", - "LeadSource", - "Birthdate", - "Description", - "OwnerId", - "CreatedDate", - "CreatedById", - "LastModifiedDate", - "LastModifiedById", - "SystemModstamp", - "LastActivityDate", - "LastCURequestDate", - "LastCUUpdateDate", - "LastViewedDate", - "LastReferencedDate", - "EmailBouncedReason", - "EmailBouncedDate", - "IsEmailBounced", - "PhotoUrl", - "Jigsaw", - "JigsawContactId", - "CleanStatus", - "IndividualId", - ], - ) + access_results = await salesforce_runner.access_request( + access_policy=policy, identities={"phone_number": salesforce_identity_phone_number} + ) - assert_rows_match( - v[f"{dataset_name}:case_list"], min_size=1, keys=["attributes", "Id"] - ) + dataset_name = "salesforce_instance" - assert_rows_match( - v[f"{dataset_name}:cases"], - min_size=1, - keys=[ - "attributes", - "Id", - "IsDeleted", - "MasterRecordId", - "CaseNumber", - "ContactId", - "AccountId", - "AssetId", - "SourceId", - "ParentId", - "SuppliedName", - "SuppliedEmail", - "SuppliedPhone", - "SuppliedCompany", - "Type", - "Status", - "Reason", - "Origin", - "Subject", - "Priority", - "Description", - "IsClosed", - "ClosedDate", - "IsEscalated", - "OwnerId", - "CreatedDate", - "CreatedById", - "LastModifiedDate", - "LastModifiedById", - "SystemModstamp", - "ContactPhone", - "ContactMobile", - "ContactEmail", - "ContactFax", - "Comments", - "LastViewedDate", - "LastReferencedDate", - ], - ) + # verify we only returned data for our identity + for contact in access_results[f"{dataset_name}:contacts"]: + assert contact["Phone"] == salesforce_identity_phone_number + assert contact["Email"] == salesforce_identity_email - assert_rows_match( - v[f"{dataset_name}:campaign_member_list"], min_size=1, keys=["attributes", "Id"] - ) + for lead in access_results[f"{dataset_name}:leads"]: + assert lead["Phone"] == salesforce_identity_phone_number + assert lead["Email"] == salesforce_identity_email - assert_rows_match( - v[f"{dataset_name}:campaign_members"], - min_size=1, - keys=[ - "attributes", - "Id", - "IsDeleted", - "CampaignId", - "LeadId", - "ContactId", - "Status", - "HasResponded", - "CreatedDate", - "CreatedById", - "LastModifiedDate", - "LastModifiedById", - "SystemModstamp", - "FirstRespondedDate", - "Salutation", - "Name", - "FirstName", - "LastName", - "Title", - "Street", - "City", - "State", - "PostalCode", - "Country", - "Email", - "Phone", - "Fax", - "MobilePhone", - "Description", - "DoNotCall", - "HasOptedOutOfEmail", - "HasOptedOutOfFax", - "LeadSource", - "CompanyOrAccount", - "Type", - "LeadOrContactId", - "LeadOrContactOwnerId", - ], - ) + for campaign_member in access_results[f"{dataset_name}:campaign_members"]: + assert campaign_member["Phone"] == salesforce_identity_phone_number + assert campaign_member["Email"] == salesforce_identity_email - assert_rows_match( - v[f"{dataset_name}:lead_list"], min_size=1, keys=["attributes", "Id"] - ) - assert_rows_match( - v[f"{dataset_name}:leads"], - min_size=1, - keys=[ - "attributes", - "Id", - "IsDeleted", - "MasterRecordId", - "LastName", - "FirstName", - "Salutation", - "Name", - "Title", - "Company", - "Street", - "City", - "State", - "PostalCode", - "Country", - "Latitude", - "Longitude", - "GeocodeAccuracy", - "Address", - "Phone", - "MobilePhone", - "Fax", - "Email", - "Website", - "PhotoUrl", - "Description", - "LeadSource", - "Status", - "Industry", - "Rating", - "AnnualRevenue", - "NumberOfEmployees", - "OwnerId", - "IsConverted", - "ConvertedDate", - "ConvertedAccountId", - "ConvertedContactId", - "ConvertedOpportunityId", - "IsUnreadByOwner", - "CreatedDate", - "CreatedById", - "LastModifiedDate", - "LastModifiedById", - "SystemModstamp", - "LastActivityDate", - "LastViewedDate", - "LastReferencedDate", - "Jigsaw", - "JigsawContactId", - "CleanStatus", - "CompanyDunsNumber", - "DandbCompanyId", - "EmailBouncedReason", - "EmailBouncedDate", - "IndividualId", - ], + @pytest.mark.asyncio + @pytest.mark.parametrize( + "dsr_version", + ["use_dsr_3_0", "use_dsr_2_0"], ) - - assert_rows_match( - v[f"{dataset_name}:accounts"], - min_size=1, - keys=[ - "attributes", - "Id", - "IsDeleted", - "MasterRecordId", - "Name", - "Type", - "ParentId", - "BillingStreet", - "BillingCity", - "BillingState", - "BillingPostalCode", - "BillingCountry", - "BillingLatitude", - "BillingLongitude", - "BillingGeocodeAccuracy", - "BillingAddress", - "ShippingStreet", - "ShippingCity", - "ShippingState", - "ShippingPostalCode", - "ShippingCountry", - "ShippingLatitude", - "ShippingLongitude", - "ShippingGeocodeAccuracy", - "ShippingAddress", - "Phone", - "Fax", - "AccountNumber", - "Website", - "PhotoUrl", - "Sic", - "Industry", - "AnnualRevenue", - "NumberOfEmployees", - "Ownership", - "TickerSymbol", - "Description", - "Rating", - "Site", - "OwnerId", - "CreatedDate", - "CreatedById", - "LastModifiedDate", - "LastModifiedById", - "SystemModstamp", - "LastActivityDate", - "LastViewedDate", - "LastReferencedDate", - "Jigsaw", - "JigsawCompanyId", - "CleanStatus", - "AccountSource", - "DunsNumber", - "Tradestyle", - "NaicsCode", - "NaicsDesc", - "YearStarted", - "SicDesc", - "DandbCompanyId", - "OperatingHoursId", - ], - ) - - masking_strict = CONFIG.execution.masking_strict - CONFIG.execution.masking_strict = True - - x = erasure_runner_tester( - privacy_request, - erasure_policy_string_rewrite, - graph, - [salesforce_connection_config], - {"email": salesforce_erasure_identity_email}, - get_cached_data_for_erasures(privacy_request.id), + async def test_salesforce_erasure_request_task( + self, db, - ) - - # verify masking request was issued for endpoints with update actions - assert x == { - f"{dataset_name}:accounts": 1, - f"{dataset_name}:campaign_member_list": 0, - f"{dataset_name}:campaign_members": 1, - f"{dataset_name}:case_list": 0, - f"{dataset_name}:cases": 1, - f"{dataset_name}:contact_list": 0, - f"{dataset_name}:contacts": 1, - f"{dataset_name}:lead_list": 0, - f"{dataset_name}:leads": 1, - } - - salesforce_secrets = salesforce_connection_config.secrets - base_url = f"https://{salesforce_secrets['domain']}" - headers = { - "Authorization": f"Bearer {salesforce_secrets['access_token']}", - } - - # account - response = requests.get( - url=f"{base_url}/services/data/v54.0/sobjects/Account/{account_id}", - headers=headers, - ) - account = response.json() - assert account["Name"] == "MASKED" - - # campaign_members - response = requests.get( - url=f"{base_url}/services/data/v54.0/sobjects/CampaignMember/{campaign_member_id}", - headers=headers, - ) - campaign_member = response.json() - assert campaign_member["FirstName"] == "MASKED" - assert campaign_member["LastName"] == "MASKED" - - # cases - # no name on cases - - # contacts - response = requests.get( - url=f"{base_url}/services/data/v54.0/sobjects/Contact/{contact_id}", - headers=headers, - ) - contacts = response.json() - assert contacts["FirstName"] == "MASKED" - assert contacts["LastName"] == "MASKED" - - # leads - response = requests.get( - url=f"{base_url}/services/data/v54.0/sobjects/Lead/{lead_id}", - headers=headers, - ) - lead = response.json() - assert lead["FirstName"] == "MASKED" - assert lead["LastName"] == "MASKED" - - # reset - CONFIG.execution.masking_strict = masking_strict + dsr_version, + request, + salesforce_secrets, + policy: Policy, + salesforce_runner: ConnectorRunner, + erasure_policy_string_rewrite_name_and_email, + salesforce_erasure_identity_email, + salesforce_create_erasure_data, + ) -> None: + """Full erasure request based on the Salesforce SaaS config""" + request.getfixturevalue(dsr_version) # REQUIRED to test both DSR 3.0 and 2.0 + + ( + account_id, + contact_id, + case_id, + lead_id, + campaign_member_id, + ) = salesforce_create_erasure_data + + + ( + _, + erasure_results, + ) = await salesforce_runner.non_strict_erasure_request( + access_policy=policy, + erasure_policy=erasure_policy_string_rewrite_name_and_email, + identities={"email": salesforce_erasure_identity_email}, + ) + + dataset_name = "salesforce_instance" + + + # verify masking request was issued for endpoints with update actions + assert erasure_results == { + f"{dataset_name}:campaign_members": 0, + f"{dataset_name}:campaign_member_list": 0, + f"{dataset_name}:case_list": 0, + f"{dataset_name}:cases": 1, + f"{dataset_name}:contact_list": 0, + f"{dataset_name}:contacts": 1, + f"{dataset_name}:lead_list": 0, + f"{dataset_name}:leads": 1, + } + + base_url = f"https://{salesforce_secrets['domain']}" + headers = { + "Authorization": f"Bearer {salesforce_secrets['access_token']}", + } + + # campaign_members + response = requests.get( + url=f"{base_url}/services/data/v54.0/sobjects/CampaignMember/{campaign_member_id}", + headers=headers, + ) + campaign_member = response.json() + assert campaign_member["FirstName"] == "MASKED" + assert campaign_member["LastName"] == "MASKED" + + # cases + # no name on cases + + # contacts + response = requests.get( + url=f"{base_url}/services/data/v54.0/sobjects/Contact/{contact_id}", + headers=headers, + ) + contacts = response.json() + assert contacts["FirstName"] == "MASKED" + assert contacts["LastName"] == "MASKED" + + # leads + response = requests.get( + url=f"{base_url}/services/data/v54.0/sobjects/Lead/{lead_id}", + headers=headers, + ) + lead = response.json() + assert lead["FirstName"] == "MASKED" + assert lead["LastName"] == "MASKED"