events: add context manager to ignore/modify audit events being written (#9181)

This commit is contained in:
Jens L
2024-04-09 01:42:36 +02:00
committed by GitHub
parent 16b8edd082
commit 2ec8a445c3
4 changed files with 110 additions and 24 deletions

View File

@ -1,6 +1,8 @@
"""Events middleware""" """Events middleware"""
from collections.abc import Callable from collections.abc import Callable
from contextlib import contextmanager
from contextvars import ContextVar
from functools import partial from functools import partial
from threading import Thread from threading import Thread
from typing import Any 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: def should_log_model(model: Model) -> bool:
"""Return true if operation on `model` should be logged""" """Return true if operation on `model` should be logged"""
@ -44,6 +49,28 @@ def should_log_m2m(model: Model) -> bool:
return False 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): class EventNewThread(Thread):
"""Create Event in background thread""" """Create Event in background thread"""
@ -158,6 +185,10 @@ class AuditMiddleware:
"""Signal handler for all object's post_save""" """Signal handler for all object's post_save"""
if not should_log_model(instance): if not should_log_model(instance):
return 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 action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED
thread = EventNewThread(action, request, user=user, model=model_to_dict(instance)) 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""" """Signal handler for all object's pre_delete"""
if not should_log_model(instance): # pragma: no cover if not should_log_model(instance): # pragma: no cover
return return
if _CTX_IGNORE.get():
return
if _new_user := _CTX_OVERWRITE_USER.get():
user = _new_user
EventNewThread( EventNewThread(
EventAction.MODEL_DELETED, EventAction.MODEL_DELETED,
@ -184,6 +219,10 @@ class AuditMiddleware:
return return
if not should_log_m2m(instance): if not should_log_m2m(instance):
return return
if _CTX_IGNORE.get():
return
if _new_user := _CTX_OVERWRITE_USER.get():
user = _new_user
EventNewThread( EventNewThread(
EventAction.MODEL_UPDATED, EventAction.MODEL_UPDATED,

View File

@ -5,7 +5,9 @@ from rest_framework.test import APITestCase
from authentik.core.models import Application from authentik.core.models import Application
from authentik.core.tests.utils import create_test_admin_user 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.events.models import Event, EventAction
from authentik.lib.generators import generate_id
class TestEventsMiddleware(APITestCase): class TestEventsMiddleware(APITestCase):
@ -15,35 +17,74 @@ class TestEventsMiddleware(APITestCase):
super().setUp() super().setUp()
self.user = create_test_admin_user() self.user = create_test_admin_user()
self.client.force_login(self.user) self.client.force_login(self.user)
Event.objects.all().delete()
def test_create(self): def test_create(self):
"""Test model creation event""" """Test model creation event"""
uid = generate_id()
self.client.post( self.client.post(
reverse("authentik_api:application-list"), 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( self.assertTrue(
Event.objects.filter( Event.objects.filter(
action=EventAction.MODEL_CREATED, action=EventAction.MODEL_CREATED,
context__model__model_name="application", context__model__model_name="application",
context__model__app="authentik_core", context__model__app="authentik_core",
context__model__name="test-create", context__model__name=uid,
).exists() ).exists()
) )
def test_delete(self): def test_delete(self):
"""Test model creation event""" """Test model creation event"""
Application.objects.create(name="test-delete", slug="test-delete") uid = generate_id()
self.client.delete( Application.objects.create(name=uid, slug=uid)
reverse("authentik_api:application-detail", kwargs={"slug": "test-delete"}) self.client.delete(reverse("authentik_api:application-detail", kwargs={"slug": uid}))
)
self.assertFalse(Application.objects.filter(name="test").exists()) self.assertFalse(Application.objects.filter(name="test").exists())
self.assertTrue( self.assertTrue(
Event.objects.filter( Event.objects.filter(
action=EventAction.MODEL_DELETED, action=EventAction.MODEL_DELETED,
context__model__model_name="application", context__model__model_name="application",
context__model__app="authentik_core", 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() ).exists()
) )

View File

@ -31,6 +31,7 @@ from authentik.core.models import (
User, User,
UserTypes, UserTypes,
) )
from authentik.events.middleware import audit_ignore
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.events.signals import get_login_event from authentik.events.signals import get_login_event
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION 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): def __create_user_from_jwt(self, token: dict[str, Any], app: Application, source: OAuthSource):
"""Create user from JWT""" """Create user from JWT"""
self.user, created = User.objects.update_or_create( with audit_ignore():
username=f"{self.provider.name}-{token.get('sub')}", self.user, created = User.objects.update_or_create(
defaults={ username=f"{self.provider.name}-{token.get('sub')}",
"attributes": { defaults={
USER_ATTRIBUTE_GENERATED: True, "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)", exp = token.get("exp")
"path": source.get_user_path(), if created and exp:
"type": UserTypes.SERVICE_ACCOUNT, 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") @method_decorator(csrf_exempt, name="dispatch")

View File

@ -21,6 +21,7 @@ from webauthn.helpers.structs import UserVerificationRequirement
from authentik.core.api.utils import JSONDictField, PassiveSerializer from authentik.core.api.utils import JSONDictField, PassiveSerializer
from authentik.core.models import Application, User from authentik.core.models import Application, User
from authentik.core.signals import login_failed from authentik.core.signals import login_failed
from authentik.events.middleware import audit_ignore
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.flows.stage import StageView from authentik.flows.stage import StageView
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE 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 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 return device