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

Add email_templates module #1123

Merged
merged 22 commits into from
Aug 23, 2022
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -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
Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions src/fidesops/ops/common_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""

Expand Down
1 change: 1 addition & 0 deletions src/fidesops/ops/email_templates/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .get_email_template import get_email_template
29 changes: 29 additions & 0 deletions src/fidesops/ops/email_templates/get_email_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import logging
import pathlib

from jinja2 import Environment, FileSystemLoader, Template, select_autoescape
TheAndrewJackson marked this conversation as resolved.
Show resolved Hide resolved

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}"
)
1 change: 1 addition & 0 deletions src/fidesops/ops/email_templates/template_names.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SUBJECT_IDENTITY_VERIFICATION_TEMPLATE = "subject_identity_verification.html"
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ID Code</title>
</head>
<body>
<main>
<p>
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
sanders41 marked this conversation as resolved.
Show resolved Hide resolved
</p>
</main>
</body>
</html>
11 changes: 9 additions & 2 deletions src/fidesops/ops/schemas/email/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
10 changes: 8 additions & 2 deletions src/fidesops/ops/service/email/email_dispatch_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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"<html>Your one-time code is {body_params.access_code}. Hurry! It expires in 10 minutes.</html>",
body=template.render(
{
"code": body_params.verification_code,
"minutes": body_params.get_verification_code_ttl_minutes(),
sanders41 marked this conversation as resolved.
Show resolved Hide resolved
}
),
)
logger.error(f"Email action type {action_type} is not implemented")
raise EmailDispatchException(f"Email action type {action_type} is not implemented")
Expand Down
Empty file.
18 changes: 18 additions & 0 deletions tests/ops/email_templates/test_get_email_template.py
Original file line number Diff line number Diff line change
@@ -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"
sanders41 marked this conversation as resolved.
Show resolved Hide resolved
with pytest.raises(EmailTemplateUnhandledActionType) as e:
get_email_template(fake_template)
assert e.value == f"No corresponding template linked to the {fake_template}"
11 changes: 11 additions & 0 deletions tests/ops/schemas/email/email_test.py
Original file line number Diff line number Diff line change
@@ -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
18 changes: 12 additions & 6 deletions tests/ops/service/email/email_dispatch_service_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,16 @@ def test_email_dispatch_mailgun_success(
db=db,
action_type=EmailActionType.SUBJECT_IDENTITY_VERIFICATION,
to_email="[email protected]",
email_body_params=SubjectIdentityVerificationBodyParams(access_code="2348"),
email_body_params=SubjectIdentityVerificationBodyParams(
verification_code="2348", verification_code_ttl_seconds=600
),
)

body = '<!DOCTYPE html>\n<html lang="en">\n<head>\n <meta charset="UTF-8">\n <title>ID Code</title>\n</head>\n<body>\n<main>\n <p>\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 </p>\n</main>\n</body>\n</html>'
mock_mailgun_dispatcher.assert_called_with(
email_config=email_config,
email=EmailForActionType(
subject="Your one-time code",
body=f"<html>Your one-time code is 2348. Hurry! It expires in 10 minutes.</html>",
body=body,
),
to_email="[email protected]",
)
Expand All @@ -51,7 +53,9 @@ def test_email_dispatch_mailgun_config_not_found(
db=db,
action_type=EmailActionType.SUBJECT_IDENTITY_VERIFICATION,
to_email="[email protected]",
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."

Expand Down Expand Up @@ -80,7 +84,9 @@ def test_email_dispatch_mailgun_config_no_secrets(
db=db,
action_type=EmailActionType.SUBJECT_IDENTITY_VERIFICATION,
to_email="[email protected]",
email_body_params=SubjectIdentityVerificationBodyParams(access_code="2348"),
email_body_params=SubjectIdentityVerificationBodyParams(
verification_code="2348", verification_code_ttl_seconds=600
),
)
assert (
exc.value.args[0]
Expand Down Expand Up @@ -108,7 +114,7 @@ def test_email_dispatch_mailgun_failed_email(db: Session, email_config) -> None:
action_type=EmailActionType.SUBJECT_IDENTITY_VERIFICATION,
to_email="[email protected]",
email_body_params=SubjectIdentityVerificationBodyParams(
access_code="2348"
verification_code="2348", verification_code_ttl_seconds=600
),
)
assert (
Expand Down