2018-06-30 01:25:46 +00:00
|
|
|
# -*- mode: python; coding: utf-8 -*-
|
2018-06-17 01:06:58 +00:00
|
|
|
# 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.
|
|
|
|
"""
|
|
|
|
|
2018-05-26 21:08:03 +00:00
|
|
|
from rest_framework import permissions, exceptions
|
2018-06-17 01:06:58 +00:00
|
|
|
|
2018-04-20 20:17:50 +00:00
|
|
|
from re2o.acl import can_create, can_edit, can_delete, can_view_all
|
|
|
|
|
2018-04-20 23:24:10 +00:00
|
|
|
from . import acl
|
|
|
|
|
2018-06-17 01:06:58 +00:00
|
|
|
|
2018-06-13 22:39:37 +00:00
|
|
|
def can_see_api(*_, **__):
|
2018-06-17 01:06:58 +00:00
|
|
|
"""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.
|
|
|
|
"""
|
2018-04-20 23:24:10 +00:00
|
|
|
return lambda user: acl.can_view(user)
|
|
|
|
|
|
|
|
|
2018-06-13 22:39:37 +00:00
|
|
|
def _get_param_in_view(view, param_name):
|
2018-06-17 01:06:58 +00:00
|
|
|
"""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.
|
|
|
|
"""
|
2018-06-13 22:39:37 +00:00
|
|
|
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):
|
2018-06-17 01:06:58 +00:00
|
|
|
"""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.
|
2018-06-13 22:39:37 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
def get_required_permissions(self, method, view):
|
2018-06-17 01:06:58 +00:00
|
|
|
"""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.
|
2018-06-13 22:39:37 +00:00
|
|
|
"""
|
|
|
|
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):
|
2018-06-17 01:06:58 +00:00
|
|
|
"""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.
|
|
|
|
"""
|
2018-06-13 22:39:37 +00:00
|
|
|
# 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):
|
2018-06-17 01:06:58 +00:00
|
|
|
"""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.
|
2018-04-20 20:17:50 +00:00
|
|
|
"""
|
2018-06-17 01:06:58 +00:00
|
|
|
|
2018-04-20 20:17:50 +00:00
|
|
|
perms_map = {
|
2018-04-20 23:24:10 +00:00
|
|
|
'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],
|
2018-05-26 21:08:03 +00:00
|
|
|
'PUT': [], # No restrictions, apply to objects
|
|
|
|
'PATCH': [], # No restrictions, apply to objects
|
|
|
|
'DELETE': [], # No restrictions, apply to objects
|
2018-04-20 20:17:50 +00:00
|
|
|
}
|
|
|
|
perms_obj_map = {
|
2018-04-20 23:24:10 +00:00
|
|
|
'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],
|
2018-05-26 21:08:03 +00:00
|
|
|
'POST': [], # No restrictions, apply to models
|
2018-04-20 23:24:10 +00:00
|
|
|
'PUT': [can_see_api, lambda obj: obj.can_edit],
|
2018-05-26 21:08:03 +00:00
|
|
|
'PATCH': [can_see_api, lambda obj: obj.can_edit],
|
2018-04-20 23:24:10 +00:00
|
|
|
'DELETE': [can_see_api, lambda obj: obj.can_delete],
|
2018-04-20 20:17:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
def get_required_permissions(self, method, model):
|
2018-06-17 01:06:58 +00:00
|
|
|
"""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.
|
2018-04-20 20:17:50 +00:00
|
|
|
"""
|
|
|
|
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):
|
2018-06-17 01:06:58 +00:00
|
|
|
"""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.
|
2018-04-20 20:17:50 +00:00
|
|
|
"""
|
2018-06-13 22:39:37 +00:00
|
|
|
if method not in self.perms_obj_map:
|
2018-04-20 20:17:50 +00:00
|
|
|
raise exceptions.MethodNotAllowed(method)
|
|
|
|
|
2018-06-13 22:39:37 +00:00
|
|
|
return [perm(obj) for perm in self.perms_obj_map[method]]
|
2018-04-20 20:17:50 +00:00
|
|
|
|
|
|
|
def _queryset(self, view):
|
2018-06-13 22:39:37 +00:00
|
|
|
return _get_param_in_view(view, 'queryset')
|
2018-04-20 20:17:50 +00:00
|
|
|
|
|
|
|
def has_permission(self, request, view):
|
2018-06-17 01:06:58 +00:00
|
|
|
"""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.
|
|
|
|
"""
|
2018-04-20 20:17:50 +00:00
|
|
|
# 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):
|
2018-06-17 01:06:58 +00:00
|
|
|
"""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.
|
|
|
|
"""
|
2018-04-20 20:17:50 +00:00
|
|
|
# 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
|
|
|
|
|