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

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

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L 2023-12-04 12:02:39 +02:00 committed by GitHub
parent 8daff472a5
commit 893b8376cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 90 additions and 19 deletions

View File

@ -38,7 +38,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
managed = ReadOnlyField() managed = ReadOnlyField()
component = SerializerMethodField() component = SerializerMethodField()
icon = ReadOnlyField(source="get_icon") icon = ReadOnlyField(source="icon_url")
def get_component(self, obj: Source) -> str: def get_component(self, obj: Source) -> str:
"""Get object component so that we know how to edit the object""" """Get object component so that we know how to edit the object"""

View File

@ -167,7 +167,11 @@ class ChallengeStageView(StageView):
stage_type=self.__class__.__name__, method="get_challenge" stage_type=self.__class__.__name__, method="get_challenge"
).time(), ).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( with Hub.current.start_span(
op="authentik.flow.stage._get_challenge", op="authentik.flow.stage._get_challenge",
description=self.__class__.__name__, description=self.__class__.__name__,

View File

@ -5,6 +5,7 @@ from uuid import uuid4
from django.contrib import messages from django.contrib import messages
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.http.request import QueryDict from django.http.request import QueryDict
from django.template.exceptions import TemplateSyntaxError
from django.urls import reverse from django.urls import reverse
from django.utils.text import slugify from django.utils.text import slugify
from django.utils.timezone import now from django.utils.timezone import now
@ -12,11 +13,14 @@ from django.utils.translation import gettext as _
from rest_framework.fields import CharField from rest_framework.fields import CharField
from rest_framework.serializers import ValidationError from rest_framework.serializers import ValidationError
from authentik.events.models import Event, EventAction
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
from authentik.flows.exceptions import StageInvalidException
from authentik.flows.models import FlowDesignation, FlowToken from authentik.flows.models import FlowDesignation, FlowToken
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import ChallengeStageView from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import QS_KEY_TOKEN, QS_QUERY 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.models import EmailStage
from authentik.stages.email.tasks import send_mails from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage from authentik.stages.email.utils import TemplateEmailMessage
@ -103,18 +107,27 @@ class EmailStageView(ChallengeStageView):
current_stage: EmailStage = self.executor.current_stage current_stage: EmailStage = self.executor.current_stage
token = self.get_token() token = self.get_token()
# Send mail to user # Send mail to user
message = TemplateEmailMessage( try:
subject=_(current_stage.subject), message = TemplateEmailMessage(
to=[email], subject=_(current_stage.subject),
language=pending_user.locale(self.request), to=[email],
template_name=current_stage.template, language=pending_user.locale(self.request),
template_context={ template_name=current_stage.template,
"url": self.get_full_url(**{QS_KEY_TOKEN: token.key}), template_context={
"user": pending_user, "url": self.get_full_url(**{QS_KEY_TOKEN: token.key}),
"expires": token.expires, "user": pending_user,
}, "expires": token.expires,
) },
send_mails(current_stage, message) )
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: def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
# Check if the user came back from the email link to verify # Check if the user came back from the email link to verify
@ -135,7 +148,11 @@ class EmailStageView(ChallengeStageView):
return self.executor.stage_invalid() return self.executor.stage_invalid()
# Check if we've already sent the initial e-mail # Check if we've already sent the initial e-mail
if PLAN_CONTEXT_EMAIL_SENT not in self.executor.plan.context: 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 self.executor.plan.context[PLAN_CONTEXT_EMAIL_SENT] = True
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)

View File

@ -4,11 +4,20 @@ from pathlib import Path
from shutil import rmtree from shutil import rmtree
from tempfile import mkdtemp, mkstemp from tempfile import mkdtemp, mkstemp
from typing import Any from typing import Any
from unittest.mock import PropertyMock, patch
from django.conf import settings 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]: def get_templates_setting(temp_dir: str) -> dict[str, Any]:
@ -18,11 +27,18 @@ def get_templates_setting(temp_dir: str) -> dict[str, Any]:
return templates_setting return templates_setting
class TestEmailStageTemplates(TestCase): class TestEmailStageTemplates(FlowTestCase):
"""Email tests""" """Email tests"""
def setUp(self) -> None: 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: def tearDown(self) -> None:
rmtree(self.dir) rmtree(self.dir)
@ -38,3 +54,37 @@ class TestEmailStageTemplates(TestCase):
self.assertEqual(len(choices), 3) self.assertEqual(len(choices), 3)
unlink(file) unlink(file)
unlink(file2) 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")