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 django_postgres_cache.models import CacheEntry 17from structlog.stdlib import BoundLogger, get_logger 18 19from authentik.blueprints.v1.importer import excluded_models 20from authentik.core.models import Group, User 21from authentik.events.models import Event, EventAction, Notification 22from authentik.events.utils import model_to_dict 23from authentik.lib.models import InternallyManagedMixin 24from authentik.lib.sentry import should_ignore_exception 25from authentik.lib.utils.errors import exception_to_dict 26from authentik.stages.authenticator_static.models import StaticToken 27 28IGNORED_MODELS = tuple( 29 excluded_models() 30 + ( 31 Event, 32 Notification, 33 StaticToken, 34 Session, 35 CacheEntry, 36 ) 37) 38 39_CTX_OVERWRITE_USER = ContextVar[User | None]("authentik_events_log_overwrite_user", default=None) 40_CTX_IGNORE = ContextVar[bool]("authentik_events_log_ignore", default=False) 41_CTX_REQUEST = ContextVar[HttpRequest | None]("authentik_events_log_request", default=None) 42 43 44def should_log_model(model: Model) -> bool: 45 """Return true if operation on `model` should be logged""" 46 return model.__class__ not in IGNORED_MODELS and not isinstance(model, InternallyManagedMixin) 47 48 49def should_log_m2m(model: Model) -> bool: 50 """Return true if m2m operation should be logged""" 51 if model.__class__ in [User, Group]: 52 return True 53 return False 54 55 56@contextmanager 57def audit_overwrite_user(user: User): 58 """Overwrite user being logged for model AuditMiddleware. Commonly used 59 for example in flows where a pending user is given, but the request is not authenticated yet""" 60 _CTX_OVERWRITE_USER.set(user) 61 try: 62 yield 63 finally: 64 _CTX_OVERWRITE_USER.set(None) 65 66 67@contextmanager 68def audit_ignore(): 69 """Ignore model operations in the block. Useful for objects which need to be modified 70 but are not excluded (e.g. WebAuthn devices)""" 71 _CTX_IGNORE.set(True) 72 try: 73 yield 74 finally: 75 _CTX_IGNORE.set(False) 76 77 78class EventNewThread(Thread): 79 """Create Event in background thread""" 80 81 action: str 82 request: HttpRequest 83 kwargs: dict[str, Any] 84 user: User | None = None 85 86 def __init__(self, action: str, request: HttpRequest, user: User | None = None, **kwargs): 87 super().__init__() 88 self.action = action 89 self.request = request 90 self.user = user 91 self.kwargs = kwargs 92 93 def run(self): 94 Event.new(self.action, **self.kwargs).from_http(self.request, user=self.user) 95 96 97class AuditMiddleware: 98 """Register handlers for duration of request-response that log creation/update/deletion 99 of models""" 100 101 get_response: Callable[[HttpRequest], HttpResponse] 102 anonymous_user: User = None 103 logger: BoundLogger 104 105 def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]): 106 self.get_response = get_response 107 self.logger = get_logger().bind() 108 109 def _ensure_fallback_user(self): 110 """Defer fetching anonymous user until we have to""" 111 if self.anonymous_user: 112 return 113 from guardian.shortcuts import get_anonymous_user 114 115 self.anonymous_user = get_anonymous_user() 116 117 def get_user(self, request: HttpRequest) -> User: 118 user = _CTX_OVERWRITE_USER.get() 119 if user: 120 return user 121 user = getattr(request, "user", self.anonymous_user) 122 if not user.is_authenticated: 123 self._ensure_fallback_user() 124 return self.anonymous_user 125 return user 126 127 def connect(self, request: HttpRequest): 128 """Connect signal for automatic logging""" 129 if not hasattr(request, "request_id"): 130 return 131 post_save.connect( 132 partial(self.post_save_handler, request=request), 133 dispatch_uid=request.request_id, 134 weak=False, 135 ) 136 pre_delete.connect( 137 partial(self.pre_delete_handler, request=request), 138 dispatch_uid=request.request_id, 139 weak=False, 140 ) 141 m2m_changed.connect( 142 partial(self.m2m_changed_handler, request=request), 143 dispatch_uid=request.request_id, 144 weak=False, 145 ) 146 147 def disconnect(self, request: HttpRequest): 148 """Disconnect signals""" 149 if not hasattr(request, "request_id"): 150 return 151 post_save.disconnect(dispatch_uid=request.request_id) 152 pre_delete.disconnect(dispatch_uid=request.request_id) 153 m2m_changed.disconnect(dispatch_uid=request.request_id) 154 155 def __call__(self, request: HttpRequest) -> HttpResponse: 156 _CTX_REQUEST.set(request) 157 self.connect(request) 158 159 response = self.get_response(request) 160 161 self.disconnect(request) 162 _CTX_REQUEST.set(None) 163 return response 164 165 def process_exception(self, request: HttpRequest, exception: Exception): 166 """Disconnect handlers in case of exception""" 167 self.disconnect(request) 168 169 if settings.DEBUG: 170 return 171 # Special case for SuspiciousOperation, we have a special event action for that 172 if isinstance(exception, SuspiciousOperation): 173 thread = EventNewThread( 174 EventAction.SUSPICIOUS_REQUEST, 175 request, 176 message=str(exception), 177 exception=exception_to_dict(exception), 178 ) 179 thread.run() 180 elif not should_ignore_exception(exception): 181 thread = EventNewThread( 182 EventAction.SYSTEM_EXCEPTION, 183 request, 184 message=str(exception), 185 exception=exception_to_dict(exception), 186 ) 187 thread.run() 188 189 def post_save_handler( 190 self, 191 request: HttpRequest, 192 sender, 193 instance: Model, 194 created: bool, 195 thread_kwargs: dict | None = None, 196 **_, 197 ): 198 """Signal handler for all object's post_save""" 199 if not should_log_model(instance): 200 return 201 if _CTX_IGNORE.get(): 202 return 203 current_request = _CTX_REQUEST.get() 204 if current_request is None or request.request_id != current_request.request_id: 205 return 206 user = self.get_user(request) 207 208 action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED 209 thread = EventNewThread(action, request, user=user, model=model_to_dict(instance)) 210 thread.kwargs.update(thread_kwargs or {}) 211 thread.run() 212 213 def pre_delete_handler(self, request: HttpRequest, sender, instance: Model, **_): 214 """Signal handler for all object's pre_delete""" 215 if not should_log_model(instance): # pragma: no cover 216 return 217 if _CTX_IGNORE.get(): 218 return 219 current_request = _CTX_REQUEST.get() 220 if current_request is None or request.request_id != current_request.request_id: 221 return 222 user = self.get_user(request) 223 224 EventNewThread( 225 EventAction.MODEL_DELETED, 226 request, 227 user=user, 228 model=model_to_dict(instance), 229 ).run() 230 231 def m2m_changed_handler( 232 self, 233 request: HttpRequest, 234 sender, 235 instance: Model, 236 action: str, 237 thread_kwargs: dict | None = None, 238 **_, 239 ): 240 """Signal handler for all object's m2m_changed""" 241 if action not in ["pre_add", "pre_remove", "post_clear"]: 242 return 243 if not should_log_m2m(instance): 244 return 245 if _CTX_IGNORE.get(): 246 return 247 current_request = _CTX_REQUEST.get() 248 if current_request is None or request.request_id != current_request.request_id: 249 return 250 user = self.get_user(request) 251 252 EventNewThread( 253 EventAction.MODEL_UPDATED, 254 request, 255 user=user, 256 model=model_to_dict(instance), 257 **thread_kwargs, 258 ).run()
45def should_log_model(model: Model) -> bool: 46 """Return true if operation on `model` should be logged""" 47 return model.__class__ not in IGNORED_MODELS and not isinstance(model, InternallyManagedMixin)
Return true if operation on model should be logged
50def should_log_m2m(model: Model) -> bool: 51 """Return true if m2m operation should be logged""" 52 if model.__class__ in [User, Group]: 53 return True 54 return False
Return true if m2m operation should be logged
57@contextmanager 58def audit_overwrite_user(user: User): 59 """Overwrite user being logged for model AuditMiddleware. Commonly used 60 for example in flows where a pending user is given, but the request is not authenticated yet""" 61 _CTX_OVERWRITE_USER.set(user) 62 try: 63 yield 64 finally: 65 _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
68@contextmanager 69def audit_ignore(): 70 """Ignore model operations in the block. Useful for objects which need to be modified 71 but are not excluded (e.g. WebAuthn devices)""" 72 _CTX_IGNORE.set(True) 73 try: 74 yield 75 finally: 76 _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)
79class EventNewThread(Thread): 80 """Create Event in background thread""" 81 82 action: str 83 request: HttpRequest 84 kwargs: dict[str, Any] 85 user: User | None = None 86 87 def __init__(self, action: str, request: HttpRequest, user: User | None = None, **kwargs): 88 super().__init__() 89 self.action = action 90 self.request = request 91 self.user = user 92 self.kwargs = kwargs 93 94 def run(self): 95 Event.new(self.action, **self.kwargs).from_http(self.request, user=self.user)
Create Event in background thread
87 def __init__(self, action: str, request: HttpRequest, user: User | None = None, **kwargs): 88 super().__init__() 89 self.action = action 90 self.request = request 91 self.user = user 92 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.
98class AuditMiddleware: 99 """Register handlers for duration of request-response that log creation/update/deletion 100 of models""" 101 102 get_response: Callable[[HttpRequest], HttpResponse] 103 anonymous_user: User = None 104 logger: BoundLogger 105 106 def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]): 107 self.get_response = get_response 108 self.logger = get_logger().bind() 109 110 def _ensure_fallback_user(self): 111 """Defer fetching anonymous user until we have to""" 112 if self.anonymous_user: 113 return 114 from guardian.shortcuts import get_anonymous_user 115 116 self.anonymous_user = get_anonymous_user() 117 118 def get_user(self, request: HttpRequest) -> User: 119 user = _CTX_OVERWRITE_USER.get() 120 if user: 121 return user 122 user = getattr(request, "user", self.anonymous_user) 123 if not user.is_authenticated: 124 self._ensure_fallback_user() 125 return self.anonymous_user 126 return user 127 128 def connect(self, request: HttpRequest): 129 """Connect signal for automatic logging""" 130 if not hasattr(request, "request_id"): 131 return 132 post_save.connect( 133 partial(self.post_save_handler, request=request), 134 dispatch_uid=request.request_id, 135 weak=False, 136 ) 137 pre_delete.connect( 138 partial(self.pre_delete_handler, request=request), 139 dispatch_uid=request.request_id, 140 weak=False, 141 ) 142 m2m_changed.connect( 143 partial(self.m2m_changed_handler, request=request), 144 dispatch_uid=request.request_id, 145 weak=False, 146 ) 147 148 def disconnect(self, request: HttpRequest): 149 """Disconnect signals""" 150 if not hasattr(request, "request_id"): 151 return 152 post_save.disconnect(dispatch_uid=request.request_id) 153 pre_delete.disconnect(dispatch_uid=request.request_id) 154 m2m_changed.disconnect(dispatch_uid=request.request_id) 155 156 def __call__(self, request: HttpRequest) -> HttpResponse: 157 _CTX_REQUEST.set(request) 158 self.connect(request) 159 160 response = self.get_response(request) 161 162 self.disconnect(request) 163 _CTX_REQUEST.set(None) 164 return response 165 166 def process_exception(self, request: HttpRequest, exception: Exception): 167 """Disconnect handlers in case of exception""" 168 self.disconnect(request) 169 170 if settings.DEBUG: 171 return 172 # Special case for SuspiciousOperation, we have a special event action for that 173 if isinstance(exception, SuspiciousOperation): 174 thread = EventNewThread( 175 EventAction.SUSPICIOUS_REQUEST, 176 request, 177 message=str(exception), 178 exception=exception_to_dict(exception), 179 ) 180 thread.run() 181 elif not should_ignore_exception(exception): 182 thread = EventNewThread( 183 EventAction.SYSTEM_EXCEPTION, 184 request, 185 message=str(exception), 186 exception=exception_to_dict(exception), 187 ) 188 thread.run() 189 190 def post_save_handler( 191 self, 192 request: HttpRequest, 193 sender, 194 instance: Model, 195 created: bool, 196 thread_kwargs: dict | None = None, 197 **_, 198 ): 199 """Signal handler for all object's post_save""" 200 if not should_log_model(instance): 201 return 202 if _CTX_IGNORE.get(): 203 return 204 current_request = _CTX_REQUEST.get() 205 if current_request is None or request.request_id != current_request.request_id: 206 return 207 user = self.get_user(request) 208 209 action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED 210 thread = EventNewThread(action, request, user=user, model=model_to_dict(instance)) 211 thread.kwargs.update(thread_kwargs or {}) 212 thread.run() 213 214 def pre_delete_handler(self, request: HttpRequest, sender, instance: Model, **_): 215 """Signal handler for all object's pre_delete""" 216 if not should_log_model(instance): # pragma: no cover 217 return 218 if _CTX_IGNORE.get(): 219 return 220 current_request = _CTX_REQUEST.get() 221 if current_request is None or request.request_id != current_request.request_id: 222 return 223 user = self.get_user(request) 224 225 EventNewThread( 226 EventAction.MODEL_DELETED, 227 request, 228 user=user, 229 model=model_to_dict(instance), 230 ).run() 231 232 def m2m_changed_handler( 233 self, 234 request: HttpRequest, 235 sender, 236 instance: Model, 237 action: str, 238 thread_kwargs: dict | None = None, 239 **_, 240 ): 241 """Signal handler for all object's m2m_changed""" 242 if action not in ["pre_add", "pre_remove", "post_clear"]: 243 return 244 if not should_log_m2m(instance): 245 return 246 if _CTX_IGNORE.get(): 247 return 248 current_request = _CTX_REQUEST.get() 249 if current_request is None or request.request_id != current_request.request_id: 250 return 251 user = self.get_user(request) 252 253 EventNewThread( 254 EventAction.MODEL_UPDATED, 255 request, 256 user=user, 257 model=model_to_dict(instance), 258 **thread_kwargs, 259 ).run()
Register handlers for duration of request-response that log creation/update/deletion of models
128 def connect(self, request: HttpRequest): 129 """Connect signal for automatic logging""" 130 if not hasattr(request, "request_id"): 131 return 132 post_save.connect( 133 partial(self.post_save_handler, request=request), 134 dispatch_uid=request.request_id, 135 weak=False, 136 ) 137 pre_delete.connect( 138 partial(self.pre_delete_handler, request=request), 139 dispatch_uid=request.request_id, 140 weak=False, 141 ) 142 m2m_changed.connect( 143 partial(self.m2m_changed_handler, request=request), 144 dispatch_uid=request.request_id, 145 weak=False, 146 )
Connect signal for automatic logging
148 def disconnect(self, request: HttpRequest): 149 """Disconnect signals""" 150 if not hasattr(request, "request_id"): 151 return 152 post_save.disconnect(dispatch_uid=request.request_id) 153 pre_delete.disconnect(dispatch_uid=request.request_id) 154 m2m_changed.disconnect(dispatch_uid=request.request_id)
Disconnect signals
166 def process_exception(self, request: HttpRequest, exception: Exception): 167 """Disconnect handlers in case of exception""" 168 self.disconnect(request) 169 170 if settings.DEBUG: 171 return 172 # Special case for SuspiciousOperation, we have a special event action for that 173 if isinstance(exception, SuspiciousOperation): 174 thread = EventNewThread( 175 EventAction.SUSPICIOUS_REQUEST, 176 request, 177 message=str(exception), 178 exception=exception_to_dict(exception), 179 ) 180 thread.run() 181 elif not should_ignore_exception(exception): 182 thread = EventNewThread( 183 EventAction.SYSTEM_EXCEPTION, 184 request, 185 message=str(exception), 186 exception=exception_to_dict(exception), 187 ) 188 thread.run()
Disconnect handlers in case of exception
190 def post_save_handler( 191 self, 192 request: HttpRequest, 193 sender, 194 instance: Model, 195 created: bool, 196 thread_kwargs: dict | None = None, 197 **_, 198 ): 199 """Signal handler for all object's post_save""" 200 if not should_log_model(instance): 201 return 202 if _CTX_IGNORE.get(): 203 return 204 current_request = _CTX_REQUEST.get() 205 if current_request is None or request.request_id != current_request.request_id: 206 return 207 user = self.get_user(request) 208 209 action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED 210 thread = EventNewThread(action, request, user=user, model=model_to_dict(instance)) 211 thread.kwargs.update(thread_kwargs or {}) 212 thread.run()
Signal handler for all object's post_save
214 def pre_delete_handler(self, request: HttpRequest, sender, instance: Model, **_): 215 """Signal handler for all object's pre_delete""" 216 if not should_log_model(instance): # pragma: no cover 217 return 218 if _CTX_IGNORE.get(): 219 return 220 current_request = _CTX_REQUEST.get() 221 if current_request is None or request.request_id != current_request.request_id: 222 return 223 user = self.get_user(request) 224 225 EventNewThread( 226 EventAction.MODEL_DELETED, 227 request, 228 user=user, 229 model=model_to_dict(instance), 230 ).run()
Signal handler for all object's pre_delete
232 def m2m_changed_handler( 233 self, 234 request: HttpRequest, 235 sender, 236 instance: Model, 237 action: str, 238 thread_kwargs: dict | None = None, 239 **_, 240 ): 241 """Signal handler for all object's m2m_changed""" 242 if action not in ["pre_add", "pre_remove", "post_clear"]: 243 return 244 if not should_log_m2m(instance): 245 return 246 if _CTX_IGNORE.get(): 247 return 248 current_request = _CTX_REQUEST.get() 249 if current_request is None or request.request_id != current_request.request_id: 250 return 251 user = self.get_user(request) 252 253 EventNewThread( 254 EventAction.MODEL_UPDATED, 255 request, 256 user=user, 257 model=model_to_dict(instance), 258 **thread_kwargs, 259 ).run()
Signal handler for all object's m2m_changed