Skip to content

Commit

Permalink
Migrate ext::auth::SMTPConfig -> cfg::EmailProvider (#7942)
Browse files Browse the repository at this point in the history
This will provide multiple choices for email providers allowing
switching
between providers. Moves the SMTP config into the main config namespace
to allow for instance-wide configuration.

---------

Co-authored-by: Michael J. Sullivan <[email protected]>
  • Loading branch information
scotttrinh and msullivan authored Nov 21, 2024
1 parent 26fb658 commit c037ac1
Show file tree
Hide file tree
Showing 13 changed files with 533 additions and 352 deletions.
75 changes: 75 additions & 0 deletions edb/lib/cfg.edgeql
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,71 @@ CREATE TYPE cfg::Auth EXTENDING cfg::ConfigObject {
};
};

CREATE SCALAR TYPE cfg::SMTPSecurity EXTENDING enum<
PlainText,
TLS,
STARTTLS,
STARTTLSOrPlainText,
>;

CREATE ABSTRACT TYPE cfg::EmailProviderConfig EXTENDING cfg::ConfigObject {
CREATE REQUIRED PROPERTY name -> std::str {
CREATE CONSTRAINT std::exclusive;
CREATE ANNOTATION std::description :=
"The name of the email provider.";
};
};

CREATE TYPE cfg::SMTPProviderConfig EXTENDING cfg::EmailProviderConfig {
CREATE PROPERTY sender -> std::str {
CREATE ANNOTATION std::description :=
"\"From\" address of system emails sent for e.g. \
password reset, etc.";
};
CREATE PROPERTY host -> std::str {
CREATE ANNOTATION std::description :=
"Host of SMTP server to use for sending emails. \
If not set, \"localhost\" will be used.";
};
CREATE PROPERTY port -> std::int32 {
CREATE ANNOTATION std::description :=
"Port of SMTP server to use for sending emails. \
If not set, common defaults will be used depending on security: \
465 for TLS, 587 for STARTTLS, 25 otherwise.";
};
CREATE PROPERTY username -> std::str {
CREATE ANNOTATION std::description :=
"Username to login as after connected to SMTP server.";
};
CREATE PROPERTY password -> std::str {
SET secret := true;
CREATE ANNOTATION std::description :=
"Password for login after connected to SMTP server.";
};
CREATE REQUIRED PROPERTY security -> cfg::SMTPSecurity {
SET default := cfg::SMTPSecurity.STARTTLSOrPlainText;
CREATE ANNOTATION std::description :=
"Security mode of the connection to SMTP server. \
By default, initiate a STARTTLS upgrade if supported by the \
server, or fallback to PlainText.";
};
CREATE REQUIRED PROPERTY validate_certs -> std::bool {
SET default := true;
CREATE ANNOTATION std::description :=
"Determines if SMTP server certificates are validated.";
};
CREATE REQUIRED PROPERTY timeout_per_email -> std::duration {
SET default := <std::duration>'60 seconds';
CREATE ANNOTATION std::description :=
"Maximum time to send an email, including retry attempts.";
};
CREATE REQUIRED PROPERTY timeout_per_attempt -> std::duration {
SET default := <std::duration>'15 seconds';
CREATE ANNOTATION std::description :=
"Maximum time for each SMTP request.";
};
};

CREATE ABSTRACT TYPE cfg::AbstractConfig extending cfg::ConfigObject;

CREATE ABSTRACT TYPE cfg::ExtensionConfig EXTENDING cfg::ConfigObject {
Expand Down Expand Up @@ -158,6 +223,16 @@ ALTER TYPE cfg::AbstractConfig {
CREATE ANNOTATION cfg::system := 'true';
};

CREATE MULTI LINK email_providers -> cfg::EmailProviderConfig {
CREATE ANNOTATION std::description :=
'The list of email providers that can be used to send emails.';
};

CREATE PROPERTY current_email_provider_name -> std::str {
CREATE ANNOTATION std::description :=
'The name of the current email provider.';
};

CREATE PROPERTY allow_dml_in_functions -> std::bool {
SET default := false;
CREATE ANNOTATION cfg::affects_compilation := 'true';
Expand Down
52 changes: 0 additions & 52 deletions edb/lib/ext/auth.edgeql
Original file line number Diff line number Diff line change
Expand Up @@ -469,58 +469,6 @@ CREATE EXTENSION PACKAGE auth VERSION '1.0' {
};
};

create scalar type ext::auth::SMTPSecurity extending enum<PlainText, TLS, STARTTLS, STARTTLSOrPlainText>;

create type ext::auth::SMTPConfig extending cfg::ExtensionConfig {
create property sender: std::str {
create annotation std::description :=
"\"From\" address of system emails sent for e.g. \
password reset, etc.";
};
create property host: std::str {
create annotation std::description :=
"Host of SMTP server to use for sending emails. \
If not set, \"localhost\" will be used.";
};
create property port: std::int32 {
create annotation std::description :=
"Port of SMTP server to use for sending emails. \
If not set, common defaults will be used depending on security: \
465 for TLS, 587 for STARTTLS, 25 otherwise.";
};
create property username: std::str {
create annotation std::description :=
"Username to login as after connected to SMTP server.";
};
create property password: std::str {
set secret := true;
create annotation std::description :=
"Password for login after connected to SMTP server.";
};
create required property security: ext::auth::SMTPSecurity {
set default := ext::auth::SMTPSecurity.STARTTLSOrPlainText;
create annotation std::description :=
"Security mode of the connection to SMTP server. \
By default, initiate a STARTTLS upgrade if supported by the \
server, or fallback to PlainText.";
};
create required property validate_certs: std::bool {
set default := true;
create annotation std::description :=
"Determines if SMTP server certificates are validated.";
};
create required property timeout_per_email: std::duration {
set default := <std::duration>'60 seconds';
create annotation std::description :=
"Maximum time to send an email, including retry attempts.";
};
create required property timeout_per_attempt: std::duration {
set default := <std::duration>'15 seconds';
create annotation std::description :=
"Maximum time for each SMTP request.";
};
};

create function ext::auth::signing_key_exists() -> std::bool {
using (
select exists cfg::Config.extensions[is ext::auth::AuthConfig]
Expand Down
81 changes: 80 additions & 1 deletion edb/server/compiler/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -927,6 +927,62 @@ def describe_database_dump(
blocks=descriptors,
)

def _reprocess_restore_config(
self,
stmts: list[qlast.Base],
) -> list[qlast.Base]:
'''Do any rewrites to the restore script needed.
This is intended to patch over certain backwards incompatible
changes to config. We try not to do that too much, but when we
do, dumps still need to work.
'''

new_stmts = []
smtp_config = {}

for stmt in stmts:
# ext::auth::SMTPConfig got removed and moved into a cfg
# object, so intercept those and rewrite them.
if (
isinstance(stmt, qlast.ConfigSet)
and stmt.name.module == 'ext::auth::SMTPConfig'
):
smtp_config[stmt.name.name] = stmt.expr
else:
new_stmts.append(stmt)

if smtp_config:
# Do the rewrite of SMTPConfig
smtp_config['name'] = qlast.Constant.string('_default')

new_stmts.append(
qlast.ConfigInsert(
scope=qltypes.ConfigScope.DATABASE,
name=qlast.ObjectRef(
module='cfg', name='SMTPProviderConfig'
),
shape=[
qlast.ShapeElement(
expr=qlast.Path(steps=[qlast.Ptr(name=name)]),
compexpr=expr,
)
for name, expr in smtp_config.items()
],
)
)
new_stmts.append(
qlast.ConfigSet(
scope=qltypes.ConfigScope.DATABASE,
name=qlast.ObjectRef(
name='current_email_provider_name'
),
expr=qlast.Constant.string('_default'),
)
)

return new_stmts

def describe_database_restore(
self,
user_schema_pickle: bytes,
Expand Down Expand Up @@ -1006,7 +1062,11 @@ def describe_database_restore(

# The state serializer generated below is somehow inappropriate,
# so it's simply ignored here and the I/O process will do it on its own
units = compile(ctx=ctx, source=ddl_source).units
statements = edgeql.parse_block(ddl_source)
statements = self._reprocess_restore_config(statements)
units = _try_compile_ast(
ctx=ctx, source=ddl_source, statements=statements
).units

_check_force_database_error(ctx, scope='restore')

Expand Down Expand Up @@ -2504,6 +2564,25 @@ def _try_compile(
time.sleep(float(text[len(sentinel):text.index("\n")]))

statements = edgeql.parse_block(source)
return _try_compile_ast(statements=statements, source=source, ctx=ctx)


def _try_compile_ast(
*,
ctx: CompileContext,
statements: list[qlast.Base],
source: edgeql.Source,
) -> dbstate.QueryUnitGroup:
if _get_config_val(ctx, '__internal_testmode'):
# This is a bad but simple way to emulate a slow compilation for tests.
# Ideally, we should have a testmode function that is hooked to sleep
# as `simple_special_case`, or wait for a notification from the test.
sentinel = "# EDGEDB_TEST_COMPILER_SLEEP = "
text = source.text()
if text.startswith(sentinel):
time.sleep(float(text[len(sentinel):text.index("\n")]))

default_cardinality = enums.Cardinality.NO_RESULT
statements_len = len(statements)

if not len(statements): # pragma: no cover
Expand Down
28 changes: 8 additions & 20 deletions edb/server/protocol/auth_ext/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import random

from typing import Any, Coroutine
from edb.server import tenant
from edb.server import tenant, smtp

from . import util, ui, smtp
from . import util, ui


async def send_password_reset_email(
Expand All @@ -15,7 +15,6 @@ async def send_password_reset_email(
reset_url: str,
test_mode: bool,
) -> None:
from_addr = util.get_config(db, "ext::auth::SMTPConfig::sender")
app_details_config = util.get_app_details_config(db)
if app_details_config is None:
email_args = {}
Expand All @@ -27,16 +26,13 @@ async def send_password_reset_email(
brand_color=app_details_config.brand_color,
)
msg = ui.render_password_reset_email(
from_addr=from_addr,
to_addr=to_addr,
reset_url=reset_url,
**email_args,
)
coro = smtp.send_email(
db,
smtp_provider = smtp.SMTP(db)
coro = smtp_provider.send(
msg,
sender=from_addr,
recipients=to_addr,
test_mode=test_mode,
)
await _protected_send(coro, tenant)
Expand All @@ -51,7 +47,6 @@ async def send_verification_email(
provider: str,
test_mode: bool,
) -> None:
from_addr = util.get_config(db, "ext::auth::SMTPConfig::sender")
app_details_config = util.get_app_details_config(db)
verification_token_params = urllib.parse.urlencode(
{
Expand All @@ -71,16 +66,13 @@ async def send_verification_email(
brand_color=app_details_config.brand_color,
)
msg = ui.render_verification_email(
from_addr=from_addr,
to_addr=to_addr,
verify_url=verify_url,
**email_args,
)
coro = smtp.send_email(
db,
smtp_provider = smtp.SMTP(db)
coro = smtp_provider.send(
msg,
sender=from_addr,
recipients=to_addr,
test_mode=test_mode,
)
await _protected_send(coro, tenant)
Expand All @@ -93,7 +85,6 @@ async def send_magic_link_email(
link: str,
test_mode: bool,
) -> None:
from_addr = util.get_config(db, "ext::auth::SMTPConfig::sender")
app_details_config = util.get_app_details_config(db)
if app_details_config is None:
email_args = {}
Expand All @@ -105,16 +96,13 @@ async def send_magic_link_email(
brand_color=app_details_config.brand_color,
)
msg = ui.render_magic_link_email(
from_addr=from_addr,
to_addr=to_addr,
link=link,
**email_args,
)
coro = smtp.send_email(
db,
smtp_provider = smtp.SMTP(db)
coro = smtp_provider.send(
msg,
sender=from_addr,
recipients=to_addr,
test_mode=test_mode,
)
await _protected_send(coro, tenant)
Expand Down
Loading

0 comments on commit c037ac1

Please sign in to comment.