From 2b887dce420de80612e22a13ddd1d29c79dcfbab Mon Sep 17 00:00:00 2001 From: Dawn Pattison Date: Thu, 22 Jun 2023 11:39:28 -0500 Subject: [PATCH 1/4] Add systems_applicable query parameter to the privacy experience list endpoint. if systems_applicable=True, then notices embedded in the response must match a data use on the system. (note that the data use hierarchy is taken into account here too). In other words, if a privacy notice data use matches a system data use, or a privacy notice data use is a parent of the system data use, this is a match, but not if a system data use is a parent of a privacy notice data use --- .../endpoints/privacy_experience_endpoints.py | 3 +- src/fides/api/models/privacy_experience.py | 6 ++ .../test_privacy_experience_endpoints.py | 56 +++++++++++++++++++ tests/ops/models/test_privacy_experience.py | 19 ++++++- 4 files changed, 82 insertions(+), 2 deletions(-) diff --git a/src/fides/api/api/v1/endpoints/privacy_experience_endpoints.py b/src/fides/api/api/v1/endpoints/privacy_experience_endpoints.py index 1afacfedfb..750f4f9106 100644 --- a/src/fides/api/api/v1/endpoints/privacy_experience_endpoints.py +++ b/src/fides/api/api/v1/endpoints/privacy_experience_endpoints.py @@ -70,6 +70,7 @@ def privacy_experience_list( has_notices: Optional[bool] = None, has_config: Optional[bool] = None, fides_user_device_id: Optional[str] = None, + systems_applicable: Optional[bool] = False, request: Request, # required for rate limiting response: Response, # required for rate limiting ) -> AbstractPage[PrivacyExperience]: @@ -130,7 +131,7 @@ def privacy_experience_list( privacy_notices: List[ PrivacyNotice ] = privacy_experience.get_related_privacy_notices( - db, show_disabled, fides_user_provided_identity + db, show_disabled, systems_applicable, fides_user_provided_identity ) if should_unescape: # Unescape both the experience config and the embedded privacy notices diff --git a/src/fides/api/models/privacy_experience.py b/src/fides/api/models/privacy_experience.py index 69571ff2d3..d4fd9847f4 100644 --- a/src/fides/api/models/privacy_experience.py +++ b/src/fides/api/models/privacy_experience.py @@ -19,6 +19,7 @@ ) from fides.api.models.privacy_preference import CurrentPrivacyPreference from fides.api.models.privacy_request import ProvidedIdentity +from fides.api.models.sql_models import System # type: ignore[attr-defined] BANNER_CONSENT_MECHANISMS: Set[ConsentMechanism] = { ConsentMechanism.notice_only, @@ -243,6 +244,7 @@ def get_related_privacy_notices( self, db: Session, show_disabled: Optional[bool] = True, + systems_applicable: Optional[bool] = False, fides_user_provided_identity: Optional[ProvidedIdentity] = None, ) -> List[PrivacyNotice]: """Return privacy notices that overlap on at least one region @@ -260,6 +262,10 @@ def get_related_privacy_notices( PrivacyNotice.disabled.is_(False) ) + if systems_applicable: + data_uses: set[str] = System.get_data_uses(System.all(db), include_parents=True) + privacy_notice_query = privacy_notice_query.filter(PrivacyNotice.data_uses.overlap(data_uses)) # type: ignore + if not fides_user_provided_identity: return privacy_notice_query.order_by(PrivacyNotice.created_at.desc()).all() diff --git a/tests/ops/api/v1/endpoints/test_privacy_experience_endpoints.py b/tests/ops/api/v1/endpoints/test_privacy_experience_endpoints.py index 27d79126c3..f149717dcd 100644 --- a/tests/ops/api/v1/endpoints/test_privacy_experience_endpoints.py +++ b/tests/ops/api/v1/endpoints/test_privacy_experience_endpoints.py @@ -367,6 +367,62 @@ def test_filter_on_notices_and_region( assert notices[1]["id"] == privacy_notice.id assert notices[1]["displayed_in_privacy_center"] + @pytest.mark.usefixtures( + "privacy_notice_us_co_provide_service_operations", # not displayed in overlay or privacy center + "privacy_notice_eu_cy_provide_service_frontend_only", # doesn't overlap with any regions, + "privacy_experience_overlay", # us_ca + "privacy_notice_eu_fr_provide_service_frontend_only", # eu_fr + "privacy_notice_us_ca_provide", # us_ca + "privacy_experience_privacy_center", + ) + def test_filter_on_systems_applicable( + self, + api_client: TestClient, + url, + privacy_experience_privacy_center, + privacy_notice, + system, + privacy_notice_us_co_third_party_sharing, + ): + """For systems applicable filter, notices are only embedded if they are relevant to a system""" + resp = api_client.get( + url + "?region=us_co", + ) + assert resp.status_code == 200 + data = resp.json() + + assert data["total"] == 1 + assert len(data["items"]) == 1 + + notices = data["items"][0]["privacy_notices"] + assert len(notices) == 2 + assert notices[0]["regions"] == ["us_co"] + assert notices[0]["id"] == privacy_notice_us_co_third_party_sharing.id + assert notices[0]["displayed_in_privacy_center"] + assert notices[0]["data_uses"] == ["third_party_sharing"] + + assert notices[1]["regions"] == ["us_ca", "us_co"] + assert notices[1]["id"] == privacy_notice.id + assert notices[1]["displayed_in_privacy_center"] + assert notices[1]["data_uses"] == [ + "marketing.advertising", + "third_party_sharing", + ] + + resp = api_client.get( + url + "?region=us_co&systems_applicable=True", + ) + notices = resp.json()["items"][0]["privacy_notices"] + assert len(notices) == 1 + assert notices[0]["regions"] == ["us_ca", "us_co"] + assert notices[0]["id"] == privacy_notice.id + assert notices[0]["displayed_in_privacy_center"] + assert notices[0]["data_uses"] == [ + "marketing.advertising", + "third_party_sharing", + ] + assert system.privacy_declarations[0].data_use == "marketing.advertising" + @pytest.mark.usefixtures( "privacy_notice_us_co_provide_service_operations", # not displayed in overlay or privacy center "privacy_notice_eu_cy_provide_service_frontend_only", # doesn't overlap with any regions, diff --git a/tests/ops/models/test_privacy_experience.py b/tests/ops/models/test_privacy_experience.py index 27f6ee539a..0f9f623b40 100644 --- a/tests/ops/models/test_privacy_experience.py +++ b/tests/ops/models/test_privacy_experience.py @@ -314,7 +314,7 @@ def test_update_privacy_experience(self, db, experience_config_overlay): exp.delete(db) - def test_get_related_privacy_notices(self, db): + def test_get_related_privacy_notices(self, db, system): """Test PrivacyExperience.get_related_privacy_notices that are embedded in PrivacyExperience request""" privacy_experience = PrivacyExperience.create( db=db, @@ -369,6 +369,23 @@ def test_get_related_privacy_notices(self, db): == [] ) + # Privacy notice is applicable to a system - they share a data use + assert privacy_experience.get_related_privacy_notices( + db, systems_applicable=True + ) == [privacy_notice] + + system.privacy_declarations[0].delete(db) + db.refresh(system) + + # Privacy notice is no longer applicable to any systems + assert ( + privacy_experience.get_related_privacy_notices(db, systems_applicable=True) + == [] + ) + + privacy_notice.histories[0].delete(db) + privacy_notice.delete(db) + def test_get_should_show_banner(self, db): """Test PrivacyExperience.get_should_show_banner that is calculated at runtime""" privacy_experience = PrivacyExperience.create( From 9ac00a1fb90cb32ecb6340a9e768ab562d120813 Mon Sep 17 00:00:00 2001 From: Dawn Pattison Date: Thu, 22 Jun 2023 11:50:09 -0500 Subject: [PATCH 2/4] Update changelog --- CHANGELOG.md | 5 +++++ src/fides/api/models/privacy_experience.py | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 921da9ec50..3516a79544 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ The types of changes are: ## [Unreleased](https://github.com/ethyca/fides/compare/2.15.0...main) +### Fixed + +- Add Systems Applicable Filter to Privacy Experience List [#3654](https://github.com/ethyca/fides/pull/3654) + + ## [2.15.0](https://github.com/ethyca/fides/compare/2.14.1...2.15.0) ### Added diff --git a/src/fides/api/models/privacy_experience.py b/src/fides/api/models/privacy_experience.py index d4fd9847f4..b5fdcd3087 100644 --- a/src/fides/api/models/privacy_experience.py +++ b/src/fides/api/models/privacy_experience.py @@ -263,7 +263,9 @@ def get_related_privacy_notices( ) if systems_applicable: - data_uses: set[str] = System.get_data_uses(System.all(db), include_parents=True) + data_uses: set[str] = System.get_data_uses( + System.all(db), include_parents=True + ) privacy_notice_query = privacy_notice_query.filter(PrivacyNotice.data_uses.overlap(data_uses)) # type: ignore if not fides_user_provided_identity: From c587d6ac67971cecc87db46915b9e2005903f9f1 Mon Sep 17 00:00:00 2001 From: Allison King Date: Fri, 23 Jun 2023 15:33:51 -0400 Subject: [PATCH 3/4] Refetch privacy notices when systems are deleted Since a relevant data use may have been deleted --- clients/admin-ui/src/features/system/system.slice.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/admin-ui/src/features/system/system.slice.ts b/clients/admin-ui/src/features/system/system.slice.ts index 6a53b2a3fc..a4f616a7ec 100644 --- a/clients/admin-ui/src/features/system/system.slice.ts +++ b/clients/admin-ui/src/features/system/system.slice.ts @@ -46,7 +46,7 @@ const systemApi = baseApi.injectEndpoints({ params: { resource_type: "system" }, method: "DELETE", }), - invalidatesTags: ["System", "Datastore Connection"], + invalidatesTags: ["System", "Datastore Connection", "Privacy Notices"], }), upsertSystems: build.mutation({ query: (systems) => ({ From 0ef6392423ec712c51f7658a3b8e5a187b8b2949 Mon Sep 17 00:00:00 2001 From: Allison King Date: Fri, 23 Jun 2023 15:34:06 -0400 Subject: [PATCH 4/4] Add systems_applicable flag --- clients/fides-js/src/services/fides/api.ts | 1 + clients/privacy-center/features/consent/consent.slice.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/clients/fides-js/src/services/fides/api.ts b/clients/fides-js/src/services/fides/api.ts index 1e53c6840b..20d089f270 100644 --- a/clients/fides-js/src/services/fides/api.ts +++ b/clients/fides-js/src/services/fides/api.ts @@ -34,6 +34,7 @@ export const fetchExperience = async ( component: ComponentType.OVERLAY, has_notices: "true", has_config: "true", + systems_applicable: "true", fides_user_device_id: fidesUserDeviceId, }); const response = await fetch( diff --git a/clients/privacy-center/features/consent/consent.slice.ts b/clients/privacy-center/features/consent/consent.slice.ts index a8b045e544..40a4ed3947 100644 --- a/clients/privacy-center/features/consent/consent.slice.ts +++ b/clients/privacy-center/features/consent/consent.slice.ts @@ -67,6 +67,8 @@ export const consentApi = baseApi.injectEndpoints({ component: ComponentType.PRIVACY_CENTER, has_notices: true, show_disabled: false, + has_config: true, + systems_applicable: true, ...payload, }, }),