django-auditlog icon indicating copy to clipboard operation
django-auditlog copied to clipboard

X_Forwarded_For format is not guaranteed -> problems in Azure Web App

Open vikoivun opened this issue 5 years ago • 4 comments

Hello!

At https://github.com/jjkester/django-auditlog/blob/bcf1dbf0bf1781553877c4e806bab85dde96673e/src/auditlog/middleware.py#L41-L42 you read X-Forwarded-For if it is present and use it as an IP. Unfortunately at least Azure Web App Service also appends a colon separated port number. Ie. the format is X-Forwarded-For: {ip_address}:{port_number} which then breaks logging at database insertion time.

Might you consider adding option for using different headers for this purpose, and/or disabling this entirely? FWIW, Azure implements a "X-Client-IP" headers containing only the IP-address.

vikoivun avatar Feb 21 '20 11:02 vikoivun

I duplicated the middleware to fix this issue, might help someone in the future.

Couldn't just override as threading.local() stopped working that way.

import threading
import time
from functools import partial

from auditlog.models import LogEntry
from django.apps import apps
from django.conf import settings
from django.db.models.signals import pre_save
from django.utils.deprecation import MiddlewareMixin

threadlocal = threading.local()


class AuditlogMiddleware(MiddlewareMixin):
    def process_request(self, request):
        """
        Overridden to remove port numbers from remote_addr causing 500s on Azure.
        """
        # 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]

        # Azure fix, remove port number from remote_addr
        split_ip_and_port = threadlocal.auditlog['remote_addr'].split(':')
        if len(split_ip_and_port) == 2:
            threadlocal.auditlog['remote_addr'] = split_ip_and_port[0]

        # Connect signal for automatic logging
        if hasattr(request, 'user') and getattr(request.user, 'is_authenticated', False):
            set_actor = partial(
                self.set_actor,
                user=request.user,
                signal_duid=threadlocal.auditlog['signal_duid']
            )
            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.
        """
        if hasattr(threadlocal, 'auditlog'):
            pre_save.disconnect(sender=LogEntry, dispatch_uid=threadlocal.auditlog['signal_duid'])

        return response

    def process_exception(self, request, exception):
        """
        Disconnects the signal receiver to prevent it from staying active in case of an exception.
        """
        if hasattr(threadlocal, 'auditlog'):
            pre_save.disconnect(sender=LogEntry, dispatch_uid=threadlocal.auditlog['signal_duid'])

        return None

    @staticmethod
    def set_actor(user, sender, instance, signal_duid, **kwargs):
        """
        Signal receiver with an extra, required 'user' kwarg. This method becomes a real (valid) signal receiver when
        it is curried with the actor.
        """
        if hasattr(threadlocal, 'auditlog'):
            if signal_duid != threadlocal.auditlog['signal_duid']:
                return
            try:
                app_label, model_name = settings.AUTH_USER_MODEL.split('.')
                auth_user_model = apps.get_model(app_label, model_name)
            except ValueError:
                auth_user_model = apps.get_model('auth', 'user')
            if sender == LogEntry and isinstance(user, auth_user_model) and instance.actor is None:
                instance.actor = user

            instance.remote_addr = threadlocal.auditlog['remote_addr']

maziar-dandc avatar Apr 27 '21 11:04 maziar-dandc

Amazing answer @maziar-dandc why can't this be pushed as contrib PR?

cloudlessdreams avatar Feb 15 '22 15:02 cloudlessdreams

Amazing answer @maziar-dandc why can't this be pushed as contrib PR?

Feel free to PR it.

maziar-dandc avatar Feb 15 '22 16:02 maziar-dandc

I think I owe it that!

cloudlessdreams avatar Feb 15 '22 18:02 cloudlessdreams

fixed in https://github.com/jazzband/django-auditlog/commit/18868aaaed06131ef927e951940e654b1e20cdcd

hramezani avatar Aug 17 '22 11:08 hramezani