123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195 |
- # -*- coding: utf-8 -*-
- # Part of Odoo. See LICENSE file for full copyright and licensing details.
- import json
- import logging
- import time
- import requests
- from werkzeug.urls import url_encode, url_join
- from odoo import _, api, fields, models
- from odoo.exceptions import AccessError, UserError
- from odoo.tools.misc import hmac
- _logger = logging.getLogger(__name__)
- class MicrosoftOutlookMixin(models.AbstractModel):
- _name = 'microsoft.outlook.mixin'
- _description = 'Microsoft Outlook Mixin'
- _OUTLOOK_SCOPE = None
- is_microsoft_outlook_configured = fields.Boolean('Is Outlook Credential Configured',
- compute='_compute_is_microsoft_outlook_configured')
- microsoft_outlook_refresh_token = fields.Char(string='Outlook Refresh Token',
- groups='base.group_system', copy=False)
- microsoft_outlook_access_token = fields.Char(string='Outlook Access Token',
- groups='base.group_system', copy=False)
- microsoft_outlook_access_token_expiration = fields.Integer(string='Outlook Access Token Expiration Timestamp',
- groups='base.group_system', copy=False)
- microsoft_outlook_uri = fields.Char(compute='_compute_outlook_uri', string='Authentication URI',
- help='The URL to generate the authorization code from Outlook', groups='base.group_system')
- def _compute_is_microsoft_outlook_configured(self):
- Config = self.env['ir.config_parameter'].sudo()
- microsoft_outlook_client_id = Config.get_param('microsoft_outlook_client_id')
- microsoft_outlook_client_secret = Config.get_param('microsoft_outlook_client_secret')
- self.is_microsoft_outlook_configured = microsoft_outlook_client_id and microsoft_outlook_client_secret
- @api.depends('is_microsoft_outlook_configured')
- def _compute_outlook_uri(self):
- Config = self.env['ir.config_parameter'].sudo()
- base_url = self.get_base_url()
- microsoft_outlook_client_id = Config.get_param('microsoft_outlook_client_id')
- for record in self:
- if not record.id or not record.is_microsoft_outlook_configured:
- record.microsoft_outlook_uri = False
- continue
- record.microsoft_outlook_uri = url_join(self._get_microsoft_endpoint(), 'authorize?%s' % url_encode({
- 'client_id': microsoft_outlook_client_id,
- 'response_type': 'code',
- 'redirect_uri': url_join(base_url, '/microsoft_outlook/confirm'),
- 'response_mode': 'query',
- # offline_access is needed to have the refresh_token
- 'scope': 'offline_access %s' % self._OUTLOOK_SCOPE,
- 'state': json.dumps({
- 'model': record._name,
- 'id': record.id,
- 'csrf_token': record._get_outlook_csrf_token(),
- })
- }))
- def open_microsoft_outlook_uri(self):
- """Open the URL to accept the Outlook permission.
- This is done with an action, so we can force the user the save the form.
- We need him to save the form so the current mail server record exist in DB and
- we can include the record ID in the URL.
- """
- self.ensure_one()
- if not self.env.user.has_group('base.group_system'):
- raise AccessError(_('Only the administrator can link an Outlook mail server.'))
- if not self.is_microsoft_outlook_configured:
- raise UserError(_('Please configure your Outlook credentials.'))
- return {
- 'type': 'ir.actions.act_url',
- 'url': self.microsoft_outlook_uri,
- }
- def _fetch_outlook_refresh_token(self, authorization_code):
- """Request the refresh token and the initial access token from the authorization code.
- :return:
- refresh_token, access_token, access_token_expiration
- """
- response = self._fetch_outlook_token('authorization_code', code=authorization_code)
- return (
- response['refresh_token'],
- response['access_token'],
- int(time.time()) + int(response['expires_in']),
- )
- def _fetch_outlook_access_token(self, refresh_token):
- """Refresh the access token thanks to the refresh token.
- :return:
- access_token, access_token_expiration
- """
- response = self._fetch_outlook_token('refresh_token', refresh_token=refresh_token)
- return (
- response['refresh_token'],
- response['access_token'],
- int(time.time()) + int(response['expires_in']),
- )
- def _fetch_outlook_token(self, grant_type, **values):
- """Generic method to request an access token or a refresh token.
- Return the JSON response of the Outlook API and manage the errors which can occur.
- :param grant_type: Depends the action we want to do (refresh_token or authorization_code)
- :param values: Additional parameters that will be given to the Outlook endpoint
- """
- Config = self.env['ir.config_parameter'].sudo()
- base_url = self.get_base_url()
- microsoft_outlook_client_id = Config.get_param('microsoft_outlook_client_id')
- microsoft_outlook_client_secret = Config.get_param('microsoft_outlook_client_secret')
- response = requests.post(
- url_join(self._get_microsoft_endpoint(), 'token'),
- data={
- 'client_id': microsoft_outlook_client_id,
- 'client_secret': microsoft_outlook_client_secret,
- 'scope': 'offline_access %s' % self._OUTLOOK_SCOPE,
- 'redirect_uri': url_join(base_url, '/microsoft_outlook/confirm'),
- 'grant_type': grant_type,
- **values,
- },
- timeout=10,
- )
- if not response.ok:
- try:
- error_description = response.json()['error_description']
- except Exception:
- error_description = _('Unknown error.')
- raise UserError(_('An error occurred when fetching the access token. %s', error_description))
- return response.json()
- def _generate_outlook_oauth2_string(self, login):
- """Generate a OAuth2 string which can be used for authentication.
- :param user: Email address of the Outlook account to authenticate
- :return: The SASL argument for the OAuth2 mechanism.
- """
- self.ensure_one()
- now_timestamp = int(time.time())
- if not self.microsoft_outlook_access_token \
- or not self.microsoft_outlook_access_token_expiration \
- or self.microsoft_outlook_access_token_expiration < now_timestamp:
- if not self.microsoft_outlook_refresh_token:
- raise UserError(_('Please connect with your Outlook account before using it.'))
- (
- self.microsoft_outlook_refresh_token,
- self.microsoft_outlook_access_token,
- self.microsoft_outlook_access_token_expiration,
- ) = self._fetch_outlook_access_token(self.microsoft_outlook_refresh_token)
- _logger.info(
- 'Microsoft Outlook: fetch new access token. It expires in %i minutes',
- (self.microsoft_outlook_access_token_expiration - now_timestamp) // 60)
- else:
- _logger.info(
- 'Microsoft Outlook: reuse existing access token. It expires in %i minutes',
- (self.microsoft_outlook_access_token_expiration - now_timestamp) // 60)
- return 'user=%s\1auth=Bearer %s\1\1' % (login, self.microsoft_outlook_access_token)
- def _get_outlook_csrf_token(self):
- """Generate a CSRF token that will be verified in `microsoft_outlook_callback`.
- This will prevent a malicious person to make an admin user disconnect the mail servers.
- """
- self.ensure_one()
- _logger.info('Microsoft Outlook: generate CSRF token for %s #%i', self._name, self.id)
- return hmac(
- env=self.env(su=True),
- scope='microsoft_outlook_oauth',
- message=(self._name, self.id),
- )
- @api.model
- def _get_microsoft_endpoint(self):
- return self.env["ir.config_parameter"].sudo().get_param(
- 'microsoft_outlook.endpoint',
- 'https://login.microsoftonline.com/common/oauth2/v2.0/',
- )
|