Skip to content

Commit

Permalink
WIP Refactor so instance-level hybrid properties on PrivacyDeclaratio…
Browse files Browse the repository at this point in the history
…ns use async sessions so they can be accessed via existing System API routes.

Current strategy is to stash these on the declaration under a new key since declaring the hybrid_property directly on the Privacy Declaration Schema causes coroutine issues when the PrivacyDeclaration is serialized under "serialize_response"
  • Loading branch information
pattisdr committed Nov 28, 2023
1 parent 8e4235e commit a6c099f
Show file tree
Hide file tree
Showing 5 changed files with 338 additions and 183 deletions.
35 changes: 31 additions & 4 deletions src/fides/api/api/v1/endpoints/system.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Dict, List, Optional
from typing import Dict, List, Optional, Union

from fastapi import Depends, HTTPException, Response, Security
from fastapi_pagination import Page, Params
Expand Down Expand Up @@ -258,6 +258,10 @@ async def update(
updated_system, _ = await update_system(
resource, db, current_user.id if current_user else None
)
await supplement_privacy_declaration_response_with_legal_basis_override(
updated_system
)

return updated_system


Expand Down Expand Up @@ -353,7 +357,11 @@ async def create(
Override `System` create/POST to handle `.privacy_declarations` defined inline,
for backward compatibility and ease of use for API users.
"""
return await create_system(resource, db, current_user.id if current_user else None)
created = await create_system(
resource, db, current_user.id if current_user else None
)
await supplement_privacy_declaration_response_with_legal_basis_override(created)
return created


@SYSTEM_ROUTER.get(
Expand All @@ -371,7 +379,10 @@ async def ls( # pylint: disable=invalid-name
db: AsyncSession = Depends(get_async_db),
) -> List:
"""Get a list of all of the resources of this type."""
return await list_resource(System, db)
systems = await list_resource(System, db)
for system in systems:
await supplement_privacy_declaration_response_with_legal_basis_override(system)
return systems


@SYSTEM_ROUTER.get(
Expand All @@ -389,7 +400,10 @@ async def get(
db: AsyncSession = Depends(get_async_db),
) -> Dict:
"""Get a resource by its fides_key."""
return await get_resource_with_custom_fields(System, fides_key, db)

resp = await get_resource_with_custom_fields(System, fides_key, db)
await supplement_privacy_declaration_response_with_legal_basis_override(resp)
return resp


@SYSTEM_CONNECTION_INSTANTIATE_ROUTER.post(
Expand All @@ -412,3 +426,16 @@ def instantiate_connection_from_template(

system = get_system(db, fides_key)
return instantiate_connection(db, saas_connector_type, template_values, system)


async def supplement_privacy_declaration_response_with_legal_basis_override(resp: Union[Dict, System]) -> None:
"""At runtime, adds a "legal_basis_for_processing_override" to each PrivacyDeclaration"""

for privacy_declaration in (
resp.get("privacy_declarations")
if isinstance(resp, Dict)
else resp.privacy_declarations
):
privacy_declaration.legal_basis_for_processing_override = (
await privacy_declaration.overridden_legal_basis_for_processing
)
47 changes: 27 additions & 20 deletions src/fides/api/models/sql_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@
from sqlalchemy.dialects.postgresql import ARRAY, BIGINT, BYTEA
from sqlalchemy.engine import Row
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession, async_object_session
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import Session, relationship
from sqlalchemy.orm import Session, object_session, relationship
from sqlalchemy.orm.attributes import InstrumentedAttribute
from sqlalchemy.sql import func
from sqlalchemy.sql import func, Select
from sqlalchemy.sql.elements import Case
from sqlalchemy.sql.selectable import ScalarSelect
from sqlalchemy.sql.sqltypes import DateTime
Expand Down Expand Up @@ -551,15 +552,15 @@ def purpose(cls) -> Case:
)

@hybrid_property
def _publisher_override_legal_basis_join(self) -> Optional[str]:
async def _publisher_override_legal_basis_join(self) -> Optional[str]:
"""Returns the instance-level overridden required legal basis"""
db: Session = Session.object_session(self)
required_legal_basis: Optional[Row] = (
db.query(TCFPublisherOverride.required_legal_basis)
.filter(TCFPublisherOverride.purpose == self.purpose)
.first()
query: Select = select([TCFPublisherOverride.required_legal_basis]).where(
TCFPublisherOverride.purpose == self.purpose
)
return required_legal_basis[0] if required_legal_basis else None
async_session: AsyncSession = async_object_session(self)
async with async_session.begin():
result = await async_session.execute(query)
return result.scalars().first()

@_publisher_override_legal_basis_join.expression
def _publisher_override_legal_basis_join(cls) -> ScalarSelect:
Expand All @@ -571,15 +572,15 @@ def _publisher_override_legal_basis_join(cls) -> ScalarSelect:
)

@hybrid_property
def _publisher_override_is_included_join(self) -> Optional[bool]:
async def _publisher_override_is_included_join(self) -> Optional[bool]:
"""Returns the instance-level indication of whether the purpose should be included"""
db: Session = Session.object_session(self)
is_included: Optional[Row] = (
db.query(TCFPublisherOverride.is_included)
.filter(TCFPublisherOverride.purpose == self.purpose)
.first()
query: Select = select([TCFPublisherOverride.is_included]).where(
TCFPublisherOverride.purpose == self.purpose
)
return is_included[0] if is_included else None
async_session: AsyncSession = async_object_session(self)
async with async_session.begin():
result = await async_session.execute(query)
return result.scalars().first()

@_publisher_override_is_included_join.expression
def _publisher_override_is_included_join(cls) -> ScalarSelect:
Expand All @@ -591,7 +592,7 @@ def _publisher_override_is_included_join(cls) -> ScalarSelect:
)

@hybrid_property
def overridden_legal_basis_for_processing(self) -> Optional[str]:
async def overridden_legal_basis_for_processing(self) -> Optional[str]:
"""
Instance-level override of the legal basis for processing based on
publisher preferences.
Expand All @@ -602,15 +603,21 @@ def overridden_legal_basis_for_processing(self) -> Optional[str]:
):
return self.legal_basis_for_processing

if self._publisher_override_is_included_join is False:
is_included: Optional[bool] = await self._publisher_override_is_included_join

if is_included is False:
# Overriding to False to match behavior of class-level override.
# Class-level override of legal basis to None removes Privacy Declaration
# from Experience
return None

overridden_legal_basis: Optional[str] = (
await self._publisher_override_legal_basis_join
)

return (
self._publisher_override_legal_basis_join
if self._publisher_override_legal_basis_join # pylint: disable=using-constant-test
overridden_legal_basis
if overridden_legal_basis # pylint: disable=using-constant-test
else self.legal_basis_for_processing
)

Expand Down
4 changes: 4 additions & 0 deletions src/fides/api/schemas/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ class PrivacyDeclarationResponse(PrivacyDeclaration):
)
cookies: Optional[List[Cookies]] = []

legal_basis_for_processing_override: Optional[str] = Field(
description="Global overrides for this purpose's legal basis for processing if applicable. Defaults to the legal_basis_for_processing otherwise."
)


class BasicSystemResponse(System):
"""
Expand Down
Loading

0 comments on commit a6c099f

Please sign in to comment.