Django revoke session key if logged in from another device/browser - django

i want to revoke the old session as soon as the user logs-in from another device but if i use session flush here i get logged out from all browsers. has smb a solution for this:
middleware.py
from django.contrib.sessions.models import Session
# Session model stores the session data
from .models import LoggedInUser
class OneSessionPerUserMiddleware:
# Called only once when the web server starts
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if request.user.is_authenticated:
session_key = request.session.session_key
try:
logged_in_user = request.user.logged_in_user
stored_session_key = logged_in_user.session_key
if stored_session_key != session_key:
Session.objects.filter(session_key=stored_session_key).delete()
logged_in_user.session_key = session_key
logged_in_user.save()
except LoggedInUser.DoesNotExist:
LoggedInUser.objects.create(user=request.user, session_key=session_key)
response = self.get_response(request)
return response

Related

Django Channels - Custom Authentication Middleware raises: 'coroutine' object is not callable

I have created a custom token authentication middleware.
from rest_framework.authtoken.models import Token
from django.contrib.auth.models import AnonymousUser
from django.db import close_old_connections
from asgiref.sync import sync_to_async
class TokenAuthMiddleware:
"""
Token authorization middleware for Django Channels 2
"""
def __init__(self, inner):
self.inner = inner
def __call__(self, scope):
# Close old database connections to prevent usage of timed out connections
sync_to_async(close_old_connections)()
headers = dict(scope['headers'])
try:
token_name, token_key = headers[b'sec-websocket-protocol'].decode().split(', ')
if token_name == 'Token':
token = sync_to_async(Token.objects.get, thread_sensitive=True)(key=token_name)
scope['user'] = token.user
else:
scope['user'] = AnonymousUser()
except Token.DoesNotExist:
scope['user'] = AnonymousUser()
return self.inner(scope)
When I run it, an exception happens when I run scope['user'] = token.user
[Failure instance: Traceback: <class 'AttributeError'>: 'coroutine' object has no attribute 'user'
I tried awaiting the Token query like this:
token = await sync_to_async(Token.objects.get, thread_sensitive=True)(key=token_name)
and I added async in front of the __call__ function, but then the following error is raised before any of the code inside the __call__ function runs:
[Failure instance: Traceback: <class 'TypeError'>: 'coroutine' object is not callable
I am using Django v3.0.6 and Django Channels v2.4.0
Here is the solution that worked for me:
from rest_framework.authtoken.models import Token
from django.contrib.auth.models import AnonymousUser
from channels.db import database_sync_to_async
#database_sync_to_async
def get_user(token):
try:
return Token.objects.get(key=token).user
except Token.DoesNotExist:
return AnonymousUser()
class TokenAuthMiddleware:
"""
Token authorization middleware for Django Channels 2
"""
def __init__(self, inner):
# Store the ASGI application we were passed
self.inner = inner
def __call__(self, scope):
return TokenAuthMiddlewareInstance(scope, self)
class TokenAuthMiddlewareInstance:
"""
Inner class that is instantiated once per scope.
"""
def __init__(self, scope, middleware):
self.middleware = middleware
self.scope = dict(scope)
self.inner = self.middleware.inner
async def __call__(self, receive, send):
headers = dict(self.scope['headers'])
token_name, token_key = headers[b'sec-websocket-protocol'].decode().split(', ')
if token_name == 'Token':
self.scope['user'] = await get_user(token_key)
else:
self.scope['user'] = AnonymousUser()
# Instantiate our inner application
inner = self.inner(self.scope)
return await inner(receive, send)
Just wrap your function in database_sync_to_async, it will handle the connections for you
class TokenAuthMiddleware:
"""
Token authorization middleware for Django Channels 2
"""
def __init__(self, inner):
self.inner = inner
async def __call__(self, scope):
# Close old database connections to prevent usage of timed out connections
sync_to_async(close_old_connections)()
headers = dict(scope['headers'])
try:
token_name, token_key = headers[b'sec-websocket-protocol'].decode().split(', ')
if token_name == 'Token':
user = await self.get_user(token)
scope['user'] = user
else:
scope['user'] = AnonymousUser()
except Token.DoesNotExist:
scope['user'] = AnonymousUser()
#database_sync_to_async
def get_user(self, token):
token = Token.ojbects.get(key=token)
return token.user
In case it helps anyone, I was having the same issue along with others. I updated django channels and then changed my routing from
websocket_urlpatterns = [
re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer),
]
to
websocket_urlpatterns = [
re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()),
]
You need django channels 3.0+ to do this (https://channels.readthedocs.io/en/stable/releases/3.0.0.html). Then you can follow https://channels.readthedocs.io/en/stable/topics/authentication.html#django-authentication to setup your custom middleware.
Reference:
Django Channel Custom Authentication Middleware __call__() missing 2 required positional arguments: 'receive' and 'send'

How do I password protect all but one URL in Django without authentication?

Is there a tool that allows me to password protect all but one URL in Django without requiring authentication?
You can write a custom middleware for this:
class LoggedInUserCheckMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
full_path = request.get_full_path()
if not full_path == '/no-need-auth-url' and not request.user.is_authenticated:
raise Http404
response = self.get_response(request)
return response
And add it to MIDDLEWARE in settings:
MIDDLEWARE = [
# rest of middlewares
'path.to.LoggedInUserCheckMiddleware'
]
you can use #loging_required decorator
from django.contrib.auth.decorators import login_required
on all your views except that one view/url.

Cannot change superusers own django admin inline

I have recently updated from django 2.0.4 to 3.0.5.
I have a UserAdmin with the following inline:
class PreferencesInline(admin.StackedInline):
model = Preferences
can_delete = False
classes = ['collapse']
When I login as a superuser, I can change the preferences of other users through the inline, but not my own. Why is that? On our server with django 2.0.4 I can both change other users preferences but also my own preferences through the inline. I could not find any explanation for this strange behaviour...
EDIT The same behaviour applies when I try to change my Preferences directly in the admin, so this is not specifically an issue with the inline.
EDIT2 The problem is the following middleware. Any ideas how it could be modified to solve the problem?
class UserLanguageMiddleware:
""" Middleware to set user language """
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if request.user.is_authenticated:
user_language = request.user.profile.language
activate(user_language)
response = self.get_response(request)
return response
By following the official Django advice to explicity setting the active language, the problem was solved:
settings.py:
LANGUAGE_COOKIE_NAME = 'language'
middleware.py:
class UserLanguageMiddleware:
""" Middleware to set user language cookie """
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
if request.user.is_authenticated and not request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME):
# Set cookie
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, request.user.profile.language)
elif not request.user.is_authenticated and request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME):
# Delete cookie
response.delete_cookie(settings.LANGUAGE_COOKIE_NAME)
return response
models.py
from django.dispatch import receiver
from django.contrib.auth.signals import user_logged_in
# Update user language on login
#receiver(user_logged_in)
def post_login(sender, user, request, **kwargs):
user_language = user.profile.language
translation.activate(user_language)

Accessing Middleware value for testing a Django DetailView

I am writing a test for a DetailView that queries get_object() by accessing a value set in a Middleware. This is for a Companies application and Company Model. Each user is in a Company.
To access the company throughout the project, I set the current user's Company.uuid on the request via a custom middleware.
Middleware
from django.utils.deprecation import MiddlewareMixin
class DynamicCompanyUUIDMiddleware(MiddlewareMixin):
""" Adds the current organization's UUID from the current user."""
def process_request(self, request):
try:
company_uuid = request.user.company_uuid
except:
company_uuid = None
request.company_uuid = company_uuid
That is used in the CompanyDetailView's get_object() method via a Mixin that I use for the other Company Views.
Mixin
class CompanyMixin(LoginRequiredMixin, SetHeadlineMixin):
model = Company
def get_object(self):
return get_object_or_404(
self.model,
uuid=self.request.user.company_uuid)
Test
The test that I'm trying to write is:
from django.test import RequestFactory
from django.urls import reverse, resolve
from test_plus.test import TestCase
from ..models import Company
from ..views import CompanyDetailView
class BaseCompanyTestCase(TestCase):
def setUp(self):
self.user = self.make_user()
self.object = Company.objects.create(owner=self.user, name="testcompany")
self.user.company_uuid = self.object.uuid
self.factory = RequestFactory()
class TestCompanyDetailView(BaseCompanyTestCase):
def setUp(self):
super(TestCompanyDetailView, self).setUp()
self.client.login(username="testuser", password="password")
self.view = CompanyDetailView()
self.view.object = self.object
request = self.factory.get(reverse('companies:detail'))
request.user = self.user
request.company_uuid = self.user.company_uuid
response = CompanyDetailView.as_view()(request)
self.assertEqual(response.status_code, 200)
def test_get_headline(self):
self.assertEqual(
self.view.get_headline(),
'%s Members' % self.object.name
Result
This results in a 404 with the testuser's company not being found.
Walking through it:
I create the user
Create the company for this new testuser
Set the user.company_uuid
This should allow the mixin to access the company_uuid
Therefore return the user's company in the request
However I'm not returning the company as the 404 shows.
Question
Where am I going wrong on this? Thanks in advance for your help.
Answer
I was mixing Django's Client & RequestFactory. I have corrected the code above which is correct.
I was mixing Django's Client & RequestFactory. After stepping away, I figured it out below -
from django.test import RequestFactory
from django.urls import reverse, resolve
from test_plus.test import TestCase
from ..models import Company
from ..views import CompanyDetailView
class BaseCompanyTestCase(TestCase):
def setUp(self):
self.user = self.make_user()
self.object = Company.objects.create(owner=self.user, name="testcompany")
self.user.company_uuid = self.object.uuid
self.factory = RequestFactory()
class TestCompanyDetailView(BaseCompanyTestCase):
def setUp(self):
super(TestCompanyDetailView, self).setUp()
self.client.login(username="testuser", password="password")
self.view = CompanyDetailView()
self.view.object = self.object
request = self.factory.get(reverse('companies:detail'))
request.user = self.user
request.company_uuid = self.user.company_uuid
response = CompanyDetailView.as_view()(request)
self.assertEqual(response.status_code, 200)
def test_get_headline(self):
self.assertEqual(
self.view.get_headline(),
'%s Members' % self.object.name

Writing a test in Django with request.user is not passing when it should

Here's what I'm trying to do.
Use a mixin to validate ownership of an object.
Test the detail url to make sure that request.user == obj.owner
I expect the detail test to pass with a 200 assertion. But it's giving me a 302. But when I do a print from the mixing the request.user and owner are the same.
Here's my mixin:
from django.contrib.auth.mixins import UserPassesTestMixin
class IsOwnerMixin(UserPassesTestMixin):
"""
a custom mixin that checks to see if the user is the owner of the object
"""
def test_func(self):
# get the object
obj = self.get_object()
# if the obj.user == the logged in user they can see it otherwise boo!
if self.request.user == obj.owner:
return True
else:
return False
Here's my View:
class AwesomeDetail(LoginRequiredMixin, IsOwnerMixin, DetailView):
"""
An awesome detail
"""
model = models.Awesome
template_name = "awesomeness/detail.html"
Here's my Test:
from django.test import TestCase, RequestFactory
from django.test.client import Client
from django.contrib.auth.models import AnonymousUser, User, Group
from project.awesomness import views, models
class UrlsViewsTest(TestCase):
def setUp(self):
self.factory = RequestFactory()
self.user = User.objects.create_user(id='1', username='name', email='email#email.com', password='top_secret')
self.awesome = models.Awesome.objects.create(id='2', owner=self.user)
self.not_owner = User.objects.create_user(id='3', username='trouble', email='otheremail#otheremail.com', password='top_secret')
def test_awesome_detail(self):
"""
Test the awesome detail URL
"""
request = self.factory.get('/awesome/2/')
request.user = self.user
response = views.AwesomeDetail.as_view()(request, pk=2)
self.assertEqual(response.status_code, 200)
def test_awesome_not_owner(self):
"""
Test the awesome detail with a user that is not the owner
"""
request = self.factory.get('/awesome/2/')
request.user = self.not_owner
response = views.AwesomeDetail.as_view()(request, pk=2)
self.assertEqual(response.status_code, 302)
def test_awesome_detail_anonymous_user(self):
"""
Test the awesome detail with a user that is anonymous
"""
request = self.factory.get('/awesome/2/')
request.user = AnonymousUser()
response = views.AwesomeDetail.as_view()(request, pk=2)
self.assertEqual(response.status_code, 302)
And finally, here's the result:
Creating test database for alias 'default'...
F..
======================================================================
FAIL: test_awesome_detail (project.awesomeness.tests.UrlsViewsTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/dave/sandbox/project/project/awesomeness/tests.py", line 25, in test_awesome_detail
self.assertEqual(response.status_code, 200)
AssertionError: 302 != 200
----------------------------------------------------------------------
Ran 3 tests in 0.202s
FAILED (failures=1)
Destroying test database for alias 'default'...