Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatic Logging doesn't log the Actor #115

Open
Aalphones opened this issue May 10, 2017 · 61 comments
Open

Automatic Logging doesn't log the Actor #115

Aalphones opened this issue May 10, 2017 · 61 comments
Labels

Comments

@Aalphones
Copy link

Aalphones commented May 10, 2017

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?

@audiolion
Copy link
Contributor

I would make your request add add import pdb; pdb.set_trace() right above

        if hasattr(request, 'user') and hasattr(request.user, 'is_authenticated') and request.user.is_authenticated():
            set_actor = curry(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)

Those lines of code in the middleware.py of auditlog and see if the request object has user attr and see if is_authenticated is returning the right value.

This could be an issue with your Django version, I believe 1.10+ removed is_authenticated() and replaced it with a property, so it could be that auditlog calling is_authenticated() is failing on new versions of django and actor isn't being set

@Aalphones
Copy link
Author

Aalphones commented May 15, 2017

@audiolion
It seems like the requesting User is set to "AnonymousUser" and therefore the Method "is_authenticated" returns false.

Edit:
Ok my bad it was indeed my fault...
Problem was the 'django.contrib.auth.middleware.AuthenticationMiddleware', in the MIDDLEWARE Array. This alongside with the 'django.contrib.auth.middleware.SessionAuthenticationMiddleware' suggested by the OAuth-toolkit lead to the AnonymousUser as mentioned above.
Deleting this line solves the problem.

@kdenny
Copy link

kdenny commented Jun 6, 2017

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

@audiolion
Copy link
Contributor

see my comment to help debug

@kdenny
Copy link

kdenny commented Jun 6, 2017

@audiolion Thank you for the prompt response, however I'm not sure how to leverage this to fix the issue.

From the Django documentation (I'm running 1.10.5), it seems that is_authenticated is still included...

@kdenny
Copy link

kdenny commented Jun 6, 2017

@audiolion see below for debugging results. Seems to be something else that is causing the actor to be saved as system.

-> if hasattr(request, 'user') and hasattr(request.user, 'is_authenticated') and request.user.is_authenticated():
(Pdb) print(hasattr(request, 'user'))
True
(Pdb) print(hasattr(request.user, 'is_authenticated'))
True
(Pdb) print(request.user.is_authenticated())
True

@audiolion
Copy link
Contributor

what is the username of request.user?

@kdenny
Copy link

kdenny commented Jun 6, 2017

The username is my username (kdenny):

-> if hasattr(request, 'user') and hasattr(request.user, 'is_authenticated') and request.user.is_authenticated():
(Pdb) print(request.user)
kdenny

@audiolion
Copy link
Contributor

Very strange, it seems like all the stuff to set the actor is happening correctly, I am not sure why it is not being saved. My only advice is to step through the code with the debugger to see where things are going wrong. 😢

@audiolion
Copy link
Contributor

Closing due to inactivity

@marianobrc
Copy link

I'm having the same problem in Django 1.11, using DRF and TokenAuthentication.

If i modify an object from the admin then I see the user correctly, but if i modify it from an update api call it says just "system" for all the users.

@audiolion
Copy link
Contributor

ok ill reopen to investigate

@audiolion audiolion reopened this Dec 16, 2017
@marianobrc
Copy link

Hi @audiolion, I've debugged it and I found that I was getting an AnonymusUser in the middleware. I was researching about it and it's something related to DRF. When TokenAuthentication is used, the user is set in views or viewsets, so we can't get the user in the middleware.

So I've ended uninstalling django-auditlog and implementing two mixins to use in my views

class CreatedByAuditAPIViewMixin(object):
    """
    Helper class to set the user that has created the object in a CreateAPIView.
    """

    def perform_create(self, serializer):
        serializer.save(created_by=self.request.user)


class UpdatedByAuditAPIViewMixin(object):
    """
    Helper class to set the user that has created the object in an UpdateAPIView.
    """

    def perform_update(self, serializer):
        serializer.save(updated_by=self.request.user)

Thanks

@audiolion
Copy link
Contributor

thanks for the investigating @marianobrc

I wouldn't be opposed to adding some helper utilities to this library and some docs for integrating with DRF, but it does seem a little out of scope.

The other option is add a note with this code in the documentation.

I am surprised that DRF doesn't set the user in the middleware piece, this isn't just a matter of putting the auditlog middleware after the DRF middleware is it?

@waydeechen
Copy link

DRF how to log the ractor? I have the same problem.

@agfunder
Copy link

I also had a related issue setting actor using rest_framework_jwt.authentication (was showing "system") , we solved it with custom middleware and now auditlog has the correct actor.

@PeterBeklemishev
Copy link

verificient@3650226 - possible solution

@audiolion
Copy link
Contributor

@PeterBeklemishev if you want to send a PR this way I will review 👍

@kunalgrover05
Copy link

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/

@robguttman
Copy link
Contributor

Any progress on this? @PeterBeklemishev, do you intend to submit a pull request with your solution?

@dekabrist89
Copy link

Does anybody has any solution here yet? @PeterBeklemishev, can you create a pull request with your fix?

@ivan-fedorov-probegin
Copy link

ivan-fedorov-probegin commented Feb 23, 2019

I have solution
you need create custom Token Auth an connect it with auditlog

from auditlog.middleware import threadlocal, AuditlogMiddleware
from auditlog.models import LogEntry
from django.db.models.signals import pre_save
from django.utils.functional import curry
from rest_framework.authentication import TokenAuthentication


class CustomAuthentication(TokenAuthentication):

    def authenticate(self, request):
        result = super().authenticate(request)
        try:
            if result:
                user, token = result
                set_actor = curry(
                    AuditlogMiddleware.set_actor,
                    user=user,
                    signal_duid=threadlocal.auditlog['signal_duid']
                )
                pre_save.connect(
                    set_actor,
                    sender=LogEntry,
                    dispatch_uid=threadlocal.auditlog['signal_duid'],
                    weak=False
                )
                return result

@saranshbansal
Copy link

Having the exact issue. @ivan-fedorov-probegin tried your solution but it still doesn't work and saves system as user.
I've put this at the bottom under AUTHENTICATION_BACKENDS in my settings file.

I have solution
you need create custom Token Auth an connect it with auditlog

from auditlog.middleware import threadlocal, AuditlogMiddleware
from auditlog.models import LogEntry
from django.db.models.signals import pre_save
from django.utils.functional import curry
from rest_framework.authentication import TokenAuthentication


class CustomAuthentication(TokenAuthentication):

    def authenticate(self, request):
        result = super().authenticate(request)
        try:
            if result:
                user, token = result
                set_actor = curry(
                    AuditlogMiddleware.set_actor,
                    user=user,
                    signal_duid=threadlocal.auditlog['signal_duid']
                )
                pre_save.connect(
                    set_actor,
                    sender=LogEntry,
                    dispatch_uid=threadlocal.auditlog['signal_duid'],
                    weak=False
                )
                return result

@malderete
Copy link

malderete commented Jan 8, 2020

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!

@Hassanzadeh-sd
Copy link

Hassanzadeh-sd commented Jan 11, 2020

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.

@melizeche
Copy link

Hi, just wanted to know if there's any update regarding this? Any plans to implement some of the solutions? btw amazing work!

@alejandromf27
Copy link

alejandromf27 commented Apr 18, 2020

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

@senbonsakura
Copy link

I have come up with this solution that looks like @ivan-fedorov-probegin answer but with shorter and more maintainable code This solution befits: 1- You don't have to decorate every View with it 2- it doesn't assume anything about AuditlogMiddleware or audit_log implementation in general. so if the code changes, this should still work 3- It doesn't force or duplicate DRF authentication.

#token_authentication_wrapper.py
from auditlog.middleware import AuditlogMiddleware
from rest_framework.authentication import TokenAuthentication


class TokenAuthenticationWrapper(TokenAuthentication):
    def authenticate(self, request):
        user, token = super().authenticate(request)
        request.user = user # necessary for preventing recursion
        AuditlogMiddleware().process_request(request)
        return user, token

inherit from your favorite Authentication service e.g. BasicAuthentication SessionAuthentication, TokenAuthentication, etc...

and in setting.py

    'DEFAULT_AUTHENTICATION_CLASSES': [
        'path.to.file.token_authentication_wrapper.TokenAuthenticationWrapper',
    ]

This is the most elegant solution with but it is broken with the new update with the removal of process_request method. Is there any way to accomplish this on the middleware level, without subclassing every viewset?

@hassaanalansary
Copy link

@senbonsakura unfortunately, i didn't check the new update
I guess it could modified to work the new update.
Will let you know if i look into it.

Right now i am depending an older commit

@arkaanchanto
Copy link

@hassaanalansary @senbonsakura
Did you guys find any workaround to this issue ?
I checked with the new update things are getting broken.

@alexsavio
Copy link

alexsavio commented Jul 25, 2022

On line of what @ALgracnar did, the solution I could find for DRF and auditlog 2.1.0 was:

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 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) -> 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

@demmojo demmojo linked a pull request Jul 29, 2022 that will close this issue
@normic
Copy link

normic commented Aug 21, 2022

@alexsavio: I've tried your solution with auditlog 2.1.0, but it complains about missing LogEntry, Request and Optional, threadlocal missing auditlog, etc.

Would you mind to tell which Django and maybe Python version you've tested this against?

Are there any plans/forks that solve this issue?

@alexsavio
Copy link

@normic we are using Django 4.0.7 and Python 3.10 and djangorestframework 3.13.1.

I hope this helps

@quincykwende
Copy link

@normic I managed to get @alexsavio solution working.
Add the following imports

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

@alexsavio: I've tried your solution with auditlog 2.1.0, but it complains about missing LogEntry, Request and Optional, threadlocal missing auditlog, etc.

Would you mind to tell which Django and maybe Python version you've tested this against?

Are there any plans/forks that solve this issue?

@quincykwende
Copy link

quincykwende commented Aug 24, 2022

@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

@Tekpakma
Copy link

@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`

@Chenlf-666
Copy link

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

This way works for me and it is very simple. It should be noted that if you are using the RTF, SessionAuthentication should be loaded after JWTAuthentication. Otherwise csrf error will happen. My setting is like this:

'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication',
    ),

@mcanu
Copy link

mcanu commented May 9, 2023

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.

@kinhvan017
Copy link

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.

I tried your solution. it work for me.

@kgatz
Copy link

kgatz commented Aug 4, 2023

I tried it as well - also worked for me.

@fracturedface
Copy link

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.

Just adding my experience here, I ended up going with @mcanu's solution. It's working in production for me with django-auditlog==2.3.0 and django==4.1.5.

@alexsavio's solution also worked for me after adding imports like @quincykwende , however the custom middleware was my final go-to.

I'd also want to mention that for the mixin-based solution, it might be worth changing the should_connect_signals function to utilize is_authenticated instead of searching for an email. Currently, if an user with a null email or blank email makes a change it will log as system. I haven't tested the behavior if there is no email field on the User object.

Here's how I would change it if you plan to go this route:

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, "is_authenticated") and bool(
            request.user.is_authenticated
        )

@magdumsuraj07
Copy link

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.

@umangbisht
Copy link

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

@esorribas
Copy link

esorribas commented Sep 26, 2023

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!

@boardshepherd
Copy link

Putting 'auditlog.middleware.AuditlogMiddleware', as the last item in MIDDLEWARE in settings.py solved the issue for me.

@ashuvik03
Copy link

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.

@qurm
Copy link

qurm commented Mar 28, 2024

Works well for me too, with auditlog 2.5.0 and Django 5.02.
To clarify how to apply this (if not familiar with middleware) the above code is in a file /myapp/middleware.py and settings.py has this:

MIDDLEWARE = [
    ...
    'myapp.middleware.AuditlogMiddleware', # custom patch
]

I am not clear if this is fixed in the upcoming release 3, per comment here #613 @hramezani ?

@hramezani
Copy link
Member

Works well for me too, with auditlog 2.5.0 and Django 5.02. To clarify how to apply this (if not familiar with middleware) the above code is in a file /myapp/middleware.py and settings.py has this:

MIDDLEWARE = [
    ...
    'myapp.middleware.AuditlogMiddleware', # custom patch
]

I am not clear if this is fixed in the upcoming release 3, per comment here #613 @hramezani ?

I need someone to check it on V3 beta and if it isn't fixed there create a patch for fixing

@jforsman
Copy link

@hramezani Not working auditlog 3.0.0 and django 4.2.13.
I will try with adding call to my custom middleware and let you if it works or not.

@hramezani
Copy link
Member

hramezani commented Jun 13, 2024

@jforsman Yes, this is not working. I am still waiting for a PR that fixes the problem in django-auditlog. There are some workarounds here but we need a PR.

@jforsman
Copy link

I don't actually think that there should be anything done to auditlog as it is DRF design decision (faulty one in my opinion) to do the authentication at view level so it should be somehow fixed there.

I think I am gonna go with the custom DRF authentication classes.

@jforsman
Copy link

Well, don't think there will be any fixes to anywhere soon, this DRF bug encode/django-rest-framework#760 (yes an old one but still valid) states in the last comment that you should write separate authentication middlewares. That was my approach in the end.

@ChandraprakashGajwani
Copy link

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.

IT worked for me also, Thank you so much @mcanu

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests