auditlog with Django and DRF - python

I need to implement auditlog feature in one of my project which is using Django 1.8 and Django-Rest-Framework 3.2.2. I have extended BaseUserManager class to create user model since I had to use email as a username in my application ( if this information matters ).
Below is my db design which will hold logs :
**fields type desc**
id pk ( auto_increment)
cust_id FK customer
customer_name FK customer
user_id FK user
user_name FK user
module Varchar(100) sales,order,billing,etc
action Varchar(10) Create/Update/Delete
previous_value varchar(500)
current_value varchar(500)
Datetime Datetime timestamp of change
I have tried https://pypi.python.org/pypi/django-audit-log but it has 2 issues as per my requirement-
It does not capture data as per my requirement which I understand is my issue and so I modified it's code and added my fields into it's model.
It is not capturing module information. Behaviour is random.
I am seeking advice to proceed with this feature. Which package would be best suitable for my task.
P.S I have also tried Django-reversion and I have no requirement of data versioning.
Thanks

I achieved what I needed by modifying auditlog code -
Added required field in LogEntry model of auditlog.
Modified log_create,log_update,log_delete functions of receivers.py to save information in newly added fields.
Using this I am halfway done. Now only issue I am facing is that since model instance of 1 table contains information of other tables as well due to FKs used in the table.
To solve this I could come up with a solution which works well but I am not satisfied with it.
I added a function like include_in_model() in each model and modified auditlog's registry.py register() function to get those fields and only use that to save information in LogEntry model.
This approach will require me to create this include_in_model() function in each of my model class and pass required fields for particular model. This way I am avoiding FK related information.

Django Simple History is an excellent app that I've used in production projects in the past, it will give you per model Audits against your users.
Furthermore, you should create your own Authentication Class which will be responsible for logging requests. Let's assume that a User uses a Token to authenticate with your API. It gets sent in the header of each HTTP Request to your API like so: Authorization: Bearer <My Token>. We should then log the User associated with the request, the time, the user's IP and the body.
This is pretty easy:
settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'common.authentication.MyTokenAuthenticationClass'
),
...
}
common/authentication.py
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from ipware.ip import get_real_ip
from rest_framework import authentication
from rest_framework import exceptions
from accounts.models import Token, AuditLog
class MyTokenAuthenticationClass(authentication.BaseAuthentication):
def authenticate(self, request):
# Grab the Athorization Header from the HTTP Request
auth = authentication.get_authorization_header(request).split()
if not auth or auth[0].lower() != b'bearer':
return None
# Check that Token header is properly formatted and present, raise errors if not
if len(auth) == 1:
msg = _('Invalid token header. No credentials provided.')
raise exceptions.AuthenticationFailed(msg)
elif len(auth) > 2:
msg = _('Invalid token header. Credentials string should not contain spaces.')
raise exceptions.AuthenticationFailed(msg)
try:
token = Token.objects.get(token=auth[1])
# Using the `ipware.ip` module to get the real IP (if hosted on ElasticBeanstalk or Heroku)
token.last_ip = get_real_ip(request)
token.last_login = timezone.now()
token.save()
# Add the saved token instance to the request context
request.token = token
except Token.DoesNotExist:
raise exceptions.AuthenticationFailed('Invalid token.')
# At this point, insert the Log into your AuditLog table:
AuditLog.objects.create(
user_id=token.user,
request_payload=request.body,
# Additional fields
...
)
# Return the Authenticated User associated with the Token
return (token.user, token)

Another solution would be to use django auditlog and use a custom middleware which does not capture the 'request.user' directly but at the moment when it is needed, by this time DRF will have set the correct 'request.user' so that it is no longer missing the username in the audit logs.
Create a file named (for example) auditlog_middleware.py and include it in the MIDDLEWARE in your settings.py instead of the default auditlog middleware.
from __future__ import unicode_literals
import threading
import time
from django.conf import settings
from django.db.models.signals import pre_save
from django.utils.functional import curry
from django.apps import apps
from auditlog.models import LogEntry
from auditlog.compat import is_authenticated
# Use MiddlewareMixin when present (Django >= 1.10)
try:
from django.utils.deprecation import MiddlewareMixin
except ImportError:
MiddlewareMixin = object
threadlocal = threading.local()
class AuditlogMiddleware(MiddlewareMixin):
"""
Middleware to couple the request's user to log items. This is accomplished by currying the signal receiver with the
user from the request (or None if the user is not authenticated).
"""
def process_request(self, request):
"""
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
set_actor = curry(self.set_actor, request=request, 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(request, 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 not hasattr(request, 'user') or not is_authenticated(request.user):
return
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(request.user, auth_user_model) and instance.actor is None:
instance.actor = request.user
instance.remote_addr = threadlocal.auditlog['remote_addr']

I know that this answer is coming very late, but here it goes
Because DRF authenticates on the View level NOT on the Middleware level, the user is not yet attached to the request when AuditlogMiddleware runs, resulting in AnonymousUser
You can attach the logic from AuditlogMiddleware after your authentication
This logic connects some signals
This solution befits:
You don't have to decorate every View with it
it doesn't assume anything about AuditlogMiddleware or audit_log implementation in general. so if the code changes, this should still work
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',
]

First of all you can user package: https://github.com/jcugat/django-custom-user, to solve Email as Username field.
Then you can try to focus development with: http://django-reversion.readthedocs.io/en/stable/

The answer by #hassaan-alansary would have been ideal, but unfortunately the Auditlog devs made significant changes since he posted his answer, and I couldn't figure out how to reconcile their changes with Hassaan's answer.
The solution I ended up finding is based on what was shared here. Instead of writing a new DRF authentication method which invokes the middleware to do the logging, it creates a mixin which needs to be added to each of the DRF views you want added to the audit log. The solution below is the modified version of the one I ended up using from the link above.
# mixins.py
import threading
import time
from functools import partial
from django.db.models.signals import pre_save
from auditlog.models import LogEntry
threadlocal = threading.local()
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
"""
#staticmethod
def _set_actor(user, sender, instance, signal_duid, **kwargs):
# This is a reimplementation of auditlog.context._set_actor.
# Unfortunately the original logic cannot be used, because
# there is a type mismatch between user and auth_user_model.
if signal_duid != threadlocal.auditlog["signal_duid"]:
return
if (
sender == LogEntry
#and isinstance(user, auth_user_model)
and instance.actor is None
):
instance.actor = user
instance.remote_addr = threadlocal.auditlog["remote_addr"]
def initial(self, request, *args, **kwargs):
"""Overwritten to use django-auditlog if needed."""
super().initial(request, *args, **kwargs)
remote_addr = AuditlogMiddleware._get_remote_addr(request)
actor = request.user
set_actor = partial(
self._set_actor,
user=actor,
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'])
del threadlocal.auditlog
return response
You then need to add this mixin to each of your views:
# views.py
...
class CustomerViewSet(DRFDjangoAuditModelMixin, ModelViewSet):
queryset = Client.objects.all()
serializer = ClientSerializer
....
The down side of this implementation is that it isn't DRY on a couple of levels. Not only do you need to add the mixin to each DRF view, but it copies code from nearly all the logging behaviour of auditlog, particularly private methods. I therefore expect this solution to either need adjustment in the future, or for it to also become obsolete.
The solution above is based on this revision of auditlog.

Related

Relogin after N minutes with django and JWT

Scenario: I want a user to re-login when passing to a security sensible area after N minutes, e.g. when user is about to pay an order, however he logged in 1 hour ago, I would like to be sure it's him. This by using rest_framework_jwt.
Long description:
I've been recently testing django for modern web development (so, backend with rest-api). However, I encountered a problem which I have not yet found a solution.
In rest_framework_jwt you set the authentication class as follows.
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
This will do a great work for the general purpose. However, I want the user to reidentify (re-login) when entering an area with sensible information 10 minutes after login, e.g. sensible information can be a payment area. Thus, I would like to send a parameter to the Authentication class telling that the user is in a sensible area.
What I think as a possible solution but I don't know how to do it yet, is: The rest_framework_jwt creates the variable orig_iat when using the option JWT_ALLOW_REFRESH. I could send a flag to authentication class to tell that current view is a sensitive area or not, if so and the user logged in more than 10 minutes ago, I can send a message to say that the user needs to re-login to continue.
I don't mind forking the rest_framework_jwt project and adapting it for my purposes, however I would like to know how to send the parameter from the view to the authentication class (in this case: rest_framework_jwt.authentication.JSONWebTokenAuthentication).
Also, If there is already something done with rest_framework_jwt for this scenario, I would like to avoid re-inventing the wheel.
Well... So far, what I've done it's to create a decorator for a function view. The code for the decorator is as follows:
from functools import wraps
from rest_framework_jwt.settings import api_settings
from django.utils.translation import ugettext as _
from calendar import timegm
import datetime
jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
def recently_authenticated():
def decorator(func):
#wraps(func)
def inner(request, *args, **kwargs):
jwt_payload = jwt_decode_handler(request._auth)
rencent_auth_limit = api_settings.JWT_RECENT_AUTHENTICATION_DELTA
if isinstance(rencent_auth_limit, datetime.timedelta):
rencent_auth_limit = (rencent_auth_limit.days * 24 * 3600 +
rencent_auth_limit.seconds) + jwt_payload["orig_iat"]
timenow = timegm(datetime.datetime.utcnow().utctimetuple())
if timenow>rencent_auth_limit:
return Response({"detail":_("you have to reidentify to enter this area")},
status=401)
return func(request, *args, **kwargs)
return inner
return decorator
The response format is given in the same format as rest_framework_jwt.authentication.JSONWebTokenAuthentication. The constant JWT_RECENT_AUTHENTICATION_DELTA is an ad-hoc parameter inserted in the settings.py of the rest_framework_jwt package (a fork).
Finally, in order to use it, one can add the decorator to any view. For example:
#api_view()
#recently_authenticated()
def index_view(request):
data = User.objects.filter()
return Response(UserSerializer(data, many=True).data)
And when the user has been authenticated a while ago, it will send the message {"detail":"you have to reidentify to enter this area"} with the code 401. This can be evaluated and parsed by the frontend and redirect the user to the login.
Note: The decorator only evaluates the time and the time passed. The verification telling whether or not it's a correct user and a correct token is still performed by rest_framework_jwt.authentication.JSONWebTokenAuthentication.
According to the manual: https://getblimp.github.io/django-rest-framework-jwt/#additional-settings
Disallowing refresh token should do the job. The thing is that you will get only 1 token and you won't be able to refresh it after 1 hour.
JWT_AUTH = {
'JWT_ALLOW_REFRESH': False,
'JWT_REFRESH_EXPIRATION_DELTA': timedelta(hours=1),
}
Other problems should be solved on the frontend side. You should check whether user is trying to get to "sensitive" view. If yes, then check if token is valid. If invalid - redirect to login page. If view is "insensitive" - your choice.
The recommended way to handle this is to separate token freshness from token validation. Most views require a valid token, secure views require a fresh token, one that is not only valid but also has been issued at login and has not been refreshed since.
You can do this by setting a flag on the token to mark it as 'fresh' on login, but unset the flag when refreshing. The flow then becomes:
Client accesses the site without a token -> Deny access
Client authenticates with obtain-token endpoint -> Issue token with fresh=True
Client (or server) refreshes valid token -> Issue token with fresh=False
Client accesses non-secure endpoint with valid token -> Accept token
Client accesses secure endpoint -> Accept token only if fresh=True is set on token.
The only way to obtain a fresh token is to log in again, no refresh allowed.
So you need to be able to:
distinguish between obtaining a new token and refreshing when generating the JWT payload.
inspect the fresh key in the current JWT token in order to create a custom permission.
The first requirement means you'll have to do some view subclassing, as the jwt_payload_handler callback is not given any information to determine what called it.
The easiest way to handle the first requirement is to just subclass the serializers used to produce a fresh or refreshed token, decode the token they produce, inject the applicable fresh key value, then re-encode. Then use the subclassed serializers to create a new set of API views:
from rest_framework_jwt.settings import api_settings
from rest_framework_jwt.serializers import JSONWebTokenSerializer, RefreshJSONWebTokenSerializer
from rest_framework_jwt.views import JSONWebTokenAPIView
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
class FreshJSONWebTokenSerializer(JSONWebTokenSerializer):
"""Add a 'fresh=True' flag to the JWT token when issuing"""
def validate(self, *args, **kwargs):
result = super().validate(*args, **kwargs)
payload = jwt_decode_handler(result['token'])
return {
**result,
'token': jwt_encode_handler({**payload, fresh=True})
}
class NonFreshRefreshJSONWebTokenSerializer(RefreshJSONWebTokenSerializer):
"""Set the 'fresh' flag False on refresh"""
def validate(self, *args, **kwargs):
result = super().validate(*args, **kwargs)
payload = jwt_decode_handler(result['token'])
return {
**result,
'token': jwt_encode_handler({**payload, fresh=False})
}
class ObtainFreshJSONWebToken(JSONWebTokenAPIView):
serializer_class = JSONWebTokenSerializer
class NonFreshRefreshJSONWebToken(JSONWebTokenAPIView):
serializer_class = NonFreshRefreshJSONWebTokenSerializer
obtain_jwt_token = ObtainFreshJSONWebToken.as_view()
refresh_jwt_token = NonFreshRefreshJSONWebToken.as_view()
Then register these two views as API endpoints instead of those provided by the Django REST Framework JWT project, for the obtain and refresh paths.
Next up is the permission; because the JSONWebTokenAuthentication class returns the decoded payload when authenticating the request.auth attribute is set to the payload dictionary, letting us inspect it directly in a custom permission:
class HashFreshTokenPermission(permissions.BasePermission):
message = 'This endpoint requires a fresh token, please obtain a new token.'
def has_permission(self, request, view):
return (
request.user and
request.user.is_authenticated and
request.auth and
isinstance(request.auth, dict) and
request.auth.get('fresh', False)
)
Register this permission with the REST framework:
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
'yourmodule.HashFreshTokenPermission',
),
# ...
}
and use this in views that are security sensitive:
class SampleSecureView(APIView):
permission_classes = (
permissions.IsAuthenticated,
HashFreshTokenPermission,
)
# ...

Django rest auth user_logged_in signal

I have a django rest app using django rest auth.
I'm trying to log something everytime a user log in using signals.
I've searched on the web on how to use signals and I haven't found any interesting material on how to make it work. I think the problem may be with allauth signals. Is there any problem with the following configuration?
signals.py
import logging
from allauth.account.signals import user_logged_in
from django.dispatch import receiver
logger = logging.getLogger(__name__)
#receiver(user_logged_in)
def login_logger(request, user, **kwargs):
logger.info("{} logged in with {}".format(user.email, request))
apps.py
from django.apps import AppConfig
class UsersConfig(AppConfig):
name = 'users'
def ready(self):
import users.signals
__init__.py
default_app_config = 'users.apps.UsersConfig'
Here's how I solved it using djangorestframework-jwt==1.11.0:
settings.py
from django.contrib.auth.signals import user_logged_in
def jwt_response_payload_handler(token, user=None, request=None):
if user and request:
user_logged_in.send(sender=user.__class__, request=request, user=user)
return {
'token': token,
}
JWT_AUTH = {
'JWT_RESPONSE_PAYLOAD_HANDLER': jwt_response_payload_handler,
}
models.py
from django.contrib.auth.signals import user_logged_in
def login_handler(sender, user, request, **kwargs):
print('logged in')
user_logged_in.connect(login_handler)
It seems that Django Rest Framework doesn't emit user_logged_in signal when token-based authentication is configured: https://github.com/encode/django-rest-framework/issues/3869
For future googlers. The OP's original question was that he wanted to log something everytime a user logs in when using rest_auth. You would expect a signal to be emitted by rest_auth for something like this but rest_auth does different things depending on the type of login. For a session login, rest_auth calls into django's normal login routines and a signal does get emitted. But for token-based authentication, rest_auth creates the token and returns it and there is no call into django proper and no signal is emitted.
Here's the rest_auth login code
To get the behavior you want, you have to override rest_auth's default token handler (it's straightforward) so you know when a token gets created and then log the event however you want.
In django's settings.py file add:
REST_AUTH_TOKEN_CREATOR = '<your_dotted_project_path>.create_login_token'
In some file in your project:
# this is the same as the default rest_auth token handler except we
# don't throw away the 'created' part because we care whether it was
# created or just retrieved.
def create_login_token(token_model, user, serializer):
token, created = token_model.objects.get_or_create(user=user)
if created:
>>> log it or emit your own signal or whatever <<<
return token
For some strange reason, this doesn't seem to work when placed in signals.py, this implementation of signals.py works fine for every part of project except for allauth(I had my personal experience too). Check this github issueshttps://github.com/pennersr/django-allauth/issues/347
For some strange reason moving this code(signals.py) into models.py of the same app will work.
# place this in models.py
from allauth.account.signals import user_logged_in
from django.dispatch import receiver
logger = logging.getLogger(__name__)
#receiver(user_logged_in)
def login_logger(request, user, **kwargs):
logger.info("{} logged in with {}".format(user.email, request))
It doesn't pick up from signals.py for some strange reason. However, you can still maintain the separation of concerns using the following. This worked for me while still keeping the signal logic away from my models.
signals.py
import logging
from allauth.account.signals import user_logged_in
from django.dispatch import receiver
logger = logging.getLogger(__name__)
#receiver(user_logged_in)
def login_logger(request, user, **kwargs):
logger.info("{} logged in with {}".format(user.email, request))
models.py
from .signals import *
// Your models here
class Foo(models.Model):
pass

How to disconnect update_last_login?

I implemented my own User class from scratch in Django. But when I log in I have this error:
The following fields do not exist in this model or are m2m fields: last_login
I really don't want the field last_login.
I do some reasearch and the problem is here: contrib.aut.models.py
def update_last_login(sender, user, **kwargs):
"""
A signal receiver which updates the last_login date for
the user logging in.
"""
user.last_login = timezone.now()
user.save(update_fields=['last_login'])
user_logged_in.connect(update_last_login)
I found a workaround but it's not an ellegant solution. I added user_logged_in.disconnect(update_last_login) in my models.py file, where my User class is defined.
Is there any better solution for this?
Not sure if this is related to a newer version of django or what, but in my case
user_logged_in.disconnect(update_last_login)
didn't work. This is what works for me (django 2.1):
user_logged_in.disconnect(update_last_login, dispatch_uid='update_last_login')
Currently in Django 1.7...
I think the workaround you defined is the only valid solution (besides from a monkey patch) currently when using the Django auth login() method. I'm just going to assume you are using the standard login() method which is raising this exception.
If we take a look at the source for the login method, we find at the end of the method, a call to execute user_logged_in.send(sender=user.__class__, request=request, user=user). We can't prevent this signal from executing besides from disconnecting it as you have pointed out.
Alternatively, we could monkey patch the login() method to remove that signal call.
from django.contrib.auth import login
def monkey_patch_login(request, user):
"""
Persist a user id and a backend in the request. This way a user doesn't
have to reauthenticate on every request. Note that data set during
the anonymous session is retained when the user logs in.
"""
session_auth_hash = ''
if user is None:
user = request.user
if hasattr(user, 'get_session_auth_hash'):
session_auth_hash = user.get_session_auth_hash()
if SESSION_KEY in request.session:
if _get_user_session_key(request) != user.pk or (
session_auth_hash and
request.session.get(HASH_SESSION_KEY) != session_auth_hash):
# To avoid reusing another user's session, create a new, empty
# session if the existing session corresponds to a different
# authenticated user.
request.session.flush()
else:
request.session.cycle_key()
request.session[SESSION_KEY] = user._meta.pk.value_to_string(user)
request.session[BACKEND_SESSION_KEY] = user.backend
request.session[HASH_SESSION_KEY] = session_auth_hash
if hasattr(request, 'user'):
request.user = user
rotate_token(request)
login = monkey_patch_login
We would put the monkey patch code at the top of the file that needs to call the login() method.

Django and Middleware which uses request.user is always Anonymous

I'm trying to make middleware which alters some fields for the user based on subdomain, etc...
The only problem is the request.user always comes in as AnonymousUser within the middleware, but is then the correct user within the views. I've left the default authentication and session middleware django uses within the settings.
There is a similar question here: Django, request.user is always Anonymous User
But doesn't overly answer the total question because I'm not using different authentication methods, and djangos authentication is running before I invoke my own middleware.
Is there a way, while using DRF, to get the request.user within the middleware? I'll show some sample code here:
class SampleMiddleware(object):
def process_view(self, request, view_func, view_args, view_kwargs):
#This will be AnonymousUser. I need it to be the actual user making the request.
print (request.user)
def process_response(self, request, response):
return response
with process_request:
class SampleMiddleware(object):
def process_request(self, request):
#This will be AnonymousUser. I need it to be the actual user making the request.
print (request.user)
def process_response(self, request, response):
return response
I've solved this problem by getting DRF token from the requests and loading request.user to the user associated to that model.
I had the default django authentication and session middleware, but it seems DRF was using it's token auth after middleware to resolve the user (All requests were CORS requests, this might have been why). Here's my updated middleware class:
from re import sub
from rest_framework.authtoken.models import Token
from core.models import OrganizationRole, Organization, User
class OrganizationMiddleware(object):
def process_view(self, request, view_func, view_args, view_kwargs):
header_token = request.META.get('HTTP_AUTHORIZATION', None)
if header_token is not None:
try:
token = sub('Token ', '', header_token)
token_obj = Token.objects.get(key = token)
request.user = token_obj.user
except Token.DoesNotExist:
pass
#This is now the correct user
print (request.user)
This can be used on process_view or process_request as well.
Hopefully this can help someone out in the future.
Came across this today while having the same problem.
TL;DR;
Skip below for code example
Explanation
Thing is DRF have their own flow of things, right in the middle of the django request life-cycle.
So if the normal middleware flow is :
request_middleware (before starting to work on the request)
view_middleware (before calling the view)
template_middleware (before render)
response_middleware (before final response)
DRF code, overrides the default django view code, and executes their own code.
In the above link, you can see that they wrap the original request with their own methods, where one of those methods is DRF authentication.
So back to your question, this is the reason using request.user in a middleware is premature, as it only gets it's value after view_middleware** executes.
The solution I went with, is having my middleware set a LazyObject.
This helps, because my code (the actual DRF ApiVIew) executes when the actual user is already set by DRF's authentication.
This solution was proposed here together with a discussion.
Might have been better if DRF had a better way to extend their functionality, but as things are, this seems better than the provided solution (both performance and readability wise).
Code Example
from django.utils.functional import SimpleLazyObject
def get_actual_value(request):
if request.user is None:
return None
return request.user #here should have value, so any code using request.user will work
class MyCustomMiddleware(object):
def process_request(self, request):
request.custom_prop = SimpleLazyObject(lambda: get_actual_value(request))
The accepted answer only takes TokenAuthentication in consideration - in my case, there are more authentication methods configured. I thus went with initializing the DRF's Request directly which invokes DRF's authentication machinery and loops through all configured authentication methods.
Unfortunately, it still introduces an additional load on the database since the Token object must be queried (the accepted answer has this problem as well). The trick with SimpleLazyObject in this answer is a much better solution, but it didn't work for my use case because I need the user info in the middleware directly - I'm extending the metrics in django_prometheus and it processes the request before get_response is called.
from rest_framework.request import Request as RestFrameworkRequest
from rest_framework.views import APIView
class MyMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
drf_request: RestFrameworkRequest = APIView().initialize_request(request)
user = drf_request.user
...
return self.get_response(request)
Based on Daniel Dubovski's very elegant solution above, here's an example of middleware for Django 1.11:
from django.utils.functional import SimpleLazyObject
from organization.models import OrganizationMember
from django.core.exceptions import ObjectDoesNotExist
def get_active_member(request):
try:
active_member = OrganizationMember.objects.get(user=request.user)
except (ObjectDoesNotExist, TypeError):
active_member = None
return active_member
class OrganizationMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Code to be executed for each request before
# the view (and later middleware) are called.
request.active_member = SimpleLazyObject(lambda: get_active_member(request))
response = self.get_response(request)
# Code to be executed for each request/response after
# the view is called.
return response
Daniel Dubovski's solution is probably the best in most cases.
The problem with the lazy object approach is if you need to rely on the side effects. In my case, I need something to happen for each request, no matter what.
If I'd use a special value like request.custom_prop, it has to be evaluated for each request for the side effects to happen. I noticed that other people are setting request.user, but it doesn't work for me since some middleware or authentication class overwrites this property.
What if DRF supported its own middleware? Where could I plug it in? The easiest way in my case (I don't need to access the request object, only the authenticated user) seems to be to hook into the authentication class itself:
from rest_framework.authentication import TokenAuthentication
class TokenAuthenticationWithSideffects(TokenAuthentication):
def authenticate(self, request):
user_auth_tuple = super().authenticate(request)
if user_auth_tuple is None:
return
(user, token) = user_auth_tuple
# Do stuff with the user here!
return (user, token)
Then I could just replace this line in my settings:
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
#"rest_framework.authentication.TokenAuthentication",
"my_project.authentication.TokenAuthenticationWithSideffects",
),
# ...
}
I'm not promoting this solution, but maybe it will help someone else.
Pros:
It to solves this specific problem
There's no double authentication
Easy to maintain
Cons:
Not tested in production
Things happen in an unexpected place
Side effects...
I know it's not exactly answering the 'can we access that from the middleware' question, but I think it's a more elegant solution VS doing the same work in the middleware VS what DRJ does in its base view class. At least for what I needed, it made more sense to add here.
Basically, I'm just overriding the method 'perform_authentication()' from DRF's code, since I needed to add more things related to the current user in the request. The method just originally call 'request.user'.
class MyGenericViewset(viewsets.GenericViewSet):
def perform_authentication(self, request):
request.user
if request.user and request.user.is_authenticated():
request.my_param1 = 'whatever'
After that in your own views, instead of settings APIView from DRF as a parent class, simply set that class as a parent.
I wasn't quite happy with the solutions out there. Here's a solution that uses some DRF internals to make sure that the correct authentication is applied in the middleware, even if the view has specific permissions classes. It uses the middleware hook process_view which gives us access to the view we're about to hit:
class CustomTenantMiddleware():
def process_view(self, request, view_func, view_args, view_kwargs):
# DRF saves the class of the view function as the .cls property
view_class = view_func.cls
try:
# We need to instantiate the class
view = view_class()
# And give it an action_map. It's not relevant for us, but otherwise it errors.
view.action_map = {}
# Here's our fully formed and authenticated (or not, depending on credentials) request
request = view.initialize_request(request)
except (AttributeError, TypeError):
# Can't initialize the request from this view. Fallback to using default permission classes
request = APIView().initialize_request(request)
# Here the request is fully formed, with the correct permissions depending on the view.
Note that this doesn't avoid having to authenticate twice. DRF will still happily authenticate right afterwards.
I had the same issue and decided to change my design. Instead of using a Middleware I simply monkey-patch rest_framework.views.APIView.
In my case I needed to patch check_permissions but you can patch whatever fits your problem. Have a look at the the source code.
settings.py
INSTALLED_APPS = [
..
'myapp',
]
myapp/patching.py
import sys
from rest_framework.views import APIView as OriginalAPIView
class PatchedAPIView(OriginalAPIView):
def check_permissions(self, request):
print(f"We should do something with user {request.user}"
return OriginalAPIView.check_permissions(self, request)
# We replace the Django REST view with our patched one
sys.modules['rest_framework'].views.APIView = PatchedAPIView
myapp/__init__.py
from .patching import *

Django 1.3 post login/logout signals in relation with authentication

First of all both methods below return True. I'd expect the second one to return False using the django standard admin authentication procedure or am I wrong?
def post_login(sender, **kwargs):
"""
Django 1.3 post login signal handler
"""
# do stuff
user = kwargs['user']
print user.is_authenticated()
user_logged_in.connect(post_login)
def post_logout(sender, **kwargs):
"""
Django 1.3 post logout signal handler
"""
# do stuff
user = kwargs['user']
print user.is_authenticated()
user_logged_out.connect(post_logout)
Anyway I'm trying to understand why django doesn't have a hook on authentication failure also.. I can use my own backend for users to login and out of their account, however I would like to hook onto the admin procedure as well to cover everything in one portion of code.. I found some topics but no real awnser how to fix this.
I came up with:
import settings
from django.dispatch import Signal
failed_login = Signal(providing_args=['user'])
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User
class AuthSignalBackend(ModelBackend):
def authenticate(self, username=None, password=None):
try:
user = User.objects.get(username=username)
if user.check_password(password):
return user
else:
failed_login.send(sender=None, user=user)
except User.DoesNotExist:
return None
def login_handler(sender, **kwargs):
if settings.DEBUG:
print "failed login detected...!"
failed_login.connect(login_handler)
That works great, however there's no request in the ModelBackend, while the post_login and logout signals do have the request.. This is unfortunate because it would be great for IP logging
Any advise is welcome, I'm pretty sure people should have come across this one before..
If user is an instance of User model, user.is_authenticated() will always return True. Models instance can't know what's going on on request level. This method is for views.
If you want to deal with failed login attempts, take a look at django-axes. You can just use it, or look at the code and reimplement some ideas as you like.

Categories