django-auditlog: Automatic Logging doesn't log the Actor

I have installed the plugin and on an Django Rest Framework instance. All the logging is working as expected, except the Logging of the Actor. I added the Middleware as Described in the Docs but its still not logging any Actor.

Here is my configuration:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'corsheaders',
    'oauth2_provider',
    'rest_framework',
    'auditlog',
    'stammdaten.apps.StammdatenConfig',
    'main.apps.MainConfig',
    'rest_framework_swagger',
]

OAUTH2_PROVIDER = {
    # this is the list of available scopes
    'SCOPES': {'read': 'Read scope', 'write': 'Write scope', 'groups': 'Access to your groups'},
    'ACCESS_TOKEN_EXPIRE_SECONDS': 43200
}

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'oauth2_provider.ext.rest_framework.OAuth2Authentication',
    ),
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),
    'PAGE_SIZE': 10
}

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
    'oauth2_provider.middleware.OAuth2TokenMiddleware',
    'auditlog.middleware.AuditlogMiddleware'
]

What am I missing here? Is it a problem with the OAuth Toolkit or did I missconfig anything?

About this issue

  • Original URL
  • State: open
  • Created 7 years ago
  • Comments: 56 (11 by maintainers)

Most upvoted comments

In my case I solved this by modifying the AuditlogMiddleware and making the user a lazy object.

from auditlog.context import set_actor
from auditlog.middleware import AuditlogMiddleware as _AuditlogMiddleware
from django.utils.functional import SimpleLazyObject


class AuditlogMiddleware(_AuditlogMiddleware):
    def __call__(self, request):
        remote_addr = self._get_remote_addr(request)

        user = SimpleLazyObject(lambda: getattr(request, "user", None))

        context = set_actor(actor=user, remote_addr=remote_addr)

        with context:
            return self.get_response(request)

And then use this custom middleware in settings.MIDDLEWARE.

Hello everyone!

I had the same issue when trying to integrate django-auditlog with Django Rest Framework (DRF).

I have been digging into the source code of both projects to understand the reason the actor is not being set and found a reason.

django-auditlog expects the user being logged at Django’s middleware layer as usual but DRF, for design decision, does not perform the authentication at middleware level instead it performs the authentication at View level by using tte configured mechanisms. It means just before executing the code which processes the request and generates a response. Here is the difference with Django itself! That is the reason why is_autenticated is always False at the middleware layer, as result the handler to set the actor on pre_save signal is not being connected.

I’ve created a glue code which works as integration between both projects. The approach I took is to use mixins as many components of DRF does . This integrations is not coupled with any AUTH mechanism which is good, it leaves that behavior to DRF, that is a big difference with the approach taken by @ivan-fedorov-probegin (Im not telling it is bad!) . I’ve been using this integration in production without issues at all 😃. I would like to know if you are open to merge some DRF integrations… In that case I’ll be happy to propose a pull request. (cc @jjkester )

Integration mixin

# mixins.py
from django.db.models.signals import pre_save
from django.utils.functional import curry

from auditlog.compat import is_authenticated
from auditlog.middleware import threadlocal, AuditlogMiddleware
from auditlog.models import LogEntry


class DRFDjangoAuditModelMixin:
    """
    Mixin to integrate django-auditlog with Django Rest Framework.

    This is needed because DRF does not perform the authentication at middleware layer
    instead it performs the authentication at View layer.

    This mixin adds behavior to connect/disconnect the signals needed by django-auditlog to auto
    log changes on models.
    It assumes that AuditlogMiddleware is activated in settings.MIDDLEWARE_CLASSES
    """

    def should_connect_signals(self, request):
        """Determines if the signals should be connected for the incoming request."""
        # By default only makes sense to audit when the user is authenticated
        return is_authenticated(request.user)

    def initial(self, request, *args, **kwargs):
        """Overwritten to use django-auditlog if needed."""
        super().initial(request, *args, **kwargs)

        if self.should_connect_signals(request):
            set_actor = curry(AuditlogMiddleware.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 finalize_response(self, request, response, *args, **kwargs):
        """Overwritten to cleanup django-auditlog if needed."""
        response = super().finalize_response(request, response, *args, **kwargs)

        if hasattr(threadlocal, 'auditlog'):
            pre_save.disconnect(sender=LogEntry, dispatch_uid=threadlocal.auditlog['signal_duid'])
        return response

Minimal usage example

# views.py
from rest_framework import  viewsets

from some_app.mixins import DRFDjangoAuditModelMixin
from some_app import models, serializers


class SomeModelViewSet(DRFDjangoAuditModelMixin, viewsets.ReadOnlyModelViewSet):
    queryset = models.MyModel.objects.all()
    serializer_class = serializers.MyModelSerializer

I would like to hear feedback from the projects! Anyways, you could take this code/ideas for you projects.

Thanks in advance!

for rest_framework_simplejwt

from django.contrib.auth.middleware import get_user
from django.utils.deprecation import MiddlewareMixin
from django.utils.functional import SimpleLazyObject

from rest_framework_simplejwt.authentication import JWTAuthentication


class JWTAuthenticationMiddleware(MiddlewareMixin):

    def process_request(self, request):
        request.user = SimpleLazyObject(
            lambda: self.__class__.get_jwt_user(request))

    @staticmethod
    def get_jwt_user(request):
        user = get_user(request)
        if user.is_authenticated:
            return user
        jwt_authentication = JWTAuthentication()
        if jwt_authentication.get_header(request):
            user, jwt = jwt_authentication.authenticate(request)
        return user

In my case I solved this by modifying the AuditlogMiddleware and making the user a lazy object.

from auditlog.context import set_actor
from auditlog.middleware import AuditlogMiddleware as _AuditlogMiddleware
from django.utils.functional import SimpleLazyObject


class AuditlogMiddleware(_AuditlogMiddleware):
    def __call__(self, request):
        remote_addr = self._get_remote_addr(request)

        user = SimpleLazyObject(lambda: getattr(request, "user", None))

        context = set_actor(actor=user, remote_addr=remote_addr)

        with context:
            return self.get_response(request)

And then use this custom middleware in settings.MIDDLEWARE.

@mcanu I tried your solution. It works flawlessly. I wonder what’s stopping people from including this solution directly in audit log middleware.

First thanks to @malderete for the solution, it worked for me with a little change. For All that came across this problem in 2022, here is @malderete solution updated for latest auditlog version:

#mixins.py 

from django.db.models.signals import pre_save
from functools import partial
from auditlog.middleware import threadlocal, AuditlogMiddleware
from auditlog.models import LogEntry

class DRFDjangoAuditModelMixin:
    """
    Mixin to integrate django-auditlog with Django Rest Framework.

    This is needed because DRF does not perform the authentication at middleware layer
    instead it performs the authentication at View layer.

    This mixin adds behavior to connect/disconnect the signals needed by django-auditlog to auto
    log changes on models.
    It assumes that AuditlogMiddleware is activated in settings.MIDDLEWARE_CLASSES
    """

    def should_connect_signals(self, request):
        """Determines if the signals should be connected for the incoming request."""
        # By default only makes sense to audit when the user is authenticated
        return request.user.is_authenticated

    def initial(self, request, *args, **kwargs):
        """Overwritten to use django-auditlog if needed."""
        super().initial(request, *args, **kwargs)

        if self.should_connect_signals(request):
            set_actor = partial(AuditlogMiddleware.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 finalize_response(self, request, response, *args, **kwargs):
        """Overwritten to cleanup django-auditlog if needed."""
        response = super().finalize_response(request, response, *args, **kwargs)

        if hasattr(threadlocal, 'auditlog'):
            pre_save.disconnect(sender=LogEntry, dispatch_uid=threadlocal.auditlog['signal_duid'])
        return response

#########

And it matters in which order inheritance is listed:

#views.py :
from .mixins import DRFDjangoAuditModelMixin

class MyviewView(DRFDjangoAuditModelMixin, APIView ):
    def put(self,request,id):

        data = request.data
        .....

In my case I solved this by modifying the AuditlogMiddleware and making the user a lazy object.

from auditlog.context import set_actor
from auditlog.middleware import AuditlogMiddleware as _AuditlogMiddleware
from django.utils.functional import SimpleLazyObject


class AuditlogMiddleware(_AuditlogMiddleware):
    def __call__(self, request):
        remote_addr = self._get_remote_addr(request)

        user = SimpleLazyObject(lambda: getattr(request, "user", None))

        context = set_actor(actor=user, remote_addr=remote_addr)

        with context:
            return self.get_response(request)

And then use this custom middleware in settings.MIDDLEWARE.

@mcanu This is working for me. Thank you for solution.

@malderete @ALgracnar @alexsavio Thank you. I added missing imports.

It works for me

from auditlog.models import LogEntry
from auditlog.middleware import AuditlogMiddleware
from rest_framework.request import Request
from typing import Optional
from functools import partial
from django.db.models.signals import pre_save
import time
import threading

from django.contrib.auth import get_user_model

threadlocal = threading.local()


def set_log_entry_actor(user, sender, instance, signal_duid, **kwargs):
    """Signal receiver with extra 'user' and 'signal_duid' kwargs.
    This function becomes a valid signal receiver when it is curried with the actor and a dispatch id.

    This is a copy of AuditlogMiddleware.context._set_actor.
    """
    try:
        auditlog = threadlocal.auditlog
    except AttributeError:
        pass
    else:
        if signal_duid != auditlog["signal_duid"]:
            return
        auth_user_model = get_user_model()
        if (
            sender == LogEntry
            and isinstance(user, auth_user_model)
            and instance.actor is None
        ):
            instance.actor = user
        instance.remote_addr = auditlog["remote_addr"]


def get_remote_addr(request: Request) -> Optional[str]:
    """Return the remote address of the request."""
    if forwarded_for := request.META.get("HTTP_X_FORWARDED_FOR"):
        # In case of proxy, set 'original' address
        return forwarded_for.split(",")[0]
    return request.META.get("REMOTE_ADDR")


class AuditModelMixin:
    """
    Mixin to integrate django-auditlog with Django Rest Framework.

    This is needed because DRF does not perform the authentication at middleware layer
    instead it performs the authentication at View layer.

    This mixin adds behavior to connect/disconnect the signals needed by django-auditlog to auto
    log changes on models.
    It assumes that AuditlogMiddleware is activated in settings.MIDDLEWARE_CLASSES
    """

    def should_connect_signals(self, request) -> bool:
        """Determines if the signals should be connected for the incoming request."""
        # By default only makes sense to audit when the user is authenticated
        return hasattr(request.user, "email") and bool(request.user.email)

    def initial(self, request, *args, **kwargs):
        """Overwritten to use django-auditlog if needed."""
        super().initial(request, *args, **kwargs)
        if self.should_connect_signals(request):
            threadlocal.auditlog = {
                "signal_duid": ("set_actor", time.time()),
                "remote_addr": get_remote_addr(request),
            }
            set_actor = partial(
                set_log_entry_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 finalize_response(self, request, response, *args, **kwargs):
        """Overwritten to cleanup django-auditlog if needed."""
        response = super().finalize_response(
            request,
            response,
            *args,
            **kwargs,
        )

        if hasattr(threadlocal, "auditlog"):
            pre_save.disconnect(
                sender=LogEntry,
                dispatch_uid=threadlocal.auditlog["signal_duid"],
            )
        return response

For anyone stuck in this, you can create a custom middleware to populate the request.user in a middleware before running the auditlog middleware. https://crondev.wordpress.com/2018/05/13/django-middlewares-with-rest-framework/

I’m having this same issue when using this package with the Django REST Framework and Django-REST-JWT. See below for settings.py:

INSTALLED_APPS = [
....
    'rest_framework',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'auditlog'
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'auditlog.middleware.AuditlogMiddleware'
]

Screenshot of my Admin dashboard. The user for all Log Entry objects is displayed as ‘system’

image

The problem is the INSTALLED_APPS’s order, you just need to put auditlog app above djangorestframework app on settings.py file and @mcanu middleware, that’s it.

Hope this helps!

In my case when i am using your code my user variable is printing AnonymousUser which again lead to the same problem showing system value of actor field @magdumsuraj07

@hassaanalansary 's Idea can be modified in newer versions with the introduced -> from auditlog.context import set_actor I modified my SimpleJWT Based Auth like this ` def authenticate(self, request): …

    validated_token = self.get_validated_token(raw_token)
    user = self.get_user(validated_token)
    set_actor(user)
    return user, validated_token`

I was a lot of time checking all these things, but nothing work for me , I was making a research about DRF and middleware, how it work and found the problem , nothing is wrong with DRF or with django-auditlog. The problem is from the middleware you can’t get the request.user when the request come from a view like is the case with DRF APIviews, and how we are registering the log in the models you will not get the user according to the token in this moment. I recommend register the log (auditlog.register(AnyModel)) in the model when we can have the log when someone manage the data from django admin panel, but to know the actor from the api or views whe need to register the log in the view , after middleware , and after view is complete loaded.

  1. On model: from auditlog.registry import auditlog

class AnyModel(models.Model): …

auditlog.register(AnyModel)

  1. On any view (APIview or Viewset) from auditlog.registry import auditlog

class MyApiview(APIView):

@staticmethod def post(request): card, created = AnyModel.objects.update_or_create( name=‘Jhon’ ) auditlog.register(AnyModel)

# in this case you can safe the actor because he was loaded in the view request

I know this is not the best solution but work for DRF and django-auditlog lib, remember this lib (django-auditlog) is not for DRF , it is for django, it will log everything if you use django admin or web, but not DRF

my middleware:

from django.utils.functional import SimpleLazyObject from django.contrib.auth import get_user from oauth2_provider.models import AccessToken

class RestOauthMiddleware(object):

def __init__(self, get_response):
    self.get_response = get_response

@staticmethod
def get_user(request):
    user = get_user(request)
    if user.is_authenticated:
        return user
    try:
        bearer = request.META.get('HTTP_AUTHORIZATION', False)
        if bearer:
            token = bearer.split(' ')[1]
            token_obj = AccessToken.objects.get(token=token)
            if token_obj:
                return token_obj.user
    except ValueError:
        pass
    return user

def __call__(self, request):
    request.user = SimpleLazyObject(
        lambda: self.__class__.get_user(request))
    response = self.get_response(request)
    return response

THIS WORK FOR ME , AND I CAN NOW REGISTER THE ACTOR FROM MY APIs views

for oauth2_provider i fix issue wit create middleware :

from django.utils.functional import SimpleLazyObject
from django.contrib.auth import get_user
from rest_framework.response import Response
from oauth2_provider.models import AccessToken
from django.contrib.auth.models import User

class RestOauthMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    @staticmethod
    def get_user(request):
        user = get_user(request)
        if user.is_authenticated:
            return user
        try:
            Token = AccessToken.objects.get(
                token=request.headers['Authorization'].split()[1])
            user = User.objects.get(pk=Token.user_id)
        except:
            pass
        return user

    def __call__(self, request):
        request.user = SimpleLazyObject(
            lambda: self.__class__.get_user(request))
        response = self.get_response(request)
        return response

and add middleware in setting file above of auditlog middleware.