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")
Related
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
TL;DR
How to write a custom permission class to check object-level permissions (has_object_permission) before the api-level ones (has_permission)?
Definitions
Suppose we are building an online annotation tool. We have Images belonging to Projects and Users working on annotating Images. Users have a role field -- Guests have read-only access, Developers may change the Image fields and Owners may change both Projects and Images.
The models:
class Image(models.Model):
data = JSONField()
project = models.ForeignKey(Project)
class Project(models.Model):
pass
class User(AbstractUser):
ROLE_GUEST, ROLE_DEVELOPER, ROLE_OWNER = range(3)
ROLE_CHOICES = [(ROLE_GUEST, "Guest"), (ROLE_DEVELOPER, "Developer"), (ROLE_OWNER, "Owner")]
role = models.IntegerField(default=ROLE_GUEST, choices=ROLE_CHOICES)
Suppose we wish to restrict access to and within some Projects. We are adding m2m model UserProjectRole:
class UserProjectRole(models.Model):
user = models.ForeignKey(User)
project = models.ForeignKey(Project)
project_role = ... # same as in User
Now we wish to set up permissions in a way that, if user requests access to API endpoints regarding some Project:
if there is no UserProjectRole with this user and this project, permissions defaults to his global role
if there is such UserProjectRole, the permissions are using his project_role instead of role
Problem
We are implementing custom permission class:
# permissions.py
from rest_framework import permissions
class AtLeastDeveloper(permissions.BasePermission):
def has_permission(self, request, view):
return request.user.is_staff or request.user.role >= User.ROLE_DEVELOPER
class AtLeastOwner(permissions.BasePermission):
def has_permission(self, request, view):
return request.user.is_staff or request.user.role >= User.ROLE_OWNER
class InProjectPermissions(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if isinstance(obj, Project):
project = obj
elif isinstance(obj, Image):
project = obj.project
else:
raise ValueError(obj)
upr = UserProjectRole.objects.filter(user = request.user, project = project).first()
if upr is None:
return False
if isinstance(obj, Project):
return upr.role == User.ROLE_OWNER
return upr.role >= User.ROLE_DEVELOPER
# views.py
class ProjectGetDeleteUpdateView(RetrieveUpdateDestroyAPIView):
permission_classes = [IsAuthenticated, AtLeastOwner, InProjectPermissions]
...
class ImageGetDeleteUpdateView(RetrieveUpdateDestroyAPIView):
permission_classes = [IsAuthenticated, AtLeastDeveloper, InProjectPermissions]
...
The problem here is that has_permission is called before has_object_permissions (as it is stated in https://www.django-rest-framework.org/api-guide/permissions/#custom-permissions). Therefore, if the User's global role is not sufficient, he will be denied access (IsAtLeastSomething.has_permissions will return False and therefore InProjectPermissions.has_object_permissions will not be executed).
Please advise on how to write a custom permission class to check object-level permissions before the api-level ones.
TLDR; - Try overriding check_object_permissions(request, obj)
As per DRF documentations :
Before running the main body of the view each permission in the list
is checked. If any permission check fails an
exceptions.PermissionDenied or exceptions.NotAuthenticated exception
will be raised, and the main body of the view will not run.
Which means all the permissions which we have mentioned in permission_classes MUST pass. If anyone of them also fails, then it'll not authenticate.
So for our custom need we can override check_object_permissions(request, obj) and write our custom authentication.
So, that's what i came up with.
I could, as suggested by #Hafnernuss, get access to the object in question by view.get_object(id) (taking id from request.GET/POST), but this is exactly what i wished to avoid: manual tackling with internals of structures.
#umair-mohammad suggested to override view.check_object_permissions, but again, that i wished to avoid, because it meant that all my views should be inherited from one base class. In most cases this is OK and thank you for your answer -- that indeed solves the problem -- but i really wanted to separate this logic into permissions.py without touching the views.
So, let's instead override the base permission class:
class AbstractPermission(permissions.BasePermission):
def has_permission(self, request, view):
return True
def has_permission_by_global_role(self, request, view):
raise NotImplementedError()
def has_object_permission(self, request, view, obj):
if isinstance(obj, Project):
project = obj
elif isinstance(obj, Image):
project = obj.project
else:
raise ValueError(obj)
# if user has global role big enough -- permit
if self.has_permission_by_global_role(request, view):
return True
# otherwise -- assess his 'local' role (as before)
upr = UserProjectRole.objects.filter(user = request.user, project = project).first()
if upr is None:
return False
if isinstance(obj, Project):
return upr.role == User.ROLE_OWNER
return upr.role >= User.ROLE_DEVELOPER
Then we inherit our AtLeastSomething classes, changing has_permission to has_permission_by_global_role:
class AtLeastOwner(AbstractPermission):
def has_permission_by_global_role(self, request, view):
return request.user.is_staff or request.user.role >= User.ROLE_OWNER
...
Thus, in views.py, we only need to specify permission_classes based on a permitted global role -- in our case, that is AtLeastOwner for Project and AtLeastDeveloper for Images.
I believe this is the most pythonic approach to this. One can further make this cleaner by providing each model with something like .allowed_roles() method.
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.
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()
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.