Include authenticated user in dictionary for all views - python

I am working through the Pyramid authorization tutorial and I have noticed the pattern where
logged_in = request.authenticated_userid
is added to each view dictionary. Can it be avoided? I.e. is there a configuration which automatically ads user id to each view. Or is there a way to create a base, abstract view with the user id and inherit from it?
Part of the code from the tutorial:
#view_config(context='.models.Page', renderer='templates/view.pt', permission='view')
def view_page(context, request):
# not relevant code
return dict(page = context, content = content, edit_url = edit_url,
logged_in = request.authenticated_userid)
#view_config(name='add_page', context='.models.Wiki', renderer='templates/edit.pt',
permission='edit')
def add_page(context, request):
# not relevant code
return dict(page=page, save_url=save_url,
logged_in=request.authenticated_userid)

It's been awhile since I last looked, but I think logged_in in the samples is just an example to use to conditionally check if there is a logged on user or not. You could probably just as easily refer to request.authenticated_userid within any of your views or templates, too, and get the same behavior and not have to explicitly add a status to the response dict. The request object should be available to be referenced in your view templates, too.
Alternatively, I've used their cookbook to add a user object to the request to make a friendly request.user object that I can use to both check for logged in status where needed, plus get at my other user object details if I need to as well.

Related

Django Rest Framework gives 302 in Unit tests when force_login() on detail view?

I'm using Django Rest Framework to serve an API. I've got a couple tests which work great. To do a post the user needs to be logged in and I also do some checks for the detail view for a logged in user. I do this as follows:
class DeviceTestCase(APITestCase):
USERNAME = "username"
EMAIL = 'a#b.com'
PASSWORD = "password"
def setUp(self):
self.sa_group, _ = Group.objects.get_or_create(name=settings.KEYCLOAK_SA_WRITE_PERMISSION_NAME)
self.authorized_user = User.objects.create_user(self.USERNAME, self.EMAIL, self.PASSWORD)
self.sa_group.user_set.add(self.authorized_user)
def test_post(self):
device = DeviceFactory.build()
url = reverse('device-list')
self.client.force_login(self.authorized_user)
response = self.client.post(url, data={'some': 'test', 'data': 'here'}, format='json')
self.client.logout()
self.assertEqual(status.HTTP_201_CREATED, response.status_code)
# And some more tests here
def test_detail_logged_in(self):
device = DeviceFactory.create()
url = reverse('device-detail', kwargs={'pk': device.pk})
self.client.force_login(self.authorized_user)
response = self.client.get(url)
self.client.logout()
self.assertEqual(status.HTTP_200_OK, response.status_code, 'Wrong response code for {}'.format(url))
# And some more tests here
The first test works great. It posts the new record and all checks pass. The second test fails though. It gives an error saying
AssertionError: 200 != 302 : Wrong response code for /sa/devices/1/
It turns out the list view redirects the user to the login screen. Why does the first test log the user in perfectly, but does the second test redirect the user to the login screen? Am I missing something?
Here is the view:
class APIAuthGroup(InAuthGroup):
"""
A permission to allow all GETS, but only allow a POST if a user is logged in,
and is a member of the slimme apparaten role inside keycloak.
"""
allowed_group_names = [settings.KEYCLOAK_SA_WRITE_PERMISSION_NAME]
def has_permission(self, request, view):
return request.method in SAFE_METHODS \
or super(APIAuthGroup, self).has_permission(request, view)
class DevicesViewSet(DatapuntViewSetWritable):
"""
A view that will return the devices and makes it possible to post new ones
"""
queryset = Device.objects.all().order_by('id')
serializer_class = DeviceSerializer
serializer_detail_class = DeviceSerializer
http_method_names = ['post', 'list', 'get']
permission_classes = [APIAuthGroup]
Here is why you are getting this error.
Dependent Libraries
I did some searching by Class Names to find which libraries you were using so that I can re-create the problem on my machine. The library causing the problem is the one called keycloak_idc. This library installs another library mozilla_django_oidc which would turn out to be the reason you are getting this.
Why This Library Is Causing The Problem
Inside the README file of this library, it gives you instructions on how to set it up. These are found in this file. Inside these instructions, it instructed you to add the AUTHENTICATION_BACKENDS
AUTHENTICATION_BACKENDS = [
'keycloak_oidc.auth.OIDCAuthenticationBackend',
...
]
When you add this authentication backend, all your requests pass through a Middleware defined inside the SessionRefresh class defined inside mozilla_django_oidc/middleware.py. Inside this class, the method process_request() is always called.
The first thing this method does is call the is_refreshable_url() method which always returns False if the request method was POST. Otherwise (when the request method is GET), it will return True.
Now the body of this if condition was as follows.
if not self.is_refreshable_url(request):
LOGGER.debug('request is not refreshable')
return
# lots of stuff in here
return HttpResponseRedirect(redirect_url)
Since this is a middleware, if the request was POST and the return was None, Django would just proceed with actually doing your request. However when the request is GET and the line return HttpResponseRedirect(redirect_url) is triggered instead, Django will not even proceed with calling your view and will return the 302 response immediately.
The Solution
After a couple of hours debugging this, I do not the exact logic behind this middleware or what exactly are you trying to do to provide a concrete solution since this all started based off guess-work but a naive fix can be that you remove the AUTHENTICATION_BACKENDS from your settings file. While I feel that this is not acceptable, maybe you can try using another library that accomplishes what you're trying to do or find an alternative way to do it. Also, maybe you can contact the author and see what they think.
So i guess you have tested this and you get still the same result:
class APIAuthGroup(InAuthGroup):
def has_permission(self, request, view):
return True
Why do you use DeviceFactory.build() in the first test and DeviceFactory.create() in the second?
Maybe a merge of the two can help you:
def test_get(self):
device = DeviceFactory.build()
url = reverse('device-list')
response = self.client.get(url)
self.assertEqual(status.HTTP_200_OK, response.status_code)
Is this a problem with the setUp() method? From what I see, you may be setting self.authorize_user to a user that was already created on the first test.
Instead, I would create the user on each test, making sure that the user doesn't exist already, like so:
user_exists = User.objects.filter(username=self.USERNAME, email=self.EMAIL).exists()
if not user_exists:
self.authorize_user = User.objects.create_user....
That would explain why your first test did pass, why your second didn't, and why #anupam-chaplot's answer didn't reproduce the error.
Your reasoning and code looks ok.
However you are not giving the full code, there must be error you are not seeing.
Suspicious fact
It isn't be default 302 when you are not logged in.
(#login_required, etc redirects but your code doesn't have it)
Your APIAuthGroup permission does allow GET requests for non-logged-in user ( return request.method in SAFE_METHODS), and you are using GET requests (self.client.get(url))
So it means you are not hitting the endpoint that you think you are hitting (your get request is not hitting the DevicesViewSet method)
Or it could be the case you have some global permission / redirect related setting in your settings.py which could be DRF related..
eg :
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
]
}
Guess
url = reverse('device-detail', kwargs={'pk': device.pk})
might not point to the url you are thinking..
maybe there's another url (/sa/devices/1/) that overrides the viewset's url. (You might have a django view based url)
And I didn't address why you are getting redirected after force_login.
If it's indeed login related redirect, all I can think of is self.authorized_user.refresh_from_db() or refreshing the request ..
I guess some loggin related property (such as session, or request.user) might point to old instance .. (I have no evidence or fact this can happen, but just a hunch) and you better off not logging out/in for every test case)
You should make a seperate settings file for testing and add to the test command --settings=project_name.test_settings, that's how I was told to do.

How should I write view unittest in Django?

I want to write a unittest for this method in Django.
def activate(request):
id = int(request.GET.get('id'))
user = User.objects.get(id=id)
user.is_active = True
user.save()
return render(request, 'user_authentication/activation.html')
I wrote sth like this:
def test_activate_view(self):
response = self.client.get('/activation', follow=True)
self.assertTemplateUsed(response, 'user_authentication/activation.html')
It doesn't work because I get an error:
id = int(request.GET.get('id'))
TypeError: int() argument must be a string or a number, not 'NoneType':
What should I change in my test ?
Your view reads data from request.GET - you need to pass this data:
response = self.client.get('/activation?id=1', follow=True)
You also fetch this user afterwards from your database. Therefor you need to load some fixture data. Create a fixture using manage.py dumpdata and load it in your unit test like that:
class UserTestCase(TestCase):
fixtures = ['fixture.json', 'users']
Read the docs about loading fixtures for detailed explanations.
Note regarding your approach
You should not use the users id for this use case. One can easily guess this id and activate accounts.
Someone might register once with a valid email address, receive your link with the id inside and can afterwards create a bunch of accounts without providing a valid email address.
Instead you might generate a unique and random secret (aka token) and associate this token with the user. Your view should accept those tokens and resolve the user based on it. This way one can no longer easily activate.

How to prevent user changing URL <pk> to see other submission data Django

I'm new to the web development world, to Django, and to applications that require securing the URL from users that change the foo/bar/pk to access other user data.
Is there a way to prevent this? Or is there a built-in way to prevent this from happening in Django?
E.g.:
foo/bar/22 can be changed to foo/bar/14 and exposes past users data.
I have read the answers to several questions about this topic and I have had little luck in an answer that can clearly and coherently explain this and the approach to prevent this. I don't know a ton about this so I don't know how to word this question to investigate it properly. Please explain this to me like I'm 5.
There are a few ways you can achieve this:
If you have the concept of login, just restrict the URL to:
/foo/bar/
and in the code, user=request.user and display data only for the logged in user.
Another way would be:
/foo/bar/{{request.user.id}}/
and in the view:
def myview(request, id):
if id != request.user.id:
HttpResponseForbidden('You cannot view what is not yours') #Or however you want to handle this
You could even write a middleware that would redirect the user to their page /foo/bar/userid - or to the login page if not logged in.
I'd recommend using django-guardian if you'd like to control per-object access. Here's how it would look after configuring the settings and installing it (this is from django-guardian's docs):
>>> from django.contrib.auth.models import User
>>> boss = User.objects.create(username='Big Boss')
>>> joe = User.objects.create(username='joe')
>>> task = Task.objects.create(summary='Some job', content='', reported_by=boss)
>>> joe.has_perm('view_task', task)
False
If you'd prefer not to use an external library, there's also ways to do it in Django's views.
Here's how that might look:
from django.http import HttpResponseForbidden
from .models import Bar
def view_bar(request, pk):
bar = Bar.objects.get(pk=pk)
if not bar.user == request.user:
return HttpResponseForbidden("You can't view this Bar.")
# The rest of the view goes here...
Just check that the object retrieved by the primary key belongs to the requesting user. In the view this would be
if some_object.user == request.user:
...
This requires that the model representing the object has a reference to the User model.
In my project, for several models/tables, a user should only be able to see data that he/she entered, and not data that other users entered. For these models/tables, there is a user column.
In the list view, that is easy enough to implement, just filter the query set passed to the list view for model.user = loggged_id.user.
But for the detail/update/delete views, seeing the PK up there in the URL, it is conceivable that user could edit the PK in the URL and access another user's row/data.
I'm using Django's built in class based views.
The views with PK in the URL already have the LoginRequiredMixin, but that does not stop a user from changing the PK in the URL.
My solution: "Does Logged In User Own This Row Mixin"
(DoesLoggedInUserOwnThisRowMixin) -- override the get_object method and test there.
from django.core.exceptions import PermissionDenied
class DoesLoggedInUserOwnThisRowMixin(object):
def get_object(self):
'''only allow owner (or superuser) to access the table row'''
obj = super(DoesLoggedInUserOwnThisRowMixin, self).get_object()
if self.request.user.is_superuser:
pass
elif obj.iUser != self.request.user:
raise PermissionDenied(
"Permission Denied -- that's not your record!")
return obj
Voila!
Just put the mixin on the view class definition line after LoginRequiredMixin, and with a 403.html template that outputs the message, you are good to go.
In django, the currently logged in user is available in your views as the property user of the request object.
The idea is to filter your models by the logged in user first, and then if there are any results only show those results.
If the user is trying to access an object that doesn't belong to them, don't show the object.
One way to take care of all of that is to use the get_object_or_404 shortcut function, which will raise a 404 error if an object that matches the given parameters is not found.
Using this, we can just pass the primary key and the current logged in user to this method, if it returns an object, that means the primary key belongs to this user, otherwise it will return a 404 as if the page doesn't exist.
Its quite simple to plug it into your view:
from django.shortcuts import get_object_or_404, render
from .models import YourModel
def some_view(request, pk=None):
obj = get_object_or_404(YourModel, pk=pk, user=request.user)
return render(request, 'details.html', {'object': obj})
Now, if the user tries to access a link with a pk that doesn't belong to them, a 404 is raised.
You're going to want to look into user authentication and authorization, which are both supplied by [Django's Auth package] (https://docs.djangoproject.com/en/4.0/topics/auth/) . There's a big difference between the two things, as well.
Authentication is making sure someone is who they say they are. Think, logging in. You get someone to entire their user name and password to prove they are the owner of the account.
Authorization is making sure that someone is able to access what they are trying to access. So, a normal user for instance, won't be able to just switch PK's.
Authorization is well documented in the link I provided above. I'd start there and run through some of the sample code. Hopefully that answers your question. If not, hopefully it provides you with enough information to come back and ask a more specific question.
This is a recurring question and also implies a serious security flaw. My contribution is this:
There are 2 basic aspects to take care of.
The first is the view:
a) Take care to add a decorator to the function-based view (such as #login_required) or a mixin to the class-based function (such as LoginRequiredMixin). I find the official Django documentation quite helpful on this (https://docs.djangoproject.com/en/4.0/topics/auth/default/).
b) When, in your view, you define the data to be retrieved or inserted (GET or POST methods), the data of the user must be filtered by the ID of that user. Something like this:
def get(self, request, *args, **kwargs):
self.object = self.get_object(queryset=User.objects.filter(pk=self.request.user.id))
return super().get(request, *args, **kwargs)
The second aspect is the URL:
In the URL you should also limit the URL to the pk that was defined in the view. Something like this:
path('int:pk/blog-add/', AddBlogView.as_view(), name='blog-add'),
In my experience, this prevents that an user sees the data of another user, simply by changing a number in the URL.
Hope it helps.
In django CBV (class based views) you can prevent this by comparing the
user entered pk and the current logged in user:
Note: I tested it in django 4 and python 3.9.
from django.http import HttpResponseForbidden
class UserDetailView(LoginRequiredMixin, DetailView):
model = your_model
def dispatch(self, request, *args, **kwargs):
if kwargs.get('pk') != self.request.user.pk:
return HttpResponseForbidden(_('You do not have permission to view this page'))
return super().dispatch(request, *args, **kwargs)

How to use current logged in user as PK for Django DetailView?

When defining URL patterns, I am supposed to use a regular expression to acquire a PK from the URL.
What if I want a URL that has no PK, and if it's not provided, it will use the currently logged in user? Examples:
visiting /user will get a DetailView of the currently logged in user
/user/edit will show an UpdateView for the currently logged in user
I tried hard-coding the pk= in the Detail.as_view() call but it reports invalid keyword.
How do I specify that in the URL conf?
My sample code that shows PK required error when visiting /user URL:
urlpatterns = patterns('',
url(r'user/$',
DetailView.as_view(
model=Account,
template_name='user/detail.html')),
)`
An alternative approach would be overriding the get_object method of the DetailView subclass, something along the line of:
class CurrentUserDetailView(UserDetailView):
def get_object(self):
return self.request.user
Much cleaner, simpler and more in the spirit of the class-based views than the mixin approach.
EDIT: To clarify, I believe that two different URL patterns (i.e. one with a pk and the other without) should be defined separately in the urlconf. Therefore they could be served by two different views as well, especially as this makes the code cleaner. In this case the urlconf might look something like:
urlpatterns = patterns('',
url(r"^users/(?P<pk>\d+)/$", UserDetailView.as_view(), name="user_detail"),
url(r"^users/current/$", CurrentUserDetailView.as_view(), name="current_user_detail"),
url(r"^users/$", UserListView.as_view(), name="user_list"),
)
And I've updated my example above to note that it inherits the UserDetailView, which makes it even cleaner, and makes it clear what it really is: a special case of the parent view.
As far as I know, you can't define that on the URL definition, since you don't have access to that information.
However, what you can do is create your own mixin and use it to build views that behave like you want.
Your mixin would look something like this:
class CurrentUserMixin(object):
model = Account
def get_object(self, *args, **kwargs):
try:
obj = super(CurrentUserMixin, self).get_object(*args, **kwargs)
except AttributeError:
# SingleObjectMixin throws an AttributeError when no pk or slug
# is present on the url. In those cases, we use the current user
obj = self.request.user.account
return obj
and then, make your custom views:
class UserDetailView(CurrentUserMixin, DetailView):
pass
class UserUpdateView(CurrentUserMixin, UpdateView):
pass
Generic views uses always RequestContext. And this paragraph in the Django Documentation says that when using RequestContext with auth app, the template gets passed an user variable that represents current user logged in. So, go ahead, and feel free to reference user in your templates.
You can get the details of the current user from the request object. If you'd like to see a different user's details, you can pass the url as parameter. The url would be encoded like:
url(r'user/(?P<user_id>.*)$', 'views.user_details', name='user-details'),
views.user_details 2nd parameter would be user_id which is a string (you can change the regex in the url to restrict integer values, but the parameter would still of type string). Here's a list of other examples for url patterns from the Django documentation.

Converting separate functions into class-based

I have several function that need to have a 'redirect' filter. The redirect filter goes something like this --
1) if a user is not logged in and has no session data, redirect to login page.
2) if a user is logged in and has already filled out the page, redirect to user home.
3) if a user is logged in and has not already filled out the page, stay on the page.
4) if a user is not logged in and has session data, stay on the page
I've started to convert the functions into a class-based approach to make it more efficient and less code (previously my view functions were pretty massive). This is my first stab at trying make something class-based, and this is what I have so far --
def redirect_filter(request):
if request.user.is_authenticated():
user = User.objects.get(email=request.user.username)
if user.get_profile().getting_started_boolean:
return redirect('/home/') ## redirect to home if a logged-in user with profile filled out
else:
pass ## otherwise, stay on the current page
else
username = request.session.get('username')
if not username: ## if not logged in, no session info, redirect to user login
return redirect('/account/login')
else:
pass ## otherwise, stay on the current page
def getting_started_info(request, positions=[]):
location = request.session.get('location')
redirect_filter(request)
if request.method == 'POST':
form = GettingStartedForm(request.POST)
...(run the function)...
else:
form = GettingStartedForm() # inital = {'location': location}
return render_to_response('registration/getting_started_info1.html', {'form':form, 'positions': positions,}, context_instance=RequestContext(request))
Obviously, this view is not fully working yet. How would I convert this into something that's functional?
Also, I have three variables that will need to be reused in several of the getting_started functions:
user = User.objects.get(email=request.user.username)
profile = UserProfile.objects.get(user=user)
location = profile.location
Where would I put these variable definitions so I can reuse them in all the functions, and how would I call them?
Thank you.
Django actually already includes a login_required decorator that makes handling user authentication trivial. Just include the following at the top of your view.py page:
from django.contrib.auth.decorators import login_required
and then add
#login_required
before any views that require a login. It even handles redirecting the user to the appropriate page once they log in.
More info here:
https://docs.djangoproject.com/en/dev/topics/auth/#the-login-required-decorator
This should greatly simplify your views, and may result in not having to write a separate class, since all that's left is a simple re-direct.
As for the variables, each request already contains a request.user object with information on the user. You can do a search in the docs for Request and response objects to learn more.
You can use that user object to get the profile variable by extending the user module. Set AUTH_PROFILE_MODULE = 'myapp.UserProfile' in your Settings, which will allow you to access a users profile as follows:
user.get_profile().location.
More about that here:
http://www.b-list.org/weblog/2006/jun/06/django-tips-extending-user-model/

Categories

Resources