From 78da297eb4fabb730a950c176bf27c221adccef9 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 26 Jul 2021 13:07:37 +0200 Subject: [PATCH] [ADD] pms_api_rest: module created --- pms_api_rest/controllers/jwt_controller.py | 4 +- pms_api_rest/controllers/pms_rest.py | 4 +- pms_api_rest/lib_jwt/jwt_http.py | 129 +++++++++++++++++++++ pms_api_rest/lib_jwt/util.py | 85 ++++++++++++++ pms_api_rest/lib_jwt/validator.py | 108 +++++++++++++++++ pms_api_rest/models/res_users.py | 2 +- 6 files changed, 327 insertions(+), 5 deletions(-) create mode 100644 pms_api_rest/lib_jwt/jwt_http.py create mode 100644 pms_api_rest/lib_jwt/util.py create mode 100644 pms_api_rest/lib_jwt/validator.py diff --git a/pms_api_rest/controllers/jwt_controller.py b/pms_api_rest/controllers/jwt_controller.py index a5c544712..d30237b13 100644 --- a/pms_api_rest/controllers/jwt_controller.py +++ b/pms_api_rest/controllers/jwt_controller.py @@ -7,8 +7,8 @@ from odoo.http import request from odoo.addons.auth_signup.models.res_users import SignupError -from ..lib.jwt_http import jwt_http -from ..lib.validator import validator +from ..lib_jwt.jwt_http import jwt_http +from ..lib_jwt.validator import validator _logger = logging.getLogger(__name__) diff --git a/pms_api_rest/controllers/pms_rest.py b/pms_api_rest/controllers/pms_rest.py index 1123144f8..1669215d0 100644 --- a/pms_api_rest/controllers/pms_rest.py +++ b/pms_api_rest/controllers/pms_rest.py @@ -1,7 +1,7 @@ from odoo.addons.base_rest.controllers import main -from ..lib.jwt_http import jwt_http -from ..lib.validator import validator +from ..lib_jwt.jwt_http import jwt_http +from ..lib_jwt.validator import validator class BaseRestDemoPublicApiController(main.RestController): diff --git a/pms_api_rest/lib_jwt/jwt_http.py b/pms_api_rest/lib_jwt/jwt_http.py new file mode 100644 index 000000000..3ed66b340 --- /dev/null +++ b/pms_api_rest/lib_jwt/jwt_http.py @@ -0,0 +1,129 @@ +import simplejson as json + +from odoo import http +from odoo.exceptions import AccessDenied +from odoo.http import Response, request + +from .validator import validator + +return_fields = ["id", "login", "name", "company_id"] + + +class JwtHttp: + def get_state(self): + return {"d": request.session.db} + + def parse_request(self): + http_method = request.httprequest.method + try: + body = http.request.params + except Exception: + body = {} + + headers = dict(list(request.httprequest.headers.items())) + if "wsgi.input" in headers: + del headers["wsgi.input"] + if "wsgi.errors" in headers: + del headers["wsgi.errors"] + if "HTTP_AUTHORIZATION" in headers: + headers["Authorization"] = headers["HTTP_AUTHORIZATION"] + + # extract token + token = "" + if "Authorization" in headers: + try: + # Bearer token_string + token = headers["Authorization"].split(" ")[1] + except Exception: + pass + + return http_method, body, headers, token + + def date2str(self, d, f="%Y-%m-%d %H:%M:%S"): + """ + Convert datetime to string + :param self: + :param d: datetime object + :param f='%Y-%m-%d%H:%M:%S': string format + """ + try: + s = d.strftime(f) + except Exception: + s = None + + return s + + def response(self, success=True, message=None, data=None, code=200): + """ + Create a HTTP Response for controller + :param success=True indicate this response is successful or not + :param message=None message string + :param data=None data to return + :param code=200 http status code + """ + print('response') + print('response') + print('response') + print('response') + payload = json.dumps( + { + "success": success, + "message": message, + "data": data, + } + ) + + return Response( + payload, + status=code, + headers=[ + ("Content-Type", "application/json"), + ], + ) + + def response_500(self, message="Internal Server Error", data=None): + return self.response(success=False, message=message, data=data, code=500) + + def response_401(self, message="401 Unauthorized", data=None): + return self.response(success=False, message=message, data=data, code=401) + + def response_404(self, message="404 Not Found", data=None): + return self.response(success=False, message=message, data=data, code=404) + + def response_403(self, message="403 Forbidden", data=None): + return self.response(success=False, message=message, data=data, code=403) + + def errcode(self, code, message=None): + return self.response(success=False, code=code, message=message) + + def do_login(self, login, password): + # get current db + state = self.get_state() + try: + uid = request.session.authenticate(state["d"], login, password) + except AccessDenied: + return self.response_401() + if not uid: + return self.response_401() + + # login success, generate token + user = request.env.user.read(return_fields)[0] + token = validator.create_token(user) + + return self.response(data={"user": user, "token": token}) + + def do_logout(self, token): + request.session.logout() + request.env["jwt_provider.access_token"].sudo().search( + [("token", "=", token)] + ).unlink() + return self.response() + + def cleanup(self): + # Clean up things after success request + # use logout here to make request as stateless as possible + request.session.logout() + return self.response() + + +jwt_http = JwtHttp() diff --git a/pms_api_rest/lib_jwt/util.py b/pms_api_rest/lib_jwt/util.py new file mode 100644 index 000000000..8a8d465d4 --- /dev/null +++ b/pms_api_rest/lib_jwt/util.py @@ -0,0 +1,85 @@ +import logging +import os +import random +import string + +from dateutil.parser import parse + +_logger = logging.getLogger(__name__) + + +class Util: + addons_path = os.path.join(os.path.dirname(os.path.abspath(__file__))) + + def __init__(self): + self.addons_path = self.addons_path.replace("jwt_provider", "") + + def generate_verification_code(self, length=8): + return "".join( + random.choice(string.ascii_uppercase + string.digits) for _ in range(length) + ) + + def toDate(self, pgTimeStr): + return parse(pgTimeStr) + + def path(self, *paths): + """Make a path""" + return os.path.join(self.addons_path, *paths) + + def add_branch(self, tree, vector, value): + """ + Given a dict, a vector, and a value, insert the value into the dict + at the tree leaf specified by the vector. Recursive! + + Params: + data (dict): The data structure to insert the vector into. + vector (list): A list of values representing the path to the leaf node. + value (object): The object to be inserted at the leaf + + Example 1: + tree = {'a': 'apple'} + vector = ['b', 'c', 'd'] + value = 'dog' + + tree = add_branch(tree, vector, value) + + Returns: + tree = { 'a': 'apple', 'b': { 'c': {'d': 'dog'}}} + + Example 2: + vector2 = ['b', 'c', 'e'] + value2 = 'egg' + + tree = add_branch(tree, vector2, value2) + + Returns: + tree = { 'a': 'apple', 'b': { 'c': {'d': 'dog', 'e': 'egg'}}} + + Returns: + dict: The dict with the value placed at the path specified. + + Algorithm: + If we're at the leaf, add it as key/value to the tree + Else: If the subtree doesn't exist, create it. + Recurse with the subtree and the left shifted vector. + Return the tree. + + """ + key = vector[0] + tree[key] = ( + value + if len(vector) == 1 + else self.add_branch(tree[key] if key in tree else {}, vector[1:], value) + ) + return tree + + def create_dict(self, d): + res = {} + for k, v in d.items(): + ar = k.split(".") + filter(None, ar) + self.add_branch(res, ar, v) + return res + + +util = Util() diff --git a/pms_api_rest/lib_jwt/validator.py b/pms_api_rest/lib_jwt/validator.py new file mode 100644 index 000000000..9a4bbfa7b --- /dev/null +++ b/pms_api_rest/lib_jwt/validator.py @@ -0,0 +1,108 @@ +import datetime +import logging +import re +import traceback + +import jwt +from jwt import InvalidSignatureError + +from odoo.http import request +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT + +_logger = logging.getLogger(__name__) + +regex = ( + r"^[a-z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+\/=?^_`{|}~-]+)*@(?:[a-z0-9]" +) +regex += r"(?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$" + + +class Validator: + def is_valid_email(self, email): + return re.search(regex, email) + + def key(self): + # TODO: change this key before production (build an UI Form to maintain + # (in form company?) + return "CHANGE THIS KEY" + + def create_token(self, user): + try: + exp = datetime.datetime.utcnow() + datetime.timedelta(days=30) + payload = { + "exp": exp, + "iat": datetime.datetime.utcnow(), + "sub": user["id"], + "lgn": user["login"], + } + token = jwt.encode(payload, self.key(), algorithm="HS256") + + self.save_token(token, user["id"], exp) + return token + except Exception as ex: + _logger.error(ex) + raise + + def save_token(self, token, uid, exp): + request.env["jwt_provider.access_token"].sudo().create( + { + "user_id": uid, + "expires": exp.strftime(DEFAULT_SERVER_DATETIME_FORMAT), + "token": token, + } + ) + + def verify(self, token): + record = ( + request.env["jwt_provider.access_token"] + .sudo() + .search([("token", "=", token)]) + ) + + if len(record) != 1: + _logger.info("not found %s" % token) + return False + + if record.is_expired: + return False + + return record.user_id + + def verify_token(self, token): + try: + result = { + "status": False, + "message": None, + } + + if not self.verify(token): + result["message"] = "Token invalid or expired" + result["code"] = 498 + _logger.info("11111") + return result + + payload = jwt.decode(token, self.key(), algorithms=["HS256"]) + uid = request.session.authenticate( + request.session.db, login=payload["lgn"], password=token + ) + if not uid: + result["message"] = "Token invalid or expired" + result["code"] = 498 + _logger.info("2222") + return result + + result["status"] = True + return result + except ( + jwt.ExpiredSignatureError, + jwt.InvalidTokenError, + InvalidSignatureError, + Exception, + ): + result["code"] = 498 + result["message"] = "Token invalid or expired" + _logger.error(traceback.format_exc()) + return result + + +validator = Validator() diff --git a/pms_api_rest/models/res_users.py b/pms_api_rest/models/res_users.py index 65dd31fff..43230f6c3 100644 --- a/pms_api_rest/models/res_users.py +++ b/pms_api_rest/models/res_users.py @@ -3,7 +3,7 @@ import logging from odoo import _, api, fields, models from odoo.exceptions import AccessDenied, ValidationError -from ..lib.validator import validator +from ..lib_jwt.validator import validator _logger = logging.getLogger(__name__)