http_handler.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. # -*- coding: utf-8 -*-
  2. from openerp import http
  3. from openerp.http import request
  4. from passlib.context import CryptContext
  5. from werkzeug.wrappers import Response
  6. import os
  7. import jwt
  8. import json
  9. import logging
  10. RESOURCES_MAP = None
  11. JWT_SECRET_KEY = '@MjSk$2016?'
  12. JWT_HEADER = 'Authorization'
  13. JWT_HEADER_PREFIX = 'JWT'
  14. CRYPT_CONTEXT = CryptContext(['pbkdf2_sha512', 'md5_crypt'], deprecated=['md5_crypt'])
  15. LOGGER = logging.getLogger(__name__)
  16. with open(os.path.dirname(__file__) + '/resources.json') as resources:
  17. RESOURCES_MAP = json.load(resources)
  18. '''
  19. Class for manage authentication
  20. '''
  21. class Auth(http.Controller):
  22. # --------------------------------------------------------------------------
  23. # Generate JWT token based on username and password field
  24. # --------------------------------------------------------------------------
  25. @http.route(['/api/jwt'], type = 'http', auth = 'none', methods = ['POST'], cors = '*')
  26. def get_jwt(self, **args):
  27. try:
  28. user = request.env['res.users'].sudo().search([('login', '=', args['username']), ('active', '=', True)])
  29. if not user:
  30. self.make_warn_log('Invalid user received')
  31. return self.make_response({'error': 'Invalid user'}, 400) # bad request
  32. if not self.get_crypt_context().verify(args['password'], user.password_crypt):
  33. self.make_warn_log('invalid password received')
  34. return self.make_response({'error': 'Invalid password'}, 400) # bad request
  35. payload = {
  36. 'uid': user.id,
  37. 'password': args['password']
  38. }
  39. encoded = jwt.encode(payload, JWT_SECRET_KEY, algorithm = 'HS256')
  40. user.write({'jwt_token': encoded})
  41. self.make_info_log('To send token')
  42. return self.make_response({'token': encoded})
  43. except Exception, e:
  44. self.make_error_log('Fields required to generate token')
  45. return self.make_response({'error': 'fields required'}, 400) # bad request
  46. # --------------------------------------------------------------------------
  47. # Check JWT token auth
  48. # --------------------------------------------------------------------------
  49. @http.route(['/api/check'], type = 'http', auth = 'none', methods = ['POST'], cors = '*')
  50. def check_token(self, **args):
  51. try:
  52. user = request.env['res.users'].sudo().search([('jwt_token', '=', args['token'])])
  53. if not user:
  54. self.make_warn_log('Invalid token received')
  55. return self.make_response({'error' : 'invalid token'}, 400) # bad request
  56. decoded = jwt.decode(args['token'], JWT_SECRET_KEY, algorithms = ['HS256'])
  57. if not self.get_crypt_context().verify(decoded['password'], user.password_crypt):
  58. self.make_warn_log('Invalid token received')
  59. return self.make_response({'error' : 'invalid token'}, 400) # bad request
  60. self.make_info_log('Token received is valid')
  61. return self.make_response({'token': 'valid'})
  62. except Exception, e:
  63. self.make_error_log('Token not received')
  64. return self.make_response({'error': 'token required'}, 400) # bad request
  65. # --------------------------------------------------------------------------
  66. # Get context for encryption
  67. # --------------------------------------------------------------------------
  68. def get_crypt_context(self):
  69. return CRYPT_CONTEXT
  70. # --------------------------------------------------------------------------
  71. # Make JSON response
  72. # --------------------------------------------------------------------------
  73. def make_response(self, data, status = 200):
  74. return Response(json.dumps(data), status = status, content_type = 'application/json')
  75. # --------------------------------------------------------------------------
  76. # Make log for warnings
  77. # --------------------------------------------------------------------------
  78. def make_warn_log(self, log):
  79. LOGGER.warning(log)
  80. # --------------------------------------------------------------------------
  81. # Make log for infos
  82. # --------------------------------------------------------------------------
  83. def make_info_log(self, log):
  84. LOGGER.info(log)
  85. # --------------------------------------------------------------------------
  86. # Make log for errors
  87. # --------------------------------------------------------------------------
  88. def make_error_log(self, log):
  89. LOGGER.error(log)
  90. '''
  91. Class for manage rest api interaction
  92. '''
  93. class ApiManager(http.Controller):
  94. # --------------------------------------------------------------------------
  95. # Restify your request
  96. # --------------------------------------------------------------------------
  97. @http.route([
  98. '/api/<any(customers, leads, opportunities):resource>',
  99. '/api/<any(customers, leads, opportunities):resource>/<int:uid>'
  100. ],
  101. type = 'http',
  102. auth = 'none',
  103. cors = '*')
  104. def restify(self, **args):
  105. if not self.valid_token():
  106. return self.make_response({'error': 'unauthorized resource'}, 401) # access denied
  107. if not self.resource_exists(args['resource']):
  108. return self.make_response({'error': 'resource not available'}, 404) # not found
  109. http_verb = request.httprequest.method
  110. if http_verb == 'GET':
  111. return self.http_get(args)
  112. if http_verb == 'POST':
  113. return self.http_post(args)
  114. if http_verb == 'PUT' or http_verb == 'PATCH':
  115. return self.http_put_or_patch(args)
  116. if http_verb == 'DELETE':
  117. return self.http_delete(args)
  118. self.make_warn_log('Request method not allowed')
  119. return self.make_response({'error': 'method not allowed'}, 405) # method not allowed
  120. # --------------------------------------------------------------------------
  121. # Manage GET request
  122. # --------------------------------------------------------------------------
  123. def http_get(self, data):
  124. if len(data) > 2:
  125. return self.make_response({'error': 'cannot be process request'}, 400) # bad request
  126. resource = data['resource']
  127. uid = data['uid']
  128. model, filters = self.resource_inflater(resource)
  129. data = []
  130. if uid != None:
  131. filters.append(('id', '=', uid))
  132. result = request.env[model].sudo().search(filters)
  133. for item in result:
  134. data.append(item.dump())
  135. self.make_info_log('To send data response')
  136. return self.make_response(data);
  137. # --------------------------------------------------------------------------
  138. # Manage POST request
  139. # --------------------------------------------------------------------------
  140. def http_post(self, data):
  141. if len(data) <= 1 or 'uid' in data:
  142. return self.make_response({'error': 'cannot be process request'}, 400) # bad request
  143. model, filters = self.resource_inflater(data['resource'])
  144. data = self.digest_data(data)
  145. try:
  146. result = request.env[model].sudo().create(data)
  147. return self.make_response(result.id)
  148. except Exception, e:
  149. return self.make_response({'response': False})
  150. # --------------------------------------------------------------------------
  151. # Manage PUT or PATCH request
  152. # --------------------------------------------------------------------------
  153. def http_put_or_patch(self, data):
  154. if len(data) <= 2 or not 'uid' in data:
  155. return self.make_response({'error': 'cannot be process request'}, 400) # bad request
  156. uid = data['uid']
  157. model, filters = self.resource_inflater(data['resource'])
  158. data = self.digest_data(data)
  159. try:
  160. result = request.env[model].sudo().browse(uid)
  161. if not result.exists():
  162. return self.make_response({'error': 'cannot be updated'})
  163. self.make_info_log('To update object')
  164. return self.make_response({'response': result.sudo().write(data)})
  165. except Exception, e:
  166. return self.make_response({'response': False})
  167. # --------------------------------------------------------------------------
  168. # Digest data for POST request
  169. # --------------------------------------------------------------------------
  170. def digest_data(self, data):
  171. data = dict(data)
  172. if 'resource' in data:
  173. del data['resource']
  174. if 'uid' in data:
  175. del data['uid']
  176. return data
  177. # --------------------------------------------------------------------------
  178. # Manage DELETE request
  179. # --------------------------------------------------------------------------
  180. def http_delete(self, data):
  181. if len(data) > 2:
  182. return self.make_response({'error': 'cannot be process request'}, 400) # bad request
  183. resource = data['resource']
  184. uid = data['uid']
  185. if uid == None:
  186. return self.make_response({'error': 'uid not provided'})
  187. model, filters = self.resource_inflater(resource)
  188. result = request.env[model].sudo().browse(uid)
  189. if not result.exists():
  190. return self.make_response({'error': 'cannot be deleted'})
  191. self.make_info_log('To delete object')
  192. return self.make_response({'response': result.sudo().unlink()})
  193. # --------------------------------------------------------------------------
  194. # Make JSON response
  195. # --------------------------------------------------------------------------
  196. def make_response(self, data, status = 200):
  197. headers = [('Content-Type', 'application/json')]
  198. if status == 401:
  199. headers.append(('WWW-Authenticate', 'JWT'))
  200. return Response(json.dumps(data), status = status, headers = headers)
  201. # --------------------------------------------------------------------------
  202. # Check if resource is available
  203. # --------------------------------------------------------------------------
  204. def resource_exists(self, resource):
  205. try:
  206. model = RESOURCES_MAP[resource]['module']
  207. module_name = model.replace('.', '_')
  208. module = request.env['ir.module.module'].sudo().search([('name', '=', module_name)])
  209. self.make_info_log('To check resource availability')
  210. return True if module.state == 'installed' and len(module) != 0 else False
  211. except Exception, e:
  212. self.make_error_log('Requested resource is not available')
  213. return False;
  214. # --------------------------------------------------------------------------
  215. # Manage JWT token validity
  216. # --------------------------------------------------------------------------
  217. def valid_token(self):
  218. try:
  219. # print request.httprequest.user_agent.browser
  220. auth_header = request.httprequest.headers[JWT_HEADER]
  221. if not auth_header.startswith(JWT_HEADER_PREFIX):
  222. return False
  223. jwt_token = auth_header[4:]
  224. if not jwt_token:
  225. return False
  226. user = request.env['res.users'].sudo().search([('jwt_token', '=', jwt_token)])
  227. if not user:
  228. return False
  229. decoded = self.decode_token(jwt_token)
  230. if not self.get_crypt_context().verify(decoded['password'], user.password_crypt):
  231. return False
  232. self.make_info_log('Token is valid')
  233. return True
  234. except Exception, e:
  235. self.make_error_log('Token is not valid')
  236. return False
  237. # --------------------------------------------------------------------------
  238. # Decode provide JWT token
  239. # --------------------------------------------------------------------------
  240. def decode_token(self, jwt_token):
  241. return jwt.decode(jwt_token, JWT_SECRET_KEY, algorithms = ['HS256'])
  242. # --------------------------------------------------------------------------
  243. # Manage GET request
  244. # --------------------------------------------------------------------------
  245. def resource_inflater(self, resource):
  246. try:
  247. model = RESOURCES_MAP[resource]['model']
  248. filters = []
  249. for i in range(len(RESOURCES_MAP[resource]['filters'])):
  250. filters.append(tuple(RESOURCES_MAP[resource]['filters'][i]))
  251. self.make_info_log('Successfully resource inflated')
  252. return (model, filters)
  253. except Exception, e:
  254. self.make_error_log('Cannot inflate resource')
  255. return (None, None)
  256. # --------------------------------------------------------------------------
  257. # Get context for encryption
  258. # --------------------------------------------------------------------------
  259. def get_crypt_context(self):
  260. return CRYPT_CONTEXT
  261. # --------------------------------------------------------------------------
  262. # Make log for warnings
  263. # --------------------------------------------------------------------------
  264. def make_warn_log(self, log):
  265. LOGGER.warning(log)
  266. # --------------------------------------------------------------------------
  267. # Make log for infos
  268. # --------------------------------------------------------------------------
  269. def make_info_log(self, log):
  270. LOGGER.info(log)
  271. # --------------------------------------------------------------------------
  272. # Make log for errors
  273. # --------------------------------------------------------------------------
  274. def make_error_log(self, log):
  275. LOGGER.error(log)