Request method mapping is happening after permission check - django

I have a view that creates and updates user as shown below.
class UserViewSet(ViewSet):
def check_permissions(self, request):
print("Action = ", self.action)
if self.action == 'create_user':
return True
return super().check_permissions(request)
def create_user(self, request):
# user creation code
def update(self, request):
#user update code
And create_user is mapped with POST method and update is mapped with PUT method. So, create_user action should not require authenticated user but for update, user should be authenticated. Overriding check_permission did the job. Now, when I was testing the create_user end point. I wrote a test that tries to creates user using some method other than POST. I expected that the response should be HTTP_405_METHOD_NOT_ALLOWED.
def test_create_user_with_invalid_method(self):
data = self.user_data
response = self.client.put(self.url, data)
self.assertEqual(response.status_code, HTTP_405_METHOD_NOT_ALLOWED)
But the response was HTTP_401_UNAUTHORIZED. And the action variable was set as None. My url mapping is like this:
url(r'^account/register/$', UserViewSet.as_view({"post": "create_user"}),name="account_register_view"),
url(r'^account/update/$', UserViewSet.as_view({"put": "update"}), name="account_update_vew"),
So looking at this, I thought djago is either doing the mapping of request(method, url) to action either after checking permission or doing it before but setting action as None when it fails to find the proper mapping
So my concern is that whether this is the correct behaviour and the test i wrote and what i was expecting is wrong or is there something strange going on here.

Related

Restric a user to get or update a object made by specific user django

I have a task model like
class Tasks(models.Model):
made_by=models.ForeignKey(User , on_delete=models.CASCADE)
title = models.CharField(max_length=100,null=True,blank=True)
I have a view like
def get(self, request, id):
task = Tasks.objects.get(id=id)
if task:
serializer = TasksSerializer(task, many=False)
return Response(success_response(serializer.data, "Contact Information."
status=status.HTTP_200_OK)
And a PUT in same way. I want to check Only User which made_by it can access it . Is there any smart way to do this ? I dont want to query for check again in all views and hereafter.
Since it appears that you are using class-based views I would suggest that you override the dispatch method of your class. This class gets executed every time someone calls the view, no matter the method.
In this dispatch method you could first of all retrieve the task object like you do in the get-function in your current code. After that step you could then perform a check to see whether the request.user equals the object's made_by.
For example:
def dispatch(self, request, id):
self.task = Tasks.objects.get(id=id) # consider using get_object_or_404
# Check if user is owner of task, otherwise throw a 404
if request.user != self.task.made_by:
raise Http404()
# Will continue execution as normal, calling get() if a get-request was made
# the variable self.task will be available in this function, so re-retrieving the
# object is not necessary
return super().dispatch(request, id)
Additionally I would also suggest using the default LoginRequiredMixin (source) to make sure that only logged-in users can access the view. It could eliminate custom written checks in many cases.
The PermissionRequiredMixin (source) is also a great choice when dealing with more general permissions that are not related to specific instances.
For more specific - customized - permissions you could also use the UserPassesTestMixin (source) to write checks in dedicated test funcs to keep your code cleaner.
a couple different ways to go about this but I think the easiest is to test the user's made by status. i.e.
def run_task(self, request, id):
if request.user.made_by == 'Foo Bar'
. . . .
return Response(success_response ...
else
return Response(failure_response ...
another, more complicated way would be to play around with the standard account model perms and set 'task' perms to false. i.e.
def has_task_perms(self):
return False
and then in the process of user creation through whichever user type has the ability to give task perms. The most common way I am aware of to do this is to use an account manager (a whole rabbit whole in and of itself.) i.e.
class AccountManager(BaseUserManager):
def create_user(self, password):
# define regular creation process
. . .
return user
def create_special_user(self, password):
# define special creation process i.e.
has_task_perms == True
return user
I tried so many solution but At the End I made a decorator like this
def task_ownership_check(func):
def wrapper(request,*args, **kwargs):
print(kwargs['id'])
try:
task = Tasks.objects.get(id=kwargs['id'])
except Tasks.DoesNotExist:
return Response(failure_response(data={}, msg='No task matching query found'),
status=status.HTTP_404_NOT_FOUND)
if args[0].user != task.todolist.for_user:
return Response(failure_response(data={'error': 'You are not allowed to access this record'},
msg='You are not allowed to access this record'),
status=status.HTTP_400_BAD_REQUEST)
else:
return func(args[0], kwargs['id'])
return wrapper
So Now I can check easily by including #task_ownership_check on any task

DRF Object level permissions check for single field

How do you add permissions to a model so that any user can add a new instance, but only a logged in user can add a particular attribute?
Django models.py:
class Ingredient(models.Model):
name = models.CharField(max_length=100, unique=True)
recipes = models.ManyToManyField(Recipe, related_name='ingredients', blank=True)
DRF views.py:
class IngredientViewSet(viewsets.ModelViewSet):
queryset = Ingredient.objects.all()
serializer_class = IngredientSerializer
permission_classes = (IsUserForRecipeOrBasicAddReadOnly,)
DRF permissions.py:
class IsUserForRecipeOrBasicAddReadOnly(permissions.BasePermission):
"""
Custom permission to only allow logged in users to add an ingredient AND associate its recipe(s).
"""
message = 'You must be logged in to add an Ingredient to a Recipe.'
# using this method so I can access the model obj itself
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request,
if request.method in permissions.SAFE_METHODS:
return True
# Check if we are creating, and if the recipes are included, and if they are not a user. If so, return False
if request.method == 'POST' and obj.recipes.all().count() > 0 and request.user.is_anonymous:
return False
else:
return True
I see the appropriate calls/prints to the custom permission class, but I can still make a POST request with a list of recipe id's and it does not error with the message.
Notes -
I am getting two POST requests, both with the same information/print statements, and then a third to the GET (once it is added, it shows the newly created instance - which is correct behavior, but I don't know why two POSTs are going through)
I think a better approach would be to use 2 different serializer(one of them hase Recipes as a writable field and the other does not), then override get_serializer_class:
class yourMOdelviewset():
...
...
def get_serializer_class(self):
if self.action == 'create':
if self.request.user.is_authenticated:
return SerializerThatHasRecipesAsAWriteableField
else:
return SerializerThatHasNot
return super().get_serializer_class()
p.s. Drf uses object level permission for retrieving or updating (basically there should be an object already), since in create there is no object yet, drf never checks the object level permission.
The solution proposed by #changak is a good one. Including this as a more direct solution to the question posed. In DRF, has_object_permission is explicitly for an object already in the database, but you can use has_permission. From the docs, this excerpt explains why you don't see has_object_permission being called:
Note: The instance-level has_object_permission method will only be
called if the view-level has_permission checks have already passed.
In has_permission, you still have access to the data, and can add a check. Assuming your IngredientSerializer has a recipes field, you can check with something like this:
class IsUserForRecipeOrBasicAddReadOnly(permissions.BasePermission):
"""
Custom permission to only allow logged in users to add an ingredient AND associate its recipe(s).
"""
message = 'You must be logged in to add an Ingredient to a Recipe.'
def has_permission(self, request, view):
if view.action != 'create':
# Handle everything but create at the object level.
return True
if not request.data.get('recipes'):
return True
return request.user and request.user.is_authenticated()

django-fsm: Permissions not raising exception

I've got source and target rule-based transition decorators working well in django-fsm (Finite State Machine). Now I'm trying to add permissions handling. This seems straightforward, but it seems that no matter what I do, the transition is executed, regardless the user's permissions or lack thereof. I've tried with Django permission strings, and I've tried with lambda, per the documentation. I've tried all of these:
#transition(field=state, source='prog', target='appr', permission='claims.change_claim')
and
#transition(field=state, source='prog', target='appr', permission=lambda instance, user: not user.has_perm('claims.change_claim'),)
and, just as a double-check, since permission should respond to any callable returning True/False, simply:
#transition(field=state, source='prog', target='appr', permission=False)
def approve(self):
Which should raise a TransitionNotAllowed for all users when accessing the transition. But nope - even basic users with no permissions can still execute the transition (claim.approve()).
To prove that I've got permission string right:
print(has_transition_perm(claim.approve, request.user))
prints False. I am doing validation as follows (works for source/target):
class ClaimEditForm(forms.ModelForm):
'''
Some users can transition claims through allowable states
'''
def clean_state(self):
state = self.cleaned_data['state']
if state == 'appr':
try:
self.instance.approve()
except TransitionNotAllowed:
raise forms.ValidationError("Claim could not be approved")
return state
class Meta:
model = Claim
fields = (
'state',
)
and the view handler is the standard:
if request.method == "POST":
claim_edit_form = ClaimEditForm(request.POST, instance=claim)
if claim_edit_form.is_valid(): # Validate transition rules
What am I missing? Thanks.
The problem turned out to be that the permission property does validation differently from the source/target validators. Rather than the decorator raising errors, you must evaluate the permissions established in the decorator elsewhere in your code. So to perform permission validation from a form, you need to pass in the user object, receive user in the form's init, and then compare against the result of has_transition_perm. So this works:
# model
#transition(field=state, source='prog', target='appr', permission='claims.change_claim')
def approve(self):
....
# view
if request.method == "POST":
claim_edit_form = ClaimEditForm(request.user, request.POST, instance=claim)
....
# form
from django_fsm import has_transition_perm
class ClaimEditForm(forms.ModelForm):
'''
Some users can transition claims through allowable states
(see permission property on claim.approve() decorator)
'''
def __init__(self, user, *args, **kwargs):
# We need to pass the user into the form to validate permissions
self.user = user
super(ClaimEditForm, self).__init__(*args, **kwargs)
def clean_state(self):
state = self.cleaned_data['state']
if state == 'appr':
if not has_transition_perm(self.instance.approve, self.user):
raise forms.ValidationError("You do not have permission for this transition")

Accessing and Saving model objects directly without using serializers in django rest framework

I have created an api endpoint for one of my resources called "creative" . However I needed to have an action to implement pause and start of this resource (to change the field called status) . Hence I created a #detail_route() for the above actions .
What am I trying to achieve: I need to update a field of this particular resource(model) Creative as well as a field of another model whose foreign key is this model.
I am calling a custom function toggleAdGroupState inside this #detail_route() method where I directly work on the django models rather than using the serializers. Is this allowed ? Is this a proper way of implementing what i need to do ?
class CreativeSerializer(serializers.ModelSerializer):
class Meta:
model = Creative
fields = ('__all__')
#detail_route(methods=['post','get','put'])
def startcreative(self, request, pk=None):
try:
creative_object= self.get_object()
creative_object.active_status=Creative._STATUS_ACTIVE
creative_object.save()
toggleAdGroupState(creative_object.id,"active")
except Exception as e:
print 'error is ',str(e)
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return Response(status=status.HTTP_200_OK)
def toggleAdGroupState(creative_id,toggleFlag):
adgroup_obj = AdGroup.objects.filter(creative=creative_id)
for item in adgroup_obj:
if(toggleFlag == 'active'):
item.userActivate()
else:
item.userInactivate()
djangorestframework is 3.2.2
I believe are following these steps
user clicks to do post/put request.
this request active_status from
model creative Next you want to update Another Model.
You can do like this in view for each method. I have shown for put,
class View(generics.UpdateAPIView):
....
def update(self, request, *args, **kwargs):
# update creative model
response = generics.UpdateAPIView.update(self, request, *args, **kwargs)
if response.status == 200:
# update_another_model()

has_object_permission not taken into account

I'm trying to enforce a permission with Django Rest Framework where a specific user cannot post an object containing a user id which is not his.
For example i don't want a user to post a feedback with another id.
My model is something like :
class Feedback(Model):
user = ForeignKey(User)
...
I try to put a permission on my view which would compare the feedback.user.id with the request.user.id, the right work ok on a post on an object and return false, but it's still posting my object... Why?
The View
class FeedbackViewSet(ModelViewSet):
model = Feedback
permission_classes = (IsSelf,)
serializer_class = FeedbackSerializer
def get_queryset(self):
....
The Permission
class IsSelf(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
#return eval(obj.user.id) == request.user.id
return False
I've commented the line to show where the problem lies.
Again the function is correctly called and returns False, but there's just no PermissionDenied raised.
While at it, i'm wondering if this is actually the way to implement this behaviour, and if not, what would be...?
Thanks.
Your problem is that has_object_permission is only called if you're trying to access a certain object. So on creation it is never actually used.
I'd suggest you do the check on validation. Example:
class FeedbackSerializer(HyperlinkedModelSerializer):
def validate(self, attrs):
user = self.context['request'].user
if attrs['user'].id != user.id:
raise ValidationError('Some exception message')
return attrs
If you have some other super serializer class then just change it.
Now that I think of it if the user field must always be the posting user, then you should just make that field read-only and set it on pre_save() in the viewset class.
class FeedbackViewSet(ModelViewSet):
def pre_save(self, obj, *args, **kwargs):
if self.action == 'create':
obj.user = self.request.user
And in the serializer set the user field read-only
class FeedbackSerializer(HyperlinkedModelSerializer):
user = serializers.HyperlinkedRelatedField(view_name='user-detail', read_only=True)
....
I don't know if this is still open...
However, in order to work, you should move that line from "has_object_permission" to "has_permission", something like this:
class IsSelf(permissions.BasePermission):
def has_permission(self, request, view, obj):
if request.method == 'POST':
#your condition
Worked for me.
As it was stated in the selected answer
has_object_permission is only called if you're trying to access a certain object
so you have to place your condition under has_permission instead.