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 <jens@goauthentik.io> Co-authored-by: Jens L <jens@goauthentik.io>
This commit is contained in:
parent
88a3eed67e
commit
750669dcab
|
@ -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"""
|
||||||
|
|
|
@ -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(),
|
||||||
):
|
):
|
||||||
|
try:
|
||||||
challenge = self.get_challenge(*args, **kwargs)
|
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__,
|
||||||
|
|
|
@ -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
|
||||||
|
@ -104,6 +108,7 @@ 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
|
||||||
|
try:
|
||||||
message = TemplateEmailMessage(
|
message = TemplateEmailMessage(
|
||||||
subject=_(current_stage.subject),
|
subject=_(current_stage.subject),
|
||||||
to=[email],
|
to=[email],
|
||||||
|
@ -116,6 +121,14 @@ class EmailStageView(ChallengeStageView):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
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
|
||||||
|
@ -136,7 +149,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:
|
||||||
|
try:
|
||||||
self.send_email()
|
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)
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
Reference in New Issue