authentik.events.middleware
Events middleware
1"""Events middleware""" 2 3from collections.abc import Callable 4from contextlib import contextmanager 5from contextvars import ContextVar 6from functools import partial 7from threading import Thread 8from typing import Any 9 10from django.conf import settings 11from django.contrib.sessions.models import Session 12from django.core.exceptions import SuspiciousOperation 13from django.db.models import Model 14from django.db.models.signals import m2m_changed, post_save, pre_delete 15from django.http import HttpRequest, HttpResponse 16from structlog.stdlib import BoundLogger, get_logger 17 18from authentik.blueprints.v1.importer import excluded_models 19from authentik.core.models import Group, User 20from authentik.events.models import Event, EventAction, Notification 21from authentik.events.utils import model_to_dict 22from authentik.lib.models import InternallyManagedMixin 23from authentik.lib.sentry import should_ignore_exception 24from authentik.lib.utils.errors import exception_to_dict 25from authentik.stages.authenticator_static.models import StaticToken 26 27IGNORED_MODELS = tuple( 28 excluded_models() 29 + ( 30 Event, 31 Notification, 32 StaticToken, 33 Session, 34 ) 35) 36 37_CTX_OVERWRITE_USER = ContextVar[User | None]("authentik_events_log_overwrite_user", default=None) 38_CTX_IGNORE = ContextVar[bool]("authentik_events_log_ignore", default=False) 39_CTX_REQUEST = ContextVar[HttpRequest | None]("authentik_events_log_request", default=None) 40 41 42def should_log_model(model: Model) -> bool: 43 """Return true if operation on `model` should be logged""" 44 return model.__class__ not in IGNORED_MODELS and not isinstance(model, InternallyManagedMixin) 45 46 47def should_log_m2m(model: Model) -> bool: 48 """Return true if m2m operation should be logged""" 49 if model.__class__ in [User, Group]: 50 return True 51 return False 52 53 54@contextmanager 55def audit_overwrite_user(user: User): 56 """Overwrite user being logged for model AuditMiddleware. Commonly used 57 for example in flows where a pending user is given, but the request is not authenticated yet""" 58 _CTX_OVERWRITE_USER.set(user) 59 try: 60 yield 61 finally: 62 _CTX_OVERWRITE_USER.set(None) 63 64 65@contextmanager 66def audit_ignore(): 67 """Ignore model operations in the block. Useful for objects which need to be modified 68 but are not excluded (e.g. WebAuthn devices)""" 69 _CTX_IGNORE.set(True) 70 try: 71 yield 72 finally: 73 _CTX_IGNORE.set(False) 74 75 76class EventNewThread(Thread): 77 """Create Event in background thread""" 78 79 action: str 80 request: HttpRequest 81 kwargs: dict[str, Any] 82 user: User | None = None 83 84 def __init__(self, action: str, request: HttpRequest, user: User | None = None, **kwargs): 85 super().__init__() 86 self.action = action 87 self.request = request 88 self.user = user 89 self.kwargs = kwargs 90 91 def run(self): 92 Event.new(self.action, **self.kwargs).from_http(self.request, user=self.user) 93 94 95class AuditMiddleware: 96 """Register handlers for duration of request-response that log creation/update/deletion 97 of models""" 98 99 get_response: Callable[[HttpRequest], HttpResponse] 100 anonymous_user: User = None 101 logger: BoundLogger 102 103 def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]): 104 self.get_response = get_response 105 self.logger = get_logger().bind() 106 107 def _ensure_fallback_user(self): 108 """Defer fetching anonymous user until we have to""" 109 if self.anonymous_user: 110 return 111 from guardian.shortcuts import get_anonymous_user 112 113 self.anonymous_user = get_anonymous_user() 114 115 def get_user(self, request: HttpRequest) -> User: 116 user = _CTX_OVERWRITE_USER.get() 117 if user: 118 return user 119 user = getattr(request, "user", self.anonymous_user) 120 if not user.is_authenticated: 121 self._ensure_fallback_user() 122 return self.anonymous_user 123 return user 124 125 def connect(self, request: HttpRequest): 126 """Connect signal for automatic logging""" 127 if not hasattr(request, "request_id"): 128 return 129 post_save.connect( 130 partial(self.post_save_handler, request=request), 131 dispatch_uid=request.request_id, 132 weak=False, 133 ) 134 pre_delete.connect( 135 partial(self.pre_delete_handler, request=request), 136 dispatch_uid=request.request_id, 137 weak=False, 138 ) 139 m2m_changed.connect( 140 partial(self.m2m_changed_handler, request=request), 141 dispatch_uid=request.request_id, 142 weak=False, 143 ) 144 145 def disconnect(self, request: HttpRequest): 146 """Disconnect signals""" 147 if not hasattr(request, "request_id"): 148 return 149 post_save.disconnect(dispatch_uid=request.request_id) 150 pre_delete.disconnect(dispatch_uid=request.request_id) 151 m2m_changed.disconnect(dispatch_uid=request.request_id) 152 153 def __call__(self, request: HttpRequest) -> HttpResponse: 154 _CTX_REQUEST.set(request) 155 self.connect(request) 156 157 response = self.get_response(request) 158 159 self.disconnect(request) 160 _CTX_REQUEST.set(None) 161 return response 162 163 def process_exception(self, request: HttpRequest, exception: Exception): 164 """Disconnect handlers in case of exception""" 165 self.disconnect(request) 166 167 if settings.DEBUG: 168 return 169 # Special case for SuspiciousOperation, we have a special event action for that 170 if isinstance(exception, SuspiciousOperation): 171 thread = EventNewThread( 172 EventAction.SUSPICIOUS_REQUEST, 173 request, 174 message=str(exception), 175 exception=exception_to_dict(exception), 176 ) 177 thread.run() 178 elif not should_ignore_exception(exception): 179 thread = EventNewThread( 180 EventAction.SYSTEM_EXCEPTION, 181 request, 182 message=str(exception), 183 exception=exception_to_dict(exception), 184 ) 185 thread.run() 186 187 def post_save_handler( 188 self, 189 request: HttpRequest, 190 sender, 191 instance: Model, 192 created: bool, 193 thread_kwargs: dict | None = None, 194 **_, 195 ): 196 """Signal handler for all object's post_save""" 197 if not should_log_model(instance): 198 return 199 if _CTX_IGNORE.get(): 200 return 201 current_request = _CTX_REQUEST.get() 202 if current_request is None or request.request_id != current_request.request_id: 203 return 204 user = self.get_user(request) 205 206 action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED 207 thread = EventNewThread(action, request, user=user, model=model_to_dict(instance)) 208 thread.kwargs.update(thread_kwargs or {}) 209 thread.run() 210 211 def pre_delete_handler(self, request: HttpRequest, sender, instance: Model, **_): 212 """Signal handler for all object's pre_delete""" 213 if not should_log_model(instance): # pragma: no cover 214 return 215 if _CTX_IGNORE.get(): 216 return 217 current_request = _CTX_REQUEST.get() 218 if current_request is None or request.request_id != current_request.request_id: 219 return 220 user = self.get_user(request) 221 222 EventNewThread( 223 EventAction.MODEL_DELETED, 224 request, 225 user=user, 226 model=model_to_dict(instance), 227 ).run() 228 229 def m2m_changed_handler( 230 self, 231 request: HttpRequest, 232 sender, 233 instance: Model, 234 action: str, 235 thread_kwargs: dict | None = None, 236 **_, 237 ): 238 """Signal handler for all object's m2m_changed""" 239 if action not in ["pre_add", "pre_remove", "post_clear"]: 240 return 241 if not should_log_m2m(instance): 242 return 243 if _CTX_IGNORE.get(): 244 return 245 current_request = _CTX_REQUEST.get() 246 if current_request is None or request.request_id != current_request.request_id: 247 return 248 user = self.get_user(request) 249 250 EventNewThread( 251 EventAction.MODEL_UPDATED, 252 request, 253 user=user, 254 model=model_to_dict(instance), 255 **thread_kwargs, 256 ).run()
43def should_log_model(model: Model) -> bool: 44 """Return true if operation on `model` should be logged""" 45 return model.__class__ not in IGNORED_MODELS and not isinstance(model, InternallyManagedMixin)
Return true if operation on model should be logged
48def should_log_m2m(model: Model) -> bool: 49 """Return true if m2m operation should be logged""" 50 if model.__class__ in [User, Group]: 51 return True 52 return False
Return true if m2m operation should be logged
55@contextmanager 56def audit_overwrite_user(user: User): 57 """Overwrite user being logged for model AuditMiddleware. Commonly used 58 for example in flows where a pending user is given, but the request is not authenticated yet""" 59 _CTX_OVERWRITE_USER.set(user) 60 try: 61 yield 62 finally: 63 _CTX_OVERWRITE_USER.set(None)
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
66@contextmanager 67def audit_ignore(): 68 """Ignore model operations in the block. Useful for objects which need to be modified 69 but are not excluded (e.g. WebAuthn devices)""" 70 _CTX_IGNORE.set(True) 71 try: 72 yield 73 finally: 74 _CTX_IGNORE.set(False)
Ignore model operations in the block. Useful for objects which need to be modified but are not excluded (e.g. WebAuthn devices)
77class EventNewThread(Thread): 78 """Create Event in background thread""" 79 80 action: str 81 request: HttpRequest 82 kwargs: dict[str, Any] 83 user: User | None = None 84 85 def __init__(self, action: str, request: HttpRequest, user: User | None = None, **kwargs): 86 super().__init__() 87 self.action = action 88 self.request = request 89 self.user = user 90 self.kwargs = kwargs 91 92 def run(self): 93 Event.new(self.action, **self.kwargs).from_http(self.request, user=self.user)
Create Event in background thread
85 def __init__(self, action: str, request: HttpRequest, user: User | None = None, **kwargs): 86 super().__init__() 87 self.action = action 88 self.request = request 89 self.user = user 90 self.kwargs = kwargs
This constructor should always be called with keyword arguments. Arguments are:
group should be None; reserved for future extension when a ThreadGroup class is implemented.
target is the callable object to be invoked by the run() method. Defaults to None, meaning nothing is called.
name is the thread name. By default, a unique name is constructed of the form "Thread-N" where N is a small decimal number.
args is a list or tuple of arguments for the target invocation. Defaults to ().
kwargs is a dictionary of keyword arguments for the target invocation. Defaults to {}.
context is the contextvars.Context value to use for the thread. The default value is None, which means to check sys.flags.thread_inherit_context. If that flag is true, use a copy of the context of the caller. If false, use an empty context. To explicitly start with an empty context, pass a new instance of contextvars.Context(). To explicitly start with a copy of the current context, pass the value from contextvars.copy_context().
If a subclass overrides the constructor, it must make sure to invoke the base class constructor (Thread.__init__()) before doing anything else to the thread.
Method representing the thread's activity.
You may override this method in a subclass. The standard run() method invokes the callable object passed to the object's constructor as the target argument, if any, with sequential and keyword arguments taken from the args and kwargs arguments, respectively.
96class AuditMiddleware: 97 """Register handlers for duration of request-response that log creation/update/deletion 98 of models""" 99 100 get_response: Callable[[HttpRequest], HttpResponse] 101 anonymous_user: User = None 102 logger: BoundLogger 103 104 def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]): 105 self.get_response = get_response 106 self.logger = get_logger().bind() 107 108 def _ensure_fallback_user(self): 109 """Defer fetching anonymous user until we have to""" 110 if self.anonymous_user: 111 return 112 from guardian.shortcuts import get_anonymous_user 113 114 self.anonymous_user = get_anonymous_user() 115 116 def get_user(self, request: HttpRequest) -> User: 117 user = _CTX_OVERWRITE_USER.get() 118 if user: 119 return user 120 user = getattr(request, "user", self.anonymous_user) 121 if not user.is_authenticated: 122 self._ensure_fallback_user() 123 return self.anonymous_user 124 return user 125 126 def connect(self, request: HttpRequest): 127 """Connect signal for automatic logging""" 128 if not hasattr(request, "request_id"): 129 return 130 post_save.connect( 131 partial(self.post_save_handler, request=request), 132 dispatch_uid=request.request_id, 133 weak=False, 134 ) 135 pre_delete.connect( 136 partial(self.pre_delete_handler, request=request), 137 dispatch_uid=request.request_id, 138 weak=False, 139 ) 140 m2m_changed.connect( 141 partial(self.m2m_changed_handler, request=request), 142 dispatch_uid=request.request_id, 143 weak=False, 144 ) 145 146 def disconnect(self, request: HttpRequest): 147 """Disconnect signals""" 148 if not hasattr(request, "request_id"): 149 return 150 post_save.disconnect(dispatch_uid=request.request_id) 151 pre_delete.disconnect(dispatch_uid=request.request_id) 152 m2m_changed.disconnect(dispatch_uid=request.request_id) 153 154 def __call__(self, request: HttpRequest) -> HttpResponse: 155 _CTX_REQUEST.set(request) 156 self.connect(request) 157 158 response = self.get_response(request) 159 160 self.disconnect(request) 161 _CTX_REQUEST.set(None) 162 return response 163 164 def process_exception(self, request: HttpRequest, exception: Exception): 165 """Disconnect handlers in case of exception""" 166 self.disconnect(request) 167 168 if settings.DEBUG: 169 return 170 # Special case for SuspiciousOperation, we have a special event action for that 171 if isinstance(exception, SuspiciousOperation): 172 thread = EventNewThread( 173 EventAction.SUSPICIOUS_REQUEST, 174 request, 175 message=str(exception), 176 exception=exception_to_dict(exception), 177 ) 178 thread.run() 179 elif not should_ignore_exception(exception): 180 thread = EventNewThread( 181 EventAction.SYSTEM_EXCEPTION, 182 request, 183 message=str(exception), 184 exception=exception_to_dict(exception), 185 ) 186 thread.run() 187 188 def post_save_handler( 189 self, 190 request: HttpRequest, 191 sender, 192 instance: Model, 193 created: bool, 194 thread_kwargs: dict | None = None, 195 **_, 196 ): 197 """Signal handler for all object's post_save""" 198 if not should_log_model(instance): 199 return 200 if _CTX_IGNORE.get(): 201 return 202 current_request = _CTX_REQUEST.get() 203 if current_request is None or request.request_id != current_request.request_id: 204 return 205 user = self.get_user(request) 206 207 action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED 208 thread = EventNewThread(action, request, user=user, model=model_to_dict(instance)) 209 thread.kwargs.update(thread_kwargs or {}) 210 thread.run() 211 212 def pre_delete_handler(self, request: HttpRequest, sender, instance: Model, **_): 213 """Signal handler for all object's pre_delete""" 214 if not should_log_model(instance): # pragma: no cover 215 return 216 if _CTX_IGNORE.get(): 217 return 218 current_request = _CTX_REQUEST.get() 219 if current_request is None or request.request_id != current_request.request_id: 220 return 221 user = self.get_user(request) 222 223 EventNewThread( 224 EventAction.MODEL_DELETED, 225 request, 226 user=user, 227 model=model_to_dict(instance), 228 ).run() 229 230 def m2m_changed_handler( 231 self, 232 request: HttpRequest, 233 sender, 234 instance: Model, 235 action: str, 236 thread_kwargs: dict | None = None, 237 **_, 238 ): 239 """Signal handler for all object's m2m_changed""" 240 if action not in ["pre_add", "pre_remove", "post_clear"]: 241 return 242 if not should_log_m2m(instance): 243 return 244 if _CTX_IGNORE.get(): 245 return 246 current_request = _CTX_REQUEST.get() 247 if current_request is None or request.request_id != current_request.request_id: 248 return 249 user = self.get_user(request) 250 251 EventNewThread( 252 EventAction.MODEL_UPDATED, 253 request, 254 user=user, 255 model=model_to_dict(instance), 256 **thread_kwargs, 257 ).run()
Register handlers for duration of request-response that log creation/update/deletion of models
126 def connect(self, request: HttpRequest): 127 """Connect signal for automatic logging""" 128 if not hasattr(request, "request_id"): 129 return 130 post_save.connect( 131 partial(self.post_save_handler, request=request), 132 dispatch_uid=request.request_id, 133 weak=False, 134 ) 135 pre_delete.connect( 136 partial(self.pre_delete_handler, request=request), 137 dispatch_uid=request.request_id, 138 weak=False, 139 ) 140 m2m_changed.connect( 141 partial(self.m2m_changed_handler, request=request), 142 dispatch_uid=request.request_id, 143 weak=False, 144 )
Connect signal for automatic logging
146 def disconnect(self, request: HttpRequest): 147 """Disconnect signals""" 148 if not hasattr(request, "request_id"): 149 return 150 post_save.disconnect(dispatch_uid=request.request_id) 151 pre_delete.disconnect(dispatch_uid=request.request_id) 152 m2m_changed.disconnect(dispatch_uid=request.request_id)
Disconnect signals
164 def process_exception(self, request: HttpRequest, exception: Exception): 165 """Disconnect handlers in case of exception""" 166 self.disconnect(request) 167 168 if settings.DEBUG: 169 return 170 # Special case for SuspiciousOperation, we have a special event action for that 171 if isinstance(exception, SuspiciousOperation): 172 thread = EventNewThread( 173 EventAction.SUSPICIOUS_REQUEST, 174 request, 175 message=str(exception), 176 exception=exception_to_dict(exception), 177 ) 178 thread.run() 179 elif not should_ignore_exception(exception): 180 thread = EventNewThread( 181 EventAction.SYSTEM_EXCEPTION, 182 request, 183 message=str(exception), 184 exception=exception_to_dict(exception), 185 ) 186 thread.run()
Disconnect handlers in case of exception
188 def post_save_handler( 189 self, 190 request: HttpRequest, 191 sender, 192 instance: Model, 193 created: bool, 194 thread_kwargs: dict | None = None, 195 **_, 196 ): 197 """Signal handler for all object's post_save""" 198 if not should_log_model(instance): 199 return 200 if _CTX_IGNORE.get(): 201 return 202 current_request = _CTX_REQUEST.get() 203 if current_request is None or request.request_id != current_request.request_id: 204 return 205 user = self.get_user(request) 206 207 action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED 208 thread = EventNewThread(action, request, user=user, model=model_to_dict(instance)) 209 thread.kwargs.update(thread_kwargs or {}) 210 thread.run()
Signal handler for all object's post_save
212 def pre_delete_handler(self, request: HttpRequest, sender, instance: Model, **_): 213 """Signal handler for all object's pre_delete""" 214 if not should_log_model(instance): # pragma: no cover 215 return 216 if _CTX_IGNORE.get(): 217 return 218 current_request = _CTX_REQUEST.get() 219 if current_request is None or request.request_id != current_request.request_id: 220 return 221 user = self.get_user(request) 222 223 EventNewThread( 224 EventAction.MODEL_DELETED, 225 request, 226 user=user, 227 model=model_to_dict(instance), 228 ).run()
Signal handler for all object's pre_delete
230 def m2m_changed_handler( 231 self, 232 request: HttpRequest, 233 sender, 234 instance: Model, 235 action: str, 236 thread_kwargs: dict | None = None, 237 **_, 238 ): 239 """Signal handler for all object's m2m_changed""" 240 if action not in ["pre_add", "pre_remove", "post_clear"]: 241 return 242 if not should_log_m2m(instance): 243 return 244 if _CTX_IGNORE.get(): 245 return 246 current_request = _CTX_REQUEST.get() 247 if current_request is None or request.request_id != current_request.request_id: 248 return 249 user = self.get_user(request) 250 251 EventNewThread( 252 EventAction.MODEL_UPDATED, 253 request, 254 user=user, 255 model=model_to_dict(instance), 256 **thread_kwargs, 257 ).run()
Signal handler for all object's m2m_changed