events: add context manager to ignore/modify audit events being written (#9181)
This commit is contained in:
		@ -1,6 +1,8 @@
 | 
			
		||||
"""Events middleware"""
 | 
			
		||||
 | 
			
		||||
from collections.abc import Callable
 | 
			
		||||
from contextlib import contextmanager
 | 
			
		||||
from contextvars import ContextVar
 | 
			
		||||
from functools import partial
 | 
			
		||||
from threading import Thread
 | 
			
		||||
from typing import Any
 | 
			
		||||
@ -31,6 +33,9 @@ IGNORED_MODELS = tuple(
 | 
			
		||||
    )
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
_CTX_OVERWRITE_USER = ContextVar[User | None]("authentik_events_log_overwrite_user", default=None)
 | 
			
		||||
_CTX_IGNORE = ContextVar[bool]("authentik_events_log_ignore", default=False)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def should_log_model(model: Model) -> bool:
 | 
			
		||||
    """Return true if operation on `model` should be logged"""
 | 
			
		||||
@ -44,6 +49,28 @@ def should_log_m2m(model: Model) -> bool:
 | 
			
		||||
    return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@contextmanager
 | 
			
		||||
def audit_overwrite_user(user: User):
 | 
			
		||||
    """Overwrite user being logged for model AuditMiddleware. Commonly used
 | 
			
		||||
    for example in flows where a pending user is given, but the request is not authenticated yet"""
 | 
			
		||||
    _CTX_OVERWRITE_USER.set(user)
 | 
			
		||||
    try:
 | 
			
		||||
        yield
 | 
			
		||||
    finally:
 | 
			
		||||
        _CTX_OVERWRITE_USER.set(None)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@contextmanager
 | 
			
		||||
def audit_ignore():
 | 
			
		||||
    """Ignore model operations in the block. Useful for objects which need to be modified
 | 
			
		||||
    but are not excluded (e.g. WebAuthn devices)"""
 | 
			
		||||
    _CTX_IGNORE.set(True)
 | 
			
		||||
    try:
 | 
			
		||||
        yield
 | 
			
		||||
    finally:
 | 
			
		||||
        _CTX_IGNORE.set(False)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EventNewThread(Thread):
 | 
			
		||||
    """Create Event in background thread"""
 | 
			
		||||
 | 
			
		||||
@ -158,6 +185,10 @@ class AuditMiddleware:
 | 
			
		||||
        """Signal handler for all object's post_save"""
 | 
			
		||||
        if not should_log_model(instance):
 | 
			
		||||
            return
 | 
			
		||||
        if _CTX_IGNORE.get():
 | 
			
		||||
            return
 | 
			
		||||
        if _new_user := _CTX_OVERWRITE_USER.get():
 | 
			
		||||
            user = _new_user
 | 
			
		||||
 | 
			
		||||
        action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED
 | 
			
		||||
        thread = EventNewThread(action, request, user=user, model=model_to_dict(instance))
 | 
			
		||||
@ -168,6 +199,10 @@ class AuditMiddleware:
 | 
			
		||||
        """Signal handler for all object's pre_delete"""
 | 
			
		||||
        if not should_log_model(instance):  # pragma: no cover
 | 
			
		||||
            return
 | 
			
		||||
        if _CTX_IGNORE.get():
 | 
			
		||||
            return
 | 
			
		||||
        if _new_user := _CTX_OVERWRITE_USER.get():
 | 
			
		||||
            user = _new_user
 | 
			
		||||
 | 
			
		||||
        EventNewThread(
 | 
			
		||||
            EventAction.MODEL_DELETED,
 | 
			
		||||
@ -184,6 +219,10 @@ class AuditMiddleware:
 | 
			
		||||
            return
 | 
			
		||||
        if not should_log_m2m(instance):
 | 
			
		||||
            return
 | 
			
		||||
        if _CTX_IGNORE.get():
 | 
			
		||||
            return
 | 
			
		||||
        if _new_user := _CTX_OVERWRITE_USER.get():
 | 
			
		||||
            user = _new_user
 | 
			
		||||
 | 
			
		||||
        EventNewThread(
 | 
			
		||||
            EventAction.MODEL_UPDATED,
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,9 @@ from rest_framework.test import APITestCase
 | 
			
		||||
 | 
			
		||||
from authentik.core.models import Application
 | 
			
		||||
from authentik.core.tests.utils import create_test_admin_user
 | 
			
		||||
from authentik.events.middleware import audit_ignore, audit_overwrite_user
 | 
			
		||||
from authentik.events.models import Event, EventAction
 | 
			
		||||
from authentik.lib.generators import generate_id
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestEventsMiddleware(APITestCase):
 | 
			
		||||
@ -15,35 +17,74 @@ class TestEventsMiddleware(APITestCase):
 | 
			
		||||
        super().setUp()
 | 
			
		||||
        self.user = create_test_admin_user()
 | 
			
		||||
        self.client.force_login(self.user)
 | 
			
		||||
        Event.objects.all().delete()
 | 
			
		||||
 | 
			
		||||
    def test_create(self):
 | 
			
		||||
        """Test model creation event"""
 | 
			
		||||
        uid = generate_id()
 | 
			
		||||
        self.client.post(
 | 
			
		||||
            reverse("authentik_api:application-list"),
 | 
			
		||||
            data={"name": "test-create", "slug": "test-create"},
 | 
			
		||||
            data={"name": uid, "slug": uid},
 | 
			
		||||
        )
 | 
			
		||||
        self.assertTrue(Application.objects.filter(name="test-create").exists())
 | 
			
		||||
        self.assertTrue(Application.objects.filter(name=uid).exists())
 | 
			
		||||
        self.assertTrue(
 | 
			
		||||
            Event.objects.filter(
 | 
			
		||||
                action=EventAction.MODEL_CREATED,
 | 
			
		||||
                context__model__model_name="application",
 | 
			
		||||
                context__model__app="authentik_core",
 | 
			
		||||
                context__model__name="test-create",
 | 
			
		||||
                context__model__name=uid,
 | 
			
		||||
            ).exists()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_delete(self):
 | 
			
		||||
        """Test model creation event"""
 | 
			
		||||
        Application.objects.create(name="test-delete", slug="test-delete")
 | 
			
		||||
        self.client.delete(
 | 
			
		||||
            reverse("authentik_api:application-detail", kwargs={"slug": "test-delete"})
 | 
			
		||||
        )
 | 
			
		||||
        uid = generate_id()
 | 
			
		||||
        Application.objects.create(name=uid, slug=uid)
 | 
			
		||||
        self.client.delete(reverse("authentik_api:application-detail", kwargs={"slug": uid}))
 | 
			
		||||
        self.assertFalse(Application.objects.filter(name="test").exists())
 | 
			
		||||
        self.assertTrue(
 | 
			
		||||
            Event.objects.filter(
 | 
			
		||||
                action=EventAction.MODEL_DELETED,
 | 
			
		||||
                context__model__model_name="application",
 | 
			
		||||
                context__model__app="authentik_core",
 | 
			
		||||
                context__model__name="test-delete",
 | 
			
		||||
                context__model__name=uid,
 | 
			
		||||
            ).exists()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_audit_ignore(self):
 | 
			
		||||
        """Test audit_ignore context manager"""
 | 
			
		||||
        uid = generate_id()
 | 
			
		||||
        with audit_ignore():
 | 
			
		||||
            self.client.post(
 | 
			
		||||
                reverse("authentik_api:application-list"),
 | 
			
		||||
                data={"name": uid, "slug": uid},
 | 
			
		||||
            )
 | 
			
		||||
        self.assertTrue(Application.objects.filter(name=uid).exists())
 | 
			
		||||
        self.assertFalse(
 | 
			
		||||
            Event.objects.filter(
 | 
			
		||||
                action=EventAction.MODEL_CREATED,
 | 
			
		||||
                context__model__model_name="application",
 | 
			
		||||
                context__model__app="authentik_core",
 | 
			
		||||
                context__model__name=uid,
 | 
			
		||||
            ).exists()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_audit_overwrite_user(self):
 | 
			
		||||
        """Test audit_overwrite_user context manager"""
 | 
			
		||||
        uid = generate_id()
 | 
			
		||||
        new_user = create_test_admin_user()
 | 
			
		||||
        with audit_overwrite_user(new_user):
 | 
			
		||||
            self.client.post(
 | 
			
		||||
                reverse("authentik_api:application-list"),
 | 
			
		||||
                data={"name": uid, "slug": uid},
 | 
			
		||||
            )
 | 
			
		||||
        self.assertTrue(Application.objects.filter(name=uid).exists())
 | 
			
		||||
        self.assertTrue(
 | 
			
		||||
            Event.objects.filter(
 | 
			
		||||
                action=EventAction.MODEL_CREATED,
 | 
			
		||||
                context__model__model_name="application",
 | 
			
		||||
                context__model__app="authentik_core",
 | 
			
		||||
                context__model__name=uid,
 | 
			
		||||
                user__username=new_user.username,
 | 
			
		||||
            ).exists()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -31,6 +31,7 @@ from authentik.core.models import (
 | 
			
		||||
    User,
 | 
			
		||||
    UserTypes,
 | 
			
		||||
)
 | 
			
		||||
from authentik.events.middleware import audit_ignore
 | 
			
		||||
from authentik.events.models import Event, EventAction
 | 
			
		||||
from authentik.events.signals import get_login_event
 | 
			
		||||
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION
 | 
			
		||||
@ -465,22 +466,25 @@ class TokenParams:
 | 
			
		||||
 | 
			
		||||
    def __create_user_from_jwt(self, token: dict[str, Any], app: Application, source: OAuthSource):
 | 
			
		||||
        """Create user from JWT"""
 | 
			
		||||
        self.user, created = User.objects.update_or_create(
 | 
			
		||||
            username=f"{self.provider.name}-{token.get('sub')}",
 | 
			
		||||
            defaults={
 | 
			
		||||
                "attributes": {
 | 
			
		||||
                    USER_ATTRIBUTE_GENERATED: True,
 | 
			
		||||
        with audit_ignore():
 | 
			
		||||
            self.user, created = User.objects.update_or_create(
 | 
			
		||||
                username=f"{self.provider.name}-{token.get('sub')}",
 | 
			
		||||
                defaults={
 | 
			
		||||
                    "attributes": {
 | 
			
		||||
                        USER_ATTRIBUTE_GENERATED: True,
 | 
			
		||||
                    },
 | 
			
		||||
                    "last_login": timezone.now(),
 | 
			
		||||
                    "name": (
 | 
			
		||||
                        f"Autogenerated user from application {app.name} (client credentials JWT)"
 | 
			
		||||
                    ),
 | 
			
		||||
                    "path": source.get_user_path(),
 | 
			
		||||
                    "type": UserTypes.SERVICE_ACCOUNT,
 | 
			
		||||
                },
 | 
			
		||||
                "last_login": timezone.now(),
 | 
			
		||||
                "name": f"Autogenerated user from application {app.name} (client credentials JWT)",
 | 
			
		||||
                "path": source.get_user_path(),
 | 
			
		||||
                "type": UserTypes.SERVICE_ACCOUNT,
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
        exp = token.get("exp")
 | 
			
		||||
        if created and exp:
 | 
			
		||||
            self.user.attributes[USER_ATTRIBUTE_EXPIRES] = exp
 | 
			
		||||
            self.user.save()
 | 
			
		||||
            )
 | 
			
		||||
            exp = token.get("exp")
 | 
			
		||||
            if created and exp:
 | 
			
		||||
                self.user.attributes[USER_ATTRIBUTE_EXPIRES] = exp
 | 
			
		||||
                self.user.save()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@method_decorator(csrf_exempt, name="dispatch")
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,7 @@ from webauthn.helpers.structs import UserVerificationRequirement
 | 
			
		||||
from authentik.core.api.utils import JSONDictField, PassiveSerializer
 | 
			
		||||
from authentik.core.models import Application, User
 | 
			
		||||
from authentik.core.signals import login_failed
 | 
			
		||||
from authentik.events.middleware import audit_ignore
 | 
			
		||||
from authentik.events.models import Event, EventAction
 | 
			
		||||
from authentik.flows.stage import StageView
 | 
			
		||||
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE
 | 
			
		||||
@ -167,7 +168,8 @@ def validate_challenge_webauthn(data: dict, stage_view: StageView, user: User) -
 | 
			
		||||
        )
 | 
			
		||||
        raise ValidationError("Assertion failed") from exc
 | 
			
		||||
 | 
			
		||||
    device.set_sign_count(authentication_verification.new_sign_count)
 | 
			
		||||
    with audit_ignore():
 | 
			
		||||
        device.set_sign_count(authentication_verification.new_sign_count)
 | 
			
		||||
    return device
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user