Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GET Privacy Experience Meta Endpoint #4328

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ The types of changes are:
- Support for passing in an AC string as part of a fides string for the TCF overlay [#4308](https://github.com/ethyca/fides/pull/4308)
- Added support for overriding the save user preferences API call with a custom fn provided through Fides.init [#4318](https://github.com/ethyca/fides/pull/4318)
- Return AC strings in GET Privacy Experience meta and allow saving preferences against AC strings [#4295](https://github.com/ethyca/fides/pull/4295)
- New GET Privacy Experience Meta Endpoint [#4328](https://github.com/ethyca/fides/pull/4328)
- Access and erasure support for SparkPost [#4328](https://github.com/ethyca/fides/pull/4238)
- Access and erasure support for Iterate [#4332](https://github.com/ethyca/fides/pull/4332)

Expand Down
95 changes: 84 additions & 11 deletions src/fides/api/api/v1/endpoints/privacy_experience_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@
)
from fides.api.models.privacy_notice import PrivacyNotice
from fides.api.models.privacy_request import ProvidedIdentity
from fides.api.schemas.privacy_experience import PrivacyExperienceResponse
from fides.api.schemas.privacy_experience import (
PrivacyExperienceMetaResponse,
PrivacyExperienceResponse,
)
from fides.api.util.api_router import APIRouter
from fides.api.util.consent_util import (
PRIVACY_EXPERIENCE_ESCAPE_FIELDS,
Expand Down Expand Up @@ -64,6 +67,25 @@
return privacy_experience


def _filter_experiences_by_component(
component: ComponentType, experience_query: Query
) -> Query:
"""
Filters privacy experiences by component

Intentionally relaxes what is returned when querying for "overlay", by returning both types of overlays.
This way the frontend doesn't have to know which type of overlay, regular or tcf, just that it is an overlay.
"""
component_search_map: Dict = {
ComponentType.overlay: [ComponentType.overlay, ComponentType.tcf_overlay]
}
return experience_query.filter(
PrivacyExperience.component.in_(
component_search_map.get(component, [component])
)
)


pattisdr marked this conversation as resolved.
Show resolved Hide resolved
def _filter_experiences_by_region_or_country(
db: Session, region: Optional[str], experience_query: Query
) -> Query:
Expand Down Expand Up @@ -119,6 +141,64 @@
return db.query(PrivacyExperience).filter(False)


@router.get(
urls.PRIVACY_EXPERIENCE_META,
status_code=HTTP_200_OK,
response_model=Page[PrivacyExperienceMetaResponse],
)
@fides_limiter.limit(CONFIG.security.public_request_rate_limit)
async def get_privacy_experience_meta(
*,
db: Session = Depends(deps.get_db),
params: Params = Depends(),
region: Optional[str] = None,
component: Optional[ComponentType] = None,
request: Request, # required for rate limiting
response: Response, # required for rate limiting
) -> AbstractPage[PrivacyExperience]:
"""Minimal Developer Friendly Privacy Experience endpoint that returns only the meta object,
the component, and the region."""

logger.info("Fetching meta info for Experiences '{}'", params)

await asyncio.sleep(delay=0.001)
experience_query: Query = db.query(PrivacyExperience)

await asyncio.sleep(delay=0.001)
if region is not None:
experience_query = _filter_experiences_by_region_or_country(
db=db, region=region, experience_query=experience_query
)

await asyncio.sleep(delay=0.001)
if component is not None:
experience_query = _filter_experiences_by_component(component, experience_query)

Check warning on line 175 in src/fides/api/api/v1/endpoints/privacy_experience_endpoints.py

View check run for this annotation

Codecov / codecov/patch

src/fides/api/api/v1/endpoints/privacy_experience_endpoints.py#L175

Added line #L175 was not covered by tests

await asyncio.sleep(delay=0.001)
# TCF contents are the same across all EEA regions, so we can build this once.
base_tcf_contents: TCFExperienceContents = get_tcf_contents(db)

tcf_meta: Optional[Dict] = None
for (
tcf_section_name,
_,
) in TCF_SECTION_MAPPING.items():
if getattr(base_tcf_contents, tcf_section_name):
# TCF Experience meta is also the same across all EEA regions. Only build meta if there is TCF
# content under at least one section.
tcf_meta = build_experience_tcf_meta(base_tcf_contents)
break

results: List[PrivacyExperience] = []
for experience in experience_query:
if experience.component == ComponentType.tcf_overlay:
# Attach meta for TCF Experiences only. We don't yet build meta info for non-TCF experiences.
experience.meta = tcf_meta
results.append(experience)

return fastapi_paginate(results, params=params)
pattisdr marked this conversation as resolved.
Show resolved Hide resolved


@router.get(
urls.PRIVACY_EXPERIENCE,
status_code=HTTP_200_OK,
Expand Down Expand Up @@ -196,16 +276,8 @@

await asyncio.sleep(delay=0.001)
if component is not None:
# Intentionally relaxes what is returned when querying for "overlay", by returning both types of overlays.
# This way the frontend doesn't have to know which type of overlay, regular or tcf, just that it is an overlay.
component_search_map: Dict = {
ComponentType.overlay: [ComponentType.overlay, ComponentType.tcf_overlay]
}
experience_query = experience_query.filter(
PrivacyExperience.component.in_(
component_search_map.get(component, [component])
)
)
experience_query = _filter_experiences_by_component(component, experience_query)

await asyncio.sleep(delay=0.001)
if has_config is True:
experience_query = experience_query.filter(
Expand All @@ -221,6 +293,7 @@
should_unescape: Optional[str] = request.headers.get(UNESCAPE_SAFESTR_HEADER)

# Builds TCF Experience Contents once here, in case multiple TCF Experiences are requested
await asyncio.sleep(delay=0.001)
base_tcf_contents: TCFExperienceContents = get_tcf_contents(db)

await asyncio.sleep(delay=0.001)
Expand Down
11 changes: 11 additions & 0 deletions src/fides/api/schemas/privacy_experience.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,3 +220,14 @@ class PrivacyExperienceResponse(TCFExperienceContents, PrivacyExperienceWithId):
)
gvl: Optional[Dict] = None
meta: Optional[ExperienceMeta] = None


class PrivacyExperienceMetaResponse(FidesSchema):
"""
Privacy Experience Response only containing region, component, id, and meta information
"""

id: str
region: PrivacyNoticeRegion
component: Optional[ComponentType]
meta: Optional[ExperienceMeta] = None
1 change: 1 addition & 0 deletions src/fides/common/api/v1/urn_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@

# Privacy Experience URLs
PRIVACY_EXPERIENCE = "/privacy-experience"
PRIVACY_EXPERIENCE_META = "/privacy-experience-meta"
PRIVACY_EXPERIENCE_DETAIL = "/privacy-experience/{privacy_experience_id}"

# Privacy Experience Config URLs
Expand Down
162 changes: 161 additions & 1 deletion tests/ops/api/v1/endpoints/test_privacy_experience_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
)
from fides.api.models.privacy_experience import ComponentType, PrivacyExperience
from fides.api.models.privacy_notice import ConsentMechanism
from fides.common.api.v1.urn_registry import PRIVACY_EXPERIENCE, V1_URL_PREFIX
from fides.common.api.v1.urn_registry import (
PRIVACY_EXPERIENCE,
PRIVACY_EXPERIENCE_META,
V1_URL_PREFIX,
)


class TestGetPrivacyExperiences:
Expand Down Expand Up @@ -1158,3 +1162,159 @@ def test_tcf_enabled_but_we_are_not_in_eea(self, db, privacy_experience_overlay)
)
assert resp.count() == 1
assert resp.first().id == privacy_experience_overlay.id


class TestExperienceMetaEndpoint:
@pytest.fixture(scope="function")
def url(self) -> str:
return V1_URL_PREFIX + PRIVACY_EXPERIENCE_META

@staticmethod
def assert_no_meta_data(response_json):
assert response_json["meta"]["version_hash"] is None
assert response_json["meta"]["accept_all_fides_string"] is None
assert response_json["meta"]["accept_all_fides_mobile_data"] is None
assert response_json["meta"]["reject_all_fides_string"] is None
assert response_json["meta"]["reject_all_fides_mobile_data"] is None

@pytest.mark.usefixtures(
"tcf_system",
"privacy_experience_france_overlay",
"privacy_experience_france_tcf_overlay",
)
def test_tcf_not_enabled(self, url, api_client):
"""Regular overlay was returned instead of TCF overlay"""
resp = api_client.get(
url + "?region=fr",
)
assert resp.status_code == 200
assert len(resp.json()["items"]) == 1
resp_json = resp.json()["items"][0]
assert resp_json["component"] == ComponentType.overlay.value
assert resp_json["region"] == "fr"
self.assert_no_meta_data(resp_json)

@pytest.mark.usefixtures(
"tcf_system",
"privacy_experience_france_overlay",
"enable_tcf",
"privacy_experience_france_tcf_overlay",
"ac_system_with_privacy_declaration",
)
def test_tcf_enabled_get_experience_meta(self, url, api_client):
"""TCF Enabled but not AC so no AC information is returned"""
resp = api_client.get(
url + "?region=fr",
)
assert resp.status_code == 200
assert len(resp.json()["items"]) == 1
resp_json = resp.json()["items"][0]
assert resp_json["component"] == ComponentType.tcf_overlay.value
assert resp_json["region"] == "fr"
assert resp_json["meta"]["version_hash"] == "dbde7265d5dd"
assert resp_json["meta"]["accept_all_fides_string"] is not None
assert resp_json["meta"]["accept_all_fides_mobile_data"] is not None

assert (
resp_json["meta"]["accept_all_fides_string"]
== resp_json["meta"]["accept_all_fides_mobile_data"]["IABTCF_TCString"]
)
assert (
resp_json["meta"]["accept_all_fides_mobile_data"]["IABTCF_AddtlConsent"]
is None
)
assert (
resp_json["meta"]["accept_all_fides_mobile_data"]["IABTCF_PurposeConsents"]
== "000000010000000000000000"
)
assert (
resp_json["meta"]["accept_all_fides_mobile_data"]["IABTCF_VendorConsents"]
== "000000000000000000000000000000000000000001"
)
assert resp_json["meta"]["reject_all_fides_string"] is not None
assert resp_json["meta"]["reject_all_fides_mobile_data"] is not None
assert (
resp_json["meta"]["reject_all_fides_mobile_data"]["IABTCF_AddtlConsent"]
is None
)
assert (
resp_json["meta"]["reject_all_fides_string"]
== resp_json["meta"]["reject_all_fides_mobile_data"]["IABTCF_TCString"]
)

@pytest.mark.usefixtures(
"tcf_system",
"privacy_experience_france_overlay",
"enable_tcf",
"enable_ac",
"privacy_experience_france_tcf_overlay",
"ac_system_with_privacy_declaration",
)
def test_tcf_and_ac_enabled_get_experience_meta(self, url, api_client):
"""TCF Enabled and AC enabled
Purpose consents section is altered because an AC purpose is surfaced in addition to a GVL purpose.
Vendor consents section is the same because AC vendors don't show up in that section.
IABTCF_AddtlConsent is populated
"""
resp = api_client.get(
url + "?region=fr",
)
assert resp.status_code == 200
assert len(resp.json()["items"]) == 1
resp_json = resp.json()["items"][0]
assert resp_json["component"] == ComponentType.tcf_overlay.value
assert resp_json["region"] == "fr"
assert resp_json["meta"]["version_hash"] == "ac76b5d026b7"
assert resp_json["meta"]["accept_all_fides_string"] is not None
assert resp_json["meta"]["accept_all_fides_mobile_data"] is not None

assert (
resp_json["meta"]["accept_all_fides_string"]
== resp_json["meta"]["accept_all_fides_mobile_data"]["IABTCF_TCString"]
+ ","
+ "1~8"
)
assert (
resp_json["meta"]["accept_all_fides_mobile_data"]["IABTCF_AddtlConsent"]
== "1~8"
)
assert (
resp_json["meta"]["accept_all_fides_mobile_data"]["IABTCF_PurposeConsents"]
== "100000010000000000000000"
)
assert (
resp_json["meta"]["accept_all_fides_mobile_data"]["IABTCF_VendorConsents"]
== "000000000000000000000000000000000000000001"
)
assert resp_json["meta"]["reject_all_fides_string"] is not None
assert resp_json["meta"]["reject_all_fides_mobile_data"] is not None
assert (
resp_json["meta"]["reject_all_fides_mobile_data"]["IABTCF_AddtlConsent"]
== "1~"
)
assert (
resp_json["meta"]["reject_all_fides_string"]
== resp_json["meta"]["reject_all_fides_mobile_data"]["IABTCF_TCString"]
+ ","
+ "1~"
)

@pytest.mark.usefixtures(
"tcf_system",
"privacy_experience_france_overlay",
"enable_tcf",
"privacy_experience_france_tcf_overlay",
"privacy_experience_overlay",
)
def test_get_ca_experience_meta(self, url, api_client):
"""Meta data only built for TCF experiences currently,
so fetching a CA experience will just return null meta details"""
resp = api_client.get(
url + "?region=us_ca",
)
assert resp.status_code == 200
assert len(resp.json()["items"]) == 1
resp_json = resp.json()["items"][0]
assert resp_json["component"] == ComponentType.overlay.value
assert resp_json["region"] == "us_ca"
self.assert_no_meta_data(resp_json)
Loading