Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Email the "contact us" form data instead of sending to Freshdesk for sensitive services #2416

Merged
merged 21 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from 19 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
53 changes: 32 additions & 21 deletions app/clients/freshdesk.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,31 +136,42 @@ 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,
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
5 changes: 5 additions & 0 deletions app/dao/services_dao.py
Original file line number Diff line number Diff line change
Expand Up @@ -599,3 +599,8 @@ def dao_fetch_service_creator(service_id: uuid.UUID) -> User:
def dao_fetch_service_ids_of_sensitive_services():
sensitive_service_ids = Service.query.filter(Service.sensitive_service.is_(True)).with_entities(Service.id).all()
return [str(service_id) for (service_id,) in sensitive_service_ids]


def dao_fetch_service_ids_of_pt_services():
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a short list for now, and this is a temporary feature - hence the below is a question not a request to change.

Would it make sense to take the email_address of the person -> get the service_ids the user is associated with? Instead of the larger list?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point actually! We already have the organisation_type data for a user's services without doing an extra query. I've changed the code and removed the dao_fetch_service_ids_of_sensitive_services fn.

pt_service_ids = Service.query.filter(Service.organisation_type == "province_or_territory").with_entities(Service.id).all()
return [str(service_id) for (service_id,) in pt_service_ids]
15 changes: 14 additions & 1 deletion app/user/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@
from app.dao.login_event_dao import list_login_events, save_login_event
from app.dao.permissions_dao import permission_dao
from app.dao.service_user_dao import dao_get_service_user, dao_update_service_user
from app.dao.services_dao import dao_fetch_service_by_id, dao_update_service
from app.dao.services_dao import (
dao_fetch_service_by_id,
dao_fetch_service_ids_of_pt_services,
dao_update_service,
)
from app.dao.template_folder_dao import dao_get_template_folder_by_id_and_service_id
from app.dao.templates_dao import dao_get_template_by_id
from app.dao.users_dao import (
Expand Down Expand Up @@ -480,6 +484,15 @@ 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:
pt_service_ids = dao_fetch_service_ids_of_pt_services()
user_service_ids = [str(service.id) for service in user.services]
if any(service_id in user_service_ids for service_id in pt_service_ids):
# 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")
25 changes: 25 additions & 0 deletions tests/app/dao/test_services_dao.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
dao_fetch_service_by_id,
dao_fetch_service_by_inbound_number,
dao_fetch_service_creator,
dao_fetch_service_ids_of_pt_services,
dao_fetch_service_ids_of_sensitive_services,
dao_fetch_stats_for_service,
dao_fetch_todays_stats_for_all_services,
Expand Down Expand Up @@ -1549,3 +1550,27 @@ def test_sensitive_service(self, notify_db, notify_db_session):
def test_non_sensitive_service(self, notify_db, notify_db_session):
sensitive_service = dao_fetch_service_ids_of_sensitive_services()
assert sensitive_service == []


class TestPtService:
def test_pt_service(self, notify_db, notify_db_session):
user = create_user()
assert Service.query.count() == 0
service = Service(
name="test service",
email_from="email_from",
message_limit=1000,
sms_daily_limit=1000,
restricted=False,
organisation_type="province_or_territory",
created_by=user,
)
dao_create_service(service, user)
assert Service.query.count() == 1
service_db = Service.query.one()

assert service_db.name == "test service"
assert service_db.organisation_type == "province_or_territory"

pt_service = dao_fetch_service_ids_of_pt_services()
assert [str(service.id)] == pt_service
53 changes: 53 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,58 @@ 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_cetral_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_dao = mocker.patch("app.user.rest.dao_fetch_service_ids_of_pt_services", return_value=[])
mocked_freshdesk_send_ticket = mocker.patch("app.user.rest.Freshdesk.send_ticket", return_value=201)
mocked_freshdesk_email = mocker.patch("app.user.rest.Freshdesk.email_freshdesk_ticket_pt_service", return_value=201)
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_dao.assert_called_once_with()
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)

mocked_dao = mocker.patch("app.user.rest.dao_fetch_service_ids_of_pt_services", return_value=[str(service.id)])
mocked_freshdesk_send_ticket = mocker.patch("app.user.rest.Freshdesk.send_ticket", return_value=201)
mocked_freshdesk_email = mocker.patch("app.user.rest.Freshdesk.email_freshdesk_ticket_pt_service", return_value=201)

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_dao.assert_called_once_with()
mocked_freshdesk_send_ticket.assert_not_called()
mocked_freshdesk_email.assert_called_once_with()


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