http_handler.py 15 KB

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