diff --git a/docs/source/usage.rst b/docs/source/usage.rst index e043d31..f473436 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -25,7 +25,8 @@ Actors ------ When using automatic logging, the actor is empty by default. However, auditlog can set the actor from the current -request automatically. This does not need any custom code, adding a middleware class is enough. +request automatically. This does not need any custom code, adding a middleware class is enough. When an actor is logged +the remote address of that actor will be logged as well. To enable the automatic logging of the actors, simply add the following to your ``MIDDLEWARE_CLASSES`` setting in your project's configuration file:: diff --git a/src/auditlog/middleware.py b/src/auditlog/middleware.py index b62a9f4..14263cb 100644 --- a/src/auditlog/middleware.py +++ b/src/auditlog/middleware.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import threading import time from django.conf import settings @@ -9,6 +10,9 @@ from django.db.models.loading import get_model from auditlog.models import LogEntry +threadlocal = threading.local() + + class AuditlogMiddleware(object): """ Middleware to couple the request's user to log items. This is accomplished by currying the signal receiver with the @@ -20,19 +24,27 @@ class AuditlogMiddleware(object): Gets the current user from the request and prepares and connects a signal receiver with the user already attached to it. """ + # Initialize thread local storage + threadlocal.auditlog = { + 'signal_duid': (self.__class__, time.time()), + 'remote_addr': request.META.get('REMOTE_ADDR'), + } + + # In case of proxy, set 'original' address + if request.META.get('HTTP_X_FORWARDED_FOR'): + threadlocal.auditlog['remote_addr'] = request.META.get('HTTP_X_FORWARDED_FOR').split(',')[0] + + # Connect signal for automatic logging if hasattr(request, 'user') and hasattr(request.user, 'is_authenticated') and request.user.is_authenticated(): - user = request.user - request.auditlog_ts = time.time() - set_actor = curry(self.set_actor, user) - pre_save.connect(set_actor, sender=LogEntry, dispatch_uid=(self.__class__, request.auditlog_ts), weak=False) + set_actor = curry(self.set_actor, request.user) + pre_save.connect(set_actor, sender=LogEntry, dispatch_uid=threadlocal.auditlog['signal_duid'], weak=False) def process_response(self, request, response): """ Disconnects the signal receiver to prevent it from staying active. """ - # Disconnecting the signal receiver is required because it will not be garbage collected (non-weak reference) - if hasattr(request, 'auditlog_ts'): - pre_save.disconnect(sender=LogEntry, dispatch_uid=(self.__class__, request.auditlog_ts)) + if hasattr(threadlocal, 'auditlog'): + pre_save.disconnect(sender=LogEntry, dispatch_uid=threadlocal.auditlog['signal_duid']) return response @@ -40,8 +52,8 @@ class AuditlogMiddleware(object): """ Disconnects the signal receiver to prevent it from staying active in case of an exception. """ - if hasattr(request, 'auditlog_ts'): - pre_save.disconnect(sender=LogEntry, dispatch_uid=(self.__class__, request.auditlog_ts)) + if hasattr(threadlocal, 'auditlog'): + pre_save.disconnect(sender=LogEntry, dispatch_uid=threadlocal.auditlog['signal_duid']) return None @@ -58,3 +70,5 @@ class AuditlogMiddleware(object): auth_user_model = get_model('auth', 'user') if sender == LogEntry and isinstance(user, auth_user_model) and instance.actor is None: instance.actor = user + if hasattr(threadlocal, 'auditlog'): + instance.remote_addr = threading.local().auditlog['remote_addr'] diff --git a/src/auditlog/migrations/0003_logentry_remote_addr.py b/src/auditlog/migrations/0003_logentry_remote_addr.py new file mode 100644 index 0000000..948f63c --- /dev/null +++ b/src/auditlog/migrations/0003_logentry_remote_addr.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('auditlog', '0002_auto_support_long_primary_keys'), + ] + + operations = [ + migrations.AddField( + model_name='logentry', + name='remote_addr', + field=models.GenericIPAddressField(null=True, verbose_name='remote address', blank=True), + ), + ] diff --git a/src/auditlog/models.py b/src/auditlog/models.py index 724bb66..a4dc02d 100644 --- a/src/auditlog/models.py +++ b/src/auditlog/models.py @@ -143,6 +143,7 @@ class LogEntry(models.Model): action = models.PositiveSmallIntegerField(choices=Action.choices, verbose_name=_("action")) changes = models.TextField(blank=True, verbose_name=_("change message")) actor = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL, related_name='+', verbose_name=_("actor")) + remote_addr = models.GenericIPAddressField(blank=True, null=True, verbose_name=_("remote address")) timestamp = models.DateTimeField(auto_now_add=True, verbose_name=_("timestamp")) additional_data = JSONField(blank=True, null=True, verbose_name=_("additional data")) diff --git a/src/auditlog_tests/test_settings.py b/src/auditlog_tests/test_settings.py index de5c684..fa2826f 100644 --- a/src/auditlog_tests/test_settings.py +++ b/src/auditlog_tests/test_settings.py @@ -19,6 +19,6 @@ MIDDLEWARE_CLASSES = ( DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': 'auditlog_tests', + 'NAME': 'auditlog_tests.db', } }