Skip to content

Commit

Permalink
Public Endpoints Privacy Experiences and Save Privacy Preferences (#3339
Browse files Browse the repository at this point in the history
)

Make the Privacy Experience List endpoint public and add separate rate limiting to the public endpoints.
Add validation to be uuid-type on fides user device id.
  • Loading branch information
pattisdr authored May 22, 2023
1 parent c83e84e commit 52bcd75
Show file tree
Hide file tree
Showing 12 changed files with 125 additions and 386 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
28 changes: 0 additions & 28 deletions docs/fides/docs/development/postman/Fides.postman_collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
75 changes: 20 additions & 55 deletions src/fides/api/api/v1/endpoints/privacy_experience_endpoints.py
Original file line number Diff line number Diff line change
@@ -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)

Expand All @@ -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),
Expand All @@ -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
Expand All @@ -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
)
Expand Down Expand Up @@ -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
14 changes: 9 additions & 5 deletions src/fides/api/api/v1/endpoints/privacy_preference_endpoints.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
14 changes: 14 additions & 0 deletions src/fides/api/api/v1/endpoints/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]]
Expand Down
14 changes: 3 additions & 11 deletions src/fides/api/app_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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."""
Expand All @@ -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)
Expand Down
12 changes: 11 additions & 1 deletion src/fides/api/schemas/redis_cache.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
4 changes: 4 additions & 0 deletions src/fides/core/config/security_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
6 changes: 4 additions & 2 deletions tests/fixtures/application_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Loading

0 comments on commit 52bcd75

Please sign in to comment.