From ca0744a38c29b580c60813ee212e7b688d529ef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Kervella?= Date: Wed, 13 Jun 2018 22:39:37 +0000 Subject: [PATCH] Add customizable ACL-based permission --- api/permissions.py | 79 +++++++++++++++++++++++++++++++++++----------- api/settings.py | 2 +- api/views.py | 6 ++-- 3 files changed, 66 insertions(+), 21 deletions(-) diff --git a/api/permissions.py b/api/permissions.py index 66480af6..e04abdaf 100644 --- a/api/permissions.py +++ b/api/permissions.py @@ -3,14 +3,69 @@ from re2o.acl import can_create, can_edit, can_delete, can_view_all from . import acl -def can_see_api(_): +def can_see_api(*_, **__): return lambda user: acl.can_view(user) -class DefaultACLPermission(permissions.BasePermission): +def _get_param_in_view(view, param_name): + assert hasattr(view, 'get_'+param_name) \ + or getattr(view, param_name, None) is not None, ( + 'cannot apply {} on a view that does not set ' + '`.{}` or have a `.get_{}()` method.' + ).format(self.__class__.__name__, param_name, param_name) + + if hasattr(view, 'get_'+param_name): + param = getattr(view, 'get_'+param_name)() + assert param is not None, ( + '{}.get_{}() returned None' + ).format(view.__class__.__name__, param_name) + return param + return getattr(view, param_name) + + +class ACLPermission(permissions.BasePermission): + """ + Permission subclass for views that requires a specific model-based + permission or don't define a queryset + """ + + def get_required_permissions(self, method, view): + """ + Given a list of models and an HTTP method, return the list + of acl functions that the user is required to verify. + """ + perms_map = _get_param_in_view(view, 'perms_map') + + if method not in perms_map: + raise exceptions.MethodNotAllowed(method) + + return [can_see_api()] + list(perms_map[method]) + + 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 + + perms = self.get_required_permissions(request.method, view) + + return all(perm(request.user)[0] for perm in perms) + + def has_object_permission(self, request, view, obj): + # Should never be called here but documentation + # requires to implement this function + return False + + +class AutodetectACLPermission(permissions.BasePermission): """ Permission subclass in charge of checking the ACL to determine - if a user can access the models + if a user can access the models. Autodetect which ACL are required + based on a queryset. Requires `.queryset` or `.get_queryset()` + to be defined in the view. """ perms_map = { 'GET': [can_see_api, lambda model: model.can_view_all], @@ -46,29 +101,17 @@ class DefaultACLPermission(permissions.BasePermission): 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: + if method not in self.perms_obj_map: raise exceptions.MethodNotAllowed(method) - return [perm(obj) for perm in self.perms_map[method]] + return [perm(obj) for perm in self.perms_obj_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 + return _get_param_in_view(view, 'queryset') def has_permission(self, request, view): # Workaround to ensure ACLPermissions are not applied diff --git a/api/settings.py b/api/settings.py index c1ec4786..cd19594e 100644 --- a/api/settings.py +++ b/api/settings.py @@ -33,7 +33,7 @@ REST_FRAMEWORK = { 'rest_framework.authentication.SessionAuthentication', ), 'DEFAULT_PERMISSION_CLASSES': ( - 'api.permissions.DefaultACLPermission', + 'api.permissions.AutodetectACLPermission', ), 'DEFAULT_PAGINATION_CLASS': 'api.pagination.PageSizedPagination', 'PAGE_SIZE': 100 diff --git a/api/views.py b/api/views.py index fce0bc23..99ee3e44 100644 --- a/api/views.py +++ b/api/views.py @@ -43,6 +43,7 @@ from re2o.utils import all_active_interfaces, all_has_access from . import serializers from .pagination import PageSizedPagination +from .permissions import ACLPermission # COTISATIONS APP @@ -351,10 +352,11 @@ class DNSZonesView(generics.ListAPIView): class StandardMailingView(views.APIView): pagination_class = PageSizedPagination - get_queryset = lambda self: all_has_access() + permission_classes = (ACLPermission, ) + perms_map = {'GET' : [users.User.can_view_all]} def get(self, request, format=None): - adherents_data = serializers.MailingMemberSerializer(self.get_queryset(), many=True).data + adherents_data = serializers.MailingMemberSerializer(all_has_access(), many=True).data data = [{'name': 'adherents', 'members': adherents_data}] paginator = self.pagination_class() paginator.paginate_queryset(data, request)