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"""
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,

View File

@ -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()
)

View File

@ -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")

View File

@ -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