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