-
Notifications
You must be signed in to change notification settings - Fork 300
Missing permission checks in RelatedMixin views #496
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Fair point. I guess I think a hint in the documentation should note that A PR would be welcome. |
I believe the following areas need to be tested for appropriate permissions. Example URLs below based on the drf_example course and term data.
Am I missing anything? What the correct "denial" response should be is also up in the air: return nothing or raise an exception. Raising an exception seems harsh; I can imagine a resource that has several relationships and it's still useful to know about those that are permitted while not seeing those that are not permitted. I'll go over to the JSON:API forum and see if there are any best practices. |
Let's not mix up different issues in one PR but create one PR for each of those two issues. Merge conflicts can always be solved later one but splitting up a PR is much harder. I do not think this is a matter of JSON API spec but of HTTP standard hence when someone doesn't have the permission to raise a 403 error makes most sense then rendering partial of the results (which might not be obvious why by API user). When using Saying this let's not validate |
OK about mixing PRs. I can certainly do it separately.
"data": [
{
"type": "term",
"id": "f9aa1a51-bf3b-45cf-b1cc-34ce47ca9913"
},
{
"type": "term",
"id": "01163a94-fc8f-47fe-bb4a-5407ad1a35fe"
}
], In fact, disclosing that there is/are any relationships present itself is a problem if the request.user doesn't have permission to the related Model. And you certainly don't want the top-level GET to raise an exception because of unavailable ResourceRelatedFields. In a more complex example (e.g. example.blog entry, which has 9 relationships) you might want the user to be able to see 5 of them but not the other 4. Or for a toMany relationship, return some but not all items (e.g. when using row-level permissions). The toMany partial response was discussed in JSON:API's discussion group but not really with a satisfactory result. One approach -- JSONAPI extension for Drupal -- gives an overall 200 response but adds Then there's the other case of field-level permissions. This seems to just be a feature one would implement in the Model or Serializer. I'll do some more digging in the JSON:API forum and see if this is covered and will post a question if not. Certainly documenting the status quo that related Models are subject to the parent's permissions can be documented now. Perhaps we make an optional Mixin so that those who want more granular permissions for related fields can get the functionality. |
@sliverc regarding your comment here:
Yes, I think calling For example, on line 167:
And on line 170:
However, @n2ygk also raises a good point here:
In the more complex example of a toMany relationship - where we want to filter a parent object's related field set based on object-level permissions - is there a convenient way for the library to defer to the view's Spitballing:
Via step 2 above, the filtering of the related objects is done by default via the view's filter. On our views, we use a DjangoObjectPermissionsFilter by default, so this would have the desired behavior of only rendering related objects that the user has permission to, similar to how a list view would function for that related object. Finally, I think |
@n2ygk @lcary Hence I suggest the following:
|
It is called in url('api/course/(?P<pk>\d+)/(?P<related_field>\w+)', CourseViewSet.as_view({'get': 'retrieve_related'})) with url('api/course/(?P<course_pk>\d+)/items', CourseItemViewSet.as_view({'get': 'list'})) and check permissions in the CourseItemViewSet. Also, I've created a small PR that probably handles some specific permissions for related entities. Probably somebody will find it helpful. |
@Anton-Shutik Thanks. I'll take a look in the next few days and provide feedback. |
We should refrain from adding new api in DJA which are not DJA specific issues. And the issue we are discussing here I think is a DRF issue and not DJA. This said:
So I suggest we do not merge #504 #505 but might think about improving the documentation on this point. |
See also discussion at DRF encode/django-rest-framework#1985 covering the same issue. |
@sliverc Agree w.r.t. not adding #504, #505 and improving documentation to include the requirement to not use @Anton-Shutik Sorry that this issue thread and #504 (comment) are covering the same conversation. I'll try to keep it here. I guess my point in my #504 (comment) is that what I really need is a filter that checks permissions to decide if data should be retrieved. I don't want to return an error to the request because some of the related data is not permitted to be retrieved. Therefore it's a filter, not a permission, but it is based on "get" permissions. I haven't even thought about what trying to POST or PATCH those related fields would look like.... Would it make sense to add a |
Things getting complicated :) I think that retrieve_related breaks a concept one view for one resource type, since single view can return different resource types. For your situation, I'm not sure what is the best way to handle it. Is it possible to filter out items that are not permitted in the filter and simply not return them ? At least client will get all items that they permitted to see |
@n2ygk class GenericAPIView(views.APIView):
def get_object(self):
..........
self.check_object_permissions(self.request, obj)
return obj probably we need change like this: class ListModelMixin(object):
"""
List a queryset.
"""
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
########check each object############
for i in queryset:
self.check_object_permissions(i)
####################
...........
return Response(serializer.data) What do you think ? |
Will have to take a look. FWIW here's what @lcary uses for ViewSets: class ModelOrObjectPermissions(DjangoObjectPermissions):
"""
Extends `DjangoObjectPermissions` with 'view' permissions.
Additionally supports checking model-level and object-level
permissions with the `check_object_permissions()` function.
"""
perms_map = {
'GET': ['%(app_label)s.view_%(model_name)s'],
'OPTIONS': ['%(app_label)s.view_%(model_name)s'],
'HEAD': ['%(app_label)s.view_%(model_name)s'],
'POST': ['%(app_label)s.add_%(model_name)s'],
'PUT': ['%(app_label)s.change_%(model_name)s'],
'PATCH': ['%(app_label)s.change_%(model_name)s'],
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
}
def has_permission(self, request, view):
"""
All views besides the `create()` view of the generic view types
should check object-level permissions. Thus, this function defers
all model-level checks, except for POST requests.
Knobs:
* For views that require the use of POST, but want to explicitly
check only object-level permissions, the class variable
`_ignore_model_permissions` must be set to `True`. This flag
is used by the parent class.
* For views that do not use the POST method, but want to explicitly
check model permissions, `force_check_model_permissions` setting
can be set to `True` on the view.
"""
setting = 'force_check_model_permissions'
force_check_model_permissions = getattr(view, setting, False)
if request.method == 'POST' or force_check_model_permissions is True:
return super().has_permission(request, view)
else:
# defer the check to `has_object_permission()`
return True
def has_object_permission(self, request, view, obj):
"""
Allows for checking if the user has permissions at the model-level
or object level.
This differs from the `DjangoObjectPermissions` class, which
typically only checks permissions at the object-level when
`check_object_permissions()` is called. This class also checks model-
level permissions.
The user is considered as having permissions to take an action on an
object if they either have permissions for all instances (aka model-
level) of an object, or they have permission to a specific instance
(aka object-level).
"""
setting = '_ignore_model_permissions'
ignore_check_model_permissions = getattr(view, setting, False)
if (not ignore_check_model_permissions and
super().has_permission(request, view)):
# This means the user/group has model permissions:
return True
# Otherwise, verify object permissions:
return super().has_object_permission(request, view, obj)
class ModelOrObjectPermissionsFilter(BaseFilterBackend):
"""
A filter backend that limits results to those where the requesting user
has read object level permissions.
Extends django-rest-framework's DjangoObjectPermissionsFilter,
except does not enforce the accept_global_perms=False rule.
"""
perm_format = '%(app_label)s.view_%(model_name)s'
def filter_queryset(self, request, queryset, view):
user = request.user
model_cls = queryset.model
kwargs = {
'app_label': model_cls._meta.app_label,
'model_name': model_cls._meta.model_name
}
permission = self.perm_format % kwargs
return get_objects_for_user(user, permission, queryset) |
@n2ygk I am passing through all the open issues and this discussion seems to have stalled and it is not quite clear what the result of this issue actually should be at this point as it seems rather a DRF issue. Do you agree that we close this issue? |
As there hasn't been any feedback I am closing this issue. Please comment though if you think this issue should remain open. |
No object level or model level permissions appear to be checked for related models in
RelatedMixin
views.Expected behavior: The related fields should be empty in a response object if the user doesn't have view permission to the related field. Alternatively, if a response must render the related object, a permissions error should be raised.
Observed behavior: Currently, if the user has permission for a model but not related models (e.g. via a foreign key relation), they are still able to view the related object if a
RelatedMixin
view's serializer has aResourceRelatedField
. This seems to merit at least some documentation warning users about the potential for leaking data, but preferably this issue could be fixed by enabling permissions checking.Reproducing the issue requires a project that checks that a user has read (or "view") permissions for models they try to access (e.g. using extended versions of
DjangoObjectPermissions
andDjangoModelPermissions
classes mentioned in the django-rest-framework permissions documentation). In such a project, for anyRelatedMixin
view that uses a serializer with aResourceRelatedField
, grant the user permission to the serializer's model object, but do not grant the user access to the related model. If the user requests the object, they will still be able to access the related field. I'm happy to add a test case for this issue.Perhaps object level or even model level permissions checking is out of scope for the django-rest-framework-json-api project, but it seems like both should be supported. After all, django-rest-framework supports and advertises both via the
DjangoObjectPermissions
andDjangoModelPermissions
classes. In a project I'm working on, we use django-guardian with a similar class to check object-level permissions. Perhaps I can overrideRelatedMixin.get_related_instance()
in the meantime as a stopgap solution, but a solution via configuration flag or a default in this library would be preferable.Best,
Luc
The text was updated successfully, but these errors were encountered: