diff --git a/authentik/core/expression/evaluator.py b/authentik/core/expression/evaluator.py index a02d7e96d5..78340483a3 100644 --- a/authentik/core/expression/evaluator.py +++ b/authentik/core/expression/evaluator.py @@ -11,7 +11,6 @@ from authentik.core.expression.exceptions import SkipObjectException from authentik.core.models import User from authentik.events.models import Event, EventAction from authentik.lib.expression.evaluator import BaseEvaluator -from authentik.lib.utils.errors import exception_to_string from authentik.policies.types import PolicyRequest PROPERTY_MAPPING_TIME = Histogram( @@ -69,12 +68,11 @@ class PropertyMappingEvaluator(BaseEvaluator): # For dry-run requests we don't save exceptions if self.dry_run: return - error_string = exception_to_string(exc) event = Event.new( EventAction.PROPERTY_MAPPING_EXCEPTION, expression=expression_source, - message=error_string, - ) + message="Failed to execute property mapping", + ).with_exception(exc) if "request" in self._context: req: PolicyRequest = self._context["request"] if req.http_request: diff --git a/authentik/events/middleware.py b/authentik/events/middleware.py index 826db6b736..b1d1e86320 100644 --- a/authentik/events/middleware.py +++ b/authentik/events/middleware.py @@ -20,7 +20,7 @@ from authentik.core.models import Group, User from authentik.events.models import Event, EventAction, Notification from authentik.events.utils import model_to_dict 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 IGNORED_MODELS = tuple( @@ -170,14 +170,16 @@ class AuditMiddleware: thread = EventNewThread( EventAction.SUSPICIOUS_REQUEST, request, - message=exception_to_string(exception), + message=str(exception), + exception=exception_to_dict(exception), ) thread.run() elif before_send({}, {"exc_info": (None, exception, None)}) is not None: thread = EventNewThread( EventAction.SYSTEM_EXCEPTION, request, - message=exception_to_string(exception), + message=str(exception), + exception=exception_to_dict(exception), ) thread.run() diff --git a/authentik/events/models.py b/authentik/events/models.py index 5d4646c561..4ba90ca572 100644 --- a/authentik/events/models.py +++ b/authentik/events/models.py @@ -38,6 +38,7 @@ from authentik.events.utils import ( ) from authentik.lib.models import DomainlessURLValidator, SerializerModel 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.time import timedelta_from_string from authentik.policies.models import PolicyBindingModel @@ -163,6 +164,12 @@ class Event(SerializerModel, ExpiringModel): event = Event(action=action, app=app, context=cleaned_kwargs) 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": """Set `.user` based on user, ensuring the correct attributes are copied. This should only be used when self.from_http is *not* used.""" diff --git a/authentik/events/system_tasks.py b/authentik/events/system_tasks.py index dbe81853f9..0438b2887e 100644 --- a/authentik/events/system_tasks.py +++ b/authentik/events/system_tasks.py @@ -127,8 +127,8 @@ class SystemTask(TenantTask): ) Event.new( EventAction.SYSTEM_TASK_EXCEPTION, - message=f"Task {self.__name__} encountered an error: {exception_to_string(exc)}", - ).save() + message=f"Task {self.__name__} encountered an error", + ).with_exception(exc).save() def run(self, *args, **kwargs): raise NotImplementedError diff --git a/authentik/flows/views/executor.py b/authentik/flows/views/executor.py index b70b0f058c..79f74a2c0e 100644 --- a/authentik/flows/views/executor.py +++ b/authentik/flows/views/executor.py @@ -56,7 +56,6 @@ from authentik.flows.planner import ( ) from authentik.flows.stage import AccessDeniedStage, StageView 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.urls import is_url_absolute, redirect_with_qs from authentik.policies.engine import PolicyEngine @@ -238,8 +237,8 @@ class FlowExecutorView(APIView): self._logger.warning(exc) Event.new( action=EventAction.SYSTEM_EXCEPTION, - message=exception_to_string(exc), - ).from_http(self.request) + message="System exception during flow execution.", + ).with_exception(exc).from_http(self.request) challenge = FlowErrorChallenge(self.request, exc) challenge.is_valid(raise_exception=True) return to_stage_response(self.request, HttpChallengeResponse(challenge)) diff --git a/authentik/lib/sync/outgoing/base.py b/authentik/lib/sync/outgoing/base.py index 16eb1eb92b..29890c918e 100644 --- a/authentik/lib/sync/outgoing/base.py +++ b/authentik/lib/sync/outgoing/base.py @@ -14,7 +14,6 @@ from authentik.events.models import Event, EventAction from authentik.lib.expression.exceptions import ControlFlowException from authentik.lib.sync.mapper import PropertyMappingManager from authentik.lib.sync.outgoing.exceptions import NotFoundSyncException, StopSync -from authentik.lib.utils.errors import exception_to_string if TYPE_CHECKING: from django.db.models import Model @@ -106,9 +105,9 @@ class BaseOutgoingSyncClient[ # Value error can be raised when assigning invalid data to an attribute Event.new( EventAction.CONFIGURATION_ERROR, - message=f"Failed to evaluate property-mapping {exception_to_string(exc)}", + message="Failed to evaluate property-mapping", mapping=exc.mapping, - ).save() + ).with_exception(exc).save() raise StopSync(exc, obj, exc.mapping) from exc if not raw_final_object: raise StopSync(ValueError("No mappings configured"), obj) diff --git a/authentik/lib/utils/errors.py b/authentik/lib/utils/errors.py index 67bfd43f33..c12ebc0743 100644 --- a/authentik/lib/utils/errors.py +++ b/authentik/lib/utils/errors.py @@ -2,6 +2,8 @@ from traceback import extract_tb +from structlog.tracebacks import ExceptionDictTransformer + from authentik.lib.utils.reflection import class_to_path 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)}", ] ) + + +def exception_to_dict(exc: Exception) -> dict: + """Format exception as a dictionary""" + return ExceptionDictTransformer()((type(exc), exc, exc.__traceback__)) diff --git a/authentik/outposts/models.py b/authentik/outposts/models.py index 1d6a8af1bb..2fdb190158 100644 --- a/authentik/outposts/models.py +++ b/authentik/outposts/models.py @@ -35,7 +35,6 @@ from authentik.events.models import Event, EventAction from authentik.lib.config import CONFIG from authentik.lib.models import InheritanceForeignKey, SerializerModel from authentik.lib.sentry import SentryIgnoredException -from authentik.lib.utils.errors import exception_to_string from authentik.outposts.controllers.k8s.utils import get_namespace OUR_VERSION = parse(__version__) @@ -326,9 +325,8 @@ class Outpost(SerializerModel, ManagedModel): "While setting the permissions for the service-account, a " "permission was not found: Check " "https://goauthentik.io/docs/troubleshooting/missing_permission" - ) - + exception_to_string(exc), - ).set_user(user).save() + ), + ).with_exception(exc).set_user(user).save() else: app_label, perm = model_or_perm.split(".") permission = Permission.objects.filter( diff --git a/authentik/policies/process.py b/authentik/policies/process.py index 624edf047d..7d539fe890 100644 --- a/authentik/policies/process.py +++ b/authentik/policies/process.py @@ -10,7 +10,7 @@ from structlog.stdlib import get_logger from authentik.events.models import Event, EventAction 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.policies.apps import HIST_POLICIES_EXECUTION_TIME from authentik.policies.exceptions import PolicyException @@ -95,10 +95,13 @@ class PolicyProcess(PROCESS_CLASS): except PolicyException as exc: # Either use passed original exception or whatever we have 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 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) policy_result = PolicyResult(self.binding.failure_result, str(src_exc)) policy_result.source_binding = self.binding @@ -143,5 +146,5 @@ class PolicyProcess(PROCESS_CLASS): try: self.connection.send(self.profiling_wrapper()) 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))) diff --git a/authentik/providers/radius/api/providers.py b/authentik/providers/radius/api/providers.py index 0ab9d04a10..7cba141a0c 100644 --- a/authentik/providers/radius/api/providers.py +++ b/authentik/providers/radius/api/providers.py @@ -23,7 +23,6 @@ from authentik.core.models import Application from authentik.events.models import Event, EventAction from authentik.lib.expression.exceptions import ControlFlowException 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.engine import PolicyEngine 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 Event.new( EventAction.CONFIGURATION_ERROR, - message=f"Failed to evaluate property-mapping {exception_to_string(exc)}", + message="Failed to evaluate property-mapping", mapping=exc.mapping, - ).save() + ).with_exception(exc).save() return None return b64encode(packet.RequestPacket()).decode() diff --git a/authentik/root/celery.py b/authentik/root/celery.py index c7d6b077e5..f3d4b2c28c 100644 --- a/authentik/root/celery.py +++ b/authentik/root/celery.py @@ -28,7 +28,6 @@ from tenant_schemas_celery.app import CeleryApp as TenantAwareCeleryApp from authentik import get_full_version 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. 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(...) if before_send({}, {"exc_info": (None, exception, None)}) is not None: Event.new( - EventAction.SYSTEM_EXCEPTION, message=exception_to_string(exception), task_id=task_id - ).save() + EventAction.SYSTEM_EXCEPTION, message="Failed to execute task", task_id=task_id + ).with_exception(exception).save() def _get_startup_tasks_default_tenant() -> list[Callable]: diff --git a/authentik/sources/kerberos/tasks.py b/authentik/sources/kerberos/tasks.py index 93570c11c4..88bf4ec3e9 100644 --- a/authentik/sources/kerberos/tasks.py +++ b/authentik/sources/kerberos/tasks.py @@ -8,7 +8,6 @@ from authentik.events.models import TaskStatus from authentik.events.system_tasks import SystemTask from authentik.lib.config import CONFIG 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.sources.kerberos.models import KerberosSource from authentik.sources.kerberos.sync import KerberosSync @@ -64,5 +63,5 @@ def kerberos_sync_single(self, source_pk: str): syncer.sync() self.set_status(TaskStatus.SUCCESSFUL, *syncer.messages) except StopSync as exc: - LOGGER.warning(exception_to_string(exc)) + LOGGER.warning("Error syncing kerberos", exc=exc, source=source) self.set_error(exc) diff --git a/authentik/sources/ldap/tasks.py b/authentik/sources/ldap/tasks.py index 0ef6eac3f1..f800bd5abe 100644 --- a/authentik/sources/ldap/tasks.py +++ b/authentik/sources/ldap/tasks.py @@ -12,7 +12,6 @@ from authentik.events.models import TaskStatus from authentik.events.system_tasks import SystemTask from authentik.lib.config import CONFIG 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.root.celery import CELERY_APP 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) except (LDAPException, StopSync) as exc: # 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) diff --git a/authentik/stages/authenticator_email/models.py b/authentik/stages/authenticator_email/models.py index 495e1a2512..389e73f867 100644 --- a/authentik/stages/authenticator_email/models.py +++ b/authentik/stages/authenticator_email/models.py @@ -13,7 +13,6 @@ from authentik.flows.exceptions import StageInvalidException from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage from authentik.lib.config import CONFIG 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.stages.authenticator.models import SideChannelDevice from authentik.stages.email.utils import TemplateEmailMessage @@ -160,9 +159,8 @@ class EmailDevice(SerializerModel, SideChannelDevice): Event.new( EventAction.CONFIGURATION_ERROR, message=_("Exception occurred while rendering E-mail template"), - error=exception_to_string(exc), template=stage.template, - ).from_http(self.request) + ).with_exception(exc).from_http(self.request) raise StageInvalidException from exc def __str__(self): diff --git a/authentik/stages/authenticator_email/stage.py b/authentik/stages/authenticator_email/stage.py index 9a3b403823..88f0ef45a2 100644 --- a/authentik/stages/authenticator_email/stage.py +++ b/authentik/stages/authenticator_email/stage.py @@ -17,7 +17,6 @@ from authentik.flows.challenge import ( from authentik.flows.exceptions import StageInvalidException from authentik.flows.stage import ChallengeStageView 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.stages.authenticator_email.models import ( AuthenticatorEmailStage, @@ -100,9 +99,8 @@ class AuthenticatorEmailStageView(ChallengeStageView): Event.new( EventAction.CONFIGURATION_ERROR, message=_("Exception occurred while rendering E-mail template"), - error=exception_to_string(exc), template=stage.template, - ).from_http(self.request) + ).with_exception(exc).from_http(self.request) raise StageInvalidException from exc def _has_email(self) -> str | None: diff --git a/authentik/stages/authenticator_sms/models.py b/authentik/stages/authenticator_sms/models.py index 82ad6f61ee..e520ebc59c 100644 --- a/authentik/stages/authenticator_sms/models.py +++ b/authentik/stages/authenticator_sms/models.py @@ -19,7 +19,6 @@ from authentik.events.models import Event, EventAction, NotificationWebhookMappi from authentik.events.utils import sanitize_item from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage 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.stages.authenticator.models import SideChannelDevice @@ -142,10 +141,9 @@ class AuthenticatorSMSStage(ConfigurableStage, FriendlyNamedStage, Stage): Event.new( EventAction.CONFIGURATION_ERROR, message="Error sending SMS", - exc=exception_to_string(exc), status_code=response.status_code, body=response.text, - ).set_user(device.user).save() + ).with_exception(exc).set_user(device.user).save() if response.status_code >= HttpResponseBadRequest.status_code: raise ValidationError(response.text) from None raise diff --git a/authentik/stages/email/stage.py b/authentik/stages/email/stage.py index 548c2083f5..3195b52465 100644 --- a/authentik/stages/email/stage.py +++ b/authentik/stages/email/stage.py @@ -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.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.lib.utils.time import timedelta_from_string from authentik.stages.email.flow import pickle_flow_token_for_email from authentik.stages.email.models import EmailStage @@ -129,9 +128,8 @@ class EmailStageView(ChallengeStageView): 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) + ).with_exception(exc).from_http(self.request) raise StageInvalidException from exc def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: