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

stages/email: improve error handling for incorrect template syntax #7758

Merged
merged 2 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion authentik/core/api/sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):

managed = ReadOnlyField()
component = SerializerMethodField()
icon = ReadOnlyField(source="get_icon")
icon = ReadOnlyField(source="icon_url")

def get_component(self, obj: Source) -> str:
"""Get object component so that we know how to edit the object"""
Expand Down
6 changes: 5 additions & 1 deletion authentik/flows/stage.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,11 @@ def _get_challenge(self, *args, **kwargs) -> Challenge:
stage_type=self.__class__.__name__, method="get_challenge"
).time(),
):
challenge = self.get_challenge(*args, **kwargs)
try:
challenge = self.get_challenge(*args, **kwargs)
except StageInvalidException as exc:
self.logger.debug("Got StageInvalidException", exc=exc)
return self.executor.stage_invalid()
with Hub.current.start_span(
op="authentik.flow.stage._get_challenge",
description=self.__class__.__name__,
Expand Down
43 changes: 30 additions & 13 deletions authentik/stages/email/stage.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,22 @@
from django.contrib import messages
from django.http import HttpRequest, HttpResponse
from django.http.request import QueryDict
from django.template.exceptions import TemplateSyntaxError
from django.urls import reverse
from django.utils.text import slugify
from django.utils.timezone import now
from django.utils.translation import gettext as _
from rest_framework.fields import CharField
from rest_framework.serializers import ValidationError

from authentik.events.models import Event, EventAction
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
from authentik.flows.exceptions import StageInvalidException
from authentik.flows.models import FlowDesignation, FlowToken
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import QS_KEY_TOKEN, QS_QUERY
from authentik.lib.utils.errors import exception_to_string
from authentik.stages.email.models import EmailStage
from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage
Expand Down Expand Up @@ -103,18 +107,27 @@ def send_email(self):
current_stage: EmailStage = self.executor.current_stage
token = self.get_token()
# Send mail to user
message = TemplateEmailMessage(
subject=_(current_stage.subject),
to=[email],
language=pending_user.locale(self.request),
template_name=current_stage.template,
template_context={
"url": self.get_full_url(**{QS_KEY_TOKEN: token.key}),
"user": pending_user,
"expires": token.expires,
},
)
send_mails(current_stage, message)
try:
message = TemplateEmailMessage(
subject=_(current_stage.subject),
to=[email],
language=pending_user.locale(self.request),
template_name=current_stage.template,
template_context={
"url": self.get_full_url(**{QS_KEY_TOKEN: token.key}),
"user": pending_user,
"expires": token.expires,
},
)
send_mails(current_stage, message)
except TemplateSyntaxError as exc:
Event.new(
EventAction.CONFIGURATION_ERROR,
message=_("Exception occurred while rendering E-mail template"),
error=exception_to_string(exc),
template=current_stage.template,
).from_http(self.request)
raise StageInvalidException from exc

def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
# Check if the user came back from the email link to verify
Expand All @@ -135,7 +148,11 @@ def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
return self.executor.stage_invalid()
# Check if we've already sent the initial e-mail
if PLAN_CONTEXT_EMAIL_SENT not in self.executor.plan.context:
self.send_email()
try:
self.send_email()
except StageInvalidException as exc:
self.logger.debug("Got StageInvalidException", exc=exc)
return self.executor.stage_invalid()
self.executor.plan.context[PLAN_CONTEXT_EMAIL_SENT] = True
return super().get(request, *args, **kwargs)

Expand Down
58 changes: 54 additions & 4 deletions authentik/stages/email/tests/test_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,20 @@
from shutil import rmtree
from tempfile import mkdtemp, mkstemp
from typing import Any
from unittest.mock import PropertyMock, patch

from django.conf import settings
from django.test import TestCase
from django.core.mail.backends.locmem import EmailBackend
from django.urls import reverse

from authentik.stages.email.models import get_template_choices
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.events.models import Event, EventAction
from authentik.flows.markers import StageMarker
from authentik.flows.models import FlowDesignation, FlowStageBinding
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from authentik.flows.tests import FlowTestCase
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.stages.email.models import EmailStage, get_template_choices


def get_templates_setting(temp_dir: str) -> dict[str, Any]:
Expand All @@ -18,11 +27,18 @@ def get_templates_setting(temp_dir: str) -> dict[str, Any]:
return templates_setting


class TestEmailStageTemplates(TestCase):
class TestEmailStageTemplates(FlowTestCase):
"""Email tests"""

def setUp(self) -> None:
self.dir = mkdtemp()
self.dir = Path(mkdtemp())
self.user = create_test_admin_user()

self.flow = create_test_flow(FlowDesignation.AUTHENTICATION)
self.stage = EmailStage.objects.create(
name="email",
)
self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)

def tearDown(self) -> None:
rmtree(self.dir)
Expand All @@ -38,3 +54,37 @@ def test_custom_template(self):
self.assertEqual(len(choices), 3)
unlink(file)
unlink(file2)

def test_custom_template_invalid_syntax(self):
"""Test with custom template"""
with open(self.dir / Path("invalid.html"), "w+", encoding="utf-8") as _invalid:
_invalid.write("{% blocktranslate %}")
with self.settings(TEMPLATES=get_templates_setting(self.dir)):
self.stage.template = "invalid.html"
plan = FlowPlan(
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
)
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()

url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
with patch(
"authentik.stages.email.models.EmailStage.backend_class",
PropertyMock(return_value=EmailBackend),
):
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertStageResponse(
response,
self.flow,
error_message="Unknown error",
)
events = Event.objects.filter(action=EventAction.CONFIGURATION_ERROR)
self.assertEqual(len(events), 1)
event = events.first()
self.assertEqual(
event.context["message"], "Exception occurred while rendering E-mail template"
)
self.assertEqual(event.context["template"], "invalid.html")