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