112 lines
5.5 KiB
Python
112 lines
5.5 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import base64
|
|
import datetime
|
|
import hmac
|
|
import json
|
|
import logging
|
|
import odoo
|
|
import werkzeug
|
|
|
|
from odoo import _, http
|
|
from odoo.http import request
|
|
from werkzeug.exceptions import NotFound
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class Authenticate(http.Controller):
|
|
|
|
@http.route(['/mail_client_extension/auth', '/mail_plugin/auth'], type='http', auth="user", methods=['GET'], website=True)
|
|
def auth(self, **values):
|
|
"""
|
|
Once authenticated this route renders the view that shows an app wants to access Odoo.
|
|
The user is invited to allow or deny the app. The form posts to `/mail_client_extension/auth/confirm`.
|
|
|
|
old route name "/mail_client_extension/auth is deprecated as of saas-14.3,it is not needed for newer
|
|
versions of the mail plugin but necessary for supporting older versions
|
|
"""
|
|
if not request.env.user._is_internal():
|
|
return request.render('mail_plugin.app_error', {'error': _('Access Error: Only Internal Users can link their inboxes to this database.')})
|
|
return request.render('mail_plugin.app_auth', values)
|
|
|
|
@http.route(['/mail_client_extension/auth/confirm', '/mail_plugin/auth/confirm'], type='http', auth="user", methods=['POST'])
|
|
def auth_confirm(self, scope, friendlyname, redirect, info=None, do=None, **kw):
|
|
"""
|
|
Called by the `app_auth` template. If the user decided to allow the app to access Odoo, a temporary auth code
|
|
is generated and they are redirected to `redirect` with this code in the URL. It should redirect to the app, and
|
|
the app should then exchange this auth code for an access token by calling
|
|
`/mail_client/auth/access_token`.
|
|
|
|
old route name "/mail_client_extension/auth/confirm is deprecated as of saas-14.3,it is not needed for newer
|
|
versions of the mail plugin but necessary for supporting older versions
|
|
"""
|
|
parsed_redirect = werkzeug.urls.url_parse(redirect)
|
|
params = parsed_redirect.decode_query()
|
|
if do:
|
|
name = friendlyname if not info else f'{friendlyname}: {info}'
|
|
auth_code = self._generate_auth_code(scope, name)
|
|
# params is a MultiDict which does not support .update() with kwargs
|
|
# the state attribute is needed for the gmail connector
|
|
params.update({'success': 1, 'auth_code': auth_code, 'state': kw.get('state', '')})
|
|
else:
|
|
params.update({'success': 0, 'state': kw.get('state', '')})
|
|
updated_redirect = parsed_redirect.replace(query=werkzeug.urls.url_encode(params))
|
|
return request.redirect(updated_redirect.to_url(), local=False)
|
|
|
|
# In this case, an exception will be thrown in case of preflight request if only POST is allowed.
|
|
@http.route(['/mail_client_extension/auth/access_token', '/mail_plugin/auth/access_token'], type='json', auth="none", cors="*",
|
|
methods=['POST', 'OPTIONS'])
|
|
def auth_access_token(self, auth_code='', **kw):
|
|
"""
|
|
Called by the external app to exchange an auth code, which is temporary and was passed in a URL, for an
|
|
access token, which is permanent, and can be used in the `Authorization` header to authorize subsequent requests
|
|
|
|
old route name "/mail_client_extension/auth/access_token is deprecated as of saas-14.3,it is not needed for newer
|
|
versions of the mail plugin but necessary for supporting older versions
|
|
"""
|
|
if not auth_code:
|
|
return {"error": "Invalid code"}
|
|
auth_message = self._get_auth_code_data(auth_code)
|
|
if not auth_message:
|
|
return {"error": "Invalid code"}
|
|
request.update_env(user=auth_message['uid'])
|
|
scope = 'odoo.plugin.' + auth_message.get('scope', '')
|
|
api_key = request.env['res.users.apikeys']._generate(scope, auth_message['name'])
|
|
return {'access_token': api_key}
|
|
|
|
def _get_auth_code_data(self, auth_code):
|
|
data, auth_code_signature = auth_code.split('.')
|
|
data = base64.b64decode(data)
|
|
auth_code_signature = base64.b64decode(auth_code_signature)
|
|
signature = odoo.tools.misc.hmac(request.env(su=True), 'mail_plugin', data).encode()
|
|
if not hmac.compare_digest(auth_code_signature, signature):
|
|
return None
|
|
|
|
auth_message = json.loads(data)
|
|
# Check the expiration
|
|
if datetime.datetime.utcnow() - datetime.datetime.fromtimestamp(auth_message['timestamp']) > datetime.timedelta(
|
|
minutes=3):
|
|
return None
|
|
|
|
return auth_message
|
|
|
|
# Using UTC explicitly in case of a distributed system where the generation and the signature verification do not
|
|
# necessarily happen on the same server
|
|
def _generate_auth_code(self, scope, name):
|
|
if not request.env.user._is_internal():
|
|
raise NotFound()
|
|
auth_dict = {
|
|
'scope': scope,
|
|
'name': name,
|
|
'timestamp': int(datetime.datetime.utcnow().timestamp()),
|
|
# <- elapsed time should be < 3 mins when verifying
|
|
'uid': request.uid,
|
|
}
|
|
auth_message = json.dumps(auth_dict, sort_keys=True).encode()
|
|
signature = odoo.tools.misc.hmac(request.env(su=True), 'mail_plugin', auth_message).encode()
|
|
auth_code = "%s.%s" % (base64.b64encode(auth_message).decode(), base64.b64encode(signature).decode())
|
|
_logger.info('Auth code created - user %s, scope %s', request.env.user, scope)
|
|
return auth_code
|