summaryrefslogtreecommitdiff
path: root/addons/microsoft_account
diff options
context:
space:
mode:
authorjbm-odoo <jbm@odoo.com>2020-04-21 13:08:35 +0000
committerjbm-odoo <jbm@odoo.com>2020-08-05 13:47:42 +0000
commit9dc3057e8eb82d9237457167641585655f249c3e (patch)
tree11975a957335d9dd3ce7f2dc0844fa8b5e7acff3 /addons/microsoft_account
parent78ef2ec8134b09d9732849a11ff8d9e1ac886bb5 (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__.py5
-rw-r--r--addons/microsoft_account/__manifest__.py15
-rw-r--r--addons/microsoft_account/controllers/__init__.py4
-rw-r--r--addons/microsoft_account/controllers/main.py29
-rw-r--r--addons/microsoft_account/data/microsoft_account_data.xml9
-rw-r--r--addons/microsoft_account/models/__init__.py5
-rw-r--r--addons/microsoft_account/models/microsoft_service.py160
-rw-r--r--addons/microsoft_account/models/res_users.py22
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,
+ })