Skip to content

Commit

Permalink
YDA-5380: add option for validating email address
Browse files Browse the repository at this point in the history
This is primarily meant to be used on environments that have local
user accounts with a non-email address username that invite
external users.
  • Loading branch information
stsnel committed Sep 1, 2023
1 parent 28dd176 commit 528cbe5
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 45 deletions.
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
},
install_requires=[
"bcrypt==4.0.1",
"email-validator==2.0.0",
"Flask==2.3.2",
"Flask-session2==1.3.1",
"Flask-SQLAlchemy==3.0.3",
Expand Down
93 changes: 48 additions & 45 deletions yoda_eus/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from flask_sqlalchemy import SQLAlchemy
from flask_wtf.csrf import CSRFProtect
from jinja2 import ChoiceLoader, FileSystemLoader
from yoda_eus.mail import send_email_template
from yoda_eus.mail import is_email_valid, send_email_template_if_needed
from yoda_eus.password_complexity import check_password_complexity


Expand Down Expand Up @@ -278,27 +278,26 @@ def add_user() -> Response:

db.session.commit()

if app.config.get("MAIL_ENABLED").lower() != "false":
# Send invitation
hash_url = "https://{}/user/activate/{}".format(app.config.get("YODA_EUS_FQDN"),
secret_hash)
invitation_data = {'USERNAME': content['username'],
'CREATOR': content["creator_user"],
'HASH_URL': hash_url}
send_email_template(app,
content['username'],
'Welcome to Yoda!',
"invitation",
invitation_data)

# Send invitation confirmation
confirmation_data = {"USERNAME": content['username'],
"CREATOR": content['creator_user']}
send_email_template(app,
content["creator_user"],
'You have invited an external user to Yoda',
'invitation-sent',
confirmation_data)
# Send invitation
hash_url = "https://{}/user/activate/{}".format(app.config.get("YODA_EUS_FQDN"),
secret_hash)
invitation_data = {'USERNAME': content['username'],
'CREATOR': content["creator_user"],
'HASH_URL': hash_url}
send_email_template_if_needed(app,
content['username'],
'Welcome to Yoda!',
"invitation",
invitation_data)

# Send invitation confirmation
confirmation_data = {"USERNAME": content['username'],
"CREATOR": content['creator_user']}
send_email_template_if_needed(app,
content["creator_user"],
'You have invited an external user to Yoda',
'invitation-sent',
confirmation_data)

# Send response
response = {"status": "ok", "message": "User created."}
Expand Down Expand Up @@ -349,23 +348,28 @@ def process_forgot_password() -> Response:
response.status_code = 404
return response

if (not is_email_valid(username) and app.config.get("MAIL_ONLY_TO_VALID_ADDRESS").lower() == "true"):
errors = {"errors": ["Unable to send password reset email, because your user name ('{}') is not a valid email address.".format(username)]}
response = make_response(render_template('forgot-password.html', **errors))
response.status_code = 404
return response

# Generate and update user hash
secret_hash = get_random_hash()
user.hash = secret_hash
user.hash_time = datetime.now()
db.session.commit()

# Send password reset email
if app.config.get("MAIL_ENABLED").lower() != "false":
hash_url = "https://{}/user/reset-password/{}".format(app.config.get("YODA_EUS_FQDN"),
secret_hash)
reset_data = {'USERNAME': user.username,
'HASH_URL': hash_url}
send_email_template(app,
user.username,
'Yoda password reset',
"reset-password",
reset_data)
hash_url = "https://{}/user/reset-password/{}".format(app.config.get("YODA_EUS_FQDN"),
secret_hash)
reset_data = {'USERNAME': user.username,
'HASH_URL': hash_url}
send_email_template_if_needed(app,
user.username,
'Yoda password reset',
"reset-password",
reset_data)

return render_template("forgot-password-successful.html"), 200

Expand Down Expand Up @@ -439,19 +443,18 @@ def process_activate_account_form(hash: str) -> Response:
db.session.commit()

# Send confirmation emails
if app.config.get("MAIL_ENABLED").lower() != "false":
activation_data = {'USERNAME': user.username}
send_email_template(app,
user.username,
'You have successfully activated your Yoda account',
"activation-successful",
activation_data)
activation_data["CREATOR"] = user.creator_user
send_email_template(app,
user.creator_user,
'An external user has activated their Yoda account',
"invitation-accepted",
activation_data)
activation_data = {'USERNAME': user.username}
send_email_template_if_needed(app,
user.username,
'You have successfully activated your Yoda account',
"activation-successful",
activation_data)
activation_data["CREATOR"] = user.creator_user
send_email_template_if_needed(app,
user.creator_user,
'An external user has activated their Yoda account',
"invitation-accepted",
activation_data)

# Confirm activation to user
return render_template("activation-successful.html", **params), 200
Expand Down
47 changes: 47 additions & 0 deletions yoda_eus/mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,56 @@
from email.mime.text import MIMEText
from pathlib import Path

from email_validator import EmailNotValidError, validate_email
from jinja2 import BaseLoader, Environment


def is_email_valid(address):
"""Determines whether an email address is valid
:param address: the email address
:returns: boolean value that indicates whether the email address is valid
"""
try:
validate_email(address, check_deliverability=False)
return True
except EmailNotValidError:
return False


def send_email_template_if_needed(app, to, subject, template_name, template_data):
"""Send an e-mail with specified recipient, subject and body using templates if
application is configured to deliver it.
An email is sent if and only if:
- Email delivery has been enabled in the application configuration
- The email address is valid (or email address validation has been disabled)
The originating address and mail server credentials are taken from the
app configuration. The templates are retrieved from the template directory
specified in the app configuration.
:param app: Flask application, used for logging and retrieving configuration
:param to: Recipient of the mail
:param subject: Subject of mail
:param template_name: Name of the template in the template directory to use, excluding extensions
:param template_data: Variables to interpolate, as a dictionary
"""
if app.config.get("MAIL_ENABLED").lower() == "false":
app.logger.warning("Not sending email to '{}' with subject '{}', because email delivery is disabled.".format(
to, subject))
return

if (not is_email_valid(to) and app.config.get("MAIL_ONLY_TO_VALID_ADDRESS").lower() == "true"):
app.logger.warning("Not sending email to '{}' with subject '{}', because recipient address is invalid.".format(
to, subject))
return

send_email_template(app, to, subject, template_name, template_data)


def send_email_template(app, to, subject, template_name, template_data):
"""Send an e-mail with specified recipient, subject and body using templates
Expand Down
1 change: 1 addition & 0 deletions yoda_eus/tests/flask.test.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ SMTP_FROM_EMAIL = '[email protected]'
SMTP_REPLYTO_NAME = 'PLACEHOLDER'
SMTP_REPLYTO_EMAIL = '[email protected]'
MAIL_ENABLED = 'false'
MAIL_ONLY_TO_VALID_ADDRESS = 'false'
MAIL_TEMPLATE = 'uu'
MAIL_TEMPLATE_DIR = "/var/www/extuser/yoda-external-user-service/yoda_eus/templates/mail"

Expand Down
7 changes: 7 additions & 0 deletions yoda_eus/tests/test_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import string

from yoda_eus.mail import is_email_valid
from yoda_eus.password_complexity import check_password_complexity


Expand Down Expand Up @@ -46,3 +47,9 @@ def test_password_validation_multiple(self):
assert "Password is too short: it needs to be at least 10 characters." in result
assert "Password needs to contain at least one digit." in result
assert "Password needs to contain at least one punctuation character ({})".format(string.punctuation) in result

def is_email_valid_yes(self):
assert is_email_valid("[email protected]")

def is_email_valid_no(self):
assert not is_email_valid("this is not a valid email address")

0 comments on commit 528cbe5

Please sign in to comment.