Skip to content

Commit

Permalink
stages/email: improve error handling for incorrect template syntax (c…
Browse files Browse the repository at this point in the history
…herry-pick #7758) (#7936)

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

* stages/email: improve error handling for incorrect template syntax



* add tests



---------

Signed-off-by: Jens Langhammer <[email protected]>
Co-authored-by: Jens L <[email protected]>
  • Loading branch information
gcp-cherry-pick-bot[bot] and BeryJu authored Dec 19, 2023
1 parent 88a3eed commit 750669d
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 19 deletions.
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 @@ -104,18 +108,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 @@ -136,7 +149,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")

0 comments on commit 750669d

Please sign in to comment.