From 00a2a14e65f9b9df7cd061e785567b8b4d79b28d Mon Sep 17 00:00:00 2001 From: Paul Sanders Date: Wed, 28 Sep 2022 10:40:04 -0400 Subject: [PATCH 1/7] Add authenticated route to get consent preferences --- .../v1/endpoints/consent_request_endpoints.py | 52 ++++++++++++- src/fidesops/ops/api/v1/scope_registry.py | 3 + .../test_consent_request_endpoints.py | 75 +++++++++++++++++++ 3 files changed, 129 insertions(+), 1 deletion(-) diff --git a/src/fidesops/ops/api/v1/endpoints/consent_request_endpoints.py b/src/fidesops/ops/api/v1/endpoints/consent_request_endpoints.py index 0fd8461b9..b21b86ac1 100644 --- a/src/fidesops/ops/api/v1/endpoints/consent_request_endpoints.py +++ b/src/fidesops/ops/api/v1/endpoints/consent_request_endpoints.py @@ -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 ( @@ -14,6 +14,7 @@ ) 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, @@ -43,6 +44,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) @@ -133,6 +135,54 @@ def consent_request_verify( return _prepare_consent_preferences(db, provided_identity) +@router.post( + CONSENT_REQUEST_PREFERENCES, + dependencies=[Security(verify_oauth_client, scopes=[CONSENT_READ])], + status_code=HTTP_200_OK, + response_model=ConsentPreferences, +) +def get_consent_preferences( + *, db: Session = Depends(get_db), data: Identity +) -> ConsentPreferences: + 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 + ) + ), + ).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 + ] + ) + + @router.patch( CONSENT_REQUEST_PREFERENCES, status_code=HTTP_200_OK, diff --git a/src/fidesops/ops/api/v1/scope_registry.py b/src/fidesops/ops/api/v1/scope_registry.py index 15e5ef99a..9b9e8d24e 100644 --- a/src/fidesops/ops/api/v1/scope_registry.py +++ b/src/fidesops/ops/api/v1/scope_registry.py @@ -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 = ( @@ -75,6 +77,7 @@ CONNECTION_DELETE, CONNECTION_AUTHORIZE, SAAS_CONNECTION_INSTANTIATE, + CONSENT_READ, CONNECTION_TYPE_READ, DATASET_CREATE_OR_UPDATE, DATASET_DELETE, diff --git a/tests/ops/api/v1/endpoints/test_consent_request_endpoints.py b/tests/ops/api/v1/endpoints/test_consent_request_endpoints.py index 5ca3f9605..6e7ed3d75 100644 --- a/tests/ops/api/v1/endpoints/test_consent_request_endpoints.py +++ b/tests/ops/api/v1/endpoints/test_consent_request_endpoints.py @@ -6,6 +6,7 @@ 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, @@ -358,3 +359,77 @@ def test_set_consent_consent_preferences( ) 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": "test@user.com"}, + ) + + assert response.status_code == 403 + + 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": "test@email.com"}, + ) + + 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 From e7211c90090bcc1c4e308d470320486ae7758621 Mon Sep 17 00:00:00 2001 From: Paul Sanders Date: Wed, 28 Sep 2022 10:48:08 -0400 Subject: [PATCH 2/7] Add authenticated route to get consent preferences --- .../api/v1/endpoints/consent_request_endpoints.py | 3 ++- src/fidesops/ops/api/v1/urn_registry.py | 3 +++ .../v1/endpoints/test_consent_request_endpoints.py | 13 +++++++------ 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/fidesops/ops/api/v1/endpoints/consent_request_endpoints.py b/src/fidesops/ops/api/v1/endpoints/consent_request_endpoints.py index b21b86ac1..178c54b7a 100644 --- a/src/fidesops/ops/api/v1/endpoints/consent_request_endpoints.py +++ b/src/fidesops/ops/api/v1/endpoints/consent_request_endpoints.py @@ -18,6 +18,7 @@ from fidesops.ops.api.v1.urn_registry import ( CONSENT_REQUEST, CONSENT_REQUEST_PREFERENCES, + CONSENT_REQUEST_PREFERENCES_WITH_ID, CONSENT_REQUEST_VERIFY, V1_URL_PREFIX, ) @@ -184,7 +185,7 @@ def get_consent_preferences( @router.patch( - CONSENT_REQUEST_PREFERENCES, + CONSENT_REQUEST_PREFERENCES_WITH_ID, status_code=HTTP_200_OK, response_model=ConsentPreferences, ) diff --git a/src/fidesops/ops/api/v1/urn_registry.py b/src/fidesops/ops/api/v1/urn_registry.py index eab71907a..5bcbcaa0c 100644 --- a/src/fidesops/ops/api/v1/urn_registry.py +++ b/src/fidesops/ops/api/v1/urn_registry.py @@ -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" diff --git a/tests/ops/api/v1/endpoints/test_consent_request_endpoints.py b/tests/ops/api/v1/endpoints/test_consent_request_endpoints.py index 6e7ed3d75..e062e2962 100644 --- a/tests/ops/api/v1/endpoints/test_consent_request_endpoints.py +++ b/tests/ops/api/v1/endpoints/test_consent_request_endpoints.py @@ -10,6 +10,7 @@ from fidesops.ops.api.v1.urn_registry import ( CONSENT_REQUEST, CONSENT_REQUEST_PREFERENCES, + CONSENT_REQUEST_PREFERENCES_WITH_ID, CONSENT_REQUEST_VERIFY, V1_URL_PREFIX, ) @@ -233,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 @@ -251,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 @@ -269,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 @@ -297,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, ) @@ -317,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 @@ -354,7 +355,7 @@ 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 From 2528d6e19b5c6e95fc0f06488ada4f540aecec2f Mon Sep 17 00:00:00 2001 From: Paul Sanders Date: Wed, 28 Sep 2022 10:57:03 -0400 Subject: [PATCH 3/7] Update postman --- .../postman/Fidesops.postman_collection.json | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/docs/fidesops/docs/postman/Fidesops.postman_collection.json b/docs/fidesops/docs/postman/Fidesops.postman_collection.json index adb6e11c1..f8820218e 100644 --- a/docs/fidesops/docs/postman/Fidesops.postman_collection.json +++ b/docs/fidesops/docs/postman/Fidesops.postman_collection.json @@ -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" }, @@ -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" + ] + } + }, + "response": [] + }, { "name": "Verify Code and Save Preferences", "request": { @@ -5160,4 +5192,4 @@ "type": "string" } ] -} +} \ No newline at end of file From 9e1c084b0a023c23be78d6f49c65ddf2c254a775 Mon Sep 17 00:00:00 2001 From: Paul Sanders Date: Wed, 28 Sep 2022 10:58:26 -0400 Subject: [PATCH 4/7] Add docstring --- src/fidesops/ops/api/v1/endpoints/consent_request_endpoints.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/fidesops/ops/api/v1/endpoints/consent_request_endpoints.py b/src/fidesops/ops/api/v1/endpoints/consent_request_endpoints.py index 178c54b7a..a646ad523 100644 --- a/src/fidesops/ops/api/v1/endpoints/consent_request_endpoints.py +++ b/src/fidesops/ops/api/v1/endpoints/consent_request_endpoints.py @@ -145,6 +145,7 @@ def consent_request_verify( def get_consent_preferences( *, 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: From b5c6a80595ddc587979bdc4c91d45ed75e9d9e19 Mon Sep 17 00:00:00 2001 From: Paul Sanders Date: Wed, 28 Sep 2022 11:09:17 -0400 Subject: [PATCH 5/7] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87cfc3881..53896f991 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 From 26e9ef98142a245d0abea5b8caef6ba44186e740 Mon Sep 17 00:00:00 2001 From: Paul Sanders Date: Wed, 28 Sep 2022 12:22:41 -0400 Subject: [PATCH 6/7] Update from code review --- clients/ops/admin-ui/src/constants.ts | 4 ++++ .../postman/Fidesops.postman_collection.json | 2 +- .../v1/endpoints/consent_request_endpoints.py | 20 ++----------------- src/fidesops/ops/api/v1/urn_registry.py | 2 +- 4 files changed, 8 insertions(+), 20 deletions(-) diff --git a/clients/ops/admin-ui/src/constants.ts b/clients/ops/admin-ui/src/constants.ts index 63cda62ba..46b1533f4 100644 --- a/clients/ops/admin-ui/src/constants.ts +++ b/clients/ops/admin-ui/src/constants.ts @@ -41,6 +41,10 @@ export const USER_PRIVILEGES: UserPrivileges[] = [ privilege: "Delete datastore connections", scope: "connection:delete", }, + { + privilege: "View user consent preferences", + scope: "consent:read" + }, { privilege: "View Datasets", scope: "dataset:read", diff --git a/docs/fidesops/docs/postman/Fidesops.postman_collection.json b/docs/fidesops/docs/postman/Fidesops.postman_collection.json index f8820218e..3ec2a4ff0 100644 --- a/docs/fidesops/docs/postman/Fidesops.postman_collection.json +++ b/docs/fidesops/docs/postman/Fidesops.postman_collection.json @@ -72,7 +72,7 @@ "header": [], "body": { "mode": "raw", - "raw": "[\n \"client:create\",\n \"client:update\",\n \"client:read\",\n \"client:delete\",\n \"config:read\",\n \"connection_type:read\",\n \"connection:read\",\n \"connection:create_or_update\",\n \"connection:delete\",\n \"connection:instantiate\",\n \"dataset:create_or_update\",\n \"dataset:delete\",\n \"dataset:read\",\n \"encryption:exec\",\n \"email:create_or_update\",\n \"email:read\",\n \"email:delete\",\n \"policy:create_or_update\",\n \"policy:read\",\n \"policy:delete\",\n \"privacy-request:read\",\n \"privacy-request:delete\",\n \"rule:create_or_update\",\n \"rule:read\",\n \"rule:delete\",\n \"scope:read\",\n \"storage:create_or_update\",\n \"storage:delete\",\n \"storage:read\",\n \"privacy-request:resume\",\n \"webhook:create_or_update\",\n \"webhook:read\",\n \"webhook:delete\",\n \"saas_config:create_or_update\",\n \"saas_config:read\",\n \"saas_config:delete\",\n \"privacy-request:review\",\n \"user:create\",\n \"user:delete\"\n]", + "raw": "[\n \"client:create\",\n \"client:update\",\n \"client:read\",\n \"client:delete\",\n \"config:read\",\n \"connection_type:read\",\n \"connection:read\",\n \"connection:create_or_update\",\n \"connection:delete\",\n \"connection:instantiate\",\n \"consent:read\",\n \"dataset:create_or_update\",\n \"dataset:delete\",\n \"dataset:read\",\n \"encryption:exec\",\n \"email:create_or_update\",\n \"email:read\",\n \"email:delete\",\n \"policy:create_or_update\",\n \"policy:read\",\n \"policy:delete\",\n \"privacy-request:read\",\n \"privacy-request:delete\",\n \"rule:create_or_update\",\n \"rule:read\",\n \"rule:delete\",\n \"scope:read\",\n \"storage:create_or_update\",\n \"storage:delete\",\n \"storage:read\",\n \"privacy-request:resume\",\n \"webhook:create_or_update\",\n \"webhook:read\",\n \"webhook:delete\",\n \"saas_config:create_or_update\",\n \"saas_config:read\",\n \"saas_config:delete\",\n \"privacy-request:review\",\n \"user:create\",\n \"user:delete\"\n]", "options": { "raw": { "language": "json" diff --git a/src/fidesops/ops/api/v1/endpoints/consent_request_endpoints.py b/src/fidesops/ops/api/v1/endpoints/consent_request_endpoints.py index a646ad523..a280d238f 100644 --- a/src/fidesops/ops/api/v1/endpoints/consent_request_endpoints.py +++ b/src/fidesops/ops/api/v1/endpoints/consent_request_endpoints.py @@ -159,30 +159,14 @@ def get_consent_preferences( db, conditions=( (ProvidedIdentity.hashed_value == ProvidedIdentity.hash_value(lookup)) - & ( - ProvidedIdentity.privacy_request # pylint: disable=singleton-comparison - == None - ) + & (ProvidedIdentity.privacy_request_id.is_(None)) ), ).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 - ] - ) + return _prepare_consent_preferences(db, identity) @router.patch( diff --git a/src/fidesops/ops/api/v1/urn_registry.py b/src/fidesops/ops/api/v1/urn_registry.py index 5bcbcaa0c..ee67d0a3e 100644 --- a/src/fidesops/ops/api/v1/urn_registry.py +++ b/src/fidesops/ops/api/v1/urn_registry.py @@ -7,7 +7,7 @@ # Consent request URLs CONSENT_REQUEST = "/consent-request" -CONSENT_REQUEST_PREFERENCES = "/consent-request/{consent_request_id}/preferences" +CONSENT_REQUEST_PREFERENCES = "/consent-request/preferences" CONSENT_REQUEST_PREFERENCES_WITH_ID = ( "/consent-request/{consent_request_id}/preferences" ) From 0466a7e5dd81b2fd48e6ceac9d52c8c48c5284b2 Mon Sep 17 00:00:00 2001 From: Paul Sanders Date: Wed, 28 Sep 2022 12:25:05 -0400 Subject: [PATCH 7/7] Fix prettier lint error --- clients/ops/admin-ui/src/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/ops/admin-ui/src/constants.ts b/clients/ops/admin-ui/src/constants.ts index 46b1533f4..1c2104598 100644 --- a/clients/ops/admin-ui/src/constants.ts +++ b/clients/ops/admin-ui/src/constants.ts @@ -43,7 +43,7 @@ export const USER_PRIVILEGES: UserPrivileges[] = [ }, { privilege: "View user consent preferences", - scope: "consent:read" + scope: "consent:read", }, { privilege: "View Datasets",