diff --git a/CHANGELOG.md b/CHANGELOG.md index 49ee7e718dd..7922fd2c7c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,12 +25,14 @@ The types of changes are: - Refactor CSS variables for `fides-js` to match brandable color palette [#3321](https://github.com/ethyca/fides/pull/3321) - Moved all of the dirs from `fides.api.ops` into `fides.api` [#3318](https://github.com/ethyca/fides/pull/3318) - Add required notice key to privacy notices [#3337](https://github.com/ethyca/fides/pull/3337) +- Make Privacy Experience List public, and separate public endpoint rate limiting [#3339](https://github.com/ethyca/fides/pull/3339) ### Added - Add an automated test to check for `/fides-consent.js` backwards compatibility [#3289](https://github.com/ethyca/fides/pull/3289) - Add infrastructure for "overlay" consent components (Preact, CSS bundling, etc.) and initial version of consent banner [#3191](https://github.com/ethyca/fides/pull/3191) - Add the modal component of the "overlay" consent components [#3291](https://github.com/ethyca/fides/pull/3291) +- Track Privacy Experience with Privacy Preferences [#3311](https://github.com/ethyca/fides/pull/3311) ### Fixed diff --git a/docs/fides/docs/development/postman/Fides.postman_collection.json b/docs/fides/docs/development/postman/Fides.postman_collection.json index 6efd050960e..dae78da8004 100644 --- a/docs/fides/docs/development/postman/Fides.postman_collection.json +++ b/docs/fides/docs/development/postman/Fides.postman_collection.json @@ -5438,34 +5438,6 @@ }, "response": [] }, - { - "name": "Privacy Experience Detail", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{client_token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{host}}/privacy-experience/{{privacy_experience_id}}", - "host": [ - "{{host}}" - ], - "path": [ - "privacy-experience", - "{{privacy_experience_id}}" - ] - } - }, - "response": [] - }, { "name": "Experience Config", "request": { 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 6b035611903..c4ac03daa14 100644 --- a/src/fides/api/api/v1/endpoints/privacy_experience_endpoints.py +++ b/src/fides/api/api/v1/endpoints/privacy_experience_endpoints.py @@ -1,23 +1,28 @@ +import uuid from typing import List, Optional -from fastapi import Depends, HTTPException, Security +from fastapi import Depends, HTTPException, Request, Response from fastapi_pagination import Page, Params from fastapi_pagination import paginate as fastapi_paginate from fastapi_pagination.bases import AbstractPage from loguru import logger from sqlalchemy.orm import Session -from starlette.status import HTTP_200_OK, HTTP_400_BAD_REQUEST, HTTP_404_NOT_FOUND +from starlette.status import ( + HTTP_200_OK, + HTTP_404_NOT_FOUND, + HTTP_422_UNPROCESSABLE_ENTITY, +) from fides.api.api import deps -from fides.api.api.v1 import scope_registry from fides.api.api.v1 import urn_registry as urls +from fides.api.api.v1.endpoints.utils import fides_limiter from fides.api.models.privacy_experience import ComponentType, PrivacyExperience from fides.api.models.privacy_notice import PrivacyNotice, PrivacyNoticeRegion from fides.api.models.privacy_request import ProvidedIdentity -from fides.api.oauth.utils import verify_oauth_client from fides.api.schemas.privacy_experience import PrivacyExperienceResponse from fides.api.util.api_router import APIRouter from fides.api.util.consent_util import get_fides_user_device_id_provided_identity +from fides.core.config import CONFIG router = APIRouter(tags=["Privacy Experience"], prefix=urls.V1_URL_PREFIX) @@ -43,10 +48,8 @@ def get_privacy_experience_or_error( urls.PRIVACY_EXPERIENCE, status_code=HTTP_200_OK, response_model=Page[PrivacyExperienceResponse], - dependencies=[ - Security(verify_oauth_client, scopes=[scope_registry.PRIVACY_EXPERIENCE_READ]) - ], ) +@fides_limiter.limit(CONFIG.security.public_request_rate_limit) def privacy_experience_list( *, db: Session = Depends(deps.get_db), @@ -57,9 +60,11 @@ def privacy_experience_list( has_notices: Optional[bool] = None, has_config: Optional[bool] = None, fides_user_device_id: Optional[str] = None, + request: Request, # required for rate limiting + response: Response, # required for rate limiting ) -> AbstractPage[PrivacyExperience]: """ - Returns a list of PrivacyExperience records for individual regions with + Public endpoint that returns a list of PrivacyExperience records for individual regions with relevant privacy notices embedded in the response. 'show_disabled' query params are passed along to further filter @@ -71,6 +76,13 @@ def privacy_experience_list( logger.info("Finding all Privacy Experiences with pagination params '{}'", params) fides_user_provided_identity: Optional[ProvidedIdentity] = None if fides_user_device_id: + try: + uuid.UUID(fides_user_device_id, version=4) + except ValueError: + raise HTTPException( + status_code=HTTP_422_UNPROCESSABLE_ENTITY, + detail="Invalid fides user device id format", + ) fides_user_provided_identity = get_fides_user_device_id_provided_identity( db=db, fides_user_device_id=fides_user_device_id ) @@ -110,50 +122,3 @@ def privacy_experience_list( results.append(privacy_experience) return fastapi_paginate(results, params=params) - - -@router.get( - urls.PRIVACY_EXPERIENCE_DETAIL, - status_code=HTTP_200_OK, - response_model=PrivacyExperienceResponse, - dependencies=[ - Security(verify_oauth_client, scopes=[scope_registry.PRIVACY_EXPERIENCE_READ]) - ], -) -def privacy_experience_detail( - *, - db: Session = Depends(deps.get_db), - privacy_experience_id: str, - show_disabled: Optional[bool] = True, - fides_user_device_id: Optional[str] = None, -) -> PrivacyExperience: - """ - Return a privacy experience for a given region with relevant notices embedded. - - show_disabled query params are passed onto optionally filter the embedded notices. - - 'fides_user_device_id' query param will stash the current preferences of the given user - alongside each notice where applicable. - """ - logger.info("Fetching privacy experience with id '{}'.", privacy_experience_id) - experience: PrivacyExperience = get_privacy_experience_or_error( - db, privacy_experience_id - ) - - if show_disabled is False and experience.disabled: - raise HTTPException( - status_code=HTTP_400_BAD_REQUEST, - detail=f"Query param show_disabled=False not applicable for disabled privacy experience {privacy_experience_id}.", - ) - - fides_user_provided_identity: Optional[ProvidedIdentity] = None - if fides_user_device_id: - fides_user_provided_identity = get_fides_user_device_id_provided_identity( - db=db, fides_user_device_id=fides_user_device_id - ) - - # Temporarily stash the privacy notices on the experience for display - experience.privacy_notices = experience.get_related_privacy_notices( - db, show_disabled, fides_user_provided_identity - ) - return experience diff --git a/src/fides/api/api/v1/endpoints/privacy_preference_endpoints.py b/src/fides/api/api/v1/endpoints/privacy_preference_endpoints.py index d20fd54ecba..86d02b0d6fe 100644 --- a/src/fides/api/api/v1/endpoints/privacy_preference_endpoints.py +++ b/src/fides/api/api/v1/endpoints/privacy_preference_endpoints.py @@ -1,10 +1,8 @@ -from __future__ import annotations - import ipaddress from datetime import datetime from typing import List, Optional, Tuple -from fastapi import Depends, HTTPException, Request +from fastapi import Depends, HTTPException, Request, Response from fastapi.params import Security from fastapi_pagination import Page, Params from fastapi_pagination.bases import AbstractPage @@ -21,7 +19,10 @@ from fides.api.api.v1.endpoints.privacy_request_endpoints import ( create_privacy_request_func, ) -from fides.api.api.v1.endpoints.utils import validate_start_and_end_filters +from fides.api.api.v1.endpoints.utils import ( + fides_limiter, + validate_start_and_end_filters, +) from fides.api.api.v1.scope_registry import ( CURRENT_PRIVACY_PREFERENCE_READ, PRIVACY_PREFERENCE_HISTORY_READ, @@ -66,6 +67,7 @@ from fides.api.util.consent_util import ( get_or_create_fides_user_device_id_provided_identity, ) +from fides.core.config import CONFIG from fides.core.config.config_proxy import ConfigProxy router = APIRouter(tags=["Privacy Preference"], prefix=V1_URL_PREFIX) @@ -418,11 +420,13 @@ def _save_privacy_preferences_for_identities( status_code=HTTP_200_OK, response_model=List[CurrentPrivacyPreferenceSchema], ) +@fides_limiter.limit(CONFIG.security.public_request_rate_limit) def save_privacy_preferences( *, - request: Request, db: Session = Depends(get_db), data: PrivacyPreferencesRequest, + request: Request, + response: Response, # required for rate limiting ) -> List[CurrentPrivacyPreference]: """Saves privacy preferences with respect to a fides user device id. diff --git a/src/fides/api/api/v1/endpoints/utils.py b/src/fides/api/api/v1/endpoints/utils.py index 96e615aacd7..8ddd860cde3 100644 --- a/src/fides/api/api/v1/endpoints/utils.py +++ b/src/fides/api/api/v1/endpoints/utils.py @@ -4,8 +4,22 @@ from typing import Any, Callable, List, Optional, Tuple from fastapi import HTTPException +from slowapi import Limiter +from slowapi.util import get_remote_address # type: ignore from starlette.status import HTTP_400_BAD_REQUEST +from fides.core.config import CONFIG + +# Used for rate limiting with Slow API +# Decorate individual routes to deviate from the default rate limits +fides_limiter = Limiter( + default_limits=[CONFIG.security.request_rate_limit], + headers_enabled=True, + key_prefix=CONFIG.security.rate_limit_prefix, + key_func=get_remote_address, + retry_after="http-date", +) + def validate_start_and_end_filters( date_filters: List[Tuple[Optional[datetime], Optional[datetime], str]] diff --git a/src/fides/api/app_setup.py b/src/fides/api/app_setup.py index 9e90799f5f6..c3ea87ab987 100644 --- a/src/fides/api/app_setup.py +++ b/src/fides/api/app_setup.py @@ -8,14 +8,14 @@ from loguru import logger from redis.exceptions import RedisError, ResponseError from slowapi.errors import RateLimitExceeded # type: ignore -from slowapi.extension import Limiter, _rate_limit_exceeded_handler # type: ignore +from slowapi.extension import _rate_limit_exceeded_handler # type: ignore from slowapi.middleware import SlowAPIMiddleware # type: ignore -from slowapi.util import get_remote_address # type: ignore from starlette.middleware.cors import CORSMiddleware import fides from fides.api.api.deps import get_api_session from fides.api.api.v1.api import api_router +from fides.api.api.v1.endpoints.utils import fides_limiter from fides.api.api.v1.exception_handlers import ExceptionHandlers from fides.api.common_exceptions import FunctionalityNotConfigured, RedisConnectionError from fides.api.ctl.database.database import configure_db @@ -50,8 +50,6 @@ def create_fides_app( cors_origin_regex: Optional[Pattern] = CONFIG.security.cors_origin_regex, routers: List = ROUTERS, app_version: str = VERSION, - request_rate_limit: str = CONFIG.security.request_rate_limit, - rate_limit_prefix: str = CONFIG.security.rate_limit_prefix, security_env: str = CONFIG.security.env, ) -> FastAPI: """Return a properly configured application.""" @@ -65,13 +63,7 @@ def create_fides_app( ) fastapi_app = FastAPI(title="fides", version=app_version) - fastapi_app.state.limiter = Limiter( - default_limits=[request_rate_limit], - headers_enabled=True, - key_prefix=rate_limit_prefix, - key_func=get_remote_address, - retry_after="http-date", - ) + fastapi_app.state.limiter = fides_limiter fastapi_app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) for handler in ExceptionHandlers.get_handlers(): fastapi_app.add_exception_handler(FunctionalityNotConfigured, handler) diff --git a/src/fides/api/schemas/redis_cache.py b/src/fides/api/schemas/redis_cache.py index f4580ebf117..9e7acf00ad7 100644 --- a/src/fides/api/schemas/redis_cache.py +++ b/src/fides/api/schemas/redis_cache.py @@ -1,6 +1,7 @@ +import uuid from typing import Optional -from pydantic import EmailStr, Extra +from pydantic import EmailStr, Extra, validator from fides.api.custom_types import PhoneNumber from fides.api.schemas.base_class import FidesSchema @@ -32,3 +33,12 @@ class Config: """Only allow phone_number, and email.""" extra = Extra.forbid + + @validator("fides_user_device_id") + @classmethod + def validate_fides_user_device_id(cls, v: Optional[str]) -> Optional[str]: + """Validate the uuid format of the fides user device id while still keeping the data type a string""" + if not v: + return v + uuid.UUID(v, version=4) + return v diff --git a/src/fides/core/config/security_settings.py b/src/fides/core/config/security_settings.py index e621978c734..40e0a59c2a7 100644 --- a/src/fides/core/config/security_settings.py +++ b/src/fides/core/config/security_settings.py @@ -85,6 +85,10 @@ class SecuritySettings(FidesSettings): default=None, description="When using a parent/child Fides deployment, this username will be used by the child server to access the parent server.", ) + public_request_rate_limit: str = Field( + default="2000/minute", + description="The number of requests from a single IP address allowed to hit a public endpoint within the specified time period", + ) rate_limit_prefix: str = Field( default="fides-", description="The prefix given to keys in the Redis cache used by the rate limiter.", diff --git a/tests/fixtures/application_fixtures.py b/tests/fixtures/application_fixtures.py index e467883e02c..00ff1509609 100644 --- a/tests/fixtures/application_fixtures.py +++ b/tests/fixtures/application_fixtures.py @@ -1969,8 +1969,10 @@ def fides_user_provided_identity(db): provided_identity_data = { "privacy_request_id": None, "field_name": "fides_user_device_id", - "hashed_value": ProvidedIdentity.hash_value("FGHIJ_TEST_FIDES"), - "encrypted_value": {"value": "FGHIJ_TEST_FIDES"}, + "hashed_value": ProvidedIdentity.hash_value( + "051b219f-20e4-45df-82f7-5eb68a00889f" + ), + "encrypted_value": {"value": "051b219f-20e4-45df-82f7-5eb68a00889f"}, } provided_identity = ProvidedIdentity.create(db, data=provided_identity_data) 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 3ee57dd6393..f4daf8d9ce4 100644 --- a/tests/ops/api/v1/endpoints/test_privacy_experience_endpoints.py +++ b/tests/ops/api/v1/endpoints/test_privacy_experience_endpoints.py @@ -1,15 +1,10 @@ from __future__ import annotations import pytest -from starlette.status import HTTP_200_OK, HTTP_403_FORBIDDEN +from starlette.status import HTTP_200_OK from starlette.testclient import TestClient -from fides.api.api.v1 import scope_registry as scopes -from fides.api.api.v1.urn_registry import ( - PRIVACY_EXPERIENCE, - PRIVACY_EXPERIENCE_DETAIL, - V1_URL_PREFIX, -) +from fides.api.api.v1.urn_registry import PRIVACY_EXPERIENCE, V1_URL_PREFIX class TestGetPrivacyExperiences: @@ -18,18 +13,9 @@ def url(self) -> str: return V1_URL_PREFIX + PRIVACY_EXPERIENCE def test_get_privacy_experiences_unauthenticated(self, url, api_client): + """This is a public endpoint""" resp = api_client.get(url) - assert resp.status_code == 401 - - def test_get_privacy_experiences_wrong_scope( - self, url, api_client: TestClient, generate_auth_header - ): - auth_header = generate_auth_header(scopes=[scopes.STORAGE_READ]) - resp = api_client.get( - url, - headers=auth_header, - ) - assert resp.status_code == 403 + assert resp.status_code == 200 @pytest.mark.parametrize( "role,expected_status", @@ -38,12 +24,13 @@ def test_get_privacy_experiences_wrong_scope( ("contributor", HTTP_200_OK), ("viewer_and_approver", HTTP_200_OK), ("viewer", HTTP_200_OK), - ("approver", HTTP_403_FORBIDDEN), + ("approver", HTTP_200_OK), ], ) def test_get_privacy_experience_with_roles( self, role, expected_status, api_client: TestClient, url, generate_role_header ) -> None: + """This is a public endpoint""" auth_header = generate_role_header(roles=[role]) response = api_client.get(url, headers=auth_header) assert response.status_code == expected_status @@ -51,15 +38,12 @@ def test_get_privacy_experience_with_roles( def test_get_privacy_experiences( self, api_client: TestClient, - generate_auth_header, url, privacy_notice, privacy_experience_privacy_center_link, ): - auth_header = generate_auth_header(scopes=[scopes.PRIVACY_EXPERIENCE_READ]) resp = api_client.get( url, - headers=auth_header, ) assert resp.status_code == 200 data = resp.json() @@ -105,16 +89,13 @@ def test_get_privacy_experiences( def test_get_privacy_experiences_show_disabled_filter( self, api_client: TestClient, - generate_auth_header, url, privacy_experience_privacy_center_link, privacy_experience_overlay_link, privacy_experience_overlay_banner, ): - auth_header = generate_auth_header(scopes=[scopes.PRIVACY_EXPERIENCE_READ]) resp = api_client.get( url, - headers=auth_header, ) assert resp.status_code == 200 data = resp.json() @@ -123,7 +104,6 @@ def test_get_privacy_experiences_show_disabled_filter( resp = api_client.get( url + "?show_disabled=False", - headers=auth_header, ) assert resp.status_code == 200 data = resp.json() @@ -144,15 +124,12 @@ def test_get_privacy_experiences_show_disabled_filter( def test_get_privacy_experiences_region_filter( self, api_client: TestClient, - generate_auth_header, url, privacy_experience_overlay_link, privacy_experience_overlay_banner, ): - auth_header = generate_auth_header(scopes=[scopes.PRIVACY_EXPERIENCE_READ]) resp = api_client.get( url + "?region=eu_fr", - headers=auth_header, ) assert resp.status_code == 200 data = resp.json() @@ -162,7 +139,6 @@ def test_get_privacy_experiences_region_filter( assert data["items"][0]["region"] == "eu_fr" resp = api_client.get( url + "?region=us_ca", - headers=auth_header, ) assert resp.status_code == 200 data = resp.json() @@ -174,13 +150,11 @@ def test_get_privacy_experiences_region_filter( resp = api_client.get( url + "?region=bad_region", - headers=auth_header, ) assert resp.status_code == 422 resp = api_client.get( url + "?region=eu_it", - headers=auth_header, ) assert resp.status_code == 200 assert resp.json()["total"] == 0 @@ -188,16 +162,13 @@ def test_get_privacy_experiences_region_filter( def test_get_privacy_experiences_components_filter( self, api_client: TestClient, - generate_auth_header, url, privacy_experience_privacy_center_link, privacy_experience_overlay_link, privacy_experience_overlay_banner, ): - auth_header = generate_auth_header(scopes=[scopes.PRIVACY_EXPERIENCE_READ]) resp = api_client.get( url + "?component=overlay", - headers=auth_header, ) assert resp.status_code == 200 data = resp.json() @@ -210,7 +181,6 @@ def test_get_privacy_experiences_components_filter( resp = api_client.get( url + "?component=privacy_center", - headers=auth_header, ) assert resp.status_code == 200 data = resp.json() @@ -222,7 +192,6 @@ def test_get_privacy_experiences_components_filter( resp = api_client.get( url + "?component=bad_type", - headers=auth_header, ) assert resp.status_code == 422 @@ -232,12 +201,10 @@ def test_get_privacy_experiences_components_filter( "privacy_experience_overlay_banner", ) def test_get_privacy_experiences_has_notices_no_notices( - self, api_client: TestClient, generate_auth_header, url + self, api_client: TestClient, url ): - auth_header = generate_auth_header(scopes=[scopes.PRIVACY_EXPERIENCE_READ]) resp = api_client.get( url + "?has_notices=True", - headers=auth_header, ) assert resp.status_code == 200 data = resp.json() @@ -251,12 +218,10 @@ def test_get_privacy_experiences_has_notices_no_notices( "privacy_notice_eu_cy_provide_service_frontend_only", ) def test_get_privacy_experiences_has_notices_no_regions_overlap( - self, api_client: TestClient, generate_auth_header, url + self, api_client: TestClient, url ): - auth_header = generate_auth_header(scopes=[scopes.PRIVACY_EXPERIENCE_READ]) resp = api_client.get( url + "?has_notices=True", - headers=auth_header, ) assert resp.status_code == 200 data = resp.json() @@ -270,7 +235,6 @@ def test_get_privacy_experiences_has_notices_no_regions_overlap( def test_get_privacy_experiences_has_notices( self, api_client: TestClient, - generate_auth_header, url, privacy_experience_privacy_center_link, privacy_experience_overlay_link, @@ -280,10 +244,8 @@ def test_get_privacy_experiences_has_notices( privacy_notice_eu_fr_provide_service_frontend_only, privacy_notice_us_ca_provide, ): - auth_header = generate_auth_header(scopes=[scopes.PRIVACY_EXPERIENCE_READ]) resp = api_client.get( url + "?has_notices=True", - headers=auth_header, ) assert resp.status_code == 200 data = resp.json() @@ -371,17 +333,14 @@ def test_get_privacy_experiences_has_notices( def test_filter_on_notices_and_region( self, api_client: TestClient, - generate_auth_header, url, privacy_experience_privacy_center_link, privacy_notice, privacy_notice_us_co_third_party_sharing, ): """Region filter propagates through to the notices too""" - auth_header = generate_auth_header(scopes=[scopes.PRIVACY_EXPERIENCE_READ]) resp = api_client.get( url + "?has_notices=True®ion=us_co", - headers=auth_header, ) assert resp.status_code == 200 data = resp.json() @@ -413,7 +372,6 @@ def test_filter_on_notices_and_region( def test_filter_on_notices_and_region_and_show_disabled_is_false( self, api_client: TestClient, - generate_auth_header, db, url, privacy_experience_privacy_center_link, @@ -424,10 +382,8 @@ def test_filter_on_notices_and_region_and_show_disabled_is_false( privacy_notice_us_co_third_party_sharing.disabled = True privacy_notice_us_co_third_party_sharing.save(db) - auth_header = generate_auth_header(scopes=[scopes.PRIVACY_EXPERIENCE_READ]) resp = api_client.get( url + "?has_notices=True®ion=us_co&show_disabled=False", - headers=auth_header, ) assert resp.status_code == 200 data = resp.json() @@ -447,17 +403,14 @@ def test_filter_on_notices_and_region_and_show_disabled_is_false( def test_get_privacy_experiences_show_has_config_filter( self, api_client: TestClient, - generate_auth_header, url, privacy_experience_privacy_center_link, privacy_experience_overlay_link, privacy_experience_overlay_banner, db, ): - auth_header = generate_auth_header(scopes=[scopes.PRIVACY_EXPERIENCE_READ]) resp = api_client.get( url + "?has_config=False", - headers=auth_header, ) assert resp.status_code == 200 data = resp.json() @@ -466,7 +419,6 @@ def test_get_privacy_experiences_show_has_config_filter( resp = api_client.get( url + "?has_config=True", - headers=auth_header, ) assert resp.status_code == 200 data = resp.json() @@ -484,7 +436,6 @@ def test_get_privacy_experiences_show_has_config_filter( privacy_experience_privacy_center_link.save(db=db) resp = api_client.get( url + "?has_config=False", - headers=auth_header, ) assert resp.status_code == 200 data = resp.json() @@ -498,227 +449,40 @@ def test_get_privacy_experiences_show_has_config_filter( "privacy_preference_history_us_ca_provide_for_fides_user", "privacy_experience_overlay_banner", ) - def test_get_privacy_experiences_fides_user_device_id_filter( + def test_get_privacy_experiences_bad_fides_user_device_id_filter( self, api_client: TestClient, - generate_auth_header, url, ): - auth_header = generate_auth_header(scopes=[scopes.PRIVACY_EXPERIENCE_READ]) resp = api_client.get( - url + "?fides_user_device_id=FGHIJ_TEST_FIDES", - headers=auth_header, - ) - assert resp.status_code == 200 - data = resp.json() - - assert "items" in data - - # assert one experience in the response - assert data["total"] == 1 - assert len(data["items"]) == 1 - resp = data["items"][0] - - # Assert current preference is displayed for fides user device id - assert resp["privacy_notices"][0]["consent_mechanism"] == "opt_in" - assert resp["privacy_notices"][0]["default_preference"] == "opt_out" - assert resp["privacy_notices"][0]["current_preference"] == "opt_in" - assert resp["privacy_notices"][0]["outdated_preference"] is None - - -class TestPrivacyExperienceDetail: - @pytest.fixture(scope="function") - def url(self, privacy_experience_overlay_banner) -> str: - return V1_URL_PREFIX + PRIVACY_EXPERIENCE_DETAIL.format( - privacy_experience_id=privacy_experience_overlay_banner.id - ) - - def test_get_privacy_experience_detail_unauthenticated(self, url, api_client): - resp = api_client.get(url) - assert resp.status_code == 401 - - def test_get_privacy_experience_detail_wrong_scope( - self, url, api_client: TestClient, generate_auth_header - ): - auth_header = generate_auth_header(scopes=[scopes.PRIVACY_NOTICE_READ]) - resp = api_client.get( - url, - headers=auth_header, - ) - assert resp.status_code == 403 - - @pytest.mark.parametrize( - "role,expected_status", - [ - ("owner", HTTP_200_OK), - ("contributor", HTTP_200_OK), - ("viewer_and_approver", HTTP_200_OK), - ("viewer", HTTP_200_OK), - ("approver", HTTP_403_FORBIDDEN), - ], - ) - def test_get_privacy_experience_detail_with_roles( - self, - role, - expected_status, - api_client: TestClient, - url, - generate_role_header, - ) -> None: - auth_header = generate_role_header(roles=[role]) - response = api_client.get(url, headers=auth_header) - assert response.status_code == expected_status - - def test_get_privacy_experience_bad_experience( - self, url, api_client: TestClient, generate_auth_header - ): - auth_header = generate_auth_header(scopes=[scopes.PRIVACY_EXPERIENCE_READ]) - resp = api_client.get( - V1_URL_PREFIX - + PRIVACY_EXPERIENCE_DETAIL.format(privacy_experience_id="bad_id"), - headers=auth_header, - ) - assert resp.status_code == 404 - - @pytest.mark.usefixtures( - "privacy_notice", "privacy_notice_eu_fr_provide_service_frontend_only" - ) - def test_get_privacy_experience_detail( - self, - api_client: TestClient, - generate_auth_header, - url, - privacy_experience_overlay_banner, - privacy_notice_us_ca_provide, - privacy_notice, - ): - auth_header = generate_auth_header(scopes=[scopes.PRIVACY_EXPERIENCE_READ]) - - resp = api_client.get(url, headers=auth_header) - assert resp.status_code == 200 - body = resp.json() - - assert body["disabled"] is True - assert body["component"] == "overlay" - assert body["delivery_mechanism"] == "banner" - assert body["region"] == "us_ca" - experience_config = body["experience_config"] - assert experience_config["component_title"] == "Manage your consent" - assert ( - experience_config["component_description"] - == "On this page you can opt in and out of these data uses cases" - ) - assert experience_config["banner_title"] == "Manage your consent" - assert ( - experience_config["banner_description"] - == "We use cookies to recognize visitors and remember their preferences" - ) - assert experience_config["link_label"] is None - assert experience_config["confirmation_button_label"] == "Accept all" - assert experience_config["reject_button_label"] == "Reject all" - assert experience_config["acknowledgement_button_label"] == "Confirm" - assert experience_config["id"] is not None - assert experience_config["version"] == 1.0 - assert ( - experience_config["experience_config_history_id"] - == privacy_experience_overlay_banner.experience_config_history_id - ) - - assert body["id"] == privacy_experience_overlay_banner.id - assert body["created_at"] is not None - assert body["updated_at"] is not None - assert body["version"] == 1.0 - assert ( - body["privacy_experience_history_id"] - == privacy_experience_overlay_banner.histories[0].id - ) - assert len(body["privacy_notices"]) == 2 - assert body["privacy_notices"][0]["id"] == privacy_notice_us_ca_provide.id - assert body["privacy_notices"][1]["id"] == privacy_notice.id - - # Assert default preferences displayed only - assert body["privacy_notices"][0]["consent_mechanism"] == "opt_in" - assert body["privacy_notices"][0]["default_preference"] == "opt_out" - assert body["privacy_notices"][0]["current_preference"] is None - assert body["privacy_notices"][0]["outdated_preference"] is None - - # Assert default preferences displayed only - assert body["privacy_notices"][0]["consent_mechanism"] == "opt_in" - assert body["privacy_notices"][0]["default_preference"] == "opt_out" - assert body["privacy_notices"][0]["current_preference"] is None - assert body["privacy_notices"][0]["outdated_preference"] is None - - @pytest.mark.usefixtures( - "privacy_notice", "privacy_notice_eu_fr_provide_service_frontend_only" - ) - def test_get_privacy_experience_detail_bad_show_disabled_filter( - self, - api_client: TestClient, - generate_auth_header, - url, - db, - privacy_experience_overlay_banner, - ): - """Show_disabled=False can be added to privacy experience detail to only return enabled privacy notices for an experience. - However, if the experience itself is disabled, this is an invalid filter. - """ - privacy_experience_overlay_banner.disabled = True - privacy_experience_overlay_banner.save(db) - auth_header = generate_auth_header(scopes=[scopes.PRIVACY_EXPERIENCE_READ]) - - resp = api_client.get(url + "?show_disabled=False", headers=auth_header) - assert resp.status_code == 400 - assert ( - resp.json()["detail"] - == f"Query param show_disabled=False not applicable for disabled privacy experience {privacy_experience_overlay_banner.id}." + url + "?fides_user_device_id=does_not_exist", ) + assert resp.status_code == 422 + assert resp.json()["detail"] == "Invalid fides user device id format" @pytest.mark.usefixtures( - "privacy_notice", "privacy_notice_eu_fr_provide_service_frontend_only" + "privacy_notice_us_ca_provide", + "fides_user_provided_identity", + "privacy_preference_history_us_ca_provide_for_fides_user", + "privacy_experience_overlay_banner", ) - def test_get_privacy_experience_detail_disabled_filter( + def test_get_privacy_experiences_nonexistent_fides_user_device_id_filter( self, api_client: TestClient, - generate_auth_header, url, - db, - privacy_notice, - privacy_notice_us_ca_provide, - privacy_experience_overlay_banner, ): - """Show_disabled=False can be added to privacy experience detail to only return enabled privacy notices for an experience.""" - privacy_experience_overlay_banner.disabled = False - privacy_experience_overlay_banner.save(db) - privacy_notice.disabled = True - privacy_notice.save(db) - - auth_header = generate_auth_header(scopes=[scopes.PRIVACY_EXPERIENCE_READ]) - - resp = api_client.get(url, headers=auth_header) - - # Sanity check - assert resp.status_code == 200 - assert len(resp.json()["privacy_notices"]) == 2 - assert resp.json()["id"] == privacy_experience_overlay_banner.id - assert ( - resp.json()["privacy_notices"][0]["id"] == privacy_notice_us_ca_provide.id - ) - assert resp.json()["privacy_notices"][0]["disabled"] is False - assert resp.json()["privacy_notices"][1]["id"] == privacy_notice.id - assert resp.json()["privacy_notices"][1]["disabled"] is True - - # Now filter just on ca to get just the ca notices resp = api_client.get( - url + "?show_disabled=False", - headers=auth_header, - ) - assert len(resp.json()["privacy_notices"]) == 1 - assert resp.json()["id"] == privacy_experience_overlay_banner.id - assert ( - resp.json()["privacy_notices"][0]["id"] == privacy_notice_us_ca_provide.id + url + "?cd685ccd-0960-4dc1-b9ca-7e810ebc5c1b", ) + assert resp.status_code == 200 + data = resp.json() + resp = data["items"][0] - assert resp.json()["privacy_notices"][0]["disabled"] is False + # Assert current preference is displayed for fides user device id + assert resp["privacy_notices"][0]["consent_mechanism"] == "opt_in" + assert resp["privacy_notices"][0]["default_preference"] == "opt_out" + assert resp["privacy_notices"][0]["current_preference"] is None + assert resp["privacy_notices"][0]["outdated_preference"] is None @pytest.mark.usefixtures( "privacy_notice_us_ca_provide", @@ -726,20 +490,16 @@ def test_get_privacy_experience_detail_disabled_filter( "privacy_preference_history_us_ca_provide_for_fides_user", "privacy_experience_overlay_banner", ) - def test_get_privacy_experience_detail_fides_user_device_id_filter( + def test_get_privacy_experiences_fides_user_device_id_filter( self, api_client: TestClient, - generate_auth_header, url, ): - auth_header = generate_auth_header(scopes=[scopes.PRIVACY_EXPERIENCE_READ]) - resp = api_client.get( - url + "?fides_user_device_id=FGHIJ_TEST_FIDES", - headers=auth_header, + url + "?fides_user_device_id=051b219f-20e4-45df-82f7-5eb68a00889f", ) assert resp.status_code == 200 - data = resp.json() + data = resp.json()["items"][0] # Assert current preference is displayed for fides user device id assert data["privacy_notices"][0]["consent_mechanism"] == "opt_in" diff --git a/tests/ops/api/v1/endpoints/test_privacy_preference_endpoints.py b/tests/ops/api/v1/endpoints/test_privacy_preference_endpoints.py index 2418cf9d374..6ea909dde6e 100644 --- a/tests/ops/api/v1/endpoints/test_privacy_preference_endpoints.py +++ b/tests/ops/api/v1/endpoints/test_privacy_preference_endpoints.py @@ -728,9 +728,8 @@ def test_verify_then_set_privacy_preferences_with_additional_fides_user_device_i Besides having a verified identity, we also have the fides_user_device_id from the browser """ - request_body["browser_identity"][ - "fides_user_device_id" - ] = "test_fides_user_device_id" + test_device_id = "2da1690a-65b6-447b-879b-d33089c04ba5" + request_body["browser_identity"]["fides_user_device_id"] = test_device_id _, consent_request = provided_identity_and_consent_request consent_request.cache_identity_verification_code(verification_code) @@ -782,12 +781,10 @@ def test_verify_then_set_privacy_preferences_with_additional_fides_user_device_i response_json["privacy_notice_history"] == PrivacyNoticeHistorySchema.from_orm(privacy_notice.histories[0]).dict() ) - assert ( - privacy_preference_history.fides_user_device == "test_fides_user_device_id" - ) + assert privacy_preference_history.fides_user_device == test_device_id assert ( privacy_preference_history.hashed_fides_user_device - == ProvidedIdentity.hash_value("test_fides_user_device_id") + == ProvidedIdentity.hash_value(test_device_id) ) assert ( privacy_preference_history.fides_user_device_provided_identity_id @@ -802,11 +799,11 @@ def test_verify_then_set_privacy_preferences_with_additional_fides_user_device_i ) assert ( fides_user_device_provided_identity.encrypted_value["value"] - == "test_fides_user_device_id" + == test_device_id ) assert ( fides_user_device_provided_identity.hashed_value - == ProvidedIdentity.hash_value("test_fides_user_device_id") + == ProvidedIdentity.hash_value(test_device_id) ) privacy_preference_history.delete(db=db) @@ -973,7 +970,7 @@ def request_body( return { "browser_identity": { "ga_client_id": "test", - "fides_user_device_id": "ABCDE_TEST_FIDES", + "fides_user_device_id": "e4e573ba-d806-4e54-bdd8-3d2ff11d4f11", }, "preferences": [ { @@ -999,6 +996,22 @@ def test_no_fides_user_device_id_supplied(self, api_client, url, request_body): ) assert response.status_code == 422 + @pytest.mark.usefixtures( + "privacy_notice", + ) + def test_bad_fides_user_device_id_supplied(self, api_client, url, request_body): + request_body["browser_identity"][ + "fides_user_device_id" + ] = "bad_fides_user_device_id" + response = api_client.patch( + url, json=request_body, headers={"Origin": "http://localhost:8080"} + ) + assert response.status_code == 422 + assert ( + response.json()["detail"][0]["msg"] + == "badly formed hexadecimal UUID string" + ) + @pytest.mark.usefixtures( "privacy_notice", ) @@ -1044,6 +1057,7 @@ def test_save_privacy_preferences_with_respect_to_fides_user_device_id( """Assert CurrentPrivacyPreference records were updated and PrivacyPreferenceHistory records were created for recordkeeping with respect to the fides user device id in the request """ + test_device_id = "e4e573ba-d806-4e54-bdd8-3d2ff11d4f11" masked_ip = "12.214.31.0" mock_anonymize.return_value = masked_ip response = api_client.patch( @@ -1077,19 +1091,19 @@ def test_save_privacy_preferences_with_respect_to_fides_user_device_id( ) assert ( fides_user_device_provided_identity.hashed_value - == ProvidedIdentity.hash_value("ABCDE_TEST_FIDES") + == ProvidedIdentity.hash_value(test_device_id) ) assert ( fides_user_device_provided_identity.encrypted_value["value"] - == "ABCDE_TEST_FIDES" + == test_device_id ) # Values also cached on the historical record for reporting assert ( privacy_preference_history.hashed_fides_user_device - == ProvidedIdentity.hash_value("ABCDE_TEST_FIDES") + == ProvidedIdentity.hash_value(test_device_id) ) # Cached here for reporting assert ( - privacy_preference_history.fides_user_device == "ABCDE_TEST_FIDES" + privacy_preference_history.fides_user_device == test_device_id ) # Cached here for reporting # Test items that are pulled from request headers or client diff --git a/tests/ops/util/test_consent_util.py b/tests/ops/util/test_consent_util.py index 0b6cc447b88..3649158c5ca 100644 --- a/tests/ops/util/test_consent_util.py +++ b/tests/ops/util/test_consent_util.py @@ -454,6 +454,6 @@ def test_get_fides_user_device_id_provided_identity( self, db, fides_user_provided_identity ): provided_identity = get_fides_user_device_id_provided_identity( - db, "FGHIJ_TEST_FIDES" + db, "051b219f-20e4-45df-82f7-5eb68a00889f" ) assert provided_identity == fides_user_provided_identity