diff options
| author | jbm-odoo <jbm@odoo.com> | 2020-04-21 13:08:35 +0000 |
|---|---|---|
| committer | jbm-odoo <jbm@odoo.com> | 2020-08-05 13:47:42 +0000 |
| commit | 9dc3057e8eb82d9237457167641585655f249c3e (patch) | |
| tree | 11975a957335d9dd3ce7f2dc0844fa8b5e7acff3 /addons/microsoft_account | |
| parent | 78ef2ec8134b09d9732849a11ff8d9e1ac886bb5 (diff) | |
[ADD] calendar_microsoft: Synchronize Microsoft calendar with Odoo
TaskID 2223219
closes odoo/odoo#54936
Related: odoo/upgrade#1557
Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
Diffstat (limited to 'addons/microsoft_account')
| -rw-r--r-- | addons/microsoft_account/__init__.py | 5 | ||||
| -rw-r--r-- | addons/microsoft_account/__manifest__.py | 15 | ||||
| -rw-r--r-- | addons/microsoft_account/controllers/__init__.py | 4 | ||||
| -rw-r--r-- | addons/microsoft_account/controllers/main.py | 29 | ||||
| -rw-r--r-- | addons/microsoft_account/data/microsoft_account_data.xml | 9 | ||||
| -rw-r--r-- | addons/microsoft_account/models/__init__.py | 5 | ||||
| -rw-r--r-- | addons/microsoft_account/models/microsoft_service.py | 160 | ||||
| -rw-r--r-- | addons/microsoft_account/models/res_users.py | 22 |
8 files changed, 249 insertions, 0 deletions
diff --git a/addons/microsoft_account/__init__.py b/addons/microsoft_account/__init__.py new file mode 100644 index 00000000000..8c67f893b37 --- /dev/null +++ b/addons/microsoft_account/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models +from . import controllers diff --git a/addons/microsoft_account/__manifest__.py b/addons/microsoft_account/__manifest__.py new file mode 100644 index 00000000000..fcecd91b85c --- /dev/null +++ b/addons/microsoft_account/__manifest__.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Microsoft Users', + 'category': 'Hidden/Tools', + 'description': """ +The module adds Microsoft user in res user. +=========================================== +""", + 'depends': ['base_setup'], + 'data': [ + 'data/microsoft_account_data.xml', + ], +} diff --git a/addons/microsoft_account/controllers/__init__.py b/addons/microsoft_account/controllers/__init__.py new file mode 100644 index 00000000000..5d4b25db9c0 --- /dev/null +++ b/addons/microsoft_account/controllers/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import main diff --git a/addons/microsoft_account/controllers/main.py b/addons/microsoft_account/controllers/main.py new file mode 100644 index 00000000000..5256f82dca7 --- /dev/null +++ b/addons/microsoft_account/controllers/main.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import json +from werkzeug.utils import redirect + +from odoo import http, registry +from odoo.http import request + + +class MicrosoftAuth(http.Controller): + + @http.route('/microsoft_account/authentication', type='http', auth="public") + def oauth2callback(self, **kw): + """ This route/function is called by Microsoft when user Accept/Refuse the consent of Microsoft """ + state = json.loads(kw['state']) + dbname = state.get('d') + service = state.get('s') + url_return = state.get('f') + + with registry(dbname).cursor() as cr: + if kw.get('code'): + access_token, refresh_token, ttl = request.env['microsoft.service']._get_microsoft_tokens(kw['code'], service) + request.env.user._set_microsoft_auth_tokens(access_token, refresh_token, ttl) + return redirect(url_return) + elif kw.get('error'): + return redirect("%s%s%s" % (url_return, "?error=", kw['error'])) + else: + return redirect("%s%s" % (url_return, "?error=Unknown_error")) diff --git a/addons/microsoft_account/data/microsoft_account_data.xml b/addons/microsoft_account/data/microsoft_account_data.xml new file mode 100644 index 00000000000..da61e3da8ed --- /dev/null +++ b/addons/microsoft_account/data/microsoft_account_data.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <data noupdate="1"> + <record id="config_microsoft_redirect_uri" model="ir.config_parameter"> + <field name="key">microsoft_redirect_uri</field> + <field name="value">urn:ietf:wg:oauth:2.0:oob</field> + </record> + </data> +</odoo> diff --git a/addons/microsoft_account/models/__init__.py b/addons/microsoft_account/models/__init__.py new file mode 100644 index 00000000000..efaff2dc4e7 --- /dev/null +++ b/addons/microsoft_account/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import res_users +from . import microsoft_service diff --git a/addons/microsoft_account/models/microsoft_service.py b/addons/microsoft_account/models/microsoft_service.py new file mode 100644 index 00000000000..6b6de96aefb --- /dev/null +++ b/addons/microsoft_account/models/microsoft_service.py @@ -0,0 +1,160 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import datetime +import json +import logging + +import requests +from werkzeug import urls + +from odoo import api, fields, models, _ + +_logger = logging.getLogger(__name__) + +TIMEOUT = 20 + +MICROSOFT_AUTH_ENDPOINT = 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize' +MICROSOFT_TOKEN_ENDPOINT = 'https://login.microsoftonline.com/common/oauth2/v2.0/token' + + +class MicrosoftService(models.AbstractModel): + _name = 'microsoft.service' + _description = 'Microsoft Service' + + def _get_calendar_scope(self): + return 'offline_access openid Calendars.ReadWrite' + + @api.model + def generate_refresh_token(self, service, authorization_code): + """ Call Microsoft API to refresh the token, with the given authorization code + :param service : the name of the microsoft service to actualize + :param authorization_code : the code to exchange against the new refresh token + :returns the new refresh token + """ + Parameters = self.env['ir.config_parameter'].sudo() + client_id = Parameters.get_param('microsoft_%s_client_id' % service) + client_secret = Parameters.get_param('microsoft_%s_client_secret' % service) + redirect_uri = Parameters.get_param('microsoft_redirect_uri') + + scope = self._get_calendar_scope() + + # Get the Refresh Token From Microsoft And store it in ir.config_parameter + headers = {"Content-type": "application/x-www-form-urlencoded"} + data = { + 'client_id': client_id, + 'redirect_uri': redirect_uri, + 'client_secret': client_secret, + 'scope': scope, + 'grant_type': "refresh_token" + } + try: + req = requests.post(MICROSOFT_TOKEN_ENDPOINT, data=data, headers=headers, timeout=TIMEOUT) + req.raise_for_status() + content = req.json() + except IOError: + error_msg = _("Something went wrong during your token generation. Maybe your Authorization Code is invalid or already expired") + raise self.env['res.config.settings'].get_config_warning(error_msg) + + return content.get('refresh_token') + + @api.model + def _get_authorize_uri(self, from_url, service, scope): + """ This method return the url needed to allow this instance of Odoo to access to the scope + of gmail specified as parameters + """ + state = { + 'd': self.env.cr.dbname, + 's': service, + 'f': from_url + } + + get_param = self.env['ir.config_parameter'].sudo().get_param + base_url = get_param('web.base.url', default='http://www.odoo.com?NoBaseUrl') + client_id = get_param('microsoft_%s_client_id' % (service,), default=False) + + encoded_params = urls.url_encode({ + 'response_type': 'code', + 'client_id': client_id, + 'state': json.dumps(state), + 'scope': scope, + 'redirect_uri': base_url + '/microsoft_account/authentication', + 'prompt': 'consent', + 'access_type': 'offline' + }) + return "%s?%s" % (MICROSOFT_AUTH_ENDPOINT, encoded_params) + + @api.model + def _get_microsoft_tokens(self, authorize_code, service): + """ Call Microsoft API to exchange authorization code against token, with POST request, to + not be redirected. + """ + get_param = self.env['ir.config_parameter'].sudo().get_param + base_url = get_param('web.base.url', default='http://www.odoo.com?NoBaseUrl') + client_id = get_param('microsoft_%s_client_id' % (service,), default=False) + client_secret = get_param('microsoft_%s_client_secret' % (service,), default=False) + scope = self._get_calendar_scope() + + headers = {"content-type": "application/x-www-form-urlencoded"} + data = { + 'code': authorize_code, + 'client_id': client_id, + 'client_secret': client_secret, + 'grant_type': 'authorization_code', + 'scope': scope, + 'redirect_uri': base_url + '/microsoft_account/authentication' + } + try: + dummy, response, dummy = self._do_request(MICROSOFT_TOKEN_ENDPOINT, params=data, headers=headers, method='POST', preuri='') + access_token = response.get('access_token') + refresh_token = response.get('refresh_token') + ttl = response.get('expires_in') + return access_token, refresh_token, ttl + except requests.HTTPError: + error_msg = _("Something went wrong during your token generation. Maybe your Authorization Code is invalid") + raise self.env['res.config.settings'].get_config_warning(error_msg) + + @api.model + def _do_request(self, uri, params=None, headers=None, method='POST', preuri="https://graph.microsoft.com", timeout=TIMEOUT): + """ Execute the request to Microsoft API. Return a tuple ('HTTP_CODE', 'HTTP_RESPONSE') + :param uri : the url to contact + :param params : dict or already encoded parameters for the request to make + :param headers : headers of request + :param method : the method to use to make the request + :param preuri : pre url to prepend to param uri. + """ + if params is None: + params = {} + if headers is None: + headers = {} + + _logger.debug("Uri: %s - Type : %s - Headers: %s - Params : %s !" % (uri, method, headers, params)) + + ask_time = fields.Datetime.now() + try: + if method.upper() in ('GET', 'DELETE'): + res = requests.request(method.lower(), preuri + uri, headers=headers, params=params, timeout=timeout) + elif method.upper() in ('POST', 'PATCH', 'PUT'): + res = requests.request(method.lower(), preuri + uri, data=params, headers=headers, timeout=timeout) + else: + raise Exception(_('Method not supported [%s] not in [GET, POST, PUT, PATCH or DELETE]!', method)) + res.raise_for_status() + status = res.status_code + + if int(status) in (204, 404): # Page not found, no response + response = False + else: + response = res.json() + + try: + ask_time = datetime.strptime(res.headers.get('date'), "%a, %d %b %Y %H:%M:%S %Z") + except: + pass + except requests.HTTPError as error: + if error.response.status_code in (204, 404): + status = error.response.status_code + response = "" + else: + _logger.exception("Bad microsoft request : %s !", error.response.content) + raise error + return (status, response, ask_time) diff --git a/addons/microsoft_account/models/res_users.py b/addons/microsoft_account/models/res_users.py new file mode 100644 index 00000000000..d6587ffeda9 --- /dev/null +++ b/addons/microsoft_account/models/res_users.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import timedelta + + +from odoo import api, fields, models, _ + + +class User(models.Model): + _inherit = 'res.users' + + microsoft_calendar_rtoken = fields.Char('Microsoft Refresh Token', copy=False, groups="base.group_system") + microsoft_calendar_token = fields.Char('Microsoft User token', copy=False, groups="base.group_system") + microsoft_calendar_token_validity = fields.Datetime('Microsoft Token Validity', copy=False) + + def _set_microsoft_auth_tokens(self, access_token, refresh_token, ttl): + self.write({ + 'microsoft_calendar_rtoken': refresh_token, + 'microsoft_calendar_token': access_token, + 'microsoft_calendar_token_validity': fields.Datetime.now() + timedelta(seconds=ttl) if ttl else False, + }) |
