root: global email settings (#448)
* root: make global email settings configurable * stages/email: add use_global_settings * stages/email: add test_email command to test email sending * stages/email: update email template * stages/email: simplify email template path * stages/email: add support for user-supplied email templates * stages/email: add tests for sending and templates * stages/email: only add custom template if permissions are correct * docs: add custom email template docs * root: add /templates volume in docker-compose by default * stages/email: fix form not allowing custom templates * stages/email: use relative path for custom templates * stages/email: check if all templates exist on startup, reset * docs: add global email docs for docker-compose * helm: add email config to helm chart * helm: load all secrets with env prefix * helm: move s3 and smtp secret to secret * stages/email: fix test for relative name * stages/email: add argument to send email from existing stage * stages/email: set uid using slug of message id * stages/email: ensure template validation ignores migration runs * docs: add email troubleshooting docs * stages/email: fix long task_name breaking task list
This commit is contained in:
parent
774eb0388b
commit
82bb179bc2
|
@ -38,7 +38,7 @@
|
|||
{% for task in object_list %}
|
||||
<tr role="row">
|
||||
<th role="columnheader">
|
||||
<pre>{{ task.task_name }}</pre>
|
||||
<span>{{ task.html_name|join:"_­" }}</span>
|
||||
</th>
|
||||
<td role="cell">
|
||||
<span>
|
||||
|
|
|
@ -21,6 +21,17 @@ error_reporting:
|
|||
environment: customer
|
||||
send_pii: false
|
||||
|
||||
# Global email settings
|
||||
email:
|
||||
host: localhost
|
||||
port: 25
|
||||
username: ""
|
||||
password: ""
|
||||
use_tls: false
|
||||
use_ssl: false
|
||||
timeout: 10
|
||||
from: authentik@localhost
|
||||
|
||||
outposts:
|
||||
docker_image_base: "beryju/authentik" # this is prepended to -proxy:version
|
||||
|
||||
|
|
|
@ -52,6 +52,11 @@ class TaskInfo:
|
|||
|
||||
task_description: Optional[str] = field(default=None)
|
||||
|
||||
@property
|
||||
def html_name(self) -> list[str]:
|
||||
"""Get task_name, but split on underscores, so we can join in the html template."""
|
||||
return self.task_name.split("_")
|
||||
|
||||
@staticmethod
|
||||
def all() -> Dict[str, "TaskInfo"]:
|
||||
"""Get all TaskInfo objects"""
|
||||
|
|
|
@ -196,7 +196,7 @@ ROOT_URLCONF = "authentik.root.urls"
|
|||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [],
|
||||
"DIRS": ["/templates"],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
|
@ -238,6 +238,18 @@ DATABASES = {
|
|||
}
|
||||
}
|
||||
|
||||
# Email
|
||||
EMAIL_HOST = CONFIG.y("email.host")
|
||||
EMAIL_PORT = int(CONFIG.y("email.port"))
|
||||
EMAIL_HOST_USER = CONFIG.y("email.username")
|
||||
EMAIL_HOST_PASSWORD = CONFIG.y("email.password")
|
||||
EMAIL_USE_TLS = CONFIG.y("email.use_tls")
|
||||
EMAIL_USE_SSL = CONFIG.y("email.use_ssl")
|
||||
EMAIL_TIMEOUT = int(CONFIG.y("email.timeout"))
|
||||
DEFAULT_FROM_EMAIL = CONFIG.y("email.from")
|
||||
SERVER_EMAIL = DEFAULT_FROM_EMAIL
|
||||
EMAIL_SUBJECT_PREFIX = "[authentik] "
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators
|
||||
|
||||
|
|
|
@ -2,18 +2,23 @@
|
|||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.stages.email.models import EmailStage
|
||||
from authentik.stages.email.models import EmailStage, get_template_choices
|
||||
|
||||
|
||||
class EmailStageSerializer(ModelSerializer):
|
||||
"""EmailStage Serializer"""
|
||||
|
||||
def __init__(self, *args, **kwrags):
|
||||
super().__init__(*args, **kwrags)
|
||||
self.fields["template"].choices = get_template_choices()
|
||||
|
||||
class Meta:
|
||||
|
||||
model = EmailStage
|
||||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
"use_global_settings",
|
||||
"host",
|
||||
"port",
|
||||
"username",
|
||||
|
|
|
@ -2,6 +2,12 @@
|
|||
from importlib import import_module
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.db import ProgrammingError
|
||||
from django.template.exceptions import TemplateDoesNotExist
|
||||
from django.template.loader import get_template
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class AuthentikStageEmailConfig(AppConfig):
|
||||
|
@ -13,3 +19,30 @@ class AuthentikStageEmailConfig(AppConfig):
|
|||
|
||||
def ready(self):
|
||||
import_module("authentik.stages.email.tasks")
|
||||
try:
|
||||
self.validate_stage_templates()
|
||||
except ProgrammingError:
|
||||
pass
|
||||
|
||||
def validate_stage_templates(self):
|
||||
"""Ensure all stage's templates actually exist"""
|
||||
from authentik.stages.email.models import EmailStage, EmailTemplates
|
||||
from authentik.events.models import Event, EventAction
|
||||
|
||||
for stage in EmailStage.objects.all():
|
||||
try:
|
||||
get_template(stage.template)
|
||||
except TemplateDoesNotExist:
|
||||
LOGGER.warning(
|
||||
"Stage template does not exist, resetting", path=stage.template
|
||||
)
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
stage=stage,
|
||||
message=(
|
||||
f"Template {stage.template} does not exist, resetting to default."
|
||||
f" (Stage {stage.name})"
|
||||
),
|
||||
).save()
|
||||
stage.template = EmailTemplates.ACCOUNT_CONFIRM
|
||||
stage.save()
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from authentik.stages.email.models import EmailStage
|
||||
from authentik.stages.email.models import EmailStage, get_template_choices
|
||||
|
||||
|
||||
class EmailStageSendForm(forms.Form):
|
||||
|
@ -14,11 +14,17 @@ class EmailStageSendForm(forms.Form):
|
|||
class EmailStageForm(forms.ModelForm):
|
||||
"""Form to create/edit Email Stage"""
|
||||
|
||||
template = forms.ChoiceField(choices=get_template_choices)
|
||||
|
||||
class Meta:
|
||||
|
||||
model = EmailStage
|
||||
fields = [
|
||||
"name",
|
||||
"use_global_settings",
|
||||
"token_expiry",
|
||||
"subject",
|
||||
"template",
|
||||
"host",
|
||||
"port",
|
||||
"username",
|
||||
|
@ -27,9 +33,6 @@ class EmailStageForm(forms.ModelForm):
|
|||
"use_ssl",
|
||||
"timeout",
|
||||
"from_address",
|
||||
"token_expiry",
|
||||
"subject",
|
||||
"template",
|
||||
]
|
||||
widgets = {
|
||||
"name": forms.TextInput(),
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
"""Send a test-email with global settings"""
|
||||
from uuid import uuid4
|
||||
|
||||
from django.core.management.base import BaseCommand, no_translations
|
||||
|
||||
from authentik.stages.email.models import EmailStage
|
||||
from authentik.stages.email.tasks import send_mail
|
||||
from authentik.stages.email.utils import TemplateEmailMessage
|
||||
|
||||
|
||||
class Command(BaseCommand): # pragma: no cover
|
||||
"""Send a test-email with global settings"""
|
||||
|
||||
@no_translations
|
||||
def handle(self, *args, **options):
|
||||
"""Send a test-email with global settings"""
|
||||
delete_stage = False
|
||||
if options["stage"]:
|
||||
stage = EmailStage.objects.get(name=options["stage"])
|
||||
else:
|
||||
stage = EmailStage.objects.create(
|
||||
name=f"temp-global-stage-{uuid4()}", use_global_settings=True
|
||||
)
|
||||
delete_stage = True
|
||||
message = TemplateEmailMessage(
|
||||
subject="authentik Test-Email",
|
||||
template_name="email/setup.html",
|
||||
to=[options["to"]],
|
||||
template_context={},
|
||||
)
|
||||
try:
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
send_mail( # pylint: disable=no-value-for-parameter
|
||||
stage.pk, message.__dict__
|
||||
)
|
||||
finally:
|
||||
if delete_stage:
|
||||
stage.delete()
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("to", type=str)
|
||||
parser.add_argument("-s", "--stage", type=str)
|
|
@ -50,15 +50,15 @@ class Migration(migrations.Migration):
|
|||
models.TextField(
|
||||
choices=[
|
||||
(
|
||||
"stages/email/for_email/password_reset.html",
|
||||
"email/password_reset.html",
|
||||
"Password Reset",
|
||||
),
|
||||
(
|
||||
"stages/email/for_email/account_confirmation.html",
|
||||
"email/account_confirmation.html",
|
||||
"Account Confirmation",
|
||||
),
|
||||
],
|
||||
default="stages/email/for_email/password_reset.html",
|
||||
default="email/password_reset.html",
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
# Generated by Django 3.1.4 on 2021-01-04 13:15
|
||||
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations, models
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
|
||||
def update_template_path(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
EmailStage = apps.get_model("authentik_stages_email", "EmailStage")
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
for stage in EmailStage.objects.using(db_alias).all():
|
||||
stage.template = stage.template.replace("stages/email/for_email/", "email/")
|
||||
stage.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_stages_email", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="emailstage",
|
||||
name="use_global_settings",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="When enabled, global Email connection settings will be used and connection settings below will be ignored.",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(update_template_path),
|
||||
]
|
|
@ -1,6 +1,9 @@
|
|||
"""email stage models"""
|
||||
from os import R_OK, access
|
||||
from pathlib import Path
|
||||
from typing import Type
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.mail import get_connection
|
||||
from django.core.mail.backends.base import BaseEmailBackend
|
||||
from django.db import models
|
||||
|
@ -8,26 +11,60 @@ from django.forms import ModelForm
|
|||
from django.utils.translation import gettext as _
|
||||
from django.views import View
|
||||
from rest_framework.serializers import BaseSerializer
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.flows.models import Stage
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class EmailTemplates(models.TextChoices):
|
||||
"""Templates used for rendering the Email"""
|
||||
|
||||
PASSWORD_RESET = (
|
||||
"stages/email/for_email/password_reset.html",
|
||||
"email/password_reset.html",
|
||||
_("Password Reset"),
|
||||
) # nosec
|
||||
ACCOUNT_CONFIRM = (
|
||||
"stages/email/for_email/account_confirmation.html",
|
||||
"email/account_confirmation.html",
|
||||
_("Account Confirmation"),
|
||||
)
|
||||
|
||||
|
||||
def get_template_choices():
|
||||
"""Get all available Email templates, including dynamically mounted ones.
|
||||
Directories are taken from TEMPLATES.DIR setting"""
|
||||
static_choices = EmailTemplates.choices
|
||||
|
||||
dirs = [Path(x) for x in settings.TEMPLATES[0]["DIRS"]]
|
||||
for template_dir in dirs:
|
||||
if not template_dir.exists():
|
||||
continue
|
||||
for template in template_dir.glob("**/*.html"):
|
||||
path = str(template)
|
||||
if not access(path, R_OK):
|
||||
LOGGER.warning(
|
||||
"Custom template file is not readable, check permissions", path=path
|
||||
)
|
||||
continue
|
||||
rel_path = template.relative_to(template_dir)
|
||||
static_choices.append((str(rel_path), f"Custom Template: {rel_path}"))
|
||||
return static_choices
|
||||
|
||||
|
||||
class EmailStage(Stage):
|
||||
"""Sends an Email to the user with a token to confirm their Email address."""
|
||||
|
||||
use_global_settings = models.BooleanField(
|
||||
default=False,
|
||||
help_text=_(
|
||||
(
|
||||
"When enabled, global Email connection settings will be used and "
|
||||
"connection settings below will be ignored."
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
host = models.TextField(default="localhost")
|
||||
port = models.IntegerField(default=25)
|
||||
username = models.TextField(default="", blank=True)
|
||||
|
@ -42,7 +79,7 @@ class EmailStage(Stage):
|
|||
)
|
||||
subject = models.TextField(default="authentik")
|
||||
template = models.TextField(
|
||||
choices=EmailTemplates.choices, default=EmailTemplates.PASSWORD_RESET
|
||||
choices=get_template_choices(), default=EmailTemplates.PASSWORD_RESET
|
||||
)
|
||||
|
||||
@property
|
||||
|
@ -65,7 +102,9 @@ class EmailStage(Stage):
|
|||
|
||||
@property
|
||||
def backend(self) -> BaseEmailBackend:
|
||||
"""Get fully configured EMail Backend instance"""
|
||||
"""Get fully configured Email Backend instance"""
|
||||
if self.use_global_settings:
|
||||
return get_connection()
|
||||
return get_connection(
|
||||
host=self.host,
|
||||
port=self.port,
|
||||
|
|
|
@ -1,325 +1,285 @@
|
|||
/* -------------------------------------
|
||||
GLOBAL RESETS
|
||||
GLOBAL
|
||||
A very basic CSS reset
|
||||
------------------------------------- */
|
||||
|
||||
/*All the styling goes here*/
|
||||
* {
|
||||
margin: 0;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
img {
|
||||
border: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #fafafa;
|
||||
font-family: sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: none;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
line-height: 1.6em;
|
||||
/* 1.6em * 14px = 22.4px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
|
||||
/*line-height: 22px;*/
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: separate;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
width: 100%; }
|
||||
table td {
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
/* Let's make sure all tables have defaults */
|
||||
table td {
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
/* -------------------------------------
|
||||
BODY & CONTAINER
|
||||
------------------------------------- */
|
||||
------------------------------------- */
|
||||
body {
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
|
||||
.body {
|
||||
background-color: #fafafa;
|
||||
.body-wrap {
|
||||
background-color: #f6f6f6;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
|
||||
.container {
|
||||
display: block;
|
||||
.container {
|
||||
display: block !important;
|
||||
max-width: 600px !important;
|
||||
margin: 0 auto !important;
|
||||
/* makes it centered */
|
||||
max-width: 580px;
|
||||
padding: 10px;
|
||||
width: 580px;
|
||||
}
|
||||
clear: both !important;
|
||||
}
|
||||
|
||||
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||
.content {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
.content {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
max-width: 580px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
HEADER, FOOTER, MAIN
|
||||
------------------------------------- */
|
||||
.main {
|
||||
background: #ffffff;
|
||||
border-radius: 3px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.content-block {
|
||||
padding-bottom: 10px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
/* -------------------------------------
|
||||
HEADER, FOOTER, MAIN
|
||||
------------------------------------- */
|
||||
.main {
|
||||
background-color: #fff;
|
||||
border: 1px solid #e9e9e9;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
clear: both;
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
.content-wrap {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.content-block {
|
||||
padding: 0 0 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
width: 100%;
|
||||
}
|
||||
.footer td,
|
||||
.footer p,
|
||||
.footer span,
|
||||
.footer a {
|
||||
color: #999999;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: 100%;
|
||||
clear: both;
|
||||
color: #999;
|
||||
padding: 20px;
|
||||
}
|
||||
.footer p, .footer a, .footer td {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
TYPOGRAPHY
|
||||
------------------------------------- */
|
||||
h1, h2, h3 {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
|
||||
color: #000;
|
||||
margin: 40px 0 0;
|
||||
line-height: 1.2em;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 32px;
|
||||
font-weight: 500;
|
||||
/* 1.2em * 32px = 38.4px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
|
||||
/*line-height: 38px;*/
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
/* 1.2em * 24px = 28.8px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
|
||||
/*line-height: 29px;*/
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
/* 1.2em * 18px = 21.6px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
|
||||
/*line-height: 22px;*/
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p, ul, ol {
|
||||
margin-bottom: 10px;
|
||||
font-weight: normal;
|
||||
}
|
||||
p li, ul li, ol li {
|
||||
margin-left: 5px;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
LINKS & BUTTONS
|
||||
------------------------------------- */
|
||||
a {
|
||||
color: #348eda;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
text-decoration: none;
|
||||
color: #FFF;
|
||||
background-color: #348eda;
|
||||
border: solid #348eda;
|
||||
border-width: 10px 20px;
|
||||
line-height: 2em;
|
||||
/* 2em * 14px = 28px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
|
||||
/*line-height: 28px;*/
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
border-radius: 5px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
OTHER STYLES THAT MIGHT BE USEFUL
|
||||
------------------------------------- */
|
||||
.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.first {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.aligncenter {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.alignright {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.alignleft {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
ALERTS
|
||||
Change the class depending on warning email, good email or bad email
|
||||
------------------------------------- */
|
||||
.alert {
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
.alert a {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
.alert-brand {
|
||||
background-color: #fd4b2d;
|
||||
}
|
||||
.alert-warning {
|
||||
background-color: #F0AB00;
|
||||
}
|
||||
.alert-danger {
|
||||
background-color: #C9190B;
|
||||
}
|
||||
.alert-success {
|
||||
background-color: #3E8635;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
INVOICE
|
||||
Styles for the billing table
|
||||
------------------------------------- */
|
||||
.invoice {
|
||||
margin: 40px auto;
|
||||
text-align: left;
|
||||
width: 80%;
|
||||
}
|
||||
.invoice td {
|
||||
padding: 5px 0;
|
||||
}
|
||||
.invoice .invoice-items {
|
||||
width: 100%;
|
||||
}
|
||||
.invoice .invoice-items td {
|
||||
border-top: #eee 1px solid;
|
||||
}
|
||||
.invoice .invoice-items .total td {
|
||||
border-top: 2px solid #333;
|
||||
border-bottom: 2px solid #333;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||
------------------------------------- */
|
||||
@media only screen and (max-width: 640px) {
|
||||
body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
TYPOGRAPHY
|
||||
------------------------------------- */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
color: #000000;
|
||||
font-family: sans-serif;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
margin-bottom: 30px;
|
||||
h1, h2, h3, h4 {
|
||||
font-weight: 800 !important;
|
||||
margin: 20px 0 5px !important;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 35px;
|
||||
font-weight: 300;
|
||||
text-align: center;
|
||||
text-transform: capitalize;
|
||||
font-size: 22px !important;
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol {
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
p li,
|
||||
ul li,
|
||||
ol li {
|
||||
list-style-position: inside;
|
||||
margin-left: 5px;
|
||||
h2 {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #06c;
|
||||
border-radius: 3px;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
BUTTONS
|
||||
------------------------------------- */
|
||||
.btn {
|
||||
box-sizing: border-box;
|
||||
width: 100%; }
|
||||
.btn > tbody > tr > td {
|
||||
padding-bottom: 15px; }
|
||||
.btn table {
|
||||
width: auto;
|
||||
}
|
||||
.btn table td {
|
||||
background-color: #ffffff;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
.btn a {
|
||||
background-color: #ffffff;
|
||||
border: solid 1px #06c;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
color: #06c;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
padding: 12px 25px;
|
||||
text-decoration: none;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.btn-primary table td {
|
||||
background-color: #06c;
|
||||
}
|
||||
|
||||
.btn-primary a {
|
||||
background-color: #06c;
|
||||
border-color: #06c;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
OTHER STYLES THAT MIGHT BE USEFUL
|
||||
------------------------------------- */
|
||||
.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.first {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.mt0 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.mb0 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.preheader {
|
||||
color: transparent;
|
||||
display: none;
|
||||
height: 0;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
mso-hide: all;
|
||||
visibility: hidden;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.powered-by a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
border-bottom: 1px solid #fafafa;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||
------------------------------------- */
|
||||
@media only screen and (max-width: 620px) {
|
||||
table[class=body] h1 {
|
||||
font-size: 28px !important;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
table[class=body] p,
|
||||
table[class=body] ul,
|
||||
table[class=body] ol,
|
||||
table[class=body] td,
|
||||
table[class=body] span,
|
||||
table[class=body] a {
|
||||
h3 {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
table[class=body] .wrapper,
|
||||
table[class=body] .article {
|
||||
padding: 10px !important;
|
||||
}
|
||||
table[class=body] .content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
table[class=body] .container {
|
||||
|
||||
.container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .main {
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
table[class=body] .btn table {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .btn a {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .img-responsive {
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
PRESERVE THESE STYLES IN THE HEAD
|
||||
------------------------------------- */
|
||||
@media all {
|
||||
.ExternalClass {
|
||||
width: 100%;
|
||||
.content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
line-height: 100%;
|
||||
}
|
||||
.apple-link a {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
#MessageViewBody a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
.btn-primary table td:hover {
|
||||
background-color: #34495e !important;
|
||||
}
|
||||
.btn-primary a:hover {
|
||||
background-color: #34495e !important;
|
||||
border-color: #34495e !important;
|
||||
|
||||
.content-wrap {
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
.invoice {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=styles.css.map */
|
||||
|
|
|
@ -6,6 +6,7 @@ from typing import Any, Dict, List
|
|||
from celery import group
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.core.mail.utils import DNS_NAME
|
||||
from django.utils.text import slugify
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus
|
||||
|
@ -38,7 +39,7 @@ def send_mail(self: MonitoredTask, email_stage_pk: int, message: Dict[Any, Any])
|
|||
"""Send Email for Email Stage. Retries are scheduled automatically."""
|
||||
self.save_on_success = False
|
||||
message_id = make_msgid(domain=DNS_NAME)
|
||||
self.set_uid(message_id)
|
||||
self.set_uid(slugify(message_id.replace(".", "_").replace("@", "_")))
|
||||
try:
|
||||
stage: EmailStage = EmailStage.objects.get(pk=email_stage_pk)
|
||||
backend = stage.backend
|
||||
|
@ -48,6 +49,7 @@ def send_mail(self: MonitoredTask, email_stage_pk: int, message: Dict[Any, Any])
|
|||
message_object = EmailMultiAlternatives()
|
||||
for key, value in message.items():
|
||||
setattr(message_object, key, value)
|
||||
if not stage.use_global_settings:
|
||||
message_object.from_email = stage.from_address
|
||||
# Because we use the Message-ID as UID for the task, manually assign it
|
||||
message_object.extra_headers["Message-ID"] = message_id
|
||||
|
@ -61,5 +63,6 @@ def send_mail(self: MonitoredTask, email_stage_pk: int, message: Dict[Any, Any])
|
|||
)
|
||||
)
|
||||
except (SMTPException, ConnectionError) as exc:
|
||||
LOGGER.debug("Error sending email, retrying...", exc=exc)
|
||||
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
|
||||
raise exc
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'stages/email/for_email/base.html' %}
|
||||
{% extends 'email/base.html' %}
|
||||
|
||||
{% load authentik_stages_email %}
|
||||
{% load i18n %}
|
|
@ -0,0 +1,34 @@
|
|||
{% load authentik_stages_email %}
|
||||
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title></title>
|
||||
<style>{% inline_static_ascii "stages/email/css/base.css" %}</style>
|
||||
</head>
|
||||
<body itemscope itemtype="http://schema.org/EmailMessage">
|
||||
<table class="body-wrap">
|
||||
<tr>
|
||||
<td></td>
|
||||
<td class="container" width="600">
|
||||
<div class="content">
|
||||
<table class="main" width="100%" cellpadding="0" cellspacing="0" itemprop="action" itemscope itemtype="http://schema.org/ConfirmAction">
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</table>
|
||||
<div class="footer">
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td class="aligncenter content-block">Powered by <a href="https://goauthentik.io">authentik</a>.</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,20 @@
|
|||
{% extends "email/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<tr>
|
||||
<td class="alert alert-brand">
|
||||
{{ title }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="content-wrap">
|
||||
<table width="100%" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td class="content-block">
|
||||
{{ body }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
{% endblock %}
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "stages/email/for_email/base.html" %}
|
||||
{% extends "email/base.html" %}
|
||||
|
||||
{% load authentik_utils %}
|
||||
{% load i18n %}
|
|
@ -0,0 +1,25 @@
|
|||
{% extends "email/base.html" %}
|
||||
|
||||
{% load authentik_stages_email %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<tr>
|
||||
<td class="alert alert-brand">
|
||||
{% trans 'authentik Test-Email' %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="content-wrap">
|
||||
<table width="100%" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td class="content-block">
|
||||
{% blocktrans %}
|
||||
This is a test email to inform you, that you've successfully configured authentik emails.
|
||||
{% endblocktrans %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
{% endblock %}
|
|
@ -1,65 +0,0 @@
|
|||
{% load authentik_stages_email %}
|
||||
{% load authentik_utils %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title></title>
|
||||
<style>{% inline_static_ascii "stages/email/css/base.css" %}</style>
|
||||
</head>
|
||||
|
||||
<body class="">
|
||||
<span class="preheader">
|
||||
{% block pre_header %}
|
||||
{% endblock %}
|
||||
</span>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body">
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td class="container">
|
||||
<div class="content">
|
||||
|
||||
<!-- START CENTERED WHITE CONTAINER -->
|
||||
<table role="presentation" class="main">
|
||||
<img src="{% inline_static_binary config.authentik.branding.logo %}" alt="">
|
||||
<!-- START MAIN CONTENT AREA -->
|
||||
<tr>
|
||||
<td class="wrapper">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- END MAIN CONTENT AREA -->
|
||||
</table>
|
||||
<!-- END CENTERED WHITE CONTAINER -->
|
||||
|
||||
<!-- START FOOTER -->
|
||||
<div class="footer">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td class="content-block powered-by">
|
||||
Powered by <a href="https://beryju.github.io/authentik/">authentik</a>.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!-- END FOOTER -->
|
||||
|
||||
</div>
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,26 +0,0 @@
|
|||
{% extends "stages/email/for_email/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<tr>
|
||||
<td bgcolor="#3625b7" align="center" style="padding: 0px 10px 0px 10px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="480">
|
||||
<tr>
|
||||
<td bgcolor="#566572" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #8F9BA3; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; letter-spacing: 4px; line-height: 48px;">
|
||||
<h1 style="font-size: 32px; font-weight: 400; margin: 0; color: #E9ECEF;">{{ title }}!</h1>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td bgcolor="#1b2a32" align="center" style="padding: 0px 10px 0px 10px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="480">
|
||||
<tr>
|
||||
<td bgcolor="#566572" align="left" style="padding: 20px 30px 40px 30px; color: #E9ECEF; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;">
|
||||
<p style="margin: 0;">{{ body }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
{% endblock %}
|
|
@ -0,0 +1,83 @@
|
|||
"""email tests"""
|
||||
from smtplib import SMTPException
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from django.core import mail
|
||||
from django.core.mail.backends.locmem import EmailBackend
|
||||
from django.shortcuts import reverse
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.flows.markers import StageMarker
|
||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||
from authentik.flows.views import SESSION_KEY_PLAN
|
||||
from authentik.stages.email.models import EmailStage
|
||||
|
||||
|
||||
class TestEmailStageSending(TestCase):
|
||||
"""Email tests"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user = User.objects.create_user(
|
||||
username="unittest", email="test@beryju.org"
|
||||
)
|
||||
self.client = Client()
|
||||
|
||||
self.flow = Flow.objects.create(
|
||||
name="test-email",
|
||||
slug="test-email",
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
)
|
||||
self.stage = EmailStage.objects.create(
|
||||
name="email",
|
||||
)
|
||||
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
|
||||
|
||||
def test_pending_user(self):
|
||||
"""Test with pending user"""
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
|
||||
url = reverse(
|
||||
"authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
||||
)
|
||||
with self.settings(
|
||||
EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend"
|
||||
):
|
||||
response = self.client.post(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertEqual(mail.outbox[0].subject, "authentik")
|
||||
|
||||
def test_send_error(self):
|
||||
"""Test error during sending (sending will be retried)"""
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
|
||||
url = reverse(
|
||||
"authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
||||
)
|
||||
with self.settings(
|
||||
EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend"
|
||||
):
|
||||
with patch(
|
||||
"django.core.mail.backends.locmem.EmailBackend.send_messages",
|
||||
MagicMock(side_effect=[SMTPException, EmailBackend.send_messages]),
|
||||
):
|
||||
response = self.client.post(url)
|
||||
response = self.client.post(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(len(mail.outbox) >= 1)
|
||||
self.assertEqual(mail.outbox[0].subject, "authentik")
|
|
@ -87,6 +87,14 @@ class TestEmailStage(TestCase):
|
|||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertEqual(mail.outbox[0].subject, "authentik")
|
||||
|
||||
def test_use_global_settings(self):
|
||||
"""Test use_global_settings"""
|
||||
host = "some-unique-string"
|
||||
with self.settings(
|
||||
EMAIL_HOST=host, EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend"
|
||||
):
|
||||
self.assertEqual(EmailStage(use_global_settings=True).backend.host, host)
|
||||
|
||||
def test_token(self):
|
||||
"""Test with token"""
|
||||
# Make sure token exists
|
|
@ -0,0 +1,28 @@
|
|||
"""email tests"""
|
||||
from os import unlink
|
||||
from pathlib import Path
|
||||
from tempfile import gettempdir, mkstemp
|
||||
from typing import Any
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.stages.email.models import get_template_choices
|
||||
|
||||
|
||||
def get_templates_setting(temp_dir: str) -> dict[str, Any]:
|
||||
"""Patch settings TEMPLATE's dir property"""
|
||||
templates_setting = settings.TEMPLATES
|
||||
templates_setting[0]["DIRS"] = [temp_dir]
|
||||
return templates_setting
|
||||
|
||||
|
||||
class TestEmailStageTemplates(TestCase):
|
||||
"""Email tests"""
|
||||
|
||||
def test_custom_template(self):
|
||||
"""Test with custom template"""
|
||||
with self.settings(TEMPLATES=get_templates_setting(gettempdir())):
|
||||
_, file = mkstemp(suffix=".html")
|
||||
self.assertEqual(get_template_choices()[-1][0], Path(file).name)
|
||||
unlink(file)
|
|
@ -29,6 +29,7 @@ services:
|
|||
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||
volumes:
|
||||
- ./media:/media
|
||||
- ./custom-templates:/templates
|
||||
ports:
|
||||
- 8000
|
||||
networks:
|
||||
|
@ -57,6 +58,7 @@ services:
|
|||
volumes:
|
||||
- ./backups:/backups
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./custom-templates:/templates
|
||||
env_file:
|
||||
- .env
|
||||
static:
|
||||
|
|
|
@ -14,6 +14,14 @@
|
|||
| config.errorReporting.environment | customer | Environment sent with the error reporting |
|
||||
| config.errorReporting.sendPii | false | Whether to send Personally-identifiable data with the error reporting |
|
||||
| config.logLevel | warning | Log level of authentik |
|
||||
| config.email.host | localhost | SMTP Host Emails are sent to |
|
||||
| config.email.port | 25 | SMTP Port Emails are sent to |
|
||||
| config.email.username | | SMTP Username |
|
||||
| config.email.password | | SMTP Password |
|
||||
| config.email.use_tls | false | Enable StartTLS |
|
||||
| config.email.use_ssl | false | Enable SSL |
|
||||
| config.email.timeout | 10 | SMTP Timeout |
|
||||
| config.email.from | authentik@localhost | Email address authentik will send from, should have a correct @domain |
|
||||
| backup.accessKey | | Optionally enable S3 Backup, Access Key |
|
||||
| backup.secretKey | | Optionally enable S3 Backup, Secret Key |
|
||||
| backup.bucket | | Optionally enable S3 Backup, Bucket |
|
||||
|
|
|
@ -8,7 +8,6 @@ data:
|
|||
POSTGRESQL__USER: "{{ .Values.postgresql.postgresqlUsername }}"
|
||||
{{- if .Values.backup }}
|
||||
POSTGRESQL__S3_BACKUP__ACCESS_KEY: "{{ .Values.backup.accessKey }}"
|
||||
POSTGRESQL__S3_BACKUP__SECRET_KEY: "{{ .Values.backup.secretKey }}"
|
||||
POSTGRESQL__S3_BACKUP__BUCKET: "{{ .Values.backup.bucket }}"
|
||||
POSTGRESQL__S3_BACKUP__REGION: "{{ .Values.backup.region }}"
|
||||
POSTGRESQL__S3_BACKUP__HOST: "{{ .Values.backup.host }}"
|
||||
|
@ -19,3 +18,10 @@ data:
|
|||
ERROR_REPORTING__SEND_PII: "{{ .Values.config.errorReporting.sendPii }}"
|
||||
LOG_LEVEL: "{{ .Values.config.logLevel }}"
|
||||
OUTPOSTS__DOCKER_IMAGE_BASE: "{{ .Values.image.name_outposts }}"
|
||||
EMAIL__HOST: "{{ .Values.config.email.host }}"
|
||||
EMAIL__PORT: "{{ .Values.config.email.port }}"
|
||||
EMAIL__USERNAM: "{{ .Values.config.email.username }}"
|
||||
EMAIL__USE_TLS: "{{ .Values.config.email.use_tls }}"
|
||||
EMAIL__USE_SSL: "{{ .Values.config.email.use_ssl }}"
|
||||
EMAIL__TIMEOUT: "{{ .Values.config.email.timeout }}"
|
||||
EMAIL__FROM: "{{ .Values.config.email.from }}"
|
||||
|
|
|
@ -6,7 +6,11 @@ metadata:
|
|||
data:
|
||||
monitoring_username: bW9uaXRvcg== # monitor in base64
|
||||
{{- if .Values.config.secretKey }}
|
||||
secret_key: {{ .Values.config.secretKey | b64enc | quote }}
|
||||
SECRET_KEY: {{ .Values.config.secretKey | b64enc | quote }}
|
||||
{{- else }}
|
||||
secret_key: {{ randAlphaNum 50 | b64enc | quote}}
|
||||
SECRET_KEY: {{ randAlphaNum 50 | b64enc | quote}}
|
||||
{{- end }}
|
||||
{{- if .Values.backup }}
|
||||
POSTGRESQL__S3_BACKUP__SECRET_KEY: "{{ .Values.backup.secretKey }}"
|
||||
{{- end}}
|
||||
EMAIL__PASSWOR: "{{ .Values.config.email.password }}"
|
||||
|
|
|
@ -51,12 +51,10 @@ spec:
|
|||
- configMapRef:
|
||||
name: {{ include "authentik.fullname" . }}-config
|
||||
prefix: AUTHENTIK_
|
||||
env:
|
||||
- name: AUTHENTIK_SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
- secretRef:
|
||||
name: {{ include "authentik.fullname" . }}-secret-key
|
||||
key: secret_key
|
||||
prefix: AUTHENTIK_
|
||||
env:
|
||||
- name: AUTHENTIK_REDIS__PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
|
|
|
@ -54,12 +54,10 @@ spec:
|
|||
- configMapRef:
|
||||
name: "{{ include "authentik.fullname" . }}-config"
|
||||
prefix: "AUTHENTIK_"
|
||||
- secretRef:
|
||||
name: {{ include "authentik.fullname" . }}-secret-key
|
||||
prefix: AUTHENTIK_
|
||||
env:
|
||||
- name: AUTHENTIK_SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "{{ include "authentik.fullname" . }}-secret-key"
|
||||
key: secret_key
|
||||
- name: AUTHENTIK_REDIS__PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
|
|
|
@ -25,6 +25,21 @@ config:
|
|||
# Log level used by web and worker
|
||||
# Can be either debug, info, warning, error
|
||||
logLevel: warning
|
||||
# Global Email settings
|
||||
email:
|
||||
# SMTP Host Emails are sent to
|
||||
host: localhost
|
||||
port: 25
|
||||
# Optionally authenticate
|
||||
username: ""
|
||||
password: ""
|
||||
# Use StartTLS
|
||||
useTls: false
|
||||
# Use SSL
|
||||
useSsl: false
|
||||
timeout: 10
|
||||
# Email address authentik will send from, should have a correct @domain
|
||||
from: authentik@localhost
|
||||
|
||||
# Enable Database Backups to S3
|
||||
# backup:
|
||||
|
|
|
@ -8579,6 +8579,11 @@ definitions:
|
|||
title: Name
|
||||
type: string
|
||||
minLength: 1
|
||||
use_global_settings:
|
||||
title: Use global settings
|
||||
description: When enabled, global Email connection settings will be used and
|
||||
connection settings below will be ignored.
|
||||
type: boolean
|
||||
host:
|
||||
title: Host
|
||||
type: string
|
||||
|
@ -8625,8 +8630,8 @@ definitions:
|
|||
title: Template
|
||||
type: string
|
||||
enum:
|
||||
- stages/email/for_email/password_reset.html
|
||||
- stages/email/for_email/account_confirmation.html
|
||||
- email/password_reset.html
|
||||
- email/account_confirmation.html
|
||||
IdentificationStage:
|
||||
description: IdentificationStage Serializer
|
||||
required:
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 83 KiB |
|
@ -5,3 +5,19 @@ title: Email stage
|
|||
This stage can be used for email verification. authentik's background worker will send an email using the specified connection details. When an email can't be delivered, delivery is automatically retried periodically.
|
||||
|
||||
![](email-recovery.png)
|
||||
|
||||
## Custom Templates
|
||||
|
||||
You can also use custom email templates, to use your own design or layout.
|
||||
|
||||
Place any custom templates in the `custom-templates` Folder, which is in the same folder as your docker-compose file. Afterwards, you'll be able to select the template when creating/editing an Email stage.
|
||||
|
||||
:::info
|
||||
This is currently only supported for docker-compose installs, and supported starting version 0.15.
|
||||
:::
|
||||
|
||||
:::info
|
||||
If you've add the line and created a file, and can't see if, check the logs using `docker-compose logs -f worker`.
|
||||
:::
|
||||
|
||||
![](custom-template.png)
|
||||
|
|
|
@ -9,7 +9,7 @@ authentik is an open-source Identity Provider focused on flexibility and versati
|
|||
|
||||
## Installation
|
||||
|
||||
See [Docker-compose](installation/docker-compose.md) or [Kubernetes](installation/kubernetes.md)
|
||||
See [Docker-compose](installation/docker-compose) or [Kubernetes](installation/kubernetes)
|
||||
|
||||
## Screenshots
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ This installation method is for test-setups and small-scale productive setups.
|
|||
- docker
|
||||
- docker-compose
|
||||
|
||||
## Install
|
||||
## Preparation
|
||||
|
||||
Download the latest `docker-compose.yml` from [here](https://raw.githubusercontent.com/BeryJu/authentik/master/docker-compose.yml). Place it in a directory of your choice.
|
||||
|
||||
|
@ -25,6 +25,30 @@ echo "PG_PASS=$(pwgen 40 1)" >> .env
|
|||
echo "AUTHENTIK_SECRET_KEY=$(pwgen 50 1)" >> .env
|
||||
```
|
||||
|
||||
## Email configuration (optional, but recommended)
|
||||
|
||||
It is also recommended to configure global email credentials. These are used by authentik to notify you about alerts, configuration issues. They can also be used by [Email stages](flow/stages/email/index.md) to send verification/recovery emails.
|
||||
|
||||
Append this block to your `.env` file
|
||||
|
||||
```
|
||||
# SMTP Host Emails are sent to
|
||||
AUTHENTIK_EMAIL__HOST=localhost
|
||||
AUTHENTIK_EMAIL__PORT=25
|
||||
# Optionally authenticate
|
||||
AUTHENTIK_EMAIL__USERNAME=""
|
||||
AUTHENTIK_EMAIL__PASSWORD=""
|
||||
# Use StartTLS
|
||||
AUTHENTIK_EMAIL__USE_TLS=false
|
||||
# Use SSL
|
||||
AUTHENTIK_EMAIL__USE_SSL=false
|
||||
AUTHENTIK_EMAIL__TIMEOUT=10
|
||||
# Email address authentik will send from, should have a correct @domain
|
||||
AUTHENTIK_EMAIL__FROM=authentik@localhost
|
||||
```
|
||||
|
||||
## Startup
|
||||
|
||||
Afterwards, run these commands to finish
|
||||
|
||||
```
|
||||
|
@ -39,8 +63,6 @@ If you plan to use this setup for production, it is also advised to change the P
|
|||
|
||||
Now you can pull the Docker images needed by running `docker-compose pull`. After this has finished, run `docker-compose up -d` to start authentik.
|
||||
|
||||
authentik will then be reachable via HTTP on port 80, and HTTPS on port 443. You can optionally configure the packaged traefik to use Let's Encrypt certificates for TLS Encryption.
|
||||
|
||||
If you plan to access authentik via a reverse proxy which does SSL Termination, make sure you use the HTTPS port, so authentik is aware of the SSL connection.
|
||||
authentik will then be reachable HTTPS on port 443. You can optionally configure the packaged traefik to use Let's Encrypt certificates for TLS Encryption.
|
||||
|
||||
The initial setup process also creates a default admin user, the username and password for which is `akadmin`. It is highly recommended to change this password as soon as you log in.
|
||||
|
|
|
@ -14,6 +14,8 @@ helm install authentik/authentik --devel -f values.yaml
|
|||
|
||||
This installation automatically applies database migrations on startup. After the installation is done, you can use `akadmin` as username and password.
|
||||
|
||||
It is also recommended to configure global email credentials. These are used by authentik to notify you about alerts, configuration issues. They can also be used by [Email stages](flow/stages/email/index.md) to send verification/recovery emails.
|
||||
|
||||
```yaml
|
||||
###################################
|
||||
# Values directly affecting authentik
|
||||
|
@ -41,6 +43,21 @@ config:
|
|||
# Log level used by web and worker
|
||||
# Can be either debug, info, warning, error
|
||||
logLevel: warning
|
||||
# Global Email settings
|
||||
email:
|
||||
# SMTP Host Emails are sent to
|
||||
host: localhost
|
||||
port: 25
|
||||
# Optionally authenticate
|
||||
username: ""
|
||||
password: ""
|
||||
# Use StartTLS
|
||||
useTls: false
|
||||
# Use SSL
|
||||
useSsl: false
|
||||
timeout: 10
|
||||
# Email address authentik will send from, should have a correct @domain
|
||||
from: authentik@localhost
|
||||
|
||||
# Enable Database Backups to S3
|
||||
# backup:
|
||||
|
@ -80,6 +97,4 @@ redis:
|
|||
master:
|
||||
persistence:
|
||||
enabled: false
|
||||
# https://stackoverflow.com/a/59189742
|
||||
disableCommands: []
|
||||
```
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
title: Troubleshooting Email sending
|
||||
---
|
||||
|
||||
To test if an email stage, or the global email settings are configured correctly, you can run the following command:
|
||||
|
||||
````
|
||||
./manage.py test_email <to address> [-s <stage name>]
|
||||
```
|
||||
|
||||
If you omit the `-s` parameter, the email will be sent using the global settings. Otherwise, the settings of the specified stage will be used.
|
||||
|
||||
To run this command with docker-compose, use
|
||||
|
||||
```
|
||||
docker-compose exec -it worker ./manage.py test_email [...]
|
||||
```
|
||||
|
||||
To run this command with Kubernetes, use
|
||||
|
||||
```
|
||||
kubectl exec -it authentik-worker-xxxxx -- ./manage.py test_email [...]
|
||||
```
|
|
@ -135,7 +135,10 @@ module.exports = {
|
|||
{
|
||||
type: "category",
|
||||
label: "Troubleshooting",
|
||||
items: ["troubleshooting/access"],
|
||||
items: [
|
||||
"troubleshooting/access",
|
||||
"troubleshooting/emails",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
|
|
|
@ -99,7 +99,7 @@
|
|||
"from_address": "system@authentik.local",
|
||||
"token_expiry": 30,
|
||||
"subject": "authentik",
|
||||
"template": "stages/email/for_email/account_confirmation.html"
|
||||
"template": "email/account_confirmation.html"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -92,7 +92,7 @@
|
|||
"from_address": "system@authentik.local",
|
||||
"token_expiry": 30,
|
||||
"subject": "authentik",
|
||||
"template": "stages/email/for_email/password_reset.html"
|
||||
"template": "email/password_reset.html"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
Reference in New Issue