Compare commits

...

2 Commits

Author SHA1 Message Date
1fcef476c3 fix
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-06-22 00:48:46 +02:00
e8b6b3366b events: improve error formatting in events
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-06-21 23:40:36 +02:00
18 changed files with 47 additions and 46 deletions

View File

@ -11,7 +11,6 @@ from authentik.core.expression.exceptions import SkipObjectException
from authentik.core.models import User from authentik.core.models import User
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.lib.expression.evaluator import BaseEvaluator from authentik.lib.expression.evaluator import BaseEvaluator
from authentik.lib.utils.errors import exception_to_string
from authentik.policies.types import PolicyRequest from authentik.policies.types import PolicyRequest
PROPERTY_MAPPING_TIME = Histogram( PROPERTY_MAPPING_TIME = Histogram(
@ -69,12 +68,11 @@ class PropertyMappingEvaluator(BaseEvaluator):
# For dry-run requests we don't save exceptions # For dry-run requests we don't save exceptions
if self.dry_run: if self.dry_run:
return return
error_string = exception_to_string(exc)
event = Event.new( event = Event.new(
EventAction.PROPERTY_MAPPING_EXCEPTION, EventAction.PROPERTY_MAPPING_EXCEPTION,
expression=expression_source, expression=expression_source,
message=error_string, message="Failed to execute property mapping",
) ).with_exception(exc)
if "request" in self._context: if "request" in self._context:
req: PolicyRequest = self._context["request"] req: PolicyRequest = self._context["request"]
if req.http_request: if req.http_request:

View File

@ -20,7 +20,7 @@ from authentik.core.models import Group, User
from authentik.events.models import Event, EventAction, Notification from authentik.events.models import Event, EventAction, Notification
from authentik.events.utils import model_to_dict from authentik.events.utils import model_to_dict
from authentik.lib.sentry import before_send from authentik.lib.sentry import before_send
from authentik.lib.utils.errors import exception_to_string from authentik.lib.utils.errors import exception_to_dict
from authentik.stages.authenticator_static.models import StaticToken from authentik.stages.authenticator_static.models import StaticToken
IGNORED_MODELS = tuple( IGNORED_MODELS = tuple(
@ -170,14 +170,16 @@ class AuditMiddleware:
thread = EventNewThread( thread = EventNewThread(
EventAction.SUSPICIOUS_REQUEST, EventAction.SUSPICIOUS_REQUEST,
request, request,
message=exception_to_string(exception), message=str(exception),
exception=exception_to_dict(exception),
) )
thread.run() thread.run()
elif before_send({}, {"exc_info": (None, exception, None)}) is not None: elif before_send({}, {"exc_info": (None, exception, None)}) is not None:
thread = EventNewThread( thread = EventNewThread(
EventAction.SYSTEM_EXCEPTION, EventAction.SYSTEM_EXCEPTION,
request, request,
message=exception_to_string(exception), message=str(exception),
exception=exception_to_dict(exception),
) )
thread.run() thread.run()

View File

@ -38,6 +38,7 @@ from authentik.events.utils import (
) )
from authentik.lib.models import DomainlessURLValidator, SerializerModel from authentik.lib.models import DomainlessURLValidator, SerializerModel
from authentik.lib.sentry import SentryIgnoredException from authentik.lib.sentry import SentryIgnoredException
from authentik.lib.utils.errors import exception_to_dict
from authentik.lib.utils.http import get_http_session from authentik.lib.utils.http import get_http_session
from authentik.lib.utils.time import timedelta_from_string from authentik.lib.utils.time import timedelta_from_string
from authentik.policies.models import PolicyBindingModel from authentik.policies.models import PolicyBindingModel
@ -163,6 +164,12 @@ class Event(SerializerModel, ExpiringModel):
event = Event(action=action, app=app, context=cleaned_kwargs) event = Event(action=action, app=app, context=cleaned_kwargs)
return event return event
def with_exception(self, exc: Exception) -> "Event":
"""Add data from 'exc' to the event in a database-saveable format"""
self.context.setdefault("message", str(exc))
self.context["exception"] = exception_to_dict(exc)
return self
def set_user(self, user: User) -> "Event": def set_user(self, user: User) -> "Event":
"""Set `.user` based on user, ensuring the correct attributes are copied. """Set `.user` based on user, ensuring the correct attributes are copied.
This should only be used when self.from_http is *not* used.""" This should only be used when self.from_http is *not* used."""

View File

@ -127,8 +127,8 @@ class SystemTask(TenantTask):
) )
Event.new( Event.new(
EventAction.SYSTEM_TASK_EXCEPTION, EventAction.SYSTEM_TASK_EXCEPTION,
message=f"Task {self.__name__} encountered an error: {exception_to_string(exc)}", message=f"Task {self.__name__} encountered an error",
).save() ).with_exception(exc).save()
def run(self, *args, **kwargs): def run(self, *args, **kwargs):
raise NotImplementedError raise NotImplementedError

View File

@ -56,7 +56,6 @@ from authentik.flows.planner import (
) )
from authentik.flows.stage import AccessDeniedStage, StageView from authentik.flows.stage import AccessDeniedStage, StageView
from authentik.lib.sentry import SentryIgnoredException from authentik.lib.sentry import SentryIgnoredException
from authentik.lib.utils.errors import exception_to_string
from authentik.lib.utils.reflection import all_subclasses, class_to_path from authentik.lib.utils.reflection import all_subclasses, class_to_path
from authentik.lib.utils.urls import is_url_absolute, redirect_with_qs from authentik.lib.utils.urls import is_url_absolute, redirect_with_qs
from authentik.policies.engine import PolicyEngine from authentik.policies.engine import PolicyEngine
@ -238,8 +237,8 @@ class FlowExecutorView(APIView):
self._logger.warning(exc) self._logger.warning(exc)
Event.new( Event.new(
action=EventAction.SYSTEM_EXCEPTION, action=EventAction.SYSTEM_EXCEPTION,
message=exception_to_string(exc), message="System exception during flow execution.",
).from_http(self.request) ).with_exception(exc).from_http(self.request)
challenge = FlowErrorChallenge(self.request, exc) challenge = FlowErrorChallenge(self.request, exc)
challenge.is_valid(raise_exception=True) challenge.is_valid(raise_exception=True)
return to_stage_response(self.request, HttpChallengeResponse(challenge)) return to_stage_response(self.request, HttpChallengeResponse(challenge))

View File

@ -14,7 +14,6 @@ from authentik.events.models import Event, EventAction
from authentik.lib.expression.exceptions import ControlFlowException from authentik.lib.expression.exceptions import ControlFlowException
from authentik.lib.sync.mapper import PropertyMappingManager from authentik.lib.sync.mapper import PropertyMappingManager
from authentik.lib.sync.outgoing.exceptions import NotFoundSyncException, StopSync from authentik.lib.sync.outgoing.exceptions import NotFoundSyncException, StopSync
from authentik.lib.utils.errors import exception_to_string
if TYPE_CHECKING: if TYPE_CHECKING:
from django.db.models import Model from django.db.models import Model
@ -106,9 +105,9 @@ class BaseOutgoingSyncClient[
# Value error can be raised when assigning invalid data to an attribute # Value error can be raised when assigning invalid data to an attribute
Event.new( Event.new(
EventAction.CONFIGURATION_ERROR, EventAction.CONFIGURATION_ERROR,
message=f"Failed to evaluate property-mapping {exception_to_string(exc)}", message="Failed to evaluate property-mapping",
mapping=exc.mapping, mapping=exc.mapping,
).save() ).with_exception(exc).save()
raise StopSync(exc, obj, exc.mapping) from exc raise StopSync(exc, obj, exc.mapping) from exc
if not raw_final_object: if not raw_final_object:
raise StopSync(ValueError("No mappings configured"), obj) raise StopSync(ValueError("No mappings configured"), obj)

View File

@ -2,6 +2,8 @@
from traceback import extract_tb from traceback import extract_tb
from structlog.tracebacks import ExceptionDictTransformer
from authentik.lib.utils.reflection import class_to_path from authentik.lib.utils.reflection import class_to_path
TRACEBACK_HEADER = "Traceback (most recent call last):" TRACEBACK_HEADER = "Traceback (most recent call last):"
@ -17,3 +19,8 @@ def exception_to_string(exc: Exception) -> str:
f"{class_to_path(exc.__class__)}: {str(exc)}", f"{class_to_path(exc.__class__)}: {str(exc)}",
] ]
) )
def exception_to_dict(exc: Exception) -> dict:
"""Format exception as a dictionary"""
return ExceptionDictTransformer()((type(exc), exc, exc.__traceback__))

View File

@ -35,7 +35,6 @@ from authentik.events.models import Event, EventAction
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.lib.models import InheritanceForeignKey, SerializerModel from authentik.lib.models import InheritanceForeignKey, SerializerModel
from authentik.lib.sentry import SentryIgnoredException from authentik.lib.sentry import SentryIgnoredException
from authentik.lib.utils.errors import exception_to_string
from authentik.outposts.controllers.k8s.utils import get_namespace from authentik.outposts.controllers.k8s.utils import get_namespace
OUR_VERSION = parse(__version__) OUR_VERSION = parse(__version__)
@ -326,9 +325,8 @@ class Outpost(SerializerModel, ManagedModel):
"While setting the permissions for the service-account, a " "While setting the permissions for the service-account, a "
"permission was not found: Check " "permission was not found: Check "
"https://goauthentik.io/docs/troubleshooting/missing_permission" "https://goauthentik.io/docs/troubleshooting/missing_permission"
) ),
+ exception_to_string(exc), ).with_exception(exc).set_user(user).save()
).set_user(user).save()
else: else:
app_label, perm = model_or_perm.split(".") app_label, perm = model_or_perm.split(".")
permission = Permission.objects.filter( permission = Permission.objects.filter(

View File

@ -10,7 +10,7 @@ from structlog.stdlib import get_logger
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.lib.utils.errors import exception_to_string from authentik.lib.utils.errors import exception_to_dict
from authentik.lib.utils.reflection import class_to_path from authentik.lib.utils.reflection import class_to_path
from authentik.policies.apps import HIST_POLICIES_EXECUTION_TIME from authentik.policies.apps import HIST_POLICIES_EXECUTION_TIME
from authentik.policies.exceptions import PolicyException from authentik.policies.exceptions import PolicyException
@ -95,10 +95,13 @@ class PolicyProcess(PROCESS_CLASS):
except PolicyException as exc: except PolicyException as exc:
# Either use passed original exception or whatever we have # Either use passed original exception or whatever we have
src_exc = exc.src_exc if exc.src_exc else exc src_exc = exc.src_exc if exc.src_exc else exc
error_string = exception_to_string(src_exc)
# Create policy exception event, only when we're not debugging # Create policy exception event, only when we're not debugging
if not self.request.debug: if not self.request.debug:
self.create_event(EventAction.POLICY_EXCEPTION, message=error_string) self.create_event(
EventAction.POLICY_EXCEPTION,
message="Policy failed to execute",
exception=exception_to_dict(src_exc),
)
LOGGER.debug("P_ENG(proc): error, using failure result", exc=src_exc) LOGGER.debug("P_ENG(proc): error, using failure result", exc=src_exc)
policy_result = PolicyResult(self.binding.failure_result, str(src_exc)) policy_result = PolicyResult(self.binding.failure_result, str(src_exc))
policy_result.source_binding = self.binding policy_result.source_binding = self.binding
@ -143,5 +146,5 @@ class PolicyProcess(PROCESS_CLASS):
try: try:
self.connection.send(self.profiling_wrapper()) self.connection.send(self.profiling_wrapper())
except Exception as exc: except Exception as exc:
LOGGER.warning("Policy failed to run", exc=exception_to_string(exc)) LOGGER.warning("Policy failed to run", exc=exc)
self.connection.send(PolicyResult(False, str(exc))) self.connection.send(PolicyResult(False, str(exc)))

View File

@ -237,4 +237,4 @@ class TestPolicyProcess(TestCase):
self.assertEqual(len(events), 1) self.assertEqual(len(events), 1)
event = events.first() event = events.first()
self.assertEqual(event.user["username"], self.user.username) self.assertEqual(event.user["username"], self.user.username)
self.assertIn("division by zero", event.context["message"]) self.assertIn("Policy failed to execute", event.context["message"])

View File

@ -23,7 +23,6 @@ from authentik.core.models import Application
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.lib.expression.exceptions import ControlFlowException from authentik.lib.expression.exceptions import ControlFlowException
from authentik.lib.sync.mapper import PropertyMappingManager from authentik.lib.sync.mapper import PropertyMappingManager
from authentik.lib.utils.errors import exception_to_string
from authentik.policies.api.exec import PolicyTestResultSerializer from authentik.policies.api.exec import PolicyTestResultSerializer
from authentik.policies.engine import PolicyEngine from authentik.policies.engine import PolicyEngine
from authentik.policies.types import PolicyResult from authentik.policies.types import PolicyResult
@ -142,9 +141,9 @@ class RadiusOutpostConfigViewSet(ListModelMixin, GenericViewSet):
# Value error can be raised when assigning invalid data to an attribute # Value error can be raised when assigning invalid data to an attribute
Event.new( Event.new(
EventAction.CONFIGURATION_ERROR, EventAction.CONFIGURATION_ERROR,
message=f"Failed to evaluate property-mapping {exception_to_string(exc)}", message="Failed to evaluate property-mapping",
mapping=exc.mapping, mapping=exc.mapping,
).save() ).with_exception(exc).save()
return None return None
return b64encode(packet.RequestPacket()).decode() return b64encode(packet.RequestPacket()).decode()

View File

@ -28,7 +28,6 @@ from tenant_schemas_celery.app import CeleryApp as TenantAwareCeleryApp
from authentik import get_full_version from authentik import get_full_version
from authentik.lib.sentry import before_send from authentik.lib.sentry import before_send
from authentik.lib.utils.errors import exception_to_string
# set the default Django settings module for the 'celery' program. # set the default Django settings module for the 'celery' program.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "authentik.root.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "authentik.root.settings")
@ -83,8 +82,8 @@ def task_error_hook(task_id: str, exception: Exception, traceback, *args, **kwar
CTX_TASK_ID.set(...) CTX_TASK_ID.set(...)
if before_send({}, {"exc_info": (None, exception, None)}) is not None: if before_send({}, {"exc_info": (None, exception, None)}) is not None:
Event.new( Event.new(
EventAction.SYSTEM_EXCEPTION, message=exception_to_string(exception), task_id=task_id EventAction.SYSTEM_EXCEPTION, message="Failed to execute task", task_id=task_id
).save() ).with_exception(exception).save()
def _get_startup_tasks_default_tenant() -> list[Callable]: def _get_startup_tasks_default_tenant() -> list[Callable]:

View File

@ -8,7 +8,6 @@ from authentik.events.models import TaskStatus
from authentik.events.system_tasks import SystemTask from authentik.events.system_tasks import SystemTask
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.lib.sync.outgoing.exceptions import StopSync from authentik.lib.sync.outgoing.exceptions import StopSync
from authentik.lib.utils.errors import exception_to_string
from authentik.root.celery import CELERY_APP from authentik.root.celery import CELERY_APP
from authentik.sources.kerberos.models import KerberosSource from authentik.sources.kerberos.models import KerberosSource
from authentik.sources.kerberos.sync import KerberosSync from authentik.sources.kerberos.sync import KerberosSync
@ -64,5 +63,5 @@ def kerberos_sync_single(self, source_pk: str):
syncer.sync() syncer.sync()
self.set_status(TaskStatus.SUCCESSFUL, *syncer.messages) self.set_status(TaskStatus.SUCCESSFUL, *syncer.messages)
except StopSync as exc: except StopSync as exc:
LOGGER.warning(exception_to_string(exc)) LOGGER.warning("Error syncing kerberos", exc=exc, source=source)
self.set_error(exc) self.set_error(exc)

View File

@ -12,7 +12,6 @@ from authentik.events.models import TaskStatus
from authentik.events.system_tasks import SystemTask from authentik.events.system_tasks import SystemTask
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.lib.sync.outgoing.exceptions import StopSync from authentik.lib.sync.outgoing.exceptions import StopSync
from authentik.lib.utils.errors import exception_to_string
from authentik.lib.utils.reflection import class_to_path, path_to_class from authentik.lib.utils.reflection import class_to_path, path_to_class
from authentik.root.celery import CELERY_APP from authentik.root.celery import CELERY_APP
from authentik.sources.ldap.models import LDAPSource from authentik.sources.ldap.models import LDAPSource
@ -149,5 +148,5 @@ def ldap_sync(self: SystemTask, source_pk: str, sync_class: str, page_cache_key:
cache.delete(page_cache_key) cache.delete(page_cache_key)
except (LDAPException, StopSync) as exc: except (LDAPException, StopSync) as exc:
# No explicit event is created here as .set_status with an error will do that # No explicit event is created here as .set_status with an error will do that
LOGGER.warning(exception_to_string(exc)) LOGGER.warning("Failed to sync LDAP", exc=exc, source=source)
self.set_error(exc) self.set_error(exc)

View File

@ -13,7 +13,6 @@ from authentik.flows.exceptions import StageInvalidException
from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.lib.models import SerializerModel from authentik.lib.models import SerializerModel
from authentik.lib.utils.errors import exception_to_string
from authentik.lib.utils.time import timedelta_string_validator from authentik.lib.utils.time import timedelta_string_validator
from authentik.stages.authenticator.models import SideChannelDevice from authentik.stages.authenticator.models import SideChannelDevice
from authentik.stages.email.utils import TemplateEmailMessage from authentik.stages.email.utils import TemplateEmailMessage
@ -160,9 +159,8 @@ class EmailDevice(SerializerModel, SideChannelDevice):
Event.new( Event.new(
EventAction.CONFIGURATION_ERROR, EventAction.CONFIGURATION_ERROR,
message=_("Exception occurred while rendering E-mail template"), message=_("Exception occurred while rendering E-mail template"),
error=exception_to_string(exc),
template=stage.template, template=stage.template,
).from_http(self.request) ).with_exception(exc).from_http(self.request)
raise StageInvalidException from exc raise StageInvalidException from exc
def __str__(self): def __str__(self):

View File

@ -17,7 +17,6 @@ from authentik.flows.challenge import (
from authentik.flows.exceptions import StageInvalidException from authentik.flows.exceptions import StageInvalidException
from authentik.flows.stage import ChallengeStageView from authentik.flows.stage import ChallengeStageView
from authentik.lib.utils.email import mask_email from authentik.lib.utils.email import mask_email
from authentik.lib.utils.errors import exception_to_string
from authentik.lib.utils.time import timedelta_from_string from authentik.lib.utils.time import timedelta_from_string
from authentik.stages.authenticator_email.models import ( from authentik.stages.authenticator_email.models import (
AuthenticatorEmailStage, AuthenticatorEmailStage,
@ -100,9 +99,8 @@ class AuthenticatorEmailStageView(ChallengeStageView):
Event.new( Event.new(
EventAction.CONFIGURATION_ERROR, EventAction.CONFIGURATION_ERROR,
message=_("Exception occurred while rendering E-mail template"), message=_("Exception occurred while rendering E-mail template"),
error=exception_to_string(exc),
template=stage.template, template=stage.template,
).from_http(self.request) ).with_exception(exc).from_http(self.request)
raise StageInvalidException from exc raise StageInvalidException from exc
def _has_email(self) -> str | None: def _has_email(self) -> str | None:

View File

@ -19,7 +19,6 @@ from authentik.events.models import Event, EventAction, NotificationWebhookMappi
from authentik.events.utils import sanitize_item from authentik.events.utils import sanitize_item
from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage
from authentik.lib.models import SerializerModel from authentik.lib.models import SerializerModel
from authentik.lib.utils.errors import exception_to_string
from authentik.lib.utils.http import get_http_session from authentik.lib.utils.http import get_http_session
from authentik.stages.authenticator.models import SideChannelDevice from authentik.stages.authenticator.models import SideChannelDevice
@ -142,10 +141,9 @@ class AuthenticatorSMSStage(ConfigurableStage, FriendlyNamedStage, Stage):
Event.new( Event.new(
EventAction.CONFIGURATION_ERROR, EventAction.CONFIGURATION_ERROR,
message="Error sending SMS", message="Error sending SMS",
exc=exception_to_string(exc),
status_code=response.status_code, status_code=response.status_code,
body=response.text, body=response.text,
).set_user(device.user).save() ).with_exception(exc).set_user(device.user).save()
if response.status_code >= HttpResponseBadRequest.status_code: if response.status_code >= HttpResponseBadRequest.status_code:
raise ValidationError(response.text) from None raise ValidationError(response.text) from None
raise raise

View File

@ -21,7 +21,6 @@ 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.lib.utils.time import timedelta_from_string from authentik.lib.utils.time import timedelta_from_string
from authentik.stages.email.flow import pickle_flow_token_for_email from authentik.stages.email.flow import pickle_flow_token_for_email
from authentik.stages.email.models import EmailStage from authentik.stages.email.models import EmailStage
@ -129,9 +128,8 @@ class EmailStageView(ChallengeStageView):
Event.new( Event.new(
EventAction.CONFIGURATION_ERROR, EventAction.CONFIGURATION_ERROR,
message=_("Exception occurred while rendering E-mail template"), message=_("Exception occurred while rendering E-mail template"),
error=exception_to_string(exc),
template=current_stage.template, template=current_stage.template,
).from_http(self.request) ).with_exception(exc).from_http(self.request)
raise StageInvalidException from exc raise StageInvalidException from exc
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: