Skip to content

Commit

Permalink
Email the "contact us" form data instead of sending to Freshdesk for …
Browse files Browse the repository at this point in the history
…sensitive services (#2416)

This PR does the following:

- adds a check when form data is submitted to our "contact us" form (this could be a go live request, branding request, help request, etc.). If the user submitting the form is logged in and belongs to at least one sensitive service, then we do not send their form data to Freshdesk, we email it to an inbox
- adds a feature flag (FF_PT_SERVICE_SKIP_FRESHDESK) to switch this feature on and off
- adds a db migration to create a new template for this email, to make it more obvious why the data was sent to email
  • Loading branch information
smcmurtry authored Jan 15, 2025
1 parent 429d2f7 commit c055cda
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 23 deletions.
56 changes: 35 additions & 21 deletions app/clients/freshdesk.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,31 +136,45 @@ def send_ticket(self) -> int:
return 201
except requests.RequestException as e:
current_app.logger.error(f"Failed to create Freshdesk ticket: {e}")
self.email_freshdesk_ticket(self._generate_ticket())
self.email_freshdesk_ticket_freshdesk_down()
return 201

def email_freshdesk_ticket(self, content: dict) -> None:
def email_freshdesk_ticket_freshdesk_down(self):
if current_app.config["CONTACT_FORM_EMAIL_ADDRESS"] is None:
current_app.logger.info("Cannot email contact us form, CONTACT_FORM_EMAIL_ADDRESS is empty")
self.email_freshdesk_ticket(
current_app.config["CONTACT_FORM_EMAIL_ADDRESS"], current_app.config["CONTACT_FORM_DIRECT_EMAIL_TEMPLATE_ID"]
)

def email_freshdesk_ticket_pt_service(self):
email_address = current_app.config.get("SENSITIVE_SERVICE_EMAIL")
template_id = current_app.config.get("CONTACT_FORM_SENSITIVE_SERVICE_EMAIL_TEMPLATE_ID")
if not email_address:
current_app.logger.error("SENSITIVE_SERVICE_EMAIL not set")
self.email_freshdesk_ticket(email_address, template_id)

def email_freshdesk_ticket(self, email_address, template_id) -> None:
content = self._generate_ticket()
try:
template = dao_get_template_by_id(current_app.config["CONTACT_FORM_DIRECT_EMAIL_TEMPLATE_ID"])
template = dao_get_template_by_id(template_id)
notify_service = dao_fetch_service_by_id(current_app.config["NOTIFY_SERVICE_ID"])

if current_app.config["CONTACT_FORM_EMAIL_ADDRESS"] is None:
current_app.logger.info("Cannot email contact us form, CONTACT_FORM_EMAIL_ADDRESS is empty")
else:
current_app.logger.info("Emailing contact us form to {}".format(current_app.config["CONTACT_FORM_EMAIL_ADDRESS"]))
saved_notification = persist_notification(
template_id=template.id,
template_version=template.version,
recipient=current_app.config["CONTACT_FORM_EMAIL_ADDRESS"],
service=notify_service,
personalisation={
"contact_us_content": json.dumps(content, indent=4),
},
notification_type=template.template_type,
api_key_id=None,
key_type=KEY_TYPE_NORMAL,
reply_to_text=notify_service.get_default_reply_to_email_address(),
)
send_notification_to_queue(saved_notification, False, queue=QueueNames.NOTIFY)
saved_notification = persist_notification(
template_id=template.id,
template_version=template.version,
recipient=email_address,
service=notify_service,
# This email will be badly formatted, but this allows us to re-use the
# _generate_description fn without having to duplicate all of that logic to get the
# description in plain text.
personalisation={
"contact_us_content": json.dumps(content, indent=4),
},
notification_type=template.template_type,
api_key_id=None,
key_type=KEY_TYPE_NORMAL,
reply_to_text=notify_service.get_default_reply_to_email_address(),
)
send_notification_to_queue(saved_notification, False, queue=QueueNames.NOTIFY)
except Exception as e:
current_app.logger.exception(f"Failed to email contact form {json.dumps(content, indent=4)}, error: {e}")
3 changes: 3 additions & 0 deletions app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ class Config(object):
REACHED_DAILY_SMS_LIMIT_TEMPLATE_ID = "a646e614-c527-4f94-a955-ed7185d577f4"
DAILY_SMS_LIMIT_UPDATED_TEMPLATE_ID = "6ec12dd0-680a-4073-8d58-91d17cc8442f"
CONTACT_FORM_DIRECT_EMAIL_TEMPLATE_ID = "b04beb4a-8408-4280-9a5c-6a046b6f7704"
CONTACT_FORM_SENSITIVE_SERVICE_EMAIL_TEMPLATE_ID = "4bf8c15b-7393-463f-b6fe-e3fd1e99a03d"
NEAR_DAILY_EMAIL_LIMIT_TEMPLATE_ID = "9aa60ad7-2d7f-46f0-8cbe-2bac3d4d77d8"
REACHED_DAILY_EMAIL_LIMIT_TEMPLATE_ID = "ee036547-e51b-49f1-862b-10ea982cfceb"
DAILY_EMAIL_LIMIT_UPDATED_TEMPLATE_ID = "97dade64-ea8d-460f-8a34-900b74ee5eb0"
Expand Down Expand Up @@ -559,6 +560,7 @@ class Config(object):
AWS_SEND_SMS_BOTO_CALL_LATENCY = os.getenv("AWS_SEND_SMS_BOTO_CALL_LATENCY", 0.06) # average delay in production

CONTACT_FORM_EMAIL_ADDRESS = os.getenv("CONTACT_FORM_EMAIL_ADDRESS", "[email protected]")
SENSITIVE_SERVICE_EMAIL = os.getenv("SENSITIVE_SERVICE_EMAIL", "[email protected]")

FROM_NUMBER = "development"

Expand Down Expand Up @@ -635,6 +637,7 @@ class Config(object):
FF_CLOUDWATCH_METRICS_ENABLED = env.bool("FF_CLOUDWATCH_METRICS_ENABLED", False)
FF_SALESFORCE_CONTACT = env.bool("FF_SALESFORCE_CONTACT", False)
FF_ANNUAL_LIMIT = env.bool("FF_ANNUAL_LIMIT", False)
FF_PT_SERVICE_SKIP_FRESHDESK = env.bool("FF_PT_SERVICE_SKIP_FRESHDESK", False)

# SRE Tools auth keys
SRE_USER_NAME = "SRE_CLIENT_USER"
Expand Down
7 changes: 7 additions & 0 deletions app/user/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,13 @@ def send_contact_request(user_id):
except Exception as e:
current_app.logger.exception(e)

# Check if user is member of any ptm services
if current_app.config.get("FF_PT_SERVICE_SKIP_FRESHDESK", False) and user:
if "province_or_territory" in [service.organisation_type for service in user.services]:
# Send to secure email instead of Freshdesk
Freshdesk(contact).email_freshdesk_ticket_pt_service()
return jsonify({"status_code": 201}), 201

status_code = Freshdesk(contact).send_ticket()
return jsonify({"status_code": status_code}), 204

Expand Down
90 changes: 90 additions & 0 deletions migrations/versions/0472_add_direct_email_2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""
Revision ID: 0472_add_direct_email_2
Revises: 0471_edit_limit_emails2
Create Date: 2025-01-13 00:00:00
"""
from datetime import datetime

from alembic import op
from flask import current_app

revision = "0472_add_direct_email_2"
down_revision = "0471_edit_limit_emails2"

contact_us_template_id = current_app.config["CONTACT_FORM_SENSITIVE_SERVICE_EMAIL_TEMPLATE_ID"]
template_ids = [contact_us_template_id]


def upgrade():
template_insert = """
INSERT INTO templates (id, name, template_type, created_at, content, archived, service_id, subject,
created_by_id, version, process_type, hidden)
VALUES ('{}', '{}', '{}', '{}', '{}', False, '{}', '{}', '{}', 1, '{}', false)
"""
template_history_insert = """
INSERT INTO templates_history (id, name, template_type, created_at, content, archived, service_id, subject,
created_by_id, version, process_type, hidden)
VALUES ('{}', '{}', '{}', '{}', '{}', False, '{}', '{}', '{}', 1, '{}', false)
"""

contact_us_content = "\n".join(
[
"Skipping Freshdesk: The user submitting the Contact Us form belongs to a sensitive Service. Contact us form data:",
"((contact_us_content))",
"",
"___",
"",
"[FR] Skipping Freshdesk: The user submitting the Contact Us form belongs to a sensitive Service. Contact us form data:",
"",
"((contact_us_content))",
]
)

templates = [
{
"id": contact_us_template_id,
"name": "Contact form direct email - sensitive service",
"subject": "Notify Contact us form for sensitive service",
"content": contact_us_content,
},
]

for template in templates:
op.execute(
template_insert.format(
template["id"],
template["name"],
"email",
datetime.utcnow(),
template["content"],
current_app.config["NOTIFY_SERVICE_ID"],
template["subject"],
current_app.config["NOTIFY_USER_ID"],
"priority",
)
)

op.execute(
template_history_insert.format(
template["id"],
template["name"],
"email",
datetime.utcnow(),
template["content"],
current_app.config["NOTIFY_SERVICE_ID"],
template["subject"],
current_app.config["NOTIFY_USER_ID"],
"priority",
)
)


def downgrade():
for template_id in template_ids:
op.execute("DELETE FROM notifications WHERE template_id = '{}'".format(template_id))
op.execute("DELETE FROM notification_history WHERE template_id = '{}'".format(template_id))
op.execute("DELETE FROM template_redacted WHERE template_id = '{}'".format(template_id))
op.execute("DELETE FROM templates_history WHERE id = '{}'".format(template_id))
op.execute("DELETE FROM templates WHERE id = '{}'".format(template_id))
37 changes: 35 additions & 2 deletions tests/app/clients/test_freshdesk.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,40 @@ def test_email_freshdesk_ticket(self, mocker, notify_api: Flask, contact_form_em
with set_config_values(notify_api, {"CONTACT_FORM_EMAIL_ADDRESS": "[email protected]"}):
with notify_api.app_context():
freshdesk_object = freshdesk.Freshdesk(ContactRequest(email_address="[email protected]"))
content = {"data": "data"}
freshdesk_object.email_freshdesk_ticket(content)
freshdesk_object.email_freshdesk_ticket_freshdesk_down()
mock_persist_notification.assert_called_once()
mock_send_notification_to_queue.assert_called_once()


class TestEmailFreshdeskSensitiveService:
def test_email_freshdesk_ticket_pt_service_success(self, mocker, notify_api):
"""Test successful sending of sensitive service email"""
mock_email_ticket = mocker.patch.object(freshdesk.Freshdesk, "email_freshdesk_ticket")

with set_config_values(
notify_api,
{
"SENSITIVE_SERVICE_EMAIL": "[email protected]",
"CONTACT_FORM_SENSITIVE_SERVICE_EMAIL_TEMPLATE_ID": "template-123",
},
):
with notify_api.app_context():
freshdesk_client = freshdesk.Freshdesk(ContactRequest(email_address="[email protected]"))
freshdesk_client.email_freshdesk_ticket_pt_service()

mock_email_ticket.assert_called_once_with("[email protected]", "template-123")

def test_email_freshdesk_ticket_pt_service_no_email(self, mocker, notify_api):
"""Test handling when sensitive service email not configured"""
mock_email_ticket = mocker.patch.object(freshdesk.Freshdesk, "email_freshdesk_ticket")
mock_logger = mocker.patch("app.clients.freshdesk.current_app.logger.error")

with set_config_values(
notify_api, {"SENSITIVE_SERVICE_EMAIL": None, "CONTACT_FORM_SENSITIVE_SERVICE_EMAIL_TEMPLATE_ID": "template-123"}
):
with notify_api.app_context():
freshdesk_client = freshdesk.Freshdesk(ContactRequest(email_address="[email protected]"))
freshdesk_client.email_freshdesk_ticket_pt_service()

mock_logger.assert_called_once_with("SENSITIVE_SERVICE_EMAIL not set")
mock_email_ticket.assert_called_once_with(None, "template-123")
52 changes: 52 additions & 0 deletions tests/app/user/test_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
create_template_folder,
create_user,
)
from tests.conftest import set_config


def test_get_user_list(admin_request, sample_service):
Expand Down Expand Up @@ -871,6 +872,57 @@ def test_send_contact_request_with_live_service(client, sample_service, mocker):
mocked_salesforce_client.engagement_update.assert_not_called()


def test_send_contact_request_with_central_service(client, mocker, notify_api):
with set_config(notify_api, "FF_PT_SERVICE_SKIP_FRESHDESK", True):
user = create_user()
data = {
"name": user.name,
"email_address": user.email_address,
"support_type": "ask_question",
"message": "test message",
}
mocked_freshdesk_send_ticket = mocker.patch("app.user.rest.Freshdesk.send_ticket", return_value=204)
mocked_freshdesk_email = mocker.patch("app.user.rest.Freshdesk.email_freshdesk_ticket_pt_service", return_value=204)
mocker.patch("app.user.rest.salesforce_client")

resp = client.post(
url_for("user.send_contact_request", user_id=str(user.id)),
data=json.dumps(data),
headers=[("Content-Type", "application/json"), create_authorization_header()],
)
assert resp.status_code == 204
mocked_freshdesk_send_ticket.assert_called_once_with()
mocked_freshdesk_email.assert_not_called()


def test_send_contact_request_with_pt_service(client, mocker, notify_api):
with set_config(notify_api, "FF_PT_SERVICE_SKIP_FRESHDESK", True):
user = create_user(name="user 2")
data = {
"name": user.name,
"email_address": user.email_address,
"support_type": "ask_question",
"message": "test message",
}
org = create_organisation(name="Ontario", organisation_type="province_or_territory")
service = create_service(user=user, service_name="test service 2", organisation=org)
service.organisation_type = "province_or_territory"
user.services = [service]

mocked_freshdesk_send_ticket = mocker.patch("app.user.rest.Freshdesk.send_ticket", return_value=204)
mocked_freshdesk_email = mocker.patch("app.user.rest.Freshdesk.email_freshdesk_ticket_pt_service", return_value=204)

resp = client.post(
url_for("user.send_contact_request", user_id=str(user.id)),
data=json.dumps(data),
headers=[("Content-Type", "application/json"), create_authorization_header()],
)
assert resp.status_code == 201
mocked_freshdesk_send_ticket.assert_not_called()
mocked_freshdesk_email.assert_called_once_with()
user.services = []


def test_send_contact_request_demo(client, sample_user, mocker):
data = {
"name": sample_user.name,
Expand Down

0 comments on commit c055cda

Please sign in to comment.