You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

290 lines
12 KiB

  1. # -*- coding: utf-8 -*-
  2. # Copyright 2016 SYLEAM
  3. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
  4. import json
  5. import logging
  6. import werkzeug.utils
  7. import werkzeug.wrappers
  8. from datetime import datetime
  9. from openerp import http, fields
  10. from openerp.addons.web.controllers.main import ensure_db
  11. _logger = logging.getLogger(__name__)
  12. try:
  13. import oauthlib
  14. from oauthlib import oauth2
  15. except ImportError:
  16. _logger.debug('Cannot `import oauthlib`.')
  17. class OAuth2ProviderController(http.Controller):
  18. def __init__(self):
  19. super(OAuth2ProviderController, self).__init__()
  20. def _get_request_information(self):
  21. """ Retrieve needed arguments for oauthlib methods """
  22. uri = http.request.httprequest.base_url
  23. http_method = http.request.httprequest.method
  24. body = oauthlib.common.urlencode(
  25. http.request.httprequest.values.items())
  26. headers = http.request.httprequest.headers
  27. return uri, http_method, body, headers
  28. def _check_access_token(self, access_token):
  29. """ Check if the provided access token is valid """
  30. token = http.request.env['oauth.provider.token'].search([
  31. ('token', '=', access_token),
  32. ])
  33. if not token:
  34. return False
  35. oauth2_server = token.client_id.get_oauth2_server()
  36. # Retrieve needed arguments for oauthlib methods
  37. uri, http_method, body, headers = self._get_request_information()
  38. # Validate request information
  39. valid, oauthlib_request = oauth2_server.verify_request(
  40. uri, http_method=http_method, body=body, headers=headers)
  41. if valid:
  42. return token
  43. return False
  44. def _json_response(self, data=None, status=200, headers=None):
  45. """ Returns a json response to the client """
  46. if headers is None:
  47. headers = {'Content-Type': 'application/json'}
  48. return werkzeug.wrappers.BaseResponse(
  49. json.dumps(data), status=status, headers=headers)
  50. @http.route('/oauth2/authorize', type='http', auth='user', methods=['GET'])
  51. def authorize(self, client_id=None, response_type=None, redirect_uri=None,
  52. scope=None, state=None, *args, **kwargs):
  53. """ Check client's request, and display an authorization page to the user,
  54. The authorization page lists allowed scopes
  55. If the client is configured to skip the authorization page, directly
  56. redirects to the requested URI
  57. """
  58. client = http.request.env['oauth.provider.client'].search([
  59. ('identifier', '=', client_id),
  60. ])
  61. if not client:
  62. return http.request.render(
  63. 'oauth_provider.authorization_error', {
  64. 'title': 'Unknown Client Identifier!',
  65. 'message': 'This client identifier is invalid.',
  66. })
  67. oauth2_server = client.get_oauth2_server()
  68. # Retrieve needed arguments for oauthlib methods
  69. uri, http_method, body, headers = self._get_request_information()
  70. try:
  71. scopes, credentials = oauth2_server.validate_authorization_request(
  72. uri, http_method=http_method, body=body, headers=headers)
  73. # Store only some values, because the pickling of the full request
  74. # object is not possible
  75. http.request.httpsession['oauth_scopes'] = scopes
  76. http.request.httpsession['oauth_credentials'] = {
  77. 'client_id': credentials['client_id'],
  78. 'redirect_uri': credentials['redirect_uri'],
  79. 'response_type': credentials['response_type'],
  80. 'state': credentials['state'],
  81. }
  82. if client.skip_authorization:
  83. # Skip the authorization page
  84. # Useful when the application is trusted
  85. return self.authorize_post()
  86. except oauth2.FatalClientError as e:
  87. return http.request.render(
  88. 'oauth_provider.authorization_error', {
  89. 'title': 'Error: {error}'.format(error=e.error),
  90. 'message': e.description,
  91. })
  92. except oauth2.OAuth2Error as e:
  93. return http.request.render(
  94. 'oauth_provider.authorization_error', {
  95. 'title': 'Error: {error}'.format(error=e.error),
  96. 'message': 'An unknown error occured! Please contact your '
  97. 'administrator',
  98. })
  99. oauth_scopes = client.scope_ids.filtered(
  100. lambda record: record.code in scopes)
  101. return http.request.render(
  102. 'oauth_provider.authorization', {
  103. 'oauth_client': client.name,
  104. 'oauth_scopes': oauth_scopes,
  105. })
  106. @http.route(
  107. '/oauth2/authorize', type='http', auth='user', methods=['POST'])
  108. def authorize_post(self, *args, **kwargs):
  109. """ Redirect to the requested URI during the authorization """
  110. client = http.request.env['oauth.provider.client'].search([
  111. ('identifier', '=', http.request.httpsession.get(
  112. 'oauth_credentials', {}).get('client_id'))])
  113. if not client:
  114. return http.request.render(
  115. 'oauth_provider.authorization_error', {
  116. 'title': 'Unknown Client Identifier!',
  117. 'message': 'This client identifier is invalid.',
  118. })
  119. oauth2_server = client.get_oauth2_server()
  120. # Retrieve needed arguments for oauthlib methods
  121. uri, http_method, body, headers = self._get_request_information()
  122. scopes = http.request.httpsession['oauth_scopes']
  123. credentials = http.request.httpsession['oauth_credentials']
  124. headers, body, status = oauth2_server.create_authorization_response(
  125. uri, http_method=http_method, body=body, headers=headers,
  126. scopes=scopes, credentials=credentials)
  127. return werkzeug.utils.redirect(headers['Location'], code=status)
  128. @http.route('/oauth2/token', type='http', auth='none', methods=['POST'],
  129. csrf=False)
  130. def token(self, client_id=None, client_secret=None, redirect_uri=None,
  131. scope=None, code=None, grant_type=None, username=None,
  132. password=None, refresh_token=None, *args, **kwargs):
  133. """ Return a token corresponding to the supplied information
  134. Not all parameters are required, depending on the application type
  135. """
  136. ensure_db()
  137. # If no client_id is specified, get it from session
  138. if client_id is None:
  139. client_id = http.request.httpsession.get(
  140. 'oauth_credentials', {}).get('client_id')
  141. client = http.request.env['oauth.provider.client'].sudo().search([
  142. ('identifier', '=', client_id),
  143. ])
  144. if not client:
  145. return self._json_response(
  146. data={'error': 'invalid_client_id'}, status=401)
  147. oauth2_server = client.get_oauth2_server()
  148. # Retrieve needed arguments for oauthlib methods
  149. uri, http_method, body, headers = self._get_request_information()
  150. credentials = {'scope': scope}
  151. # Retrieve the authorization code, if any, to get Odoo's user id
  152. existing_code = http.request.env[
  153. 'oauth.provider.authorization.code'].search([
  154. ('client_id.identifier', '=', client_id),
  155. ('code', '=', code),
  156. ])
  157. if existing_code:
  158. credentials['odoo_user_id'] = existing_code.user_id.id
  159. # Retrieve the existing token, if any, to get Odoo's user id
  160. existing_token = http.request.env['oauth.provider.token'].search([
  161. ('client_id.identifier', '=', client_id),
  162. ('refresh_token', '=', refresh_token),
  163. ])
  164. if existing_token:
  165. credentials['odoo_user_id'] = existing_token.user_id.id
  166. headers, body, status = oauth2_server.create_token_response(
  167. uri, http_method=http_method, body=body, headers=headers,
  168. credentials=credentials)
  169. return werkzeug.wrappers.BaseResponse(
  170. body, status=status, headers=headers)
  171. @http.route('/oauth2/tokeninfo', type='http', auth='none', methods=['GET'])
  172. def tokeninfo(self, access_token=None, *args, **kwargs):
  173. """ Return some information about the supplied token
  174. Similar to Google's "tokeninfo" request
  175. """
  176. ensure_db()
  177. token = self._check_access_token(access_token)
  178. if not token:
  179. return self._json_response(
  180. data={'error': 'invalid_or_expired_token'}, status=401)
  181. token_lifetime = (fields.Datetime.from_string(token.expires_at) -
  182. datetime.now()).seconds
  183. # Base data to return
  184. data = {
  185. 'audience': token.client_id.identifier,
  186. 'scopes': ' '.join(token.scope_ids.mapped('code')),
  187. 'expires_in': token_lifetime,
  188. }
  189. # Add the oauth user identifier, if user's information access is
  190. # allowed by the token's scopes
  191. user_data = token.get_data_for_model(
  192. 'res.users', res_id=token.user_id.id)
  193. if 'id' in user_data:
  194. data.update(user_id=token.generate_user_id())
  195. return self._json_response(data=data)
  196. @http.route('/oauth2/userinfo', type='http', auth='none', methods=['GET'])
  197. def userinfo(self, access_token=None, *args, **kwargs):
  198. """ Return some information about the user linked to the supplied token
  199. Similar to Google's "userinfo" request
  200. """
  201. ensure_db()
  202. token = self._check_access_token(access_token)
  203. if not token:
  204. return self._json_response(
  205. data={'error': 'invalid_or_expired_token'}, status=401)
  206. data = token.get_data_for_model('res.users', res_id=token.user_id.id)
  207. return self._json_response(data=data)
  208. @http.route('/oauth2/otherinfo', type='http', auth='none', methods=['GET'])
  209. def otherinfo(self, access_token=None, model=None, *args, **kwargs):
  210. """ Return allowed information about the requested model """
  211. ensure_db()
  212. token = self._check_access_token(access_token)
  213. if not token:
  214. return self._json_response(
  215. data={'error': 'invalid_or_expired_token'}, status=401)
  216. model_obj = http.request.env['ir.model'].search([
  217. ('model', '=', model),
  218. ])
  219. if not model_obj:
  220. return self._json_response(
  221. data={'error': 'invalid_model'}, status=400)
  222. data = token.get_data_for_model(model)
  223. return self._json_response(data=data)
  224. @http.route(
  225. '/oauth2/revoke_token', type='http', auth='none', methods=['POST'])
  226. def revoke_token(self, token=None, *args, **kwargs):
  227. """ Revoke the supplied token """
  228. ensure_db()
  229. body = oauthlib.common.urlencode(
  230. http.request.httprequest.values.items())
  231. db_token = http.request.env['oauth.provider.token'].search([
  232. ('token', '=', token),
  233. ])
  234. if not db_token:
  235. db_token = http.request.env['oauth.provider.token'].search([
  236. ('refresh_token', '=', token),
  237. ])
  238. if not db_token:
  239. return self._json_response(
  240. data={'error': 'invalid_or_expired_token'}, status=401)
  241. oauth2_server = db_token.client_id.get_oauth2_server()
  242. # Retrieve needed arguments for oauthlib methods
  243. uri, http_method, body, headers = self._get_request_information()
  244. headers, body, status = oauth2_server.create_revocation_response(
  245. uri, http_method=http_method, body=body, headers=headers)
  246. return werkzeug.wrappers.BaseResponse(
  247. body, status=status, headers=headers)