diff --git a/CHANGELOG.md b/CHANGELOG.md index cba1763160..b4f353d93b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ The types of changes are: ## [Unreleased](https://github.com/ethyca/fides/compare/2.25.0...main) +### Added +- New purposes endpoint and indices to improve system lookups [#4452](https://github.com/ethyca/fides/pull/4452) + ### Fixed - Fix type errors when TCF vendors have no dataDeclaration [#4465](https://github.com/ethyca/fides/pull/4465) diff --git a/src/fides/api/alembic/migrations/versions/7f7c2b098f5d_add_name_index_to_ctl_systems.py b/src/fides/api/alembic/migrations/versions/7f7c2b098f5d_add_name_index_to_ctl_systems.py new file mode 100644 index 0000000000..f8765e7971 --- /dev/null +++ b/src/fides/api/alembic/migrations/versions/7f7c2b098f5d_add_name_index_to_ctl_systems.py @@ -0,0 +1,30 @@ +"""add indexes to ctl_systems and privacydeclaration + +Revision ID: 7f7c2b098f5d +Revises: 1af6950f4625 +Create Date: 2023-11-21 18:52:34.508076 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "7f7c2b098f5d" +down_revision = "1af6950f4625" +branch_labels = None +depends_on = None + + +def upgrade(): + op.execute("CREATE EXTENSION IF NOT EXISTS pg_trgm;") + op.create_index( + "ix_ctl_systems_name", + "ctl_systems", + [sa.text("name gin_trgm_ops")], + postgresql_using="gin", + ) + + +def downgrade(): + op.drop_index("ix_ctl_systems_name", table_name="ctl_systems") + op.execute("DROP EXTENSION IF EXISTS pg_trgm;") diff --git a/src/fides/api/api/v1/api.py b/src/fides/api/api/v1/api.py index 0af432cfba..98c52d7dd5 100644 --- a/src/fides/api/api/v1/api.py +++ b/src/fides/api/api/v1/api.py @@ -17,6 +17,7 @@ privacy_notice_endpoints, privacy_preference_endpoints, privacy_request_endpoints, + purpose_endpoints, registration_endpoints, saas_config_endpoints, served_notice_endpoints, @@ -51,3 +52,4 @@ api_router.include_router(manual_webhook_endpoints.router) api_router.include_router(registration_endpoints.router) api_router.include_router(served_notice_endpoints.router) +api_router.include_router(purpose_endpoints.router) diff --git a/src/fides/api/api/v1/endpoints/purpose_endpoints.py b/src/fides/api/api/v1/endpoints/purpose_endpoints.py new file mode 100644 index 0000000000..28e6cf5c78 --- /dev/null +++ b/src/fides/api/api/v1/endpoints/purpose_endpoints.py @@ -0,0 +1,34 @@ +from fastapi import Depends, Security +from fideslang.gvl import MAPPED_PURPOSES, MAPPED_SPECIAL_PURPOSES +from sqlalchemy.orm import Session +from starlette.status import HTTP_200_OK + +from fides.api.api import deps +from fides.api.oauth.utils import verify_oauth_client +from fides.api.schemas.tcf import PurposesResponse +from fides.api.util.api_router import APIRouter +from fides.common.api.v1 import urn_registry as urls + +router = APIRouter(tags=["Purposes"], prefix=urls.V1_URL_PREFIX) + + +@router.get( + "/purposes", + dependencies=[Security(verify_oauth_client)], + status_code=HTTP_200_OK, + response_model=PurposesResponse, +) +def get_purposes( + db: Session = Depends(deps.get_db), +) -> PurposesResponse: + """ + Return a map of purpose and special purpose IDs to mapped purposes which include data uses. + """ + + purposes = {} + special_purposes = {} + for purpose in MAPPED_PURPOSES.values(): + purposes[purpose.id] = purpose + for special_purpose in MAPPED_SPECIAL_PURPOSES.values(): + special_purposes[special_purpose.id] = special_purpose + return PurposesResponse(purposes=purposes, special_purposes=special_purposes) diff --git a/src/fides/api/schemas/tcf.py b/src/fides/api/schemas/tcf.py index 0bab0db680..d699a71331 100644 --- a/src/fides/api/schemas/tcf.py +++ b/src/fides/api/schemas/tcf.py @@ -7,7 +7,7 @@ MAPPED_SPECIAL_PURPOSES, ) from fideslang.gvl.models import Feature, MappedPurpose -from pydantic import AnyUrl, root_validator, validator +from pydantic import AnyUrl, BaseModel, root_validator, validator from fides.api.models.privacy_notice import UserConsentPreference from fides.api.schemas.base_class import FidesSchema @@ -230,3 +230,8 @@ def validate_special_feature_id(cls, value: int) -> int: f"Cannot save preferences against invalid special feature id: '{value}'" ) return value + + +class PurposesResponse(BaseModel): + purposes: Dict[str, MappedPurpose] + special_purposes: Dict[str, MappedPurpose] diff --git a/src/fides/common/api/v1/urn_registry.py b/src/fides/common/api/v1/urn_registry.py index d34202dcf3..aa2ff55280 100644 --- a/src/fides/common/api/v1/urn_registry.py +++ b/src/fides/common/api/v1/urn_registry.py @@ -112,6 +112,9 @@ "/privacy-request/transfer/{privacy_request_id}/{rule_key}" ) +# Purpose URLs +PURPOSES = "/purposes" + # Identity Verification URLs ID_VERIFICATION_CONFIG = "/id-verification/config" diff --git a/tests/ops/api/v1/endpoints/test_purpose_endpoints.py b/tests/ops/api/v1/endpoints/test_purpose_endpoints.py new file mode 100644 index 0000000000..1f7c4be4ad --- /dev/null +++ b/tests/ops/api/v1/endpoints/test_purpose_endpoints.py @@ -0,0 +1,25 @@ +import pytest +from starlette.status import HTTP_200_OK, HTTP_401_UNAUTHORIZED + +from fides.common.api.v1.urn_registry import PURPOSES, V1_URL_PREFIX + + +class TestGetPurposes: + @pytest.fixture(scope="function") + def url(self) -> str: + return V1_URL_PREFIX + PURPOSES + + def test_get_purposes_unauthenticated(self, url, api_client): + response = api_client.get(url) + assert response.status_code == HTTP_401_UNAUTHORIZED + + def test_get_purposes(self, url, api_client, generate_auth_header): + auth_header = generate_auth_header([]) + response = api_client.get(url, headers=auth_header) + assert response.status_code == HTTP_200_OK + + data = response.json() + assert "purposes" in data + assert "special_purposes" in data + assert len(data["purposes"]) == 11 + assert len(data["special_purposes"]) == 2