diff --git a/addons/mail_client_extension/controllers/__init__.py b/addons/mail_client_extension/controllers/__init__.py index f7273e63342..2de4f0eb2d5 100644 --- a/addons/mail_client_extension/controllers/__init__.py +++ b/addons/mail_client_extension/controllers/__init__.py @@ -1,2 +1,5 @@ -# -*- coding: utf-8 -* -from . import main \ No newline at end of file +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import authenticate +from . import mail_plugin diff --git a/addons/mail_client_extension/controllers/authenticate.py b/addons/mail_client_extension/controllers/authenticate.py new file mode 100644 index 00000000000..548ac66f11f --- /dev/null +++ b/addons/mail_client_extension/controllers/authenticate.py @@ -0,0 +1,94 @@ +# -*- 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 + +_logger = logging.getLogger(__name__) + + +class Authenticate(http.Controller): + + @http.route('/mail_client_extension/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`. + """ + return request.render('mail_client_extension.app_auth', values) + + @http.route('/mail_client_extension/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 he is 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_extension/auth/access_token`. + """ + 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 + params.update({'success': 1, 'auth_code': auth_code}) + else: + params['success'] = 0 + updated_redirect = parsed_redirect.replace(query=werkzeug.urls.url_encode(params)) + return werkzeug.utils.redirect(updated_redirect.to_url()) + + # 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', 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 + """ + auth_message = self._get_auth_code_data(auth_code) + if not auth_message: + return {"error": "Invalid code"} + request.uid = 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_client_extension', 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): + 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_client_extension', 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 diff --git a/addons/mail_client_extension/controllers/main.py b/addons/mail_client_extension/controllers/mail_plugin.py similarity index 67% rename from addons/mail_client_extension/controllers/main.py rename to addons/mail_client_extension/controllers/mail_plugin.py index 42f2687b1f1..f6bd179755c 100644 --- a/addons/mail_client_extension/controllers/main.py +++ b/addons/mail_client_extension/controllers/mail_plugin.py @@ -1,11 +1,9 @@ # -*- 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 requests import werkzeug @@ -17,80 +15,7 @@ from odoo.tools.misc import formatLang _logger = logging.getLogger(__name__) -class MailClientExtensionController(http.Controller): - - @http.route('/mail_client_extension/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`. - """ - return request.render('mail_client_extension.app_auth', values) - - @http.route('/mail_client_extension/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 he is 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_extension/auth/access_token`. - """ - 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 - params.update({'success': 1, 'auth_code': auth_code}) - else: - params['success'] = 0 - updated_redirect = parsed_redirect.replace(query=werkzeug.urls.url_encode(params)) - return werkzeug.utils.redirect(updated_redirect.to_url()) - - # 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', 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 - """ - auth_message = self._get_auth_code_data(auth_code) - if not auth_message: - return {"error": "Invalid code"} - request.uid = 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_client_extension', 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): - 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_client_extension', 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 +class MailPluginController(http.Controller): def _iap_enrich(self, domain): enriched_data = {}