-
Notifications
You must be signed in to change notification settings - Fork 16
416 Adds DRP exercise endpoint #496
Changes from 4 commits
d60c734
27b0527
3d92140
a14e28e
ea0b1d8
6c817e4
579f028
4b04b9f
ac96257
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
import json | ||
import logging | ||
from typing import Dict, Any, Optional | ||
|
||
import jwt | ||
from fastapi import HTTPException, Depends, APIRouter, Security | ||
from sqlalchemy.orm import Session | ||
from starlette.status import ( | ||
HTTP_404_NOT_FOUND, | ||
HTTP_422_UNPROCESSABLE_ENTITY, | ||
HTTP_424_FAILED_DEPENDENCY, | ||
HTTP_200_OK, | ||
HTTP_400_BAD_REQUEST, | ||
) | ||
|
||
from fidesops import common_exceptions | ||
from fidesops.api import deps | ||
from fidesops.api.v1 import scope_registry as scopes | ||
from fidesops.api.v1 import urn_registry as urls | ||
from fidesops.core.config import config | ||
from fidesops.models.policy import Policy | ||
from fidesops.models.privacy_request import PrivacyRequest | ||
from fidesops.schemas.drp_privacy_request import DrpPrivacyRequestCreate, DrpIdentity | ||
from fidesops.schemas.privacy_request import PrivacyRequestDRPStatusResponse | ||
from fidesops.schemas.redis_cache import PrivacyRequestIdentity | ||
from fidesops.service.drp.drp_fidesops_mapper import DrpFidesopsMapper | ||
from fidesops.service.privacy_request.request_runner_service import PrivacyRequestRunner | ||
from fidesops.service.privacy_request.request_service import ( | ||
build_required_privacy_request_kwargs, | ||
cache_data, | ||
) | ||
from fidesops.util.cache import FidesopsRedis | ||
from fidesops.util.oauth_util import verify_oauth_client | ||
|
||
logger = logging.getLogger(__name__) | ||
router = APIRouter(tags=["DRP"], prefix=urls.V1_URL_PREFIX) | ||
EMBEDDED_EXECUTION_LOG_LIMIT = 50 | ||
|
||
|
||
@router.post( | ||
urls.DRP_EXERCISE, | ||
status_code=HTTP_200_OK, | ||
response_model=PrivacyRequestDRPStatusResponse, | ||
) | ||
def create_drp_privacy_request( | ||
*, | ||
cache: FidesopsRedis = Depends(deps.get_cache), | ||
db: Session = Depends(deps.get_db), | ||
data: DrpPrivacyRequestCreate, | ||
) -> PrivacyRequestDRPStatusResponse: | ||
""" | ||
Given a drp privacy request body, create and execute | ||
a corresponding Fidesops PrivacyRequest | ||
""" | ||
|
||
jwt_key: str = config.security.DRP_JWT_SECRET | ||
if jwt_key is None: | ||
raise HTTPException( | ||
status_code=HTTP_400_BAD_REQUEST, | ||
detail="JWT key must be provided", | ||
eastandwestwind marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) | ||
|
||
logger.info(f"Finding policy with drp action '{data.exercise[0]}'") | ||
policy: Optional[Policy] = Policy.get_by( | ||
db=db, | ||
field="drp_action", | ||
value=data.exercise[0], | ||
) | ||
|
||
if not policy: | ||
raise HTTPException( | ||
status_code=HTTP_404_NOT_FOUND, | ||
detail=f"No policy found with drp action '{data.exercise}'.", | ||
) | ||
|
||
privacy_request_kwargs: Dict[str, Any] = build_required_privacy_request_kwargs( | ||
None, policy.id | ||
) | ||
|
||
try: | ||
privacy_request: PrivacyRequest = PrivacyRequest.create( | ||
db=db, data=privacy_request_kwargs | ||
) | ||
|
||
logger.info(f"Decrypting identity for DRP privacy request {privacy_request.id}") | ||
|
||
decrypted_identity: DrpIdentity = DrpIdentity( | ||
**jwt.decode(data.identity, jwt_key, algorithms=["HS256"]) | ||
) | ||
|
||
mapped_identity: PrivacyRequestIdentity = DrpFidesopsMapper.map_identity( | ||
drp_identity=decrypted_identity | ||
) | ||
|
||
cache_data(privacy_request, policy, mapped_identity, None, data) | ||
|
||
PrivacyRequestRunner( | ||
cache=cache, | ||
privacy_request=privacy_request, | ||
).submit() | ||
|
||
return PrivacyRequestDRPStatusResponse( | ||
request_id=privacy_request.id, | ||
received_at=privacy_request.requested_at, | ||
status=DrpFidesopsMapper.map_status(privacy_request.status), | ||
) | ||
|
||
except common_exceptions.RedisConnectionError as exc: | ||
logger.error("RedisConnectionError: %s", exc) | ||
# Thrown when cache.ping() fails on cache connection retrieval | ||
raise HTTPException( | ||
status_code=HTTP_424_FAILED_DEPENDENCY, | ||
detail=exc.args[0], | ||
) | ||
except Exception as exc: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this was to be consistent with what we have for POST |
||
logger.error(f"Exception: {exc}") | ||
raise HTTPException( | ||
status_code=HTTP_422_UNPROCESSABLE_ENTITY, | ||
detail="DRP privacy request could not be exercised", | ||
) | ||
|
||
|
||
@router.get( | ||
urls.DRP_STATUS, | ||
dependencies=[Security(verify_oauth_client, scopes=[scopes.PRIVACY_REQUEST_READ])], | ||
response_model=PrivacyRequestDRPStatusResponse, | ||
) | ||
def get_request_status_drp( | ||
*, db: Session = Depends(deps.get_db), request_id: str | ||
) -> PrivacyRequestDRPStatusResponse: | ||
""" | ||
Returns PrivacyRequest information where the respective privacy request is associated with | ||
a policy that implements a Data Rights Protocol action. | ||
""" | ||
|
||
logger.info(f"Finding request for DRP with ID: {request_id}") | ||
request = PrivacyRequest.get( | ||
db=db, | ||
id=request_id, | ||
) | ||
if not request or not request.policy or not request.policy.drp_action: | ||
# If no request is found with this ID, or that request has no policy, | ||
# or that request's policy has no associated drp_action. | ||
raise HTTPException( | ||
status_code=HTTP_404_NOT_FOUND, | ||
detail=f"Privacy request with ID {request_id} does not exist, or is not associated with a data rights protocol action.", | ||
) | ||
|
||
logger.info(f"Privacy request with ID: {request_id} found for DRP status.") | ||
return PrivacyRequestDRPStatusResponse( | ||
request_id=request.id, | ||
received_at=request.requested_at, | ||
status=DrpFidesopsMapper.map_status(request.status), | ||
) | ||
eastandwestwind marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -186,10 +186,17 @@ def cache_drp_request_body(self, drp_request_body: DrpPrivacyRequestCreate) -> N | |
drp_request_body_dict: Dict[str, Any] = dict(drp_request_body) | ||
for key, value in drp_request_body_dict.items(): | ||
if value is not None: | ||
cache.set_with_autoexpire( | ||
get_drp_request_body_cache_key(self.id, key), | ||
value, | ||
) | ||
# handle nested dict/objects | ||
if not isinstance(value, (bytes, str, int, float)): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cache doesn't handle vals as bytes/str/int/float. Perhaps we should instead cache the whole req body as a str ( |
||
cache.set_with_autoexpire( | ||
get_drp_request_body_cache_key(self.id, key), | ||
repr(value), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know this probably feels hacky but good choice in effort/reward trade-off if this is only for debug purposes as you say. Looks like If we want to do more with this data though let's make sure we're breaking it down and storing it by key. We could write a util method to handle that automatically for Pydantic schemas for instance. |
||
) | ||
else: | ||
cache.set_with_autoexpire( | ||
get_drp_request_body_cache_key(self.id, key), | ||
value, | ||
) | ||
|
||
def cache_encryption(self, encryption_key: Optional[str] = None) -> None: | ||
"""Sets the encryption key in the Fidesops app cache if provided""" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,8 @@ | ||
from enum import Enum | ||
from typing import Optional, List | ||
|
||
from pydantic import validator | ||
|
||
from fidesops.models.policy import DrpAction | ||
from fidesops.schemas.base_class import BaseSchema | ||
|
||
|
@@ -22,7 +24,7 @@ class DrpPrivacyRequestCreate(BaseSchema): | |
|
||
meta: DrpMeta | ||
regime: Optional[DrpRegime] | ||
exercise: DrpAction | ||
exercise: List[DrpAction] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DRP spec dictates that this is a list. So we'll take in a list even though we don't support > 1 item |
||
relationships: Optional[List[str]] | ||
identity: str | ||
status_callback: Optional[str] | ||
|
@@ -31,3 +33,25 @@ class Config: | |
"""Populate models with the raw value of enum fields, rather than the enum itself""" | ||
|
||
use_enum_values = True | ||
|
||
@validator("exercise") | ||
def check_exercise_length(cls, exercise: [List[DrpAction]]) -> List[DrpAction]: | ||
"""Validate the only one exercise action is provided""" | ||
if len(exercise) > 1: | ||
raise ValueError("Multiple exercise actions are not supported at this time") | ||
return exercise | ||
|
||
|
||
class DrpIdentity(BaseSchema): | ||
"""Drp identity props""" | ||
|
||
aud: Optional[str] | ||
sub: Optional[str] | ||
name: Optional[str] | ||
email: Optional[str] | ||
email_verified: Optional[bool] | ||
phone_number: Optional[str] | ||
phone_number_verified: Optional[bool] | ||
address: Optional[str] | ||
address_verified: Optional[bool] | ||
owner_of_attorney: Optional[str] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is also a dep of fidesctl, so I've kept version out here to let package manager handle it. Maybe there's a better way to avoid package conflicts?