Skip to content
This repository has been archived by the owner on Nov 30, 2022. It is now read-only.

Add authenticated route to get consent preferences #1402

Merged
merged 7 commits into from
Sep 28, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ The types of changes are:
* Frontend - ability for users to manually enter PII to an IN PROGRESS subject request [#1016](https://github.com/ethyca/fidesops/pull/1377)
* Enable retries on saas connectors for failures at the http request level [#1376](https://github.com/ethyca/fidesops/pull/1376)
* Add consent request api [#1387](https://github.com/ethyca/fidesops/pull/1387)
* Add authenticated route to get consent preferences [#1402](https://github.com/ethyca/fidesops/pull/1402)

### Removed

Expand Down
36 changes: 34 additions & 2 deletions docs/fidesops/docs/postman/Fidesops.postman_collection.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"info": {
"_postman_id": "8f48b8e3-0a39-4e5e-b505-8087064dc1af",
"_postman_id": "002813f4-12b7-4467-9377-a57706b6dbc8",
"name": "Fidesops",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
Expand Down Expand Up @@ -4815,6 +4815,38 @@
},
"response": []
},
{
"name": "Authenticated Get Consent Preferences",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{client_token}}",
"type": "string"
}
]
},
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"phone_number\": \"{{phone_number}}\",\n \"email\": \"{{email}}\"\n}"
},
"url": {
"raw": "{{host}}/consent-request/preferences",
"host": [
"{{host}}"
],
"path": [
"consent-request",
"preferences"
pattisdr marked this conversation as resolved.
Show resolved Hide resolved
]
}
},
"response": []
},
{
"name": "Verify Code and Save Preferences",
"request": {
Expand Down Expand Up @@ -5160,4 +5192,4 @@
"type": "string"
}
]
}
}
56 changes: 54 additions & 2 deletions src/fidesops/ops/api/v1/endpoints/consent_request_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import logging

from fastapi import Depends, HTTPException
from fastapi import Depends, HTTPException, Security
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from starlette.status import (
Expand All @@ -14,9 +14,11 @@
)

from fidesops.ops.api.deps import get_db
from fidesops.ops.api.v1.scope_registry import CONSENT_READ
from fidesops.ops.api.v1.urn_registry import (
CONSENT_REQUEST,
CONSENT_REQUEST_PREFERENCES,
CONSENT_REQUEST_PREFERENCES_WITH_ID,
CONSENT_REQUEST_VERIFY,
V1_URL_PREFIX,
)
Expand All @@ -43,6 +45,7 @@
from fidesops.ops.service._verification import send_verification_code_to_user
from fidesops.ops.util.api_router import APIRouter
from fidesops.ops.util.logger import Pii
from fidesops.ops.util.oauth_util import verify_oauth_client

router = APIRouter(tags=["Consent"], prefix=V1_URL_PREFIX)

Expand Down Expand Up @@ -133,8 +136,57 @@ def consent_request_verify(
return _prepare_consent_preferences(db, provided_identity)


@router.patch(
@router.post(
CONSENT_REQUEST_PREFERENCES,
pattisdr marked this conversation as resolved.
Show resolved Hide resolved
dependencies=[Security(verify_oauth_client, scopes=[CONSENT_READ])],
pattisdr marked this conversation as resolved.
Show resolved Hide resolved
status_code=HTTP_200_OK,
response_model=ConsentPreferences,
)
def get_consent_preferences(
pattisdr marked this conversation as resolved.
Show resolved Hide resolved
*, db: Session = Depends(get_db), data: Identity
) -> ConsentPreferences:
"""Gets the consent preferences for the specified user."""
if data.email:
lookup = data.email
elif data.phone_number:
lookup = data.phone_number
else:
raise HTTPException(
status_code=HTTP_400_BAD_REQUEST, detail="No identity information provided"
)

identity = ProvidedIdentity.filter(
db,
conditions=(
(ProvidedIdentity.hashed_value == ProvidedIdentity.hash_value(lookup))
& (
ProvidedIdentity.privacy_request # pylint: disable=singleton-comparison
== None
pattisdr marked this conversation as resolved.
Show resolved Hide resolved
)
),
).first()

if not identity:
raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail="Identity not found")

consent = Consent.filter(
db, conditions=(Consent.provided_identity_id == identity.id)
).all()

return ConsentPreferences(
consent=[
ConsentSchema(
data_use=x.data_use,
data_use_description=x.data_use_description,
opt_in=x.opt_in,
)
for x in consent
]
)
pattisdr marked this conversation as resolved.
Show resolved Hide resolved


@router.patch(
CONSENT_REQUEST_PREFERENCES_WITH_ID,
status_code=HTTP_200_OK,
response_model=ConsentPreferences,
)
Expand Down
3 changes: 3 additions & 0 deletions src/fidesops/ops/api/v1/scope_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
CONNECTION_AUTHORIZE = "connection:authorize"
SAAS_CONNECTION_INSTANTIATE = "connection:instantiate"

CONSENT_READ = "consent:read"

PRIVACY_REQUEST_READ = "privacy-request:read"
PRIVACY_REQUEST_DELETE = "privacy-request:delete"
PRIVACY_REQUEST_CALLBACK_RESUME = (
Expand Down Expand Up @@ -75,6 +77,7 @@
CONNECTION_DELETE,
CONNECTION_AUTHORIZE,
SAAS_CONNECTION_INSTANTIATE,
CONSENT_READ,
CONNECTION_TYPE_READ,
DATASET_CREATE_OR_UPDATE,
DATASET_DELETE,
Expand Down
3 changes: 3 additions & 0 deletions src/fidesops/ops/api/v1/urn_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
# Consent request URLs
CONSENT_REQUEST = "/consent-request"
CONSENT_REQUEST_PREFERENCES = "/consent-request/{consent_request_id}/preferences"
CONSENT_REQUEST_PREFERENCES_WITH_ID = (
"/consent-request/{consent_request_id}/preferences"
)
CONSENT_REQUEST_VERIFY = "/consent-request/{consent_request_id}/verify"


Expand Down
88 changes: 82 additions & 6 deletions tests/ops/api/v1/endpoints/test_consent_request_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@

import pytest

from fidesops.ops.api.v1.scope_registry import CONNECTION_READ, CONSENT_READ
from fidesops.ops.api.v1.urn_registry import (
CONSENT_REQUEST,
CONSENT_REQUEST_PREFERENCES,
CONSENT_REQUEST_PREFERENCES_WITH_ID,
CONSENT_REQUEST_VERIFY,
V1_URL_PREFIX,
)
Expand Down Expand Up @@ -232,7 +234,7 @@ def test_set_consent_preferences_no_consent_request_id(self, api_client):
}

response = api_client.patch(
f"{V1_URL_PREFIX}{CONSENT_REQUEST_PREFERENCES.format(consent_request_id='abcd')}",
f"{V1_URL_PREFIX}{CONSENT_REQUEST_PREFERENCES_WITH_ID.format(consent_request_id='abcd')}",
json=data,
)
assert response.status_code == 404
Expand All @@ -250,7 +252,7 @@ def test_set_consent_preferences_no_consent_code(
}

response = api_client.patch(
f"{V1_URL_PREFIX}{CONSENT_REQUEST_PREFERENCES.format(consent_request_id=consent_request.id)}",
f"{V1_URL_PREFIX}{CONSENT_REQUEST_PREFERENCES_WITH_ID.format(consent_request_id=consent_request.id)}",
json=data,
)
assert response.status_code == 400
Expand All @@ -268,7 +270,7 @@ def test_set_consent_preferences_invalid_code(
"consent": [{"data_use": "email", "opt_in": True}],
}
response = api_client.patch(
f"{V1_URL_PREFIX}{CONSENT_REQUEST_PREFERENCES.format(consent_request_id=consent_request.id)}",
f"{V1_URL_PREFIX}{CONSENT_REQUEST_PREFERENCES_WITH_ID.format(consent_request_id=consent_request.id)}",
json=data,
)
assert response.status_code == 403
Expand Down Expand Up @@ -296,7 +298,7 @@ def test_set_consent_preferences_no_email_provided(self, db, api_client):
"consent": [{"data_use": "email", "opt_in": True}],
}
response = api_client.patch(
f"{V1_URL_PREFIX}{CONSENT_REQUEST_PREFERENCES.format(consent_request_id=consent_request.id)}",
f"{V1_URL_PREFIX}{CONSENT_REQUEST_PREFERENCES_WITH_ID.format(consent_request_id=consent_request.id)}",
json=data,
)

Expand All @@ -316,7 +318,7 @@ def test_set_consent_preferences_no_consent_present(
"consent": None,
}
response = api_client.patch(
f"{V1_URL_PREFIX}{CONSENT_REQUEST_PREFERENCES.format(consent_request_id=consent_request.id)}",
f"{V1_URL_PREFIX}{CONSENT_REQUEST_PREFERENCES_WITH_ID.format(consent_request_id=consent_request.id)}",
json=data,
)
assert response.status_code == 422
Expand Down Expand Up @@ -353,8 +355,82 @@ def test_set_consent_consent_preferences(
"consent": consent_data,
}
response = api_client.patch(
f"{V1_URL_PREFIX}{CONSENT_REQUEST_PREFERENCES.format(consent_request_id=consent_request.id)}",
f"{V1_URL_PREFIX}{CONSENT_REQUEST_PREFERENCES_WITH_ID.format(consent_request_id=consent_request.id)}",
json=data,
)
assert response.status_code == 200
assert response.json()["consent"] == consent_data


class TestGetConsentPreferences:
def test_get_consent_peferences_wrong_scope(self, generate_auth_header, api_client):
auth_header = generate_auth_header(scopes=[CONNECTION_READ])
response = api_client.post(
f"{V1_URL_PREFIX}{CONSENT_REQUEST_PREFERENCES}",
headers=auth_header,
json={"email": "[email protected]"},
)

assert response.status_code == 403
pattisdr marked this conversation as resolved.
Show resolved Hide resolved

def test_get_consent_preferences_no_identity_data(
self, generate_auth_header, api_client
):
auth_header = generate_auth_header(scopes=[CONSENT_READ])
response = api_client.post(
f"{V1_URL_PREFIX}{CONSENT_REQUEST_PREFERENCES}",
headers=auth_header,
json={"email": None},
)

assert response.status_code == 400
assert "No identity information" in response.json()["detail"]

def test_get_consent_preferences_identity_not_found(
self, generate_auth_header, api_client
):
auth_header = generate_auth_header(scopes=[CONSENT_READ])
response = api_client.post(
f"{V1_URL_PREFIX}{CONSENT_REQUEST_PREFERENCES}",
headers=auth_header,
json={"email": "[email protected]"},
)

assert response.status_code == 404
assert "Identity not found" in response.json()["detail"]

def test_get_consent_preferences(
self,
provided_identity_and_consent_request,
db,
generate_auth_header,
api_client,
):
provided_identity, _ = provided_identity_and_consent_request

consent_data: list[dict[str, Any]] = [
{
"data_use": "email",
"data_use_description": None,
"opt_in": True,
},
{
"data_use": "location",
"data_use_description": "Location data",
"opt_in": False,
},
]

for data in deepcopy(consent_data):
data["provided_identity_id"] = provided_identity.id
Consent.create(db, data=data)

auth_header = generate_auth_header(scopes=[CONSENT_READ])
response = api_client.post(
f"{V1_URL_PREFIX}{CONSENT_REQUEST_PREFERENCES}",
headers=auth_header,
json={"email": provided_identity.encrypted_value["value"]},
)

assert response.status_code == 200
assert response.json()["consent"] == consent_data