Setting up conditional views in Django based on user permissions - python

I'm attempting to set up an index page in django that serves different content based on the user permissions (2+ types of users). I've looked into using the #permission_required decorator but it seems a bit wasteful and repetitive to use that with a view that's been mostly repeated. There's also no good fallback method that I can see (I can't do a #permission_required(!'jobs.can_edit')).
views.py:
#permission_required('jobs.can_add')
def index(request):
jobs = Job.objects.all
context = {
"jobs": jobs,
}
return render(request, 'jobs/index.html', context)
#permission_required('jobs.can_edit')
def index(request):
jobs = some.different.data
context = {
"jobs": jobs,
}
return render(request, 'jobs/index.html', context)
Is there an easier way to hook this into the index function and change the context based on user permissions? My ideal scenario would be more like this
imaginary views.py:
def index(request):
if user.can_add:
context = x
return render(request, 'jobs/index/can-add.html', context)
context = y
return render(request, 'jobs/index/can-edit.html', context)
I've also set the three user groups up by name, but I don't see much documentation on accessing group names.

If you are using django permission then you can do this in view
def index(request):
if request.user.has_perm('app_name.permission_name'):
#return something if true
#return something in else case
I recommend using groups and assigning permission to a group and check if a user belongs to a group in the view
try this.

I was inspired with that and I want to share my response too for others but in my case I use DRF and django group.
class ShopOrderCreateList(APIView):
permission_classes = [AllowAny]
#staticmethod
def get_object(shop_id):
try:
return Shop.objects.get(id=shop_id)
except Shop.DoesNotExist:
raise Http404
def get(self, request, shop_id=None):
shop = self.get_object(shop_id)
orders = Order.objects.filter(shop__id=shop.id)
# orders = manager.order_set.all()
# shop_orders = shop
# order_numbers = orders.count()
serializer = OrderSerializer(orders, many=True)
return Response(serializer.data)
#staticmethod
def post(request, shop_id=None):
if request.user.groups.filter(name='admin').exists():
manager = request.user.manager
elif request.user.groups.filter(name='master').exists():
master = request.user.master
manager = master.manager

Related

How do I create links in Django such that other users can't access them?

I'm pretty confused about how do I prevent users' from accessing the data of other users.
The case at hand :
I'm creating a Notes + To-Do app in which a user logs in, creates their notes and tasks.
How to create links to those notes such that they aren't accessible by other users? As in the correct syntax for UserPassesTestMixin.
In the To-Do app, how do I keep the tasks of one user unique to them? Similarly for the note app, how do I achieve that?
Not sure what you mean by "create links". For what you describe, the links don't change for people that have access or not. The difference if that a user that owns note 5 and goes to /note/5/, they should be able to see their note, but if another user goes to /note/5/ they should either 1) get a 404 error (Note not found) or 403 (Permission Denied) just be redirected to another page (say, the home page), maybe with a message.
Using Class based views, this is easy to do.
Prevent access to views
from django.core.exceptions import PermissionDenied
from django.utils.decorators import method_decorator
from django.contrib.auth.decorators import login_required
class LoginRequiredAccessMixin(object):
# This will ensure the user is authenticated and should
# likely be used for other views
#method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
return super(LoginRequiredAccessMixin, self).dispatch(request, *args, **kwargs)
class AccessMixin(LoginRequiredAccessMixin):
def get_object(self, queryset=None):
obj = get_object_or_404(Note, pk=self.kwargs['id'])
# Assumes you have a notes.user, but change to created_by
# or whatever is your user field name
if obj.user == self.request.user:
# User owns object
return obj
raise PermissionDenied("User has no access to this note")
class NoteView(AccessMixin, DetailView):
# This is a regular DetilView, but with the Mixin,
# you are overwriting the get_object() function.
# If you don't want the Mixin, then you can just add
# get get_object() function here. Except that with the
# Mixin, you can reuse it for your UpdateView, DeleteView
# and even across both your notes and task views
model = Note
template_name = 'note/details.html'
def get_context_data(self, **kwargs):
context = super(NoteView, self).get_context_data(**kwargs)
# Add any special context for the template
return context
If instead you want to just direct users to another page, you would do something like:
from django.utils.decorators import method_decorator
from django.contrib.auth.decorators import login_required
from django.contrib import messages
class NoteView(DetailView):
model = Note
template_name = 'note/details.html'
def get_context_data(self, **kwargs):
context = super(NoteView, self).get_context_data(**kwargs)
# Add any special context for the template
return context
#method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
note = self.get_objet()
if note and not note.user == self.request.user:
messages.error(
self.request,
'You are not allowed to access this Note'
)
return HttpResponseRedirect('/home')
return super(NoteView, self).dispatch(request, *args, **kwargs)
You didn't supply any code so I cannot be more specific, but hopefully you get an idea of the two techniques. The first is usually a cleaner solution, and the Mixin I show can be shared across both your Note views and ToDo Tasks records, assuming they use the same user/created_by field name.
In case you are using functions (FBV) you could use if request.user == item.user
#login_required
def post_edit(request, post_id):
item = Post.objects.get(pk=post_id)
if request.user == item.user:
CBV - Class Based View - using UserPassesTestMixin
class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
# [...]
You can use the decorator in Django called "user_passes_test"
You can import like:
from django.contrib.auth.decorators import user_passes_test
For detail check docs here

django built in authenticator does not work

I am using the built in authenticater by django and have encountered 2 issues:
Despite placing login decorators as such :
class sales_home(View):
#method_decorator(login_required)
def get(self, request, *args, **kwargs):
return render(request, 'sales/home.html')
I am still able to access the home view by typing it into the URL. The login decorator only works when I clear my browser caches, which is not a solution in production.
The second issue with the built-in authenticator is that the request.user.id returns None.
I am currently trying to display data based on the user's own inputs.
Here is my API call :
class ChartData(APIView):
authentication_classes = []
permission_classes = []
def get(self, request, format=None):
current_user = request.user
sales_rev_data = Sales_project.objects.values_list('sales_project_est_rev', flat=True).filter(sales_project_status = 'p4').filter(sales_extras = current_user.id)
labels = Sales_project.objects.values_list('sales_project_closing_date', flat=True).filter(sales_project_status = 'p4')
data = {
"labels1": labels,
"default1": sales_rev_data,
"labels2": labels,
"default2": sales_rev_data,
}
return Response(data)
because current_user.id returns None, I am unable to access the data that corresponds to the user.
Your help will be greatly appreciated!

django-rest-framework: Adding bulk operation in a ModelViewSet

I have many endpoints which use ModelViewSet to manage CRUD operations for my models.
What I am trying to do, is to add bulk create, update, and delete at these same endpoints. In other words, I want to add POST, PUT, PATCH and DELETE to the collection endpoint (e.g.: /api/v1/my-model). There is a django-rest-framework-bulk package available, but it seems to be abandoned (hasn't been updated in 4 years) and I'm not comfortable using a package in production that is no longer active.
Additionally, There are several similar questions here that have solutions, as well as blog posts I've found. However, they all seem to use the base ViewSet, or APIView, which would require re-writing all of my existing ModelViewSet code.
Finally, there is the option of using the #action decorator, however this would require me to have a separate list endpoint (e.g.- /api/v1/my-model/bulk) which I'd like to avoid.
Are there any other ways to accomplish this while keeping my existing ModelViewSet views? I've been looking at GenericViewSet and mixins, and am wondering if creating my own mixin might be the way to go. However, looking at the mixin code, it doesn't appear that you can specify an HTTP Request method to be attached to a given mixin.
Finally, I have tried creating a separate ViewSet that accepts PUT and adding it to my URLs, but this doesn't work (I get a 405 Method not allowed when I try to PUT to /api/v1/my-model). The code I tried looks like this:
# views.py
class MyModelViewSet(viewsets.ModelViewSet):
serializer_class = MyModelSerializer
permission_classes = (IsAuthenticated,)
queryset = MyModel.objects.all()
paginator = None
class ListMyModelView(viewsets.ViewSet):
permission_classes = (IsAuthenticated,)
def put(self, request):
# Code for updating list of models will go here.
return Response({'test': 'list put!'})
# urls.py
router = DefaultRouter(trailing_slash=False)
router.register(r'my-model', MyModelViewSet)
router.register(r'my-model', ListMyModelView, base_name='list-my-model')
urlpatterns = [
path('api/v1/', include(router.urls)),
# more paths for auth, admin, etc..
]
Thoughts?
I know you said you wanted to avoid adding an extra action but in my opinion it's the simplest way to update your existing views for bulk create/update/delete.
You can create a mixin that you add to your views that will handle everything, you'd just be changing one line in your existing views and serializers.
Assuming your ListSerializer look similar to the DRF documentation the mixins would be as follows.
core/serializers.py
class BulkUpdateSerializerMixin:
"""
Mixin to be used with BulkUpdateListSerializer & BulkUpdateRouteMixin
that adds the ID back to the internal value from the raw input data so
that it's included in the validated data.
"""
def passes_test(self):
# Must be an update method for the ID to be added to validated data
test = self.context['request'].method in ('PUT', 'PATCH')
test &= self.context.get('bulk_update', False)
return test
def to_internal_value(self, data):
ret = super().to_internal_value(data)
if self.passes_test():
ret['id'] = self.fields['id'].get_value(data)
return ret
core/views.py
class BulkUpdateRouteMixin:
"""
Mixin that adds a `bulk_update` API route to a view set. To be used
with BulkUpdateSerializerMixin & BulkUpdateListSerializer.
"""
def get_object(self):
# Override to return None if the lookup_url_kwargs is not present.
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
if lookup_url_kwarg in self.kwargs:
return super().get_object()
return
def get_serializer(self, *args, **kwargs):
# Initialize serializer with `many=True` if the data passed
# to the serializer is a list.
if self.request.method in ('PUT', 'PATCH'):
data = kwargs.get('data', None)
kwargs['many'] = isinstance(data, list)
return super().get_serializer(*args, **kwargs)
def get_serializer_context(self):
# Add `bulk_update` flag to the serializer context so that
# the id field can be added back to the validated data through
# `to_internal_value()`
context = super().get_serializer_context()
if self.action == 'bulk_update':
context['bulk_update'] = True
return context
#action(detail=False, methods=['put'], url_name='bulk_update')
def bulk_update(self, request, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
serializer = self.get_serializer(
queryset,
data=request.data,
many=True,
)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
return Response(serializer.data, status=status.HTTP_200_OK)
Then you would just inherit from the mixins
class MyModelSerializer(BulkUpdateSerializerMixin
serializers.ModelSerializer):
class Meta:
model = MyModel
list_serializer_class = BulkUpdateListSerializer
class MyModelViewSet(BulkUpdateRouteMixin,
viewsets.ModelViewSet):
...
And your PUT request would just have to point to '/api/v1/my-model/bulk_update'
Updated mixins that don't require extra viewset action:
For bulk operations submit a POST request to the list view with data as a list.
class BulkUpdateSerializerMixin:
def passes_test(self):
test = self.context['request'].method in ('POST',)
test &= self.context.get('bulk', False)
return test
def to_internal_value(self, data):
ret = super().to_internal_value(data)
if self.passes_test():
ret['id'] = self.fields['id'].get_value(data)
return ret
In get_serializer() there's a check to ensure that only POST requests can be accepted for bulk operations. If it's a POST and the request data is a list then add a flag so the ID field can be added back to the validated data and your ListSerializer can handle the bulk operations.
class BulkUpdateViewSetMixin:
def get_serializer(self, *args, **kwargs):
serializer_class = self.get_serializer_class()
kwargs['context'] = self.get_serializer_context()
if self.request.method in ('POST',):
data = kwargs.get('data', None)
is_bulk = isinstance(data, list)
kwargs['many'] = is_bulk
kwargs['context']['bulk'] = is_bulk
return serializer_class(*args, **kwargs)
def create(self, request, *args, **kwargs):
if isinstance(request.data, list):
return self.bulk_update(request)
return super().create(request, *args, **kwargs)
def bulk_update(self, request):
queryset = self.filter_queryset(self.get_queryset())
serializer = self.get_serializer(
queryset,
data=request.data,
)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
return Response(serializer.data, status=status.HTTP_200_OK)
I've tested that this works but I have no idea how it will affect API schema documentation.

Django Model Authorisation

In my Django app, I've some CRUD operations over some models, which I want to be accessed only by the user who created that row. Here it's not role based authorisation as all users are same, I want them to be identified by the user who created it.
I tried something like:
class someModel(models.Model):
user = models.ForeignKey(User, db_index=True)
....
In my views, I would do the checking by:
def view(request, id):
model = somemodel.objects.get(id=id, user=request.user)
if model.user = request.user:
...
Would this be the easiest and yet be the correct way to do row level authorisation?
Ideally the views that need to check if the use is authenticated can have the login_required decorator. Then you can use try, except instead of if else because that's more pythonic
#login_required
def view(request, id):
try:
model = somemodel.objects.get(id=id, user=request.user) #this will raise an exception if not found
except Somemodel.DoesnotExist:
return HttpResponseRedirect('/login')
This of course begins to look boiler plate like. That's where Class Based Views come into the picture. Alternatively you can reduce the boiler plate code with
#login_required
def view(request, id):
my_object = get_object_or_404(SomeModel, pk=1, user=request.user)
# get_object_or_404 also throws a MultipleObjectsReturned exception
# when more then one object is returned, so catch it if needed.
You can even reduce one line of checking if model.user = request.user: as you already filtered the data with the logged in user in model = somemodel.objects.get(id=id, user=request.user)
class getUserData(APIView):
def get(self, request, format=None):
userName = request.data['userName']
try:
checkUserLoggedIn = YourAuthenticationModel.objects.get(id=id, user=userName)
getRow = someModel.objects.filter(user=userName)
except ObjectDoesNotExist:
print "User does not exist or logged In"

Django - being able to access page only after HttpResponseRedirect

I have a class based view in which I process the form and redirect the user on successful submission like so:
views.py
def get(self,request):
form = self.form_class()
return render(request, template_name, { 'form' : form })
def post(self, request, *args, **kwargs):
form = self.form_class(request.POST)
if form.is_valid():
...
return HttpResponseRedirect(reverse('success'))
return render(request, template_name, { 'form' : form })
urls.py
...
url(r'^submit/success', SubmitView.as_view(), name='success'),
...
It is possible to access url directly by typing success/submit. I don't use any authentication on the site and want the user only be able to access the submit/success page after redirection, so that they are not able to access it directly. How do I do it?
If you are using sessions, you can accomplish it like so:
# in the view where form is submitted
if form.is_valid():
request.session['form-submitted'] = True
return HttpResponseRedirect(reverse('success'))
# in the success view
def get(self, request):
if not request.session.get('form-submitted', False):
# handle case where form was not submitted
else:
# render the template
Instead of redirecting, you could POST to the 'success' page.
Then use if request.method == 'POST':
But beware, this is NOT secure, as headers can be spoofed.
Better to just call the success view from within the POST method, I think.
Have you tried something like this:
if form.is_valid():
...
return HttpResponseRedirect(SubmitView.as_view())
Not sure if this works out of the box, but with a few more tricks you might get what you want.
To add to the answer #miki725 posted I would also make sure you change
request.session['form-submitted'] = False
after you have entered the
if not request.session.get('form-submitted', False):
In order to prevent accessing the page directly or using the back and forward on the browser.

Categories

Resources