From 7509f5ec3e02a2667dcb507d43075edd3b2d953e Mon Sep 17 00:00:00 2001 From: Denis K Date: Tue, 25 Dec 2018 22:28:44 +0300 Subject: [PATCH] Add permissions --- jet_bridge/permissions.py | 54 +++++++++++++++++++++++++++ jet_bridge/router.py | 1 + jet_bridge/settings.py | 2 + jet_bridge/views/base/api.py | 18 +++++++++ jet_bridge/views/base/generic_api.py | 3 ++ jet_bridge/views/message.py | 2 + jet_bridge/views/model.py | 21 +++++++++++ jet_bridge/views/model_description.py | 2 + jet_bridge/views/sql.py | 2 + 9 files changed, 105 insertions(+) create mode 100644 jet_bridge/permissions.py diff --git a/jet_bridge/permissions.py b/jet_bridge/permissions.py new file mode 100644 index 00000000..9262a9bf --- /dev/null +++ b/jet_bridge/permissions.py @@ -0,0 +1,54 @@ +from jet_bridge import settings +from jet_bridge.utils.backend import project_auth + + +class BasePermission(object): + + def has_permission(self, view): + return True + + def has_object_permission(self, view, obj): + return True + + +class HasProjectPermissions(BasePermission): + token_prefix = 'Token ' + project_token_prefix = 'ProjectToken ' + + def has_permission(self, view): + token = view.request.headers.get('Authorization') + permission = getattr(view, 'required_project_permission', None) + + if not token: + return False + + if token[:len(self.token_prefix)] == self.token_prefix: + token = token[len(self.token_prefix):] + + result = project_auth(token, permission) + + if result.get('warning'): + view.headers['X-Application-Warning'] = result['warning'] + + return result['result'] + elif token[:len(self.project_token_prefix)] == self.project_token_prefix: + token = token[len(self.project_token_prefix):] + + result = project_auth(token, permission) + + if result.get('warning'): + view.headers['X-Application-Warning'] = result['warning'] + + return result['result'] + else: + return False + + +class ModifyNotInDemo(BasePermission): + + def has_permission(self, view): + if not settings.READ_ONLY: + return True + if view.action in ['create', 'update', 'partial_update', 'destroy']: + return False + return True diff --git a/jet_bridge/router.py b/jet_bridge/router.py index c2641510..242fedfe 100644 --- a/jet_bridge/router.py +++ b/jet_bridge/router.py @@ -34,6 +34,7 @@ class ActionHandler(viewset): for method, method_action in actions.items(): def create_action_method(action): def action_method(inner_self, *args, **kwargs): + inner_self.action = action return getattr(inner_self, action)(*args, **kwargs) return action_method diff --git a/jet_bridge/settings.py b/jet_bridge/settings.py index b817770f..befaedc2 100644 --- a/jet_bridge/settings.py +++ b/jet_bridge/settings.py @@ -14,6 +14,7 @@ define('port', default=8888, help='server port', type=int) define('config', default=DEFAULT_CONFIG_PATH, help='config file path') define('debug', default=False, help='debug mode', type=bool) +define('read_only', default=False, help='read only', type=bool) define('web_base_url', default='https://app.jetadmin.io', help='Jet Admin frontend application base URL') define('api_base_url', default='https://api.jetadmin.io/api', help='Jet Admin API base URL') @@ -54,6 +55,7 @@ ADDRESS = options.address PORT = options.port DEBUG = options.debug +READ_ONLY = options.read_only WEB_BASE_URL = options.web_base_url API_BASE_URL = options.api_base_url diff --git a/jet_bridge/views/base/api.py b/jet_bridge/views/base/api.py index e4db3f03..e808760d 100644 --- a/jet_bridge/views/base/api.py +++ b/jet_bridge/views/base/api.py @@ -3,6 +3,7 @@ class APIView(tornado.web.RequestHandler): + permission_classes = [] @property def data(self): @@ -12,6 +13,10 @@ def data(self): else: return self.request.body_arguments + def prepare(self): + if self.request.method != 'OPTIONS': + self.check_permissions() + def set_default_headers(self): ACCESS_CONTROL_ALLOW_ORIGIN = 'Access-Control-Allow-Origin' ACCESS_CONTROL_EXPOSE_HEADERS = 'Access-Control-Expose-Headers' @@ -25,6 +30,19 @@ def set_default_headers(self): self.set_header(ACCESS_CONTROL_EXPOSE_HEADERS, 'Content-Length,Content-Range,X-Application-Warning') self.set_header(ACCESS_CONTROL_ALLOW_CREDENTIALS, 'true') + def get_permissions(self): + return [permission() for permission in self.permission_classes] + + def check_permissions(self): + for permission in self.get_permissions(): + if not permission.has_permission(self): + raise Exception(getattr(permission, 'message', None)) + + def check_object_permissions(self, obj): + for permission in self.get_permissions(): + if not permission.has_object_permission(self, obj): + raise Exception(getattr(permission, 'message', None)) + def options(self, *args, **kwargs): self.set_status(204) self.finish() diff --git a/jet_bridge/views/base/generic_api.py b/jet_bridge/views/base/generic_api.py index 4fab72c1..c1f1538b 100644 --- a/jet_bridge/views/base/generic_api.py +++ b/jet_bridge/views/base/generic_api.py @@ -11,6 +11,7 @@ class GenericAPIView(APIView): lookup_field = 'id' lookup_url_kwarg = None session = Session() + action = None def get_model(self): raise NotImplementedError @@ -27,6 +28,8 @@ def get_object(self): model_field = getattr(self.get_model(), self.lookup_field) obj = queryset.filter(getattr(model_field, '__eq__')(self.path_kwargs[lookup_url_kwarg])).first() + self.check_object_permissions(obj) + return obj def get_filter(self, *args, **kwargs): diff --git a/jet_bridge/views/message.py b/jet_bridge/views/message.py index cf16eac6..fe320d4f 100644 --- a/jet_bridge/views/message.py +++ b/jet_bridge/views/message.py @@ -1,7 +1,9 @@ +from jet_bridge.permissions import HasProjectPermissions from jet_bridge.views.base.api import APIView class MessageHandler(APIView): + permission_classes = (HasProjectPermissions,) def post(self): pass diff --git a/jet_bridge/views/model.py b/jet_bridge/views/model.py index ebaa9fcd..da32dac6 100644 --- a/jet_bridge/views/model.py +++ b/jet_bridge/views/model.py @@ -1,4 +1,5 @@ from jet_bridge.filters.model import get_model_filter_class +from jet_bridge.permissions import HasProjectPermissions, ModifyNotInDemo from jet_bridge.serializers.model import get_model_serializer from jet_bridge.views.mixins.model import ModelAPIViewMixin from jet_bridge.db import MappedBase @@ -6,6 +7,26 @@ class ModelHandler(ModelAPIViewMixin): model = None + permission_classes = (HasProjectPermissions, ModifyNotInDemo) + + @property + def required_project_permission(self): + return { + 'permission_type': 'model', + 'permission_object': self.path_kwargs['model'], + 'permission_actions': { + 'create': 'w', + 'update': 'w', + 'partial_update': 'w', + 'destroy': 'd', + 'retrieve': 'r', + 'list': 'r', + 'aggregate': 'r', + 'group': 'r', + 'reorder': 'w', + 'reset_order': 'w' + }.get(self.action, 'w') + } def get_model(self): if self.model: diff --git a/jet_bridge/views/model_description.py b/jet_bridge/views/model_description.py index c8480952..8d5788c9 100644 --- a/jet_bridge/views/model_description.py +++ b/jet_bridge/views/model_description.py @@ -3,6 +3,7 @@ from jet_bridge.db import Session, MappedBase from jet_bridge.models import data_types +from jet_bridge.permissions import HasProjectPermissions from jet_bridge.responses.base import Response from jet_bridge.serializers.model_description import ModelDescriptionSerializer from jet_bridge.utils.db_types import map_data_type @@ -11,6 +12,7 @@ class ModelDescriptionsHandler(APIView): serializer_class = ModelDescriptionSerializer + permission_classes = (HasProjectPermissions,) session = Session() def get_queryset(self): diff --git a/jet_bridge/views/sql.py b/jet_bridge/views/sql.py index 18652c87..5fff9f64 100644 --- a/jet_bridge/views/sql.py +++ b/jet_bridge/views/sql.py @@ -1,9 +1,11 @@ +from jet_bridge.permissions import HasProjectPermissions from jet_bridge.responses.base import Response from jet_bridge.serializers.sql import SqlSerializer from jet_bridge.views.base.api import APIView class SqlHandler(APIView): + permission_classes = (HasProjectPermissions,) def post(self): serializer = SqlSerializer(data=self.data)