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

Review email configuration #2241

Merged
merged 29 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ec1671c
add use_login to mail settings
ychiucco Feb 3, 2025
ca98d2e
new settings variables
ychiucco Feb 3, 2025
dc51fa4
Merge branch 'main' into 2224-review-email-configuration
tcompa Feb 3, 2025
249ff4a
Revert "new settings variables"
ychiucco Feb 3, 2025
ac90e02
fix secrets and keys
ychiucco Feb 3, 2025
5fb7b99
Merge branch 'main' into 2224-review-email-configuration
ychiucco Feb 3, 2025
570e811
Merge remote-tracking branch 'refs/remotes/origin/2224-review-email-c…
ychiucco Feb 3, 2025
bfceb8c
not encrypted env variables
ychiucco Feb 3, 2025
04f2d81
Merge branch 'main' into 2224-review-email-configuration
tcompa Feb 4, 2025
204c1e8
fix tests
ychiucco Feb 4, 2025
58662cf
Merge remote-tracking branch 'refs/remotes/origin/2224-review-email-c…
ychiucco Feb 4, 2025
7014467
obfuscate keys
ychiucco Feb 4, 2025
d3ce2fc
changes from comments
ychiucco Feb 4, 2025
e87aba0
test unit config
ychiucco Feb 4, 2025
0cb23c3
fix test config
ychiucco Feb 4, 2025
a0741c3
solve bug with override_settings_factory
ychiucco Feb 4, 2025
eb57e63
use use_login
ychiucco Feb 4, 2025
f18111b
fix bug
ychiucco Feb 4, 2025
c644bdf
improve security
ychiucco Feb 4, 2025
f166355
fix tests
ychiucco Feb 4, 2025
fc36b13
Merge branch 'main' into 2224-review-email-configuration
tcompa Feb 4, 2025
69602e6
Rename variable in test
tcompa Feb 4, 2025
11ed043
Combine two try/except together
tcompa Feb 4, 2025
3013d86
Rename `FRACTAL_EMAIL_SETTINGS->mail_settings`
tcompa Feb 4, 2025
9fdcc78
minor
tcompa Feb 4, 2025
87097e2
Improve error message
tcompa Feb 4, 2025
e19e5f4
Complete renaming
tcompa Feb 4, 2025
ee1aa67
Fix docstrings
tcompa Feb 4, 2025
931153d
CHANGELOG
tcompa Feb 4, 2025
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
18 changes: 13 additions & 5 deletions .github/workflows/oauth.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,20 +56,28 @@ jobs:
FRACTAL_RUNNER_BACKEND: local
JWT_SECRET_KEY: jwt_secret_key
JWT_EXPIRE_SECONDS: 1000
# Postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: fractal_test
POSTGRES_HOST: localhost
POSTGRES_PORT: 5432
# FRACTAL_EMAIL_SETTINGS and KEY are generated with the following command
# `printf "fakepassword\n" | poetry run fractalctl email-settings sender@example.org localhost 1025 test --skip-starttls`
FRACTAL_EMAIL_SETTINGS: gAAAAABnYvLgoSeECnrXlv1UoP4D_c9Of0xmwMJVopBA3TIDjOvx6YDVfe2ULz8yGr8Ba5Id8rRLjCXa_Ys8iHjvuniJyvsX0mDrc3IGSoofMEeeSCvYEe4iSWLeb_qTNVNPc4IT2-SLB-F7dEvkwzyAFnEm9dVmApd4_lQLm9_wJoS-tz1Q1K8E1_jJSgpfGgwHaINHICVh1UL_qHjIa3DwFvDPvt32tLLBZTL7oN88A8RCmg00ThIZs4HN7OQkvfninfOiM060Lb-AeNViCVgBX-bIPWZaeQ==
FRACTAL_EMAIL_SETTINGS_KEY: 4otDt3R-8p4S97QT0gcUzynCalByypTv01YntqQ9XFk=
FRACTAL_EMAIL_RECIPIENTS: recipient1@example.org,recipient2@example.org
# Dex (OAuth)
OAUTH_DEXIDP_CLIENT_ID: client_test_id
OAUTH_DEXIDP_CLIENT_SECRET: client_test_secret
OAUTH_DEXIDP_REDIRECT_URL: http://localhost:8001/auth/dexidp/callback/
OAUTH_DEXIDP_OIDC_CONFIGURATION_ENDPOINT: http://127.0.0.1:5556/dex/.well-known/openid-configuration
# Email
FRACTAL_EMAIL_SENDER: sender@example.org
FRACTAL_EMAIL_SMTP_SERVER: localhost
FRACTAL_EMAIL_PORT: 1025
FRACTAL_EMAIL_INSTANCE_NAME: test
FRACTAL_EMAIL_RECIPIENTS: recipient1@example.org,recipient2@example.org
FRACTAL_EMAIL_USE_STARTTLS: false
ychiucco marked this conversation as resolved.
Show resolved Hide resolved
# FRACTAL_EMAIL_PASSWORD and FRACTAL_EMAIL_PASSWORD_KET are generated with the following command
# `printf "fakepassword\n" | poetry run fractalctl encrypt-email-password`
FRACTAL_EMAIL_PASSWORD: gAAAAABnoQUGHMsDgLkpDtwUtrKtf9T1so44ahEXExGRceAnf097mVY1EbNuMP5fjvkndvwCwBJM7lHoSgKQkZ4VbvO9t3PJZg==
FRACTAL_EMAIL_PASSWORD_KEY: lp3j2FVDkzLd0Rklnzg1pHuV9ClCuDE0aGeJfTNCaW4=
run: |
fractalctl set-db
fractalctl start --port 8001 &
Expand Down
74 changes: 11 additions & 63 deletions fractal_server/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,45 +63,15 @@
description="Apply data-migration script to an existing database.",
)

# fractalctl email-settings
email_settings_parser = subparsers.add_parser(
"email-settings",
description=(
"Generate valid values for environment variables "
"`FRACTAL_EMAIL_SETTINGS` and `FRACTAL_EMAIL_SETTINGS_KEY`."
),
)
email_settings_parser.add_argument(
"sender",
type=str,
help="Email of the sender",
)
email_settings_parser.add_argument(
"server",
type=str,
help="SMPT server used to send emails",
)
email_settings_parser.add_argument(
"port",
type=int,
help="Port of the SMPT server",
)
email_settings_parser.add_argument(
"instance",
type=str,
help="Name of the Fractal instance sending emails",
)
email_settings_parser.add_argument(
"--skip-starttls",
action="store_true",
default=False,
help="If set, skip the execution of `starttls` when sending emails",
# fractalctl encrypt-email-password
encrypt_email_password_parser = subparsers.add_parser(
"encrypt-email-password",
description="TBD",
ychiucco marked this conversation as resolved.
Show resolved Hide resolved
)


def save_openapi(dest="openapi.json"):
from fractal_server.main import start_application
import json

app = start_application()
openapi_schema = app.openapi()
Expand Down Expand Up @@ -227,31 +197,15 @@ def _slugify_version(raw_version: str) -> str:
current_update_db_data_module.fix_db()


def print_mail_settings(
sender: str,
server: str,
port: int,
instance: str,
skip_starttls: bool,
):
def print_encrypted_password():
from cryptography.fernet import Fernet

password = input(f"Insert email password for sender '{sender}': ")
password = input("Insert email password: ").encode("utf-8")
key = Fernet.generate_key().decode("utf-8")
fractal_mail_settings = json.dumps(
dict(
sender=sender,
password=password,
smtp_server=server,
port=port,
instance_name=instance,
use_starttls=(not skip_starttls),
)
).encode("utf-8")
email_settings = Fernet(key).encrypt(fractal_mail_settings).decode("utf-8")
encrypted_password = Fernet(key).encrypt(password).decode("utf-8")

print(f"\nFRACTAL_EMAIL_SETTINGS: {email_settings}")
print(f"FRACTAL_EMAIL_SETTINGS_KEY: {key}")
print(f"\nFRACTAL_EMAIL_PASSWORD: {encrypted_password}")
ychiucco marked this conversation as resolved.
Show resolved Hide resolved
print(f"FRACTAL_EMAIL_PASSWORD_KEY: {key}")


def run():
Expand All @@ -270,14 +224,8 @@ def run():
port=args.port,
reload=args.reload,
)
elif args.cmd == "email-settings":
print_mail_settings(
sender=args.sender,
server=args.server,
port=args.port,
instance=args.instance,
skip_starttls=args.skip_starttls,
)
elif args.cmd == "encrypt-email-password":
print_encrypted_password()
else:
sys.exit(f"Error: invalid command '{args.cmd}'.")

Expand Down
122 changes: 75 additions & 47 deletions fractal_server/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
# <exact-lab.it> under contract with Liberali Lab from the Friedrich Miescher
# Institute for Biomedical Research and Pelkmans Lab from the University of
# Zurich.
import json
import logging
import shutil
import sys
Expand Down Expand Up @@ -47,6 +46,7 @@
password: Sender password
instance_name: Name of SMTP server instance
use_starttls: Using or not security protocol
use_login: TBD # FIXME
ychiucco marked this conversation as resolved.
Show resolved Hide resolved
"""

sender: EmailStr
Expand All @@ -56,6 +56,7 @@
password: str
instance_name: str
use_starttls: bool
use_login: bool


class FractalConfigurationError(RuntimeError):
Expand Down Expand Up @@ -406,7 +407,7 @@
"""

@root_validator(pre=True)
def check_tasks_python(cls, values) -> None:
def check_tasks_python(cls, values):
"""
Perform multiple checks of the Python-interpreter variables.

Expand All @@ -416,7 +417,6 @@
`sys.executable` and set the corresponding
`FRACTAL_TASKS_PYTHON_X_Y` (and unset all others).
"""

# `FRACTAL_TASKS_PYTHON_X_Y` variables can only be absolute paths
for version in ["3_9", "3_10", "3_11", "3_12"]:
key = f"FRACTAL_TASKS_PYTHON_{version}"
Expand Down Expand Up @@ -575,66 +575,95 @@
###########################################################################
# SMTP SERVICE
###########################################################################
FRACTAL_EMAIL_SETTINGS: Optional[str] = None

FRACTAL_EMAIL_SENDER: Optional[str] = None
ychiucco marked this conversation as resolved.
Show resolved Hide resolved
"""
TBD
"""
FRACTAL_EMAIL_PASSWORD: Optional[str] = None
"""
Encrypted version of settings dictionary, with keys `sender`, `password`,
`smtp_server`, `port`, `instance_name`, `use_starttls`.
TBD
"""
FRACTAL_EMAIL_SETTINGS_KEY: Optional[str] = None
FRACTAL_EMAIL_PASSWORD_KEY: Optional[str] = None
"""
Key value for `cryptography.fernet` decrypt
"""
FRACTAL_EMAIL_SMTP_SERVER: Optional[str] = None
"""
TBD
"""
FRACTAL_EMAIL_PORT: Optional[int] = None
ychiucco marked this conversation as resolved.
Show resolved Hide resolved
"""
TBD
"""
FRACTAL_EMAIL_INSTANCE_NAME: Optional[str] = None
"""
TBD
"""
FRACTAL_EMAIL_RECIPIENTS: Optional[str] = None
"""
List of email receivers, separated with commas
"""
FRACTAL_EMAIL_USE_STARTTLS: Optional[bool] = True
"""
TBD
"""
FRACTAL_EMAIL_USE_LOGIN: Optional[bool] = True
"""
TBD
"""

ychiucco marked this conversation as resolved.
Show resolved Hide resolved
@root_validator(pre=True)
def validate_email_settings(cls, values):
email_values = {k: v for k, v in values.items() if "EMAIL" in k}
ychiucco marked this conversation as resolved.
Show resolved Hide resolved
if email_values:

def assert_key(key: str):
if key not in email_values:
raise ValueError(f"Missing '{key}'")

assert_key("FRACTAL_EMAIL_SENDER")
assert_key("FRACTAL_EMAIL_SMTP_SERVER")
assert_key("FRACTAL_EMAIL_PORT")
assert_key("FRACTAL_EMAIL_INSTANCE_NAME")
assert_key("FRACTAL_EMAIL_RECIPIENTS")
if email_values.get("FRACTAL_EMAIL_USE_LOGIN") is not False and (
ychiucco marked this conversation as resolved.
Show resolved Hide resolved
"FRACTAL_EMAIL_PASSWORD" not in email_values
or "FRACTAL_EMAIL_PASSWORD_KEY" not in email_values
):
raise ValueError(
"'FRACTAL_EMAIL_USE_LOGIN' is True but "
"'FRACTAL_EMAIL_PASSWORD' or 'FRACTAL_EMAIL_PASSWORD_KEY' "
"are provided."
ychiucco marked this conversation as resolved.
Show resolved Hide resolved
)
ychiucco marked this conversation as resolved.
Show resolved Hide resolved
return values

@property
def MAIL_SETTINGS(self) -> Optional[MailSettings]:
if (
self.FRACTAL_EMAIL_SETTINGS is not None
and self.FRACTAL_EMAIL_SETTINGS_KEY is not None
and self.FRACTAL_EMAIL_RECIPIENTS is not None
):
smpt_settings = (
Fernet(self.FRACTAL_EMAIL_SETTINGS_KEY)
.decrypt(self.FRACTAL_EMAIL_SETTINGS)
.decode("utf-8")
)
recipients = self.FRACTAL_EMAIL_RECIPIENTS.split(",")
mail_settings = MailSettings(
**json.loads(smpt_settings), recipients=recipients
)
return mail_settings
elif not all(
[
self.FRACTAL_EMAIL_RECIPIENTS is None,
self.FRACTAL_EMAIL_SETTINGS_KEY is None,
self.FRACTAL_EMAIL_SETTINGS is None,
]
):
raise ValueError(
"You must set all SMPT config variables: "
f"{self.FRACTAL_EMAIL_SETTINGS=}, "
f"{self.FRACTAL_EMAIL_RECIPIENTS=}, "
f"{self.FRACTAL_EMAIL_SETTINGS_KEY=}, "
)
if self.FRACTAL_EMAIL_SENDER is None:
return None

Check notice on line 644 in fractal_server/config.py

View workflow job for this annotation

GitHub Actions / Coverage

Missing coverage

Missing coverage on line 644
password = (
Fernet(self.FRACTAL_EMAIL_PASSWORD_KEY)
.decrypt(self.FRACTAL_EMAIL_PASSWORD)
.decode("utf-8")
)
recipients = self.FRACTAL_EMAIL_RECIPIENTS.split(",")
mail_settings = MailSettings(
sender=self.FRACTAL_EMAIL_SENDER,
password=password,
recipients=recipients,
smtp_server=self.FRACTAL_EMAIL_SMTP_SERVER,
port=self.FRACTAL_EMAIL_PORT,
instance_name=self.FRACTAL_EMAIL_INSTANCE_NAME,
use_starttls=self.FRACTAL_EMAIL_USE_STARTTLS,
use_login=self.FRACTAL_EMAIL_USE_LOGIN,
)
return mail_settings

###########################################################################
# BUSINESS LOGIC
###########################################################################

def check_fractal_mail_settings(self):
"""
Checks that the mail settings are properly set.
"""
try:
self.MAIL_SETTINGS
except Exception as e:
raise FractalConfigurationError(
f"Invalid email configuration settings. Original error: {e}"
)

def check_db(self) -> None:
"""
Checks that db environment variables are properly set.
Expand Down Expand Up @@ -740,7 +769,6 @@

self.check_db()
self.check_runner()
self.check_fractal_mail_settings()

def get_sanitized(self) -> dict:
def _must_be_sanitized(string) -> bool:
Expand Down
2 changes: 1 addition & 1 deletion fractal_server/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def check_settings() -> None:
logger = set_logger("fractal_server_settings")
logger.debug("Fractal Settings:")
for key, value in settings.dict().items():
if any(s in key.upper() for s in ["PASSWORD", "SECRET"]):
if any(s in key.upper() for s in ["PASSWORD", "SECRET", "KEY"]):
value = "*****"
logger.debug(f" {key}: {value}")
reset_logger_handlers(logger)
Expand Down
7 changes: 3 additions & 4 deletions tests/no_version/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,7 @@ def test_email_settings():

cmd = (
'printf "mypassword\n" | '
"poetry run fractalctl email-settings "
"sender@test.org localhost 1234 exact --skip-starttls"
"poetry run fractalctl encrypt-email-password"
)
res = subprocess.run(
cmd,
Expand All @@ -113,6 +112,6 @@ def test_email_settings():
cwd=FRACTAL_SERVER_DIR,
shell=True,
)
assert "FRACTAL_EMAIL_SETTINGS" in res.stdout
assert "FRACTAL_EMAIL_SETTINGS_KEY" in res.stdout
assert "FRACTAL_EMAIL_PASSWORD" in res.stdout
assert "FRACTAL_EMAIL_PASSWORD_KEY" in res.stdout
assert not res.stderr
18 changes: 9 additions & 9 deletions tests/no_version/test_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@

async def test_server_not_available(override_settings_factory, db, caplog):
override_settings_factory(
FRACTAL_EMAIL_SETTINGS=(
"gAAAAABnYvLgoSeECnrXlv1UoP4D_c9Of0xmwMJVopBA3TIDjOvx6YDVfe2ULz8yG"
"r8Ba5Id8rRLjCXa_Ys8iHjvuniJyvsX0mDrc3IGSoofMEeeSCvYEe4iSWLeb_qTNV"
"NPc4IT2-SLB-F7dEvkwzyAFnEm9dVmApd4_lQLm9_wJoS-tz1Q1K8E1_jJSgpfGgw"
"HaINHICVh1UL_qHjIa3DwFvDPvt32tLLBZTL7oN88A8RCmg00ThIZs4HN7OQkvfni"
"nfOiM060Lb-AeNViCVgBX-bIPWZaeQ=="
FRACTAL_EMAIL_PASSWORD=(
"gAAAAABnoQUGHMsDgLkpDtwUtrKtf9T1so44ahEXExGRceAnf097mVY1EbNuMP5fj"
"vkndvwCwBJM7lHoSgKQkZ4VbvO9t3PJZg=="
),
FRACTAL_EMAIL_SETTINGS_KEY=(
"4otDt3R-8p4S97QT0gcUzynCalByypTv01YntqQ9XFk="
FRACTAL_EMAIL_PASSWORD_KEY=(
"lp3j2FVDkzLd0Rklnzg1pHuV9ClCuDE0aGeJfTNCaW4="
),
FRACTAL_EMAIL_RECIPIENTS="test@example.org",
FRACTAL_EMAIL_SENDER="fractal@fractal.fractal",
FRACTAL_EMAIL_SMTP_SERVER="localhost",
FRACTAL_EMAIL_PORT="1234",
FRACTAL_EMAIL_INSTANCE_NAME="fractal",
)
user = UserOAuth(
email="user@example.org",
Expand Down Expand Up @@ -48,6 +49,5 @@ async def test_server_not_available(override_settings_factory, db, caplog):

async with contextlib.asynccontextmanager(get_user_manager)() as um:
await um.on_after_register(user)

assert "ERROR sending notification email" in caplog.text
assert "[Errno 111] Connection refused" in caplog.text
Loading
Loading