From 025717c63a890a177bdbb59504cf8b9d63eecdec Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Tue, 6 Sep 2022 11:27:56 -0400 Subject: [PATCH 1/9] Add `due_date` to `PrivacyRequest` model --- .../versions/d8df7ff7aab4_add_due_date.py | 28 +++++++++++++++++++ src/fidesops/ops/models/privacy_request.py | 17 +++++++++-- 2 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 src/fidesops/ops/migrations/versions/d8df7ff7aab4_add_due_date.py diff --git a/src/fidesops/ops/migrations/versions/d8df7ff7aab4_add_due_date.py b/src/fidesops/ops/migrations/versions/d8df7ff7aab4_add_due_date.py new file mode 100644 index 000000000..15aefe101 --- /dev/null +++ b/src/fidesops/ops/migrations/versions/d8df7ff7aab4_add_due_date.py @@ -0,0 +1,28 @@ +"""add_due_date + +Revision ID: d8df7ff7aab4 +Revises: bde646a6f51e +Create Date: 2022-09-06 15:21:21.181463 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'd8df7ff7aab4' +down_revision = 'bde646a6f51e' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('privacyrequest', sa.Column('due_date', sa.DateTime(timezone=True), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('privacyrequest', 'due_date') + # ### end Alembic commands ### diff --git a/src/fidesops/ops/models/privacy_request.py b/src/fidesops/ops/models/privacy_request.py index 29e2bbb5b..eebda2edc 100644 --- a/src/fidesops/ops/models/privacy_request.py +++ b/src/fidesops/ops/models/privacy_request.py @@ -4,7 +4,7 @@ import json import logging -from datetime import datetime +from datetime import datetime, timedelta from enum import Enum as EnumType from typing import Any, Dict, List, Optional @@ -195,6 +195,8 @@ class PrivacyRequest(Base): # pylint: disable=R0904 ) paused_at = Column(DateTime(timezone=True), nullable=True) identity_verified_at = Column(DateTime(timezone=True), nullable=True) + due_date = Column(DateTime(timezone=True), nullable=True) + @classmethod def create(cls, db: Session, *, data: Dict[str, Any]) -> FidesBase: @@ -202,8 +204,19 @@ def create(cls, db: Session, *, data: Dict[str, Any]) -> FidesBase: Check whether this object has been passed a `requested_at` value. Default to the current datetime if not. """ + now = datetime.utcnow() if data.get("requested_at", None) is None: - data["requested_at"] = datetime.utcnow() + data["requested_at"] = now + + policy: Policy = Policy.get_by( + db=db, + field="id", + value=data["policy_id"], + ).first() + + if policy.execution_timeframe: + data["due_date"] = now + timedelta(days=7) + return super().create(db=db, data=data) def delete(self, db: Session) -> None: From aa253755fbf1f6b2622b48f582b8a42bce32f346 Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Tue, 6 Sep 2022 14:00:42 -0400 Subject: [PATCH 2/9] Add `due_date` test and run lints --- .../versions/d8df7ff7aab4_add_due_date.py | 14 ++++++---- src/fidesops/ops/models/privacy_request.py | 10 +++---- tests/ops/fixtures/application_fixtures.py | 1 + tests/ops/models/test_privacy_request.py | 28 +++++++++++++++++++ 4 files changed, 42 insertions(+), 11 deletions(-) diff --git a/src/fidesops/ops/migrations/versions/d8df7ff7aab4_add_due_date.py b/src/fidesops/ops/migrations/versions/d8df7ff7aab4_add_due_date.py index 15aefe101..f2583e7de 100644 --- a/src/fidesops/ops/migrations/versions/d8df7ff7aab4_add_due_date.py +++ b/src/fidesops/ops/migrations/versions/d8df7ff7aab4_add_due_date.py @@ -5,24 +5,26 @@ Create Date: 2022-09-06 15:21:21.181463 """ -from alembic import op import sqlalchemy as sa - +from alembic import op # revision identifiers, used by Alembic. -revision = 'd8df7ff7aab4' -down_revision = 'bde646a6f51e' +revision = "d8df7ff7aab4" +down_revision = "bde646a6f51e" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('privacyrequest', sa.Column('due_date', sa.DateTime(timezone=True), nullable=True)) + op.add_column( + "privacyrequest", + sa.Column("due_date", sa.DateTime(timezone=True), nullable=True), + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('privacyrequest', 'due_date') + op.drop_column("privacyrequest", "due_date") # ### end Alembic commands ### diff --git a/src/fidesops/ops/models/privacy_request.py b/src/fidesops/ops/models/privacy_request.py index eebda2edc..b09acc849 100644 --- a/src/fidesops/ops/models/privacy_request.py +++ b/src/fidesops/ops/models/privacy_request.py @@ -197,25 +197,25 @@ class PrivacyRequest(Base): # pylint: disable=R0904 identity_verified_at = Column(DateTime(timezone=True), nullable=True) due_date = Column(DateTime(timezone=True), nullable=True) - @classmethod def create(cls, db: Session, *, data: Dict[str, Any]) -> FidesBase: """ Check whether this object has been passed a `requested_at` value. Default to the current datetime if not. """ - now = datetime.utcnow() if data.get("requested_at", None) is None: - data["requested_at"] = now + data["requested_at"] = datetime.utcnow() policy: Policy = Policy.get_by( db=db, field="id", value=data["policy_id"], - ).first() + ) if policy.execution_timeframe: - data["due_date"] = now + timedelta(days=7) + data["due_date"] = data["requested_at"] + timedelta( + days=policy.execution_timeframe + ) return super().create(db=db, data=data) diff --git a/tests/ops/fixtures/application_fixtures.py b/tests/ops/fixtures/application_fixtures.py index acf652b0a..dfd6a563e 100644 --- a/tests/ops/fixtures/application_fixtures.py +++ b/tests/ops/fixtures/application_fixtures.py @@ -508,6 +508,7 @@ def policy( "name": "example access request policy", "key": "example_access_request_policy", "client_id": oauth_client.id, + "execution_timeframe": 7, }, ) diff --git a/tests/ops/models/test_privacy_request.py b/tests/ops/models/test_privacy_request.py index bb5175a60..e17905ffd 100644 --- a/tests/ops/models/test_privacy_request.py +++ b/tests/ops/models/test_privacy_request.py @@ -73,6 +73,34 @@ def test_create_privacy_request_sets_requested_at( pr.delete(db) +def test_create_privacy_request_sets_due_date( + db: Session, + policy: Policy, +) -> None: + pr = PrivacyRequest.create( + db=db, + data={ + "policy_id": policy.id, + "status": "pending", + }, + ) + assert pr.due_date is not None + pr.delete(db) + + requested_at = datetime.now(timezone.utc) + due_date = timedelta(days=policy.execution_timeframe) + requested_at + pr = PrivacyRequest.create( + db=db, + data={ + "requested_at": requested_at, + "policy_id": policy.id, + "status": "pending", + }, + ) + assert pr.due_date == due_date + pr.delete(db) + + def test_update_privacy_requests(db: Session, privacy_requests: PrivacyRequest) -> None: privacy_request = privacy_requests[0] EXTERNAL_ID_TO_UPDATE = privacy_request.external_id From 8587f09705138aae2625afd0337197cd69d39ea1 Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Tue, 6 Sep 2022 14:02:34 -0400 Subject: [PATCH 3/9] Update CHANGELOG.md --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ae07dd62..fea26c8b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ The types of changes are: ### Developer Experience * Created a docker image for the privacy center [#1165](https://github.com/ethyca/fidesops/pull/1165) -* Adds email scopes to postman collection [1241](https://github.com/ethyca/fidesops/pull/1241) +* Adds email scopes to postman collection [#1241](https://github.com/ethyca/fidesops/pull/1241) ### Added @@ -31,12 +31,13 @@ The types of changes are: * Foundations for a new email connector type [#1142](https://github.com/ethyca/fidesops/pull/1142) * Have the new email connector cache action needed for each collection [#1168](https://github.com/ethyca/fidesops/pull/1168) * Added `execution_timeframe` to Policy model and schema [#1244](https://github.com/ethyca/fidesops/pull/1244) +* Added `due_date` to Privacy request model [#1259](https://github.com/ethyca/fidesops/pull/1259) ### Docs * Fix analytics opt out environment variable name [#1170](https://github.com/ethyca/fidesops/pull/1170) * Added how to view a subject request history and reprocess a subject request [#1164](https://github.com/ethyca/fidesops/pull/1164) -* Adds section on email communications, and exposes previously hidden guides in nav bar [1233](https://github.com/ethyca/fidesops/pull/1233) +* Adds section on email communications, and exposes previously hidden guides in nav bar [#1233](https://github.com/ethyca/fidesops/pull/1233) ### Fixed From 30406f817a4f352c02fbe9affde5d9cf281839ee Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Tue, 6 Sep 2022 14:31:08 -0400 Subject: [PATCH 4/9] Fix test failures --- .../api/v1/endpoints/test_privacy_request_endpoints.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/ops/api/v1/endpoints/test_privacy_request_endpoints.py b/tests/ops/api/v1/endpoints/test_privacy_request_endpoints.py index fd8c19ef7..da583ae87 100644 --- a/tests/ops/api/v1/endpoints/test_privacy_request_endpoints.py +++ b/tests/ops/api/v1/endpoints/test_privacy_request_endpoints.py @@ -585,7 +585,7 @@ def test_get_privacy_requests_by_id( "reviewer": None, "policy": { "drp_action": None, - "execution_timeframe": None, + "execution_timeframe": 7, "name": privacy_request.policy.name, "key": privacy_request.policy.key, "rules": [ @@ -640,7 +640,7 @@ def test_get_privacy_requests_by_partial_id( "paused_at": None, "reviewer": None, "policy": { - "execution_timeframe": None, + "execution_timeframe": 7, "drp_action": None, "name": privacy_request.policy.name, "key": privacy_request.policy.key, @@ -996,7 +996,7 @@ def test_verbose_privacy_requests( "paused_at": None, "reviewer": None, "policy": { - "execution_timeframe": None, + "execution_timeframe": 7, "drp_action": None, "name": privacy_request.policy.name, "key": privacy_request.policy.key, @@ -2020,7 +2020,7 @@ def test_resume_privacy_request( "reviewer": None, "paused_at": None, "policy": { - "execution_timeframe": None, + "execution_timeframe": 7, "drp_action": None, "key": privacy_request.policy.key, "name": privacy_request.policy.name, From 36717b8942b28c0f4936df384a39a42ecbd0d6b8 Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Tue, 6 Sep 2022 14:55:47 -0400 Subject: [PATCH 5/9] Fix type error --- src/fidesops/ops/models/privacy_request.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/fidesops/ops/models/privacy_request.py b/src/fidesops/ops/models/privacy_request.py index b09acc849..7a9fa4968 100644 --- a/src/fidesops/ops/models/privacy_request.py +++ b/src/fidesops/ops/models/privacy_request.py @@ -9,6 +9,7 @@ from typing import Any, Dict, List, Optional from celery.result import AsyncResult +from dateutil.parser import parse as datetime_parser from fideslib.cryptography.cryptographic_util import hash_with_salt from fideslib.db.base import Base from fideslib.db.base_class import FidesBase @@ -213,9 +214,10 @@ def create(cls, db: Session, *, data: Dict[str, Any]) -> FidesBase: ) if policy.execution_timeframe: - data["due_date"] = data["requested_at"] + timedelta( - days=policy.execution_timeframe - ) + requested_at = data["requested_at"] + if isinstance(requested_at, str): + requested_at = datetime_parser(requested_at) + data["due_date"] = requested_at + timedelta(days=policy.execution_timeframe) return super().create(db=db, data=data) From 35310bb484ef0dfdb2d9589613c62a86ea904a89 Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Tue, 6 Sep 2022 15:08:30 -0400 Subject: [PATCH 6/9] Fix mypy issue --- dev-requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/dev-requirements.txt b/dev-requirements.txt index 2ab6cfaa9..16786d04c 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -14,6 +14,7 @@ pytest-env==0.6.2 pytest==7.1.2 requests-mock==1.9.3 setuptools>=64.0.2 +types-python-dateutil==2.8.19 types-PyYAML==6.0.11 types-redis==4.3.4 types-toml==0.10.8 From eb6c2998e4deca9c16acf9e357607e9ca93f2151 Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Wed, 7 Sep 2022 10:38:37 -0400 Subject: [PATCH 7/9] Switch to `strptime` --- dev-requirements.txt | 1 - src/fidesops/ops/models/privacy_request.py | 3 +-- tests/ops/models/test_privacy_request.py | 16 ++++++++++++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 16786d04c..2ab6cfaa9 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -14,7 +14,6 @@ pytest-env==0.6.2 pytest==7.1.2 requests-mock==1.9.3 setuptools>=64.0.2 -types-python-dateutil==2.8.19 types-PyYAML==6.0.11 types-redis==4.3.4 types-toml==0.10.8 diff --git a/src/fidesops/ops/models/privacy_request.py b/src/fidesops/ops/models/privacy_request.py index 7a9fa4968..3743eda43 100644 --- a/src/fidesops/ops/models/privacy_request.py +++ b/src/fidesops/ops/models/privacy_request.py @@ -9,7 +9,6 @@ from typing import Any, Dict, List, Optional from celery.result import AsyncResult -from dateutil.parser import parse as datetime_parser from fideslib.cryptography.cryptographic_util import hash_with_salt from fideslib.db.base import Base from fideslib.db.base_class import FidesBase @@ -216,7 +215,7 @@ def create(cls, db: Session, *, data: Dict[str, Any]) -> FidesBase: if policy.execution_timeframe: requested_at = data["requested_at"] if isinstance(requested_at, str): - requested_at = datetime_parser(requested_at) + requested_at = datetime.strptime(requested_at, "%Y-%m-%dT%H:%M:%S.%fZ") data["due_date"] = requested_at + timedelta(days=policy.execution_timeframe) return super().create(db=db, data=data) diff --git a/tests/ops/models/test_privacy_request.py b/tests/ops/models/test_privacy_request.py index e17905ffd..faeb22cb5 100644 --- a/tests/ops/models/test_privacy_request.py +++ b/tests/ops/models/test_privacy_request.py @@ -100,6 +100,22 @@ def test_create_privacy_request_sets_due_date( assert pr.due_date == due_date pr.delete(db) + requested_at_str = "2021-08-30T16:09:37.359Z" + requested_at = datetime.strptime(requested_at_str, "%Y-%m-%dT%H:%M:%S.%fZ").replace( + tzinfo=timezone.utc + ) + due_date = timedelta(days=policy.execution_timeframe) + requested_at + pr = PrivacyRequest.create( + db=db, + data={ + "requested_at": requested_at_str, + "policy_id": policy.id, + "status": "pending", + }, + ) + assert pr.due_date == due_date + pr.delete(db) + def test_update_privacy_requests(db: Session, privacy_requests: PrivacyRequest) -> None: privacy_request = privacy_requests[0] From 89d754bcddc7b21ac319abacdcab013616613ba1 Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Wed, 7 Sep 2022 10:51:13 -0400 Subject: [PATCH 8/9] Fix migration down revision --- .../ops/migrations/versions/d8df7ff7aab4_add_due_date.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fidesops/ops/migrations/versions/d8df7ff7aab4_add_due_date.py b/src/fidesops/ops/migrations/versions/d8df7ff7aab4_add_due_date.py index f2583e7de..256632a3b 100644 --- a/src/fidesops/ops/migrations/versions/d8df7ff7aab4_add_due_date.py +++ b/src/fidesops/ops/migrations/versions/d8df7ff7aab4_add_due_date.py @@ -1,7 +1,7 @@ """add_due_date Revision ID: d8df7ff7aab4 -Revises: bde646a6f51e +Revises: 912d801f06c0 Create Date: 2022-09-06 15:21:21.181463 """ @@ -10,7 +10,7 @@ # revision identifiers, used by Alembic. revision = "d8df7ff7aab4" -down_revision = "bde646a6f51e" +down_revision = "912d801f06c0" branch_labels = None depends_on = None From 3405124e675c05b1e27d25b338499a8b82944dbf Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Wed, 7 Sep 2022 11:39:56 -0400 Subject: [PATCH 9/9] Move date format string into constant --- src/fidesops/ops/models/privacy_request.py | 3 ++- src/fidesops/ops/util/constants.py | 1 + tests/ops/models/test_privacy_request.py | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 src/fidesops/ops/util/constants.py diff --git a/src/fidesops/ops/models/privacy_request.py b/src/fidesops/ops/models/privacy_request.py index 809b237af..fee090cba 100644 --- a/src/fidesops/ops/models/privacy_request.py +++ b/src/fidesops/ops/models/privacy_request.py @@ -65,6 +65,7 @@ get_masking_secret_cache_key, ) from fidesops.ops.util.collection_util import Row +from fidesops.ops.util.constants import API_DATE_FORMAT logger = logging.getLogger(__name__) @@ -229,7 +230,7 @@ def create(cls, db: Session, *, data: Dict[str, Any]) -> FidesBase: if policy.execution_timeframe: requested_at = data["requested_at"] if isinstance(requested_at, str): - requested_at = datetime.strptime(requested_at, "%Y-%m-%dT%H:%M:%S.%fZ") + requested_at = datetime.strptime(requested_at, API_DATE_FORMAT) data["due_date"] = requested_at + timedelta(days=policy.execution_timeframe) return super().create(db=db, data=data) diff --git a/src/fidesops/ops/util/constants.py b/src/fidesops/ops/util/constants.py new file mode 100644 index 000000000..1c6a765c4 --- /dev/null +++ b/src/fidesops/ops/util/constants.py @@ -0,0 +1 @@ +API_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" diff --git a/tests/ops/models/test_privacy_request.py b/tests/ops/models/test_privacy_request.py index b703d8612..fa84a0fca 100644 --- a/tests/ops/models/test_privacy_request.py +++ b/tests/ops/models/test_privacy_request.py @@ -22,6 +22,7 @@ from fidesops.ops.schemas.redis_cache import PrivacyRequestIdentity from fidesops.ops.service.connectors.manual_connector import ManualAction from fidesops.ops.util.cache import FidesopsRedis, get_identity_cache_key +from fidesops.ops.util.constants import API_DATE_FORMAT paused_location = CollectionAddress("test_dataset", "test_collection") @@ -102,7 +103,7 @@ def test_create_privacy_request_sets_due_date( pr.delete(db) requested_at_str = "2021-08-30T16:09:37.359Z" - requested_at = datetime.strptime(requested_at_str, "%Y-%m-%dT%H:%M:%S.%fZ").replace( + requested_at = datetime.strptime(requested_at_str, API_DATE_FORMAT).replace( tzinfo=timezone.utc ) due_date = timedelta(days=policy.execution_timeframe) + requested_at