From 7886432eee0b37467f63a0fb15c56c562da87856 Mon Sep 17 00:00:00 2001
From: Luigi Pellecchia
Date: Tue, 10 Jun 2025 12:05:05 +0200
Subject: [PATCH 1/3] Add email notification in case of user role change. Add a
method in api_utils to add a link to the BASIL instance in the email body in
case the app_url setting is valid. Added a footer message to invite users to
join the Matrix chat room.
Signed-off-by: Luigi Pellecchia
---
api/api.py | 39 +++++++++++++++++++++++++++++++++------
api/api_utils.py | 8 ++++++++
2 files changed, 41 insertions(+), 6 deletions(-)
diff --git a/api/api.py b/api/api.py
index c6f009a..a281484 100644
--- a/api/api.py
+++ b/api/api.py
@@ -21,6 +21,7 @@
from sqlalchemy import and_, or_, update
from sqlalchemy.orm.exc import NoResultFound
from api_utils import (
+ add_html_link_to_email_body,
async_email_notification,
get_api_specification,
get_html_email_body_from_template,
@@ -44,6 +45,9 @@
TESTRUN_PRESET_FILEPATH = os.path.join(currentdir, CONFIGS_FOLDER, "testrun_plugin_presets.yaml")
SETTINGS_FILEPATH = os.path.join(currentdir, CONFIGS_FOLDER, "settings.yaml")
EMAIL_TEMPLATE_PATH = os.path.join(currentdir, CONFIGS_FOLDER, "email_template.html")
+EMAIL_MATRIX_FOOTER_MESSAGE = "Join our BASIL Matrix chat room to discuss about the tool usage and development!
"
TEST_RUNS_BASE_DIR = os.getenv("TEST_RUNS_BASE_DIR", "/var/test-runs")
USER_FILES_BASE_DIR = os.path.join(currentdir, "user-files") # forced under api to ensure tmt tree validity
PYPROJECT_FILEPATH = os.path.join(os.path.dirname(currentdir), "pyproject.toml")
@@ -5489,7 +5493,7 @@ def post(self):
# Send email notifications to admins
email_subject = "BASIL - New User"
email_body = f"{username} joined us on BASIL!"
- email_footer = ""
+ email_footer = EMAIL_MATRIX_FOOTER_MESSAGE
admins = dbi.session.query(UserModel).filter(
UserModel.role == "ADMIN").filter(
@@ -5903,13 +5907,18 @@ def get(self):
dbi.session.add(target_user)
dbi.session.commit()
dbi.engine.dispose()
+
+ # Notification
+ settings = load_settings()
email_subject = "BASIL - Confirm password reset"
- email_footer = ""
+ email_footer = EMAIL_MATRIX_FOOTER_MESSAGE
+ email_body = "Your password has been reset
"
+ email_body = add_html_link_to_email_body(settings=settings, body=email_body)
email_body = get_html_email_body_from_template(EMAIL_TEMPLATE_PATH,
email_subject,
- "Your password has been reset",
+ email_body,
email_footer)
- email_notifier = EmailNotifier(settings=load_settings())
+ email_notifier = EmailNotifier(settings=settings)
ret = email_notifier.send_email(email, email_subject, email_body, True)
if ret:
if "redirect" in request_data.keys():
@@ -5940,7 +5949,7 @@ def post(self):
# generate reset_token and reset_pwd
email_subject = "BASIL - Password reset"
- email_footer = ""
+ email_footer = EMAIL_MATRIX_FOOTER_MESSAGE
reset_pwd = secrets.token_urlsafe(10)
encoded_reset_pwd = base64.b64encode(reset_pwd.encode("utf-8")).decode("utf-8")
@@ -5948,7 +5957,8 @@ def post(self):
reset_token = secrets.token_urlsafe(90)
reset_url = f"{request.base_url}?email={email}&reset_token={reset_token}"
if "app_url" in settings.keys():
- reset_url += f"&redirect={settings['app_url']}/login?from=reset-password"
+ if str(settings["app_url"].trim()):
+ reset_url += f"&redirect={settings['app_url']}/login?from=reset-password"
target_user.reset_pwd = encoded_reset_pwd
target_user.reset_token = reset_token
@@ -6037,6 +6047,23 @@ def put(self):
target_user.role = request_data["role"]
dbi.session.commit()
+ # Notification
+ settings = load_settings()
+ email_subject = "BASIL user role changed"
+ email_footer = EMAIL_MATRIX_FOOTER_MESSAGE
+ email_body = f"Your BASIL user role changed to {target_user.role}
"
+ email_body += "See the BASIL documentation for a description of each role.
"
+ email_body = add_html_link_to_email_body(settings=settings, body=email_body)
+ email_body = get_html_email_body_from_template(EMAIL_TEMPLATE_PATH,
+ email_subject,
+ email_body,
+ email_footer)
+
+ email_notifier = EmailNotifier(settings=settings)
+ email_notifier.send_email(target_user.email, email_subject, email_body, True)
+
return {"email": request_data["email"]}
diff --git a/api/api_utils.py b/api/api_utils.py
index d2504e5..906ceaa 100644
--- a/api/api_utils.py
+++ b/api/api_utils.py
@@ -5,6 +5,14 @@
from urllib.error import HTTPError, URLError
+def add_html_link_to_email_body(settings, body):
+ """Append a link to BASIL instance if the app_url setting is populated"""
+ if "app_url" in settings.keys():
+ if str(settings["app_url"].trim()):
+ body += f"Link to BASIL website
"
+ return body
+
+
def get_html_email_body_from_template(template_path, subject, body, footer):
"""Generate the HTML email body from a template file using
custom values for subject, body and footer
From cf26354715ca3af0d7264572ebb7f0814e0c4c6e Mon Sep 17 00:00:00 2001
From: Luigi Pellecchia
Date: Tue, 10 Jun 2025 16:18:20 +0200
Subject: [PATCH 2/3] Use async email notification in case of user role
changes. Fix issue using strip(). Added a test for api_utils to test the
newly added function add_html_link_to_email_body.
Signed-off-by: Luigi Pellecchia
---
api/api.py | 15 ++++++++-------
api/api_utils.py | 11 +++++++++--
api/test/test_api_utils.py | 28 ++++++++++++++++++++++++++++
3 files changed, 45 insertions(+), 9 deletions(-)
create mode 100644 api/test/test_api_utils.py
diff --git a/api/api.py b/api/api.py
index a281484..639b54a 100644
--- a/api/api.py
+++ b/api/api.py
@@ -5957,7 +5957,7 @@ def post(self):
reset_token = secrets.token_urlsafe(90)
reset_url = f"{request.base_url}?email={email}&reset_token={reset_token}"
if "app_url" in settings.keys():
- if str(settings["app_url"].trim()):
+ if str(settings["app_url"]).strip():
reset_url += f"&redirect={settings['app_url']}/login?from=reset-password"
target_user.reset_pwd = encoded_reset_pwd
@@ -6056,13 +6056,14 @@ def put(self):
"https://basil-the-fusa-spice.readthedocs.io/en/latest/user_management.html#roles" \
"'>BASIL documentation for a description of each role.
"
email_body = add_html_link_to_email_body(settings=settings, body=email_body)
- email_body = get_html_email_body_from_template(EMAIL_TEMPLATE_PATH,
- email_subject,
- email_body,
- email_footer)
- email_notifier = EmailNotifier(settings=settings)
- email_notifier.send_email(target_user.email, email_subject, email_body, True)
+ async_email_notification(SETTINGS_FILEPATH,
+ EMAIL_TEMPLATE_PATH,
+ [target_user.email],
+ email_subject,
+ email_body,
+ email_footer,
+ True)
return {"email": request_data["email"]}
diff --git a/api/api_utils.py b/api/api_utils.py
index 906ceaa..0b8f6f1 100644
--- a/api/api_utils.py
+++ b/api/api_utils.py
@@ -4,12 +4,19 @@
from string import Template
from urllib.error import HTTPError, URLError
+LINK_BASIL_INSTANCE_HTML_MESSAGE = "Link to BASIL website"
+
def add_html_link_to_email_body(settings, body):
"""Append a link to BASIL instance if the app_url setting is populated"""
+ if not settings:
+ if not body:
+ return ""
+ return body
+
if "app_url" in settings.keys():
- if str(settings["app_url"].trim()):
- body += f"Link to BASIL website
"
+ if str(settings["app_url"]).strip():
+ body += f"{LINK_BASIL_INSTANCE_HTML_MESSAGE}
"
return body
diff --git a/api/test/test_api_utils.py b/api/test/test_api_utils.py
new file mode 100644
index 0000000..8e37ec4
--- /dev/null
+++ b/api/test/test_api_utils.py
@@ -0,0 +1,28 @@
+import os
+import sys
+
+currentdir = os.path.dirname(os.path.realpath(__file__))
+sys.path.insert(1, os.path.dirname(currentdir))
+
+import api
+import api_utils
+
+
+def test__add_html_link_to_email_body():
+ settings = None
+ initial_body = None
+ body = api_utils.add_html_link_to_email_body(settings=settings, body=initial_body)
+ assert body == ""
+ assert api_utils.LINK_BASIL_INSTANCE_HTML_MESSAGE not in body
+
+ settings = None
+ initial_body = ""
+ body = api_utils.add_html_link_to_email_body(settings=settings, body=initial_body)
+ assert body == ""
+ assert api_utils.LINK_BASIL_INSTANCE_HTML_MESSAGE not in body
+
+ settings = api.load_settings()
+ initial_body = ""
+ body = api_utils.add_html_link_to_email_body(settings=settings, body=initial_body)
+ assert body != ""
+ assert api_utils.LINK_BASIL_INSTANCE_HTML_MESSAGE in body
From e2393fe1b85b76595cf0c90f9251afeb6e5f85d7 Mon Sep 17 00:00:00 2001
From: Luigi Pellecchia
Date: Tue, 10 Jun 2025 17:26:08 +0200
Subject: [PATCH 3/3] Notifier, allow false as value for ssl and report in
different ways None or empty string setting errors
Signed-off-by: Luigi Pellecchia
---
api/notifier.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/api/notifier.py b/api/notifier.py
index 67cd8bb..6ccf2f0 100644
--- a/api/notifier.py
+++ b/api/notifier.py
@@ -44,11 +44,11 @@ def validate_settings(self) -> bool:
print(f"Field {setting} not in settings")
return False
else:
- if not self.settings[self.SMTP_SETTING_FIELD][setting]:
- print(f"Setting field {setting} is not valid")
+ if self.settings[self.SMTP_SETTING_FIELD][setting] is None:
+ print(f"Settings are not valid, field {setting} is None.")
return False
if str(self.settings[self.SMTP_SETTING_FIELD][setting]).strip() == "":
- print(f"Setting field {setting} is not valid")
+ print(f"Settings are not valid, field {setting} is empty.")
return False
return True