permissions.py 10 KB
Newer Older
1
# -*- mode: python; coding: utf-8 -*-
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
# Copyright © 2018 Maël Kervella
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

"""Defines the permission classes used in the API.
"""

25
from rest_framework import permissions, exceptions
26

27 28
from . import acl

29

30
def can_see_api(*_, **__):
31 32 33 34 35 36
    """Check if a user can view the API.

    Returns:
        A function that takes a user as an argument and returns
        an ACL tuple that assert this user can see the API.
    """
37 38 39
    return lambda user: acl.can_view(user)


40
def _get_param_in_view(view, param_name):
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
    """Utility function to retrieve an attribute in a view passed in argument.

    Uses the result of `{view}.get_{param_name}()` if existing else uses the
    value of `{view}.{param_name}` directly.

    Args:
        view: The view where to look into.
        param_name: The name of the attribute to look for.

    Returns:
        The result of the getter function if found else the value of the
        attribute itself.

    Raises:
        AssertionError: None of the getter function or the attribute are
            defined in the view.
    """
erdnaxe's avatar
erdnaxe committed
58 59
    assert hasattr(view, 'get_' + param_name) \
           or getattr(view, param_name, None) is not None, (
60 61 62 63
        'cannot apply {} on a view that does not set '
        '`.{}` or have a `.get_{}()` method.'
    ).format(self.__class__.__name__, param_name, param_name)

erdnaxe's avatar
erdnaxe committed
64 65
    if hasattr(view, 'get_' + param_name):
        param = getattr(view, 'get_' + param_name)()
66 67 68 69 70 71 72 73
        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):
74 75 76 77 78
    """A permission class used to check the ACL to validate the permissions
    of a user.

    The view must define a `.get_perms_map()` or a `.perms_map` attribute.
    See the wiki for the syntax of this attribute.
79 80
    """

erdnaxe's avatar
erdnaxe committed
81 82
    @staticmethod
    def get_required_permissions(method, view):
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
        """Build the list of permissions required for the request to be
        accepted.

        Args:
            method: The HTTP method name used for the request.
            view: The view which is responding to the request.

        Returns:
            The list of ACL functions to apply to a user in order to check
            if he has the right permissions.

        Raises:
            AssertionError: None of `.get_perms_map()` or `.perms_map` are
                defined in the view.
            rest_framework.exception.MethodNotAllowed: The requested method
                is not allowed for this view.
99 100 101 102 103 104 105 106 107
        """
        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):
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
        """Check that the user has the permissions to perform the request.

        Args:
            request: The request performed.
            view: The view which is responding to the request.

        Returns:
            A boolean indicating if the user has the permission to
            perform the request.

        Raises:
            AssertionError: None of `.get_perms_map()` or `.perms_map` are
                defined in the view.
            rest_framework.exception.MethodNotAllowed: The requested method
                is not allowed for this view.
        """
124 125 126 127 128 129 130 131 132 133 134 135 136 137
        # 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)


class AutodetectACLPermission(permissions.BasePermission):
138 139 140 141 142 143 144 145 146 147
    """A permission class used to autodetect the ACL needed to validate the
    permissions of a user based on the queryset of the view.

    The view must define a `.get_queryset()` or a `.queryset` attribute.

    Attributes:
        perms_map: The mapping of each valid HTTP method to the required
            model-based ACL permissions.
        perms_obj_map: The mapping of each valid HTTP method to the required
            object-based ACL permissions.
148
    """
149

150
    perms_map = {
151 152 153 154
        'GET': [can_see_api, lambda model: model.can_view_all],
        'OPTIONS': [can_see_api, lambda model: model.can_view_all],
        'HEAD': [can_see_api, lambda model: model.can_view_all],
        'POST': [can_see_api, lambda model: model.can_create],
erdnaxe's avatar
erdnaxe committed
155 156
        'PUT': [],  # No restrictions, apply to objects
        'PATCH': [],  # No restrictions, apply to objects
157
        'DELETE': [],  # No restrictions, apply to objects
158 159
    }
    perms_obj_map = {
160 161 162
        'GET': [can_see_api, lambda obj: obj.can_view],
        'OPTIONS': [can_see_api, lambda obj: obj.can_view],
        'HEAD': [can_see_api, lambda obj: obj.can_view],
erdnaxe's avatar
erdnaxe committed
163
        'POST': [],  # No restrictions, apply to models
164
        'PUT': [can_see_api, lambda obj: obj.can_edit],
165
        'PATCH': [can_see_api, lambda obj: obj.can_edit],
166
        'DELETE': [can_see_api, lambda obj: obj.can_delete],
167 168 169
    }

    def get_required_permissions(self, method, model):
170 171 172 173 174 175 176 177 178 179 180 181 182 183
        """Build the list of model-based permissions required for the
        request to be accepted.

        Args:
            method: The HTTP method name used for the request.
            view: The view which is responding to the request.

        Returns:
            The list of ACL functions to apply to a user in order to check
            if he has the right permissions.

        Raises:
            rest_framework.exception.MethodNotAllowed: The requested method
                is not allowed for this view.
184 185 186 187 188 189 190
        """
        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):
191 192 193 194 195 196 197 198 199 200 201 202 203 204
        """Build the list of object-based permissions required for the
        request to be accepted.

        Args:
            method: The HTTP method name used for the request.
            view: The view which is responding to the request.

        Returns:
            The list of ACL functions to apply to a user in order to check
            if he has the right permissions.

        Raises:
            rest_framework.exception.MethodNotAllowed: The requested method
                is not allowed for this view.
205
        """
206
        if method not in self.perms_obj_map:
207 208
            raise exceptions.MethodNotAllowed(method)

209
        return [perm(obj) for perm in self.perms_obj_map[method]]
210

erdnaxe's avatar
erdnaxe committed
211 212
    @staticmethod
    def _queryset(view):
213
        return _get_param_in_view(view, 'queryset')
214 215

    def has_permission(self, request, view):
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232
        """Check that the user has the model-based permissions to perform
        the request.

        Args:
            request: The request performed.
            view: The view which is responding to the request.

        Returns:
            A boolean indicating if the user has the permission to
            perform the request.

        Raises:
            AssertionError: None of `.get_queryset()` or `.queryset` are
                defined in the view.
            rest_framework.exception.MethodNotAllowed: The requested method
                is not allowed for this view.
        """
233 234 235 236 237 238 239 240 241 242 243 244 245 246
        # 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):
247 248 249 250 251 252 253 254 255 256 257 258 259 260 261
        """Check that the user has the object-based permissions to perform
        the request.

        Args:
            request: The request performed.
            view: The view which is responding to the request.

        Returns:
            A boolean indicating if the user has the permission to
            perform the request.

        Raises:
            rest_framework.exception.MethodNotAllowed: The requested method
                is not allowed for this view.
        """
262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284
        # authentication checks have already executed via has_permission
        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