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 14 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 @@ -27,6 +27,7 @@ The types of changes are:
* Add an endpoint that allows you to create a Saas connector and all supporting resources with a single request [#1076](https://github.com/ethyca/fidesops/pull/1076)
* 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)

### Developer Experience

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
26 changes: 26 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,26 @@
import logging

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

logger = logging.getLogger(__name__)

template_env = Environment(
loader=FileSystemLoader("src/fidesops/ops/email_templates/templates"),
autoescape=select_autoescape(),
)
TheAndrewJackson marked this conversation as resolved.
Show resolved Hide resolved


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}"
18 changes: 18 additions & 0 deletions tests/ops/schemas/email/email_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from fidesops.ops.schemas.email.email import SubjectIdentityVerificationBodyParams


def test_get_verification_code_ttl_minutes_calc():
model_1 = SubjectIdentityVerificationBodyParams(
verification_code="123123", verification_code_ttl_seconds=600
)
assert model_1.get_verification_code_ttl_minutes() == 10

model_2 = SubjectIdentityVerificationBodyParams(
verification_code="123123", verification_code_ttl_seconds=155
)
assert model_2.get_verification_code_ttl_minutes() == 2

model_3 = SubjectIdentityVerificationBodyParams(
verification_code="123123", verification_code_ttl_seconds=33
)
assert model_3.get_verification_code_ttl_minutes() == 0
TheAndrewJackson marked this conversation as resolved.
Show resolved Hide resolved
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="test@email.com",
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="test@email.com",
)
Expand All @@ -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."

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="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]
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="test@email.com",
email_body_params=SubjectIdentityVerificationBodyParams(
access_code="2348"
verification_code="2348", verification_code_ttl_seconds=600
),
)
assert (
Expand Down