django-auditlog
django-auditlog copied to clipboard
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?
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
@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.
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'
see my comment to help debug
@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...
@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
what is the username of request.user?
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
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. 😢
Closing due to inactivity
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.
ok ill reopen to investigate
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
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?
DRF how to log the ractor? I have the same problem.
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.
https://github.com/verificient/django-auditlog/commit/3650226dafa40b627c2e0d8d9e886ea7b4200f33 - possible solution
@PeterBeklemishev if you want to send a PR this way I will review 👍
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/
Any progress on this? @PeterBeklemishev, do you intend to submit a pull request with your solution?
Does anybody has any solution here yet? @PeterBeklemishev, can you create a pull request with your fix?
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
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
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 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.
Hi, just wanted to know if there's any update regarding this? Any plans to implement some of the solutions? btw amazing work!
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.
- On model: from auditlog.registry import auditlog
class AnyModel(models.Model): ......
auditlog.register(AnyModel)
- 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
@alejandromf27 hello! The recipe I shared works fine with DRF as I mentioned in the description, it's a valid mixin and avoid hardcoded auth-mechanisms. You could take a look to it ( cc @Hassanzadeh-sd ) https://github.com/jjkester/django-auditlog/issues/115#issuecomment-572278206
@malderete , right your solution work fine!!!!!! good job, thanks
It works
from django.contrib.auth.middleware import get_user
from django.utils.deprecation import MiddlewareMixin
from django.utils.functional import SimpleLazyObject
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
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 = JSONWebTokenAuthentication()
if jwt_authentication.get_jwt_value(request):
user, jwt = jwt_authentication.authenticate(request)
return user