Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Executing Consent Requests via the Privacy Request Execution Layer [#2146] #2125

Merged
merged 17 commits into from
Jan 10, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
979dc25
Quick POC -
pattisdr Jan 4, 2023
d63c01a
Adjust seed logging.
pattisdr Jan 4, 2023
d894ce6
Merge branch 'main' into consent-as-privacy-request-poc
pattisdr Jan 4, 2023
cd138a1
Pass in executable_preferences to Connector.run_consent_request doing…
pattisdr Jan 4, 2023
423deb5
Get rid of redundant columns on the RuleUse table. We can use the "ke…
pattisdr Jan 4, 2023
4fc9ec7
WIP Commit. Explore how to create a simpler graph for consent request…
pattisdr Jan 4, 2023
2b2d737
Merge branch 'main' into consent-as-privacy-request-poc
pattisdr Jan 8, 2023
f685a10
Bump downrev after main merge and get lint checks passing on current …
pattisdr Jan 9, 2023
0f24e95
Fix bug on whether we're adding consent_preferences to privacy reques…
pattisdr Jan 9, 2023
630f9be
Remove concept of "RuleUse" table to store whether consent options ar…
pattisdr Jan 9, 2023
2881a36
Separate out the logic to queue the privacy request to propagate cons…
pattisdr Jan 9, 2023
64ae2f6
Only create a privacy request if there are saved consent preferences …
pattisdr Jan 9, 2023
332351c
Make some adjustments to the consent graph.
pattisdr Jan 9, 2023
749a752
Add unit and integration tests as a starting point for testing the pr…
pattisdr Jan 10, 2023
6137d72
Move where we run the consent graph so it is in the try/except and fa…
pattisdr Jan 10, 2023
34f5eeb
Fix mandrill misspelling.
pattisdr Jan 10, 2023
1bfb101
Rename privacyrequest.executable_consent_preferences back to consent_…
pattisdr Jan 10, 2023
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
52 changes: 51 additions & 1 deletion src/fides/api/ctl/database/seed.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@
PRIVACY_REQUEST_READ,
PRIVACY_REQUEST_TRANSFER,
)
from fides.api.ops.models.policy import ActionType, DrpAction, Policy, Rule, RuleTarget
from fides.api.ops.models.policy import (
ActionType,
DrpAction,
Policy,
Rule,
RuleTarget,
RuleUse,
)
from fides.api.ops.models.storage import StorageConfig
from fides.api.ops.schemas.storage.storage import (
FileNaming,
Expand All @@ -41,6 +48,9 @@
DEFAULT_ERASURE_POLICY_RULE = "default_erasure_policy_rule"
DEFAULT_ERASURE_MASKING_STRATEGY = "hmac"

DEFAULT_CONSENT_POLICY = "default_consent_policy"
DEFAULT_CONSENT_RULE = "default_consent_rule"


def create_or_update_parent_user() -> None:
with sync_session() as db_session:
Expand Down Expand Up @@ -278,6 +288,46 @@ async def load_default_dsr_policies() -> None:
except KeyOrNameAlreadyExists:
# This rule target already exists against the Policy
pass

log.info("Creating: Default Consent Policy")
consent_policy = Policy.create_or_update(
db=db_session,
data={
"name": "Default Consent Policy",
"key": DEFAULT_CONSENT_POLICY,
"execution_timeframe": 45,
"client_id": client_id,
},
)

log.info("Creating: Default Consent Rule")
consent_rule = Rule.create_or_update(
db=db_session,
data={
"action_type": ActionType.consent.value,
"name": "Default Consent Rule",
"key": DEFAULT_CONSENT_RULE,
"policy_id": consent_policy.id,
"client_id": client_id,
},
)

log.info("Creating:Default Consent Rule Uses...")
for use in ["advertising", "advertising.first_party", "improve"]:
pattisdr marked this conversation as resolved.
Show resolved Hide resolved
try:
RuleUse.create(
db=db_session,
data={
"key": use,
"data_use": use, # TODO: get rid of data_use, and have this be the key.
pattisdr marked this conversation as resolved.
Show resolved Hide resolved
"rule_id": consent_rule.id,
"executable": True,
},
)
except KeyOrNameAlreadyExists:
# This rule use already exists against the Policy
pass

log.info("All Policies & Rules Seeded.")


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""add privacyrequest consent preferences and ruleuse table

Revision ID: de456534dbda
Revises: 1f61c765cd1c
Create Date: 2023-01-03 22:59:45.144538

"""
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = "de456534dbda"
down_revision = "1f61c765cd1c"
branch_labels = None
depends_on = None


def upgrade():
op.create_table(
"ruleuse",
sa.Column("id", sa.String(length=255), nullable=False),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=True,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=True,
),
sa.Column("data_use", sa.String(), nullable=False),
sa.Column("data_use_description", sa.String(), nullable=True),
sa.Column("key", sa.String(), nullable=False),
sa.Column("executable", sa.Boolean(), nullable=False),
sa.Column("rule_id", sa.String(), nullable=False),
sa.Column("client_id", sa.String(), nullable=True),
sa.ForeignKeyConstraint(
["client_id"],
["client.id"],
),
sa.ForeignKeyConstraint(
["rule_id"],
["rule.id"],
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("rule_id", "data_use", name="_rule_id_data_use_uc"),
)
op.create_index(op.f("ix_ruleuse_id"), "ruleuse", ["id"], unique=False)
op.create_index(op.f("ix_ruleuse_key"), "ruleuse", ["key"], unique=True)
op.add_column(
"privacyrequest",
sa.Column(
"consent_preferences",
postgresql.JSONB(astext_type=sa.Text()),
nullable=True,
),
)

op.add_column(
"consentrequest", sa.Column("privacy_request_id", sa.String(), nullable=True)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a privacy request is created to propagate consent request preferences, we link it on the ConsentRequest for record-keeping. A privacy request may not always be created, for example, if none of the consent preferences ended up being executable, or something failed with privacy request creation.

)
op.create_foreign_key(
None, "consentrequest", "privacyrequest", ["privacy_request_id"], ["id"]
)

# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("privacyrequest", "consent_preferences")
op.drop_index(op.f("ix_ruleuse_key"), table_name="ruleuse")
op.drop_index(op.f("ix_ruleuse_id"), table_name="ruleuse")
op.drop_table("ruleuse")

op.drop_constraint(None, "consentrequest", type_="foreignkey")
op.drop_column("consentrequest", "privacy_request_id")
# ### end Alembic commands ###
53 changes: 46 additions & 7 deletions src/fides/api/ops/api/v1/endpoints/consent_request_endpoints.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from typing import Optional
from typing import Optional, Tuple

from fastapi import Depends, HTTPException, Security
from loguru import logger
Expand All @@ -14,7 +14,11 @@
HTTP_500_INTERNAL_SERVER_ERROR,
)

from fides.api.ctl.database.seed import DEFAULT_CONSENT_POLICY
from fides.api.ops.api.deps import get_db
from fides.api.ops.api.v1.endpoints.privacy_request_endpoints import (
create_privacy_request_func,
)
from fides.api.ops.api.v1.scope_registry import CONSENT_READ
from fides.api.ops.api.v1.urn_registry import (
CONSENT_REQUEST,
Expand All @@ -34,11 +38,13 @@
ProvidedIdentity,
ProvidedIdentityType,
)
from fides.api.ops.schemas.privacy_request import BulkPostPrivacyRequests
from fides.api.ops.schemas.privacy_request import Consent as ConsentSchema
from fides.api.ops.schemas.privacy_request import (
ConsentPreferences,
ConsentPreferencesWithVerificationCode,
ConsentRequestResponse,
PrivacyRequestCreate,
VerificationCode,
)
from fides.api.ops.schemas.redis_cache import Identity
Expand Down Expand Up @@ -122,7 +128,7 @@ def consent_request_verify(
data: VerificationCode,
) -> ConsentPreferences:
"""Verifies the verification code and returns the current consent preferences if successful."""
provided_identity = _get_consent_request_and_provided_identity(
_, provided_identity = _get_consent_request_and_provided_identity(
db=db, consent_request_id=consent_request_id, verification_code=data.code
)

Expand Down Expand Up @@ -178,7 +184,7 @@ def get_consent_preferences_no_id(
"turned off.",
)

provided_identity = _get_consent_request_and_provided_identity(
_, provided_identity = _get_consent_request_and_provided_identity(
db=db, consent_request_id=consent_request_id, verification_code=None
)

Expand Down Expand Up @@ -235,7 +241,7 @@ def set_consent_preferences(
data: ConsentPreferencesWithVerificationCode,
) -> ConsentPreferences:
"""Verifies the verification code and saves the user's consent preferences if successful."""
provided_identity = _get_consent_request_and_provided_identity(
consent_request, provided_identity = _get_consent_request_and_provided_identity(
db=db,
consent_request_id=consent_request_id,
verification_code=data.code,
Expand Down Expand Up @@ -264,15 +270,48 @@ def set_consent_preferences(
raise HTTPException(
status_code=HTTP_400_BAD_REQUEST, detail=Pii(str(exc))
)
identity = Identity()
setattr(
identity,
provided_identity.field_name.value, # type:ignore[attr-defined]
provided_identity.encrypted_value["value"], # type:ignore[index]
)
pattisdr marked this conversation as resolved.
Show resolved Hide resolved

return _prepare_consent_preferences(db, provided_identity)
consent_preferences: ConsentPreferences = _prepare_consent_preferences(
db, provided_identity
)

privacy_request_results: BulkPostPrivacyRequests = create_privacy_request_func(
db=db,
data=[
PrivacyRequestCreate(
identity=identity,
policy_key=DEFAULT_CONSENT_POLICY,
pattisdr marked this conversation as resolved.
Show resolved Hide resolved
consent_preferences=[
consent.dict() for consent in consent_preferences.consent if consent
],
)
],
authenticated=True,
pattisdr marked this conversation as resolved.
Show resolved Hide resolved
)

if privacy_request_results.failed or not privacy_request_results.succeeded:
raise HTTPException(
status_code=HTTP_400_BAD_REQUEST,
detail=privacy_request_results.failed[0].message,
)

consent_request.privacy_request_id = privacy_request_results.succeeded[0].id
consent_request.save(db=db)

return consent_preferences
pattisdr marked this conversation as resolved.
Show resolved Hide resolved


def _get_consent_request_and_provided_identity(
db: Session,
consent_request_id: str,
verification_code: Optional[str],
) -> ProvidedIdentity:
) -> Tuple[ConsentRequest, ProvidedIdentity]:
"""Verifies the consent request and verification code, then return the ProvidedIdentity if successful."""
consent_request = ConsentRequest.get_by_key_or_id(
db=db, data={"id": consent_request_id}
Expand Down Expand Up @@ -306,7 +345,7 @@ def _get_consent_request_and_provided_identity(
detail="No identity found for consent request id",
)

return provided_identity
return consent_request, provided_identity


def _prepare_consent_preferences(
Expand Down
16 changes: 12 additions & 4 deletions src/fides/api/ops/api/v1/endpoints/privacy_request_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
)
from fides.api.ops.models.privacy_request import (
CheckpointActionRequired,
ConsentRequest,
ExecutionLog,
PrivacyRequest,
PrivacyRequestNotifications,
Expand Down Expand Up @@ -187,7 +188,7 @@ def create_privacy_request(
or report failure and execute them within the Fidesops system.
You cannot update privacy requests after they've been created.
"""
return _create_privacy_request(db, data, False)
return create_privacy_request_func(db, data, False)


@router.post(
Expand All @@ -207,7 +208,7 @@ def create_privacy_request_authenticated(
You cannot update privacy requests after they've been created.
This route requires authentication instead of using verification codes.
"""
return _create_privacy_request(db, data, True)
return create_privacy_request_func(db, data, True)


def _send_privacy_request_receipt_message_to_user(
Expand Down Expand Up @@ -1551,7 +1552,7 @@ def resume_privacy_request_from_requires_input(
return privacy_request


def _create_privacy_request(
def create_privacy_request_func(
db: Session,
data: conlist(PrivacyRequestCreate), # type: ignore
authenticated: bool = False,
Expand All @@ -1572,7 +1573,12 @@ def _create_privacy_request(

logger.info("Starting creation for {} privacy requests", len(data))

optional_fields = ["external_id", "started_processing_at", "finished_processing_at"]
optional_fields = [
"external_id",
"started_processing_at",
"finished_processing_at",
"consent_preferences",
]
for privacy_request_data in data:
if not any(privacy_request_data.identity.dict().values()):
logger.warning(
Expand Down Expand Up @@ -1609,6 +1615,8 @@ def _create_privacy_request(
)
for field in optional_fields:
attr = getattr(privacy_request_data, field)
if field == "consent_preferences":
attr = [consent.dict() for consent in attr]
if attr is not None:
kwargs[field] = attr

Expand Down
8 changes: 8 additions & 0 deletions src/fides/api/ops/common_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ class RuleTargetValidationError(ValueError):
"""The Rule you are trying to create has invalid data"""


class RuleUseValidationError(ValueError):
"""The Data Use you are trying to create is inalid"""


class DataCategoryNotSupported(ValueError):
"""The data category you have supplied is not supported."""

Expand Down Expand Up @@ -101,6 +105,10 @@ class CollectionDisabled(BaseException):
"""Collection is attached to disabled ConnectionConfig"""


class NotSupportedForCollection(BaseException):
"""The given action is not supported for this type of collection"""


class PrivacyRequestPaused(BaseException):
"""Halt Instruction Received on Privacy Request"""

Expand Down
Loading