diff --git a/CHANGELOG.md b/CHANGELOG.md index 17e627b443..05ad481e23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ The types of changes are: ### Fixed - Render linebreaks in the Fides.js overlay descriptions, etc. [#3665](https://github.com/ethyca/fides/pull/3665) - Broken link to Fides docs site on the About Fides page in Admin UI [#3643](https://github.com/ethyca/fides/pull/3643) +- Add Systems Applicable Filter to Privacy Experience List [#3654](https://github.com/ethyca/fides/pull/3654) ### Developer Experience 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) => ({ 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 e6ef46807e..ef412546fb 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, }, }), 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..b5fdcd3087 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,12 @@ 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(