diff --git a/internal_lib/permission_parser.py b/internal_lib/permission_parser.py index 1406f2a..efdcca9 100644 --- a/internal_lib/permission_parser.py +++ b/internal_lib/permission_parser.py @@ -1,3 +1,6 @@ +from flask_login import current_user + + def parse_permission(string): """ Parsing permission string @@ -33,13 +36,13 @@ def parse_permission(string): delete = True return { - "delete":delete, - "write":write, - "read":read, - "update":update, - "module":module, - "union":union_id, - "item_id":item_id + "delete": delete, + "write": write, + "read": read, + "update": update, + "module": module, + "union": union_id, + "item_id": item_id } @@ -53,8 +56,32 @@ def control_permission(group, module, perm_type, itemid, unionid): return True elif right.get(perm_type): return True - elif right.get('item_id') in ['*',itemid]: + elif right.get('item_id') in ['*', itemid]: return True - elif right.get('module') in ["*",module] and right.get('union') in ['*', unionid] and right.get(perm_type) and right.get('item_id') in ['*',itemid]: + elif right.get('module') in ["*", module] and right.get('union') in ['*', unionid] and right.get( + perm_type) and right.get('item_id') in ['*', itemid]: return True return False + + +def read_permission(module, qs): + union_list = [] + for right_string in current_user.group.rights: + right = parse_permission(right_string) + if right.get('module') in [module, '*']: + if right.get('read'): + if right.get('union') != "*": + union_list.append(right.get('union')) + + if len(union_list) > 0: + if module == 'union': + qs.filter(id__in=union_list, deleted=False) + else: + qs.filter(union__in=union_list, deleted=False) + return qs + + +def has_permission(module, obj, reqtype, oid): + if control_permission(current_user.group, module, reqtype, oid, obj.company): + return True + return False diff --git a/requirements.txt b/requirements.txt index 14f476c..0b6775f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,6 @@ mongoengine flask-mongoengine flask pycryptodome -flask_jwt_extended \ No newline at end of file +flask_jwt_extended +flask_views +flask_login \ No newline at end of file diff --git a/restapi/Methods.py b/restapi/Methods.py new file mode 100644 index 0000000..08d3cbc --- /dev/null +++ b/restapi/Methods.py @@ -0,0 +1,18 @@ +class Get: + method = 'GET' + + +class List: + method = 'GET' + + +class Delete: + method = 'DELETE' + + +class Update: + method = 'PUT' + + +class Create: + method = 'POST' diff --git a/restapi/__init__.py b/restapi/__init__.py new file mode 100644 index 0000000..9d5b8c4 --- /dev/null +++ b/restapi/__init__.py @@ -0,0 +1,43 @@ +from urllib.parse import urljoin + +from flask import Flask +from mongoengine import Document + +from restapi.views import ApiView +from restapi import Methods + + +class MongoApi: + app: Flask = None + + def __init__(self, app: Flask): + self.app = app + + def register_model(self, model: any, uri: str = None, name: str = None) -> None: + if not getattr(model, '_meta').get('methods'): + raise Exception("{} model methods not set".format(model.__name__)) + if not uri: + uri = model.__name__.lower() + if uri[:-1] != "/": + uri = uri + "/" + if uri[0] != "/": + uri = "/" + uri + if not name: + name = model.__name__ + self.app.add_url_rule( + uri, + methods=(method.method for method in getattr(model, '_meta').get('methods') if + method not in (Methods.Get, Methods.Update)), + view_func=ApiView.as_view(name, model=model)) + + single = [] + if Methods.Update in getattr(model, '_meta').get('methods'): + single.append('PUT') + + if Methods.Get in getattr(model, '_meta').get('methods'): + single.append('GET') + if single: + self.app.add_url_rule( + urljoin(uri, '/'), + methods=single, + view_func=ApiView.as_view("{}_GET_PUT".format(name), model=model)) diff --git a/restapi/jwt.py b/restapi/jwt.py new file mode 100644 index 0000000..45fee95 --- /dev/null +++ b/restapi/jwt.py @@ -0,0 +1,11 @@ +from flask import request +from flask_jwt_extended.utils import verify_token_claims +from flask_jwt_extended.view_decorators import _decode_jwt_from_request + + + + +def get_jwt_from_request(): + jwt_data, jwt_header = _decode_jwt_from_request(request_type='access') + verify_token_claims(jwt_data) + return jwt_data['identity'] \ No newline at end of file diff --git a/restapi/resource.py b/restapi/resource.py new file mode 100644 index 0000000..df0a8d9 --- /dev/null +++ b/restapi/resource.py @@ -0,0 +1,199 @@ +from bson import ObjectId +from flask import request +from mongoengine import Document, ObjectIdField, ListField, \ + EmbeddedDocumentField, ReferenceField, ImageField, FileField, DecimalField, QuerySet +from base64 import b64encode + +from mongoengine.base import BaseField + + +class Resource: + model: Document = None + qs: dict = {} + eqs: dict = {} + build_reference: bool = False + base64_image: bool = True + base64_image_as_full: bool = False + limit: int = 10 + offset: int = 0 + fields: list = [] + ignore_fields: list = [] + mask_fields: dict = {} + meta: dict = {} + as_pk: str = 'id' + query_document: Document = None + + def __init__(self, model: Document): + self.model = model + self.limit = 10 + if 'no_image' in request.args: + self.base64_image = False + if 'with_sub_docs' in request.args: + self.build_reference = request.args.get('with_sub_docs') + if 'thumb' in request.args: + self.base64_image = True + self.base64_image_as_full = False + if 'full_image' in request.args: + self.base64_image = True + self.base64_image_as_full = True + if 'limit' in request.args: + self.limit = int(request.args.get('limit')) + if 'fields' in request.args: + self.fields = request.args.get('fields') + if 'offset' in request.args: + self.offset = int(request.args.get('offset')) + self.meta = getattr(self.model, '_meta') + + if 'ignore_fields' in self.meta: + self.ignore_fields = self.meta.get('ignore_fields') + if 'with_sub_docs' in self.meta: + self.build_reference = self.meta.get('with_sub_docs') + if 'mask_fields' in self.meta: + self.mask_fields = self.meta.get('mask_fields') + if 'document' in self.meta: + self.model = self.meta.get('document') + if 'query' in self.meta: + self.qs = self.meta.get('query') + if 'as_pk' in self.meta: + self.as_pk = self.meta.get('as_pk') + if 'query_document' in self.meta: + self.query_document = self.meta.get('query_document') + + def external_query(self, qs: dict): + self.eqs = qs + + def query(self, qs: dict) -> None: + + self.qs = qs + + def to_json(self, pk: str = None) -> tuple: + query = {} + if self.qs: + for key, val in self.qs.items(): + if callable(val): + query.update({key: val()}) + else: + query.update({key: val}) + if self.query_document: + query = {} + if self.eqs: + for key, val in self.eqs.items(): + if callable(val): + query.update({key: val()}) + else: + query.update({key: val}) + data = self.query_document.objects.filter(**query) + field_name = "" + for i in self.model._fields_ordered: + try: + if isinstance(getattr(self.model, i).document_type, self.query_document.__class__): + field_name = i + "__in" + except: + pass + query.clear() + query = {field_name: [i.id for i in data]} + for key, val in self.qs.items(): + if callable(val): + query.update({key: val()}) + else: + query.update({key: val}) + print(query) + data = self.model.objects.filter(**query) + + count = data.count() + if pk: + json_data = self.parse_fields(data.get(**{self.as_pk: pk}), self.model) + else: + data = data[self.offset:self.limit + self.offset] + json_data = [] + for item in data: + item_data = self.parse_fields(item, self.model) + json_data.append(item_data) + return count, json_data + + def parse_field(self, field: BaseField, value: any) -> any: + + if isinstance(field, ObjectIdField): + return str(value) + elif isinstance(field, ReferenceField): + + if self.build_reference: + return self.parse_fields(value, field.document_type) + if value: + return str(value.id) + else: + return value + + elif isinstance(field, ImageField): + if self.base64_image and value: + if self.base64_image_as_full: + file = b64encode(value.read()).decode("utf-8") + else: + try: + file = b64encode(value.thumbnail.read()).decode("utf-8") + except: + file = b64encode(value.read()).decode("utf-8") + return { + "size": value.size, + "upload_date": value.gridout.upload_date, + "format": value.format, + "base64": file + } + else: + if value: + return { + "size": value.size + } + else: + return None + elif isinstance(field, DecimalField): + return float(value) + elif isinstance(field, FileField): + return "file" + elif isinstance(field, ListField): + if isinstance(field.field, EmbeddedDocumentField): + return [self.parse_fields(item, field.field.document_type) for item in value] + elif isinstance(field.field, ReferenceField) and self.build_reference: + return [self.parse_fields(item, field.field.document_type) for item in value] + else: + return [self.parse_field(field.field, item) for item in value] + elif isinstance(value, QuerySet): + if value.count() > 0: + return [self.parse_fields(i, value._document) for i in value] + + else: + return value + + def parse_fields(self, item: Document, model: Document) -> dict: + parsed_item = {} + + fields = list(getattr(model, "_fields_ordered")) + if self.model != model: + in_meta = getattr(model,'_meta') + if in_meta.get('ignore_fields'): + self.ignore_fields += in_meta.get('ignore_fields') + for key, val in model.__dict__.items(): + if isinstance(getattr(model, key), property): + fields.append(key) + for i in fields: + if i not in self.ignore_fields: + field = getattr(model, i) + if i not in self.mask_fields: + if not item is None: + value = getattr(item, i) + else: + value = None + else: + value = getattr(item, i) + if self.mask_fields.get(i) == 'all': + value = '*************' + elif self.mask_fields.get(i) == 'email': + value = '{}****@{}***.{}'.format(value[0], value.split("@")[1][0], + '.'.join(value.split("@")[1].split(".")[1:])) + else: + value = '*************' + if not value is None: + parsed_item.update({i: self.parse_field(field, value)}) + else: + parsed_item.update({i: None}) + return parsed_item diff --git a/restapi/views.py b/restapi/views.py new file mode 100644 index 0000000..2835091 --- /dev/null +++ b/restapi/views.py @@ -0,0 +1,122 @@ +import json +import time + +import mongoengine +from flask import jsonify, request +from flask_views.base import View +from mongoengine import ValidationError, DoesNotExist, InvalidQueryError +from werkzeug.exceptions import Unauthorized, NotFound + +from restapi.resource import Resource + + +class ApiView(View): + model = None + authentication_methods = [] + def __init__(self, model): + self.start = time.time() + self.model = model + self.resource = Resource(self.model) + + def dispatch_request(self, *args, **kwargs): + # keep all the logic in a helper method (_dispatch_request) so that + # it's easy for subclasses to override this method without them also having to copy/paste all the + # authentication logic, etc. + return self._dispatch_request(*args, **kwargs) + + def _dispatch_request(self, *args, **kwargs): + authorized = True if len(self.authentication_methods) == 0 else False + for authentication_method in self.authentication_methods: + if authentication_method().authorized(): + authorized = True + if not authorized: + return {'error': 'Unauthorized'}, '401 Unauthorized' + + try: + return super(ApiView, self).dispatch_request(*args, **kwargs) + except mongoengine.queryset.DoesNotExist as e: + return {'error': 'Empty query: ' + str(e)}, '404 Not Found' + except ValidationError as e: + return e.args[0], '400 Bad Request' + except Unauthorized: + return {'error': 'Unauthorized'}, '401 Unauthorized' + except NotFound as e: + return {'error': str(e)}, '404 Not Found' + + def get(self, *args, **kwargs): + + if 'pk' in kwargs: + try: + count, data = self.resource.to_json(pk=kwargs.get('pk')) + response = { + 'response': data, + 'status': True, + 'info': { + 'took': time.time() - self.start + }, + } + except (ValidationError, DoesNotExist) as e: + kwargs.update({'code': 404}) + response = { + "status": False, + "errors": "Object not found" + } + else: + if "query" in request.args and self.model._meta.get('can_query'): + self.resource.external_query(json.loads(request.args.get('query'))) + count, data = self.resource.to_json() + response = { + 'response': data, + 'info': { + 'offset': self.resource.offset, + 'limit': self.resource.limit, + 'status': True, + 'total': count, + 'took': time.time() - self.start + }, + } + + return jsonify(response), kwargs.get('code', 200) + + def post(self, *args, **kwargs): + try: + item = self.model(**request.json) + item.validate() + except ValidationError as v: + return jsonify({ + 'status': False, + 'errors': [{"field": field, "error": str(error)} for field, error in v.__dict__.get('errors').items() if + isinstance(error, ValidationError) and field[0] != "_"] + }), 400 + except Exception as e: + return jsonify({ + 'status': False, + 'errors': str(e) + }), 400 + data = item.save() + return self.get(pk=data.id, code=201) + + def put(self, *args, **kwargs): + if "pk" not in kwargs: + return jsonify({ + 'status': False, + "error":"Method not allowed" + }), 403 + else: + try: + self.model.objects(id=kwargs.get('pk')).update(**request.json) + return self.get(pk=kwargs.get('pk')) + except ValidationError as v: + print(v.__dict__) + return jsonify({ + 'status': False, + 'errors': [{"field": v.__dict__.get('field_name'), "error": v.__dict__.get('_message')}] + }), 400 + except InvalidQueryError as e: + return jsonify({ + 'status': False, + 'errors': str(e) + }), 400 + + def delete(self, *args, **kwargs): + "delete method"