From b0eee6d0c11136c31788b2b2dff48061782def25 Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Tue, 23 Aug 2022 11:56:03 -0400 Subject: [PATCH] Add email_templates module (#1123) * Add email_templates module * run isort * Add unit tests * Update ttl calculation * Add ttl minutes test * fix lint issues * fix pylint issue * fix pylint issue * fix isort * Update template constant * Update changelog * fix lints * Add jinja to requirements.txt * update templates directory * update unit test * Update imports * fix issue template path * Add templates to manifest Co-authored-by: Paul Sanders --- CHANGELOG.md | 1 + MANIFEST.in | 1 + requirements.txt | 1 + src/fidesops/ops/common_exceptions.py | 4 +++ src/fidesops/ops/email_templates/__init__.py | 1 + .../ops/email_templates/get_email_template.py | 29 +++++++++++++++++++ .../ops/email_templates/template_names.py | 1 + .../subject_identity_verification.html | 16 ++++++++++ src/fidesops/ops/schemas/email/email.py | 11 +++++-- .../service/email/email_dispatch_service.py | 10 +++++-- tests/ops/email_templates/__init__.py | 0 .../test_get_email_template.py | 18 ++++++++++++ tests/ops/schemas/email/email_test.py | 11 +++++++ .../email/email_dispatch_service_test.py | 18 ++++++++---- 14 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 src/fidesops/ops/email_templates/__init__.py create mode 100644 src/fidesops/ops/email_templates/get_email_template.py create mode 100644 src/fidesops/ops/email_templates/template_names.py create mode 100644 src/fidesops/ops/email_templates/templates/subject_identity_verification.html create mode 100644 tests/ops/email_templates/__init__.py create mode 100644 tests/ops/email_templates/test_get_email_template.py create mode 100644 tests/ops/schemas/email/email_test.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d6d57a10e9..241c4aba41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ The types of changes are: * SaaS Connector Configuration - Testing a Connection [#985](https://github.com/ethyca/fidesops/pull/1099) * Add an endpoint for verifying the user's identity before queuing the privacy request. [#1111](https://github.com/ethyca/fidesops/pull/1111) * Adds tests for email endpoints and service [#1112](https://github.com/ethyca/fidesops/pull/1112) +* Added email templates [#1123](https://github.com/ethyca/fidesops/pull/1123) * Add Retry button back into the subject request detail view [#1128](https://github.com/ethyca/fidesops/pull/1131) ### Developer Experience diff --git a/MANIFEST.in b/MANIFEST.in index 009d6d4f8e..a583bc4427 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,6 +6,7 @@ include versioneer.py include src/fidesops/ops/alembic.ini include src/fidesops/_version.py include src/fidesops/py.typed +graft src/fidesops/ops/email_templates/templates graft src/fidesops/ops/migrations exclude src/fidesops/ops/migrations/README exclude src/fidesops/ops/migrations/script.py.mako diff --git a/requirements.txt b/requirements.txt index 6ef232e941..05b522d56b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,6 +13,7 @@ fideslang==1.2.0 fideslib==3.0.3 fideslog==1.2.3 hvac==0.11.2 +Jinja2==3.1.2 multidimensional_urlencode==0.0.4 pandas==1.4.3 passlib[bcrypt]==1.7.4 diff --git a/src/fidesops/ops/common_exceptions.py b/src/fidesops/ops/common_exceptions.py index 41b2ce0c00..f5efcf33e3 100644 --- a/src/fidesops/ops/common_exceptions.py +++ b/src/fidesops/ops/common_exceptions.py @@ -117,6 +117,10 @@ class EmailDispatchException(FidesopsException): """Custom Exception - Email Dispatch Error""" +class EmailTemplateUnhandledActionType(FidesopsException): + """Custom Exception - Email Template Unhandled ActionType Error""" + + class OAuth2TokenException(FidesopsException): """Custom Exception - Unable to access or refresh OAuth2 tokens for SaaS connector""" diff --git a/src/fidesops/ops/email_templates/__init__.py b/src/fidesops/ops/email_templates/__init__.py new file mode 100644 index 0000000000..6b04484908 --- /dev/null +++ b/src/fidesops/ops/email_templates/__init__.py @@ -0,0 +1 @@ +from .get_email_template import get_email_template diff --git a/src/fidesops/ops/email_templates/get_email_template.py b/src/fidesops/ops/email_templates/get_email_template.py new file mode 100644 index 0000000000..19d5c95765 --- /dev/null +++ b/src/fidesops/ops/email_templates/get_email_template.py @@ -0,0 +1,29 @@ +import logging +import pathlib + +from jinja2 import Environment, FileSystemLoader, Template, select_autoescape + +from fidesops.ops.common_exceptions import EmailTemplateUnhandledActionType +from fidesops.ops.email_templates.template_names import ( + SUBJECT_IDENTITY_VERIFICATION_TEMPLATE, +) +from fidesops.ops.schemas.email.email import EmailActionType + +pathlib.Path(__file__).parent.resolve() +logger = logging.getLogger(__name__) + +abs_path_to_current_file_dir = pathlib.Path(__file__).parent.resolve() +template_env = Environment( + loader=FileSystemLoader(f"{abs_path_to_current_file_dir}/templates"), + autoescape=select_autoescape(), +) + + +def get_email_template(action_type: EmailActionType) -> Template: + if action_type == EmailActionType.SUBJECT_IDENTITY_VERIFICATION: + return template_env.get_template(SUBJECT_IDENTITY_VERIFICATION_TEMPLATE) + + logger.error(f"No corresponding template linked to the {action_type}") + raise EmailTemplateUnhandledActionType( + f"No corresponding template linked to the {action_type}" + ) diff --git a/src/fidesops/ops/email_templates/template_names.py b/src/fidesops/ops/email_templates/template_names.py new file mode 100644 index 0000000000..e674b46cf1 --- /dev/null +++ b/src/fidesops/ops/email_templates/template_names.py @@ -0,0 +1 @@ +SUBJECT_IDENTITY_VERIFICATION_TEMPLATE = "subject_identity_verification.html" diff --git a/src/fidesops/ops/email_templates/templates/subject_identity_verification.html b/src/fidesops/ops/email_templates/templates/subject_identity_verification.html new file mode 100644 index 0000000000..42e72947e2 --- /dev/null +++ b/src/fidesops/ops/email_templates/templates/subject_identity_verification.html @@ -0,0 +1,16 @@ + + + + + ID Code + + +
+

+ Your privacy request verification code is {{code}}. + Please return to the Privacy Center and enter the code to + continue. This code will expire in {{minutes}} minutes +

+
+ + \ No newline at end of file diff --git a/src/fidesops/ops/schemas/email/email.py b/src/fidesops/ops/schemas/email/email.py index a13691eada..827a7ccabd 100644 --- a/src/fidesops/ops/schemas/email/email.py +++ b/src/fidesops/ops/schemas/email/email.py @@ -24,13 +24,20 @@ class EmailActionType(Enum): class EmailTemplateBodyParams(Enum): """Enum for all possible email template body params""" - ACCESS_CODE = "access_code" + VERIFICATION_CODE = "verification_code" class SubjectIdentityVerificationBodyParams(BaseModel): """Body params required for subject identity verification email template""" - access_code: str + verification_code: str + verification_code_ttl_seconds: int + + def get_verification_code_ttl_minutes(self) -> int: + """returns verification_code_ttl_seconds in minutes""" + if self.verification_code_ttl_seconds < 60: + return 0 + return self.verification_code_ttl_seconds // 60 class EmailForActionType(BaseModel): diff --git a/src/fidesops/ops/service/email/email_dispatch_service.py b/src/fidesops/ops/service/email/email_dispatch_service.py index 8034cca36e..eb154ea5ab 100644 --- a/src/fidesops/ops/service/email/email_dispatch_service.py +++ b/src/fidesops/ops/service/email/email_dispatch_service.py @@ -6,6 +6,7 @@ from sqlalchemy.orm import Session from fidesops.ops.common_exceptions import EmailDispatchException +from fidesops.ops.email_templates import get_email_template from fidesops.ops.models.email import EmailConfig from fidesops.ops.schemas.email.email import ( EmailActionType, @@ -54,10 +55,15 @@ def _build_email( body_params: Union[SubjectIdentityVerificationBodyParams], ) -> EmailForActionType: if action_type == EmailActionType.SUBJECT_IDENTITY_VERIFICATION: + template = get_email_template(action_type) return EmailForActionType( subject="Your one-time code", - # for 1st iteration, below will be replaced with actual template files - body=f"Your one-time code is {body_params.access_code}. Hurry! It expires in 10 minutes.", + body=template.render( + { + "code": body_params.verification_code, + "minutes": body_params.get_verification_code_ttl_minutes(), + } + ), ) logger.error(f"Email action type {action_type} is not implemented") raise EmailDispatchException(f"Email action type {action_type} is not implemented") diff --git a/tests/ops/email_templates/__init__.py b/tests/ops/email_templates/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/ops/email_templates/test_get_email_template.py b/tests/ops/email_templates/test_get_email_template.py new file mode 100644 index 0000000000..b67c746637 --- /dev/null +++ b/tests/ops/email_templates/test_get_email_template.py @@ -0,0 +1,18 @@ +import pytest +from jinja2 import Template + +from fidesops.ops.common_exceptions import EmailTemplateUnhandledActionType +from fidesops.ops.email_templates import get_email_template +from fidesops.ops.schemas.email.email import EmailActionType + + +def test_get_email_template_returns_template(): + result = get_email_template(EmailActionType.SUBJECT_IDENTITY_VERIFICATION) + assert type(result) == Template + + +def test_get_email_template_exception(): + fake_template = "templateThatDoesNotExist" + with pytest.raises(EmailTemplateUnhandledActionType) as e: + get_email_template(fake_template) + assert e.value == f"No corresponding template linked to the {fake_template}" diff --git a/tests/ops/schemas/email/email_test.py b/tests/ops/schemas/email/email_test.py new file mode 100644 index 0000000000..ce31170588 --- /dev/null +++ b/tests/ops/schemas/email/email_test.py @@ -0,0 +1,11 @@ +import pytest + +from fidesops.ops.schemas.email.email import SubjectIdentityVerificationBodyParams + + +@pytest.mark.parametrize("ttl, expected", [(600, 10), (155, 2), (33, 0)]) +def test_get_verification_code_ttl_minutes_calc(ttl, expected): + result = SubjectIdentityVerificationBodyParams( + verification_code="123123", verification_code_ttl_seconds=ttl + ) + assert result.get_verification_code_ttl_minutes() == expected diff --git a/tests/ops/service/email/email_dispatch_service_test.py b/tests/ops/service/email/email_dispatch_service_test.py index eae86ace13..f07f12f3d5 100644 --- a/tests/ops/service/email/email_dispatch_service_test.py +++ b/tests/ops/service/email/email_dispatch_service_test.py @@ -28,14 +28,16 @@ def test_email_dispatch_mailgun_success( db=db, action_type=EmailActionType.SUBJECT_IDENTITY_VERIFICATION, to_email="test@email.com", - email_body_params=SubjectIdentityVerificationBodyParams(access_code="2348"), + email_body_params=SubjectIdentityVerificationBodyParams( + verification_code="2348", verification_code_ttl_seconds=600 + ), ) - + body = '\n\n\n \n ID Code\n\n\n
\n

\n Your privacy request verification code is 2348.\n Please return to the Privacy Center and enter the code to\n continue. This code will expire in 10 minutes\n

\n
\n\n' mock_mailgun_dispatcher.assert_called_with( email_config=email_config, email=EmailForActionType( subject="Your one-time code", - body=f"Your one-time code is 2348. Hurry! It expires in 10 minutes.", + body=body, ), to_email="test@email.com", ) @@ -51,7 +53,9 @@ def test_email_dispatch_mailgun_config_not_found( db=db, action_type=EmailActionType.SUBJECT_IDENTITY_VERIFICATION, to_email="test@email.com", - email_body_params=SubjectIdentityVerificationBodyParams(access_code="2348"), + email_body_params=SubjectIdentityVerificationBodyParams( + verification_code="2348", verification_code_ttl_seconds=600 + ), ) assert exc.value.args[0] == "No email config found." @@ -80,7 +84,9 @@ def test_email_dispatch_mailgun_config_no_secrets( db=db, action_type=EmailActionType.SUBJECT_IDENTITY_VERIFICATION, to_email="test@email.com", - email_body_params=SubjectIdentityVerificationBodyParams(access_code="2348"), + email_body_params=SubjectIdentityVerificationBodyParams( + verification_code="2348", verification_code_ttl_seconds=600 + ), ) assert ( exc.value.args[0] @@ -108,7 +114,7 @@ def test_email_dispatch_mailgun_failed_email(db: Session, email_config) -> None: action_type=EmailActionType.SUBJECT_IDENTITY_VERIFICATION, to_email="test@email.com", email_body_params=SubjectIdentityVerificationBodyParams( - access_code="2348" + verification_code="2348", verification_code_ttl_seconds=600 ), ) assert (