diff --git a/api/permissions.py b/api/permissions.py new file mode 100644 index 00000000..6ede9491 --- /dev/null +++ b/api/permissions.py @@ -0,0 +1,106 @@ +from rest_framework import permissions +from re2o.acl import can_create, can_edit, can_delete, can_view_all + +class DefaultACLPermission(permissions.BasePermission): + """ + Permission subclass in charge of checking the ACL to determine + if a user can access the models + """ + perms_map = { + 'GET': [lambda model: model.can_view_all], + 'OPTIONS': [lambda model: model.can_view_all], + 'HEAD': [lambda model: model.can_view_all], + 'POST': [lambda model: model.can_create], + #'PUT': [], + #'PATCH': [], + #'DELETE': [], + } + perms_obj_map = { + 'GET': [lambda obj: obj.can_view], + 'OPTIONS': [lambda obj: obj.can_view], + 'HEAD': [lambda obj: obj.can_view], + #'POST': [], + 'PUT': [lambda obj: obj.can_edit], + #'PATCH': [], + 'DELETE': [lambda obj: obj.can_delete], + } + + def get_required_permissions(self, method, model): + """ + Given a model and an HTTP method, return the list of acl + functions that the user is required to verify. + """ + if method not in self.perms_map: + raise exceptions.MethodNotAllowed(method) + + return [perm(model) for perm in self.perms_map[method]] + + def get_required_object_permissions(self, method, obj): + """ + Given an object and an HTTP method, return the list of acl + functions that the user is required to verify. + """ + if method not in self.perms_map: + raise exceptions.MethodNotAllowed(method) + + return [perm(obj) for perm in self.perms_map[method]] + + def _queryset(self, view): + """ + Return the queryset associated with view and raise an error + is there is none. + """ + assert hasattr(view, 'get_queryset') \ + or getattr(view, 'queryset', None) is not None, ( + 'Cannot apply {} on a view that does not set ' + '`.queryset` or have a `.get_queryset()` method.' + ).format(self.__class__.__name__) + + if hasattr(view, 'get_queryset'): + queryset = view.get_queryset() + assert queryset is not None, ( + '{}.get_queryset() returned None'.format(view.__class__.__name__) + ) + return queryset + return view.queryset + + def has_permission(self, request, view): + # Workaround to ensure ACLPermissions are not applied + # to the root view when using DefaultRouter. + if getattr(view, '_ignore_model_permissions', False): + return True + + if not request.user or not request.user.is_authenticated: + return False + + queryset = self._queryset(view) + perms = self.get_required_permissions(request.method, queryset.model) + + return all(perm(request.user)[0] for perm in perms) + + def has_object_permission(self, request, view, obj): + # authentication checks have already executed via has_permission + queryset = self._queryset(view) + user = request.user + + perms = self.get_required_object_permissions(request.method, obj) + + if not all(perm(request.user)[0] for perm in perms): + # If the user does not have permissions we need to determine if + # they have read permissions to see 403, or not, and simply see + # a 404 response. + + if request.method in SAFE_METHODS: + # Read permissions already checked and failed, no need + # to make another lookup. + raise Http404 + + read_perms = self.get_required_object_permissions('GET', obj) + if not read_perms(request.user)[0]: + raise Http404 + + # Has read permissions. + return False + + return True + diff --git a/api/settings.py b/api/settings.py index 4475a950..767082ce 100644 --- a/api/settings.py +++ b/api/settings.py @@ -32,6 +32,6 @@ REST_FRAMEWORK = { 'rest_framework.authentication.SessionAuthentication', ), 'DEFAULT_PERMISSION_CLASSES': ( - 'rest_framework.permissions.IsAuthenticated', + 'api.permissions.DefaultACLPermission', ) }