mirror of
https://github.com/bringout/oca-ocb-security.git
synced 2026-04-24 13:02:04 +02:00
Initial commit: Security packages
This commit is contained in:
commit
bb469e4763
1399 changed files with 278378 additions and 0 deletions
|
|
@ -0,0 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import microsoft_outlook_mixin
|
||||
|
||||
from . import fetchmail_server
|
||||
from . import ir_mail_server
|
||||
from . import res_config_settings
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class FetchmailServer(models.Model):
|
||||
"""Add the Outlook OAuth authentication on the incoming mail servers."""
|
||||
|
||||
_name = 'fetchmail.server'
|
||||
_inherit = ['fetchmail.server', 'microsoft.outlook.mixin']
|
||||
|
||||
_OUTLOOK_SCOPE = 'https://outlook.office.com/IMAP.AccessAsUser.All'
|
||||
|
||||
server_type = fields.Selection(selection_add=[('outlook', 'Outlook OAuth Authentication')], ondelete={'outlook': 'set default'})
|
||||
|
||||
def _compute_server_type_info(self):
|
||||
outlook_servers = self.filtered(lambda server: server.server_type == 'outlook')
|
||||
outlook_servers.server_type_info = _(
|
||||
'Connect your personal Outlook account using OAuth. \n'
|
||||
'You will be redirected to the Outlook login page to accept '
|
||||
'the permissions.')
|
||||
super(FetchmailServer, self - outlook_servers)._compute_server_type_info()
|
||||
|
||||
@api.depends('server_type')
|
||||
def _compute_is_microsoft_outlook_configured(self):
|
||||
outlook_servers = self.filtered(lambda server: server.server_type == 'outlook')
|
||||
(self - outlook_servers).is_microsoft_outlook_configured = False
|
||||
super(FetchmailServer, outlook_servers)._compute_is_microsoft_outlook_configured()
|
||||
|
||||
@api.constrains('server_type', 'is_ssl')
|
||||
def _check_use_microsoft_outlook_service(self):
|
||||
for server in self:
|
||||
if server.server_type == 'outlook' and not server.is_ssl:
|
||||
raise UserError(_('SSL is required for the server %r.', server.name))
|
||||
|
||||
@api.onchange('server_type')
|
||||
def onchange_server_type(self):
|
||||
"""Set the default configuration for a IMAP Outlook server."""
|
||||
if self.server_type == 'outlook':
|
||||
self.server = 'imap.outlook.com'
|
||||
self.is_ssl = True
|
||||
self.port = 993
|
||||
else:
|
||||
self.microsoft_outlook_refresh_token = False
|
||||
self.microsoft_outlook_access_token = False
|
||||
self.microsoft_outlook_access_token_expiration = False
|
||||
super(FetchmailServer, self).onchange_server_type()
|
||||
|
||||
def _imap_login(self, connection):
|
||||
"""Authenticate the IMAP connection.
|
||||
|
||||
If the mail server is Outlook, we use the OAuth2 authentication protocol.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.server_type == 'outlook':
|
||||
auth_string = self._generate_outlook_oauth2_string(self.user)
|
||||
connection.authenticate('XOAUTH2', lambda x: auth_string)
|
||||
connection.select('INBOX')
|
||||
else:
|
||||
super()._imap_login(connection)
|
||||
|
||||
def _get_connection_type(self):
|
||||
"""Return which connection must be used for this mail server (IMAP or POP).
|
||||
The Outlook mail server used an IMAP connection.
|
||||
"""
|
||||
self.ensure_one()
|
||||
return 'imap' if self.server_type == 'outlook' else super()._get_connection_type()
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class IrMailServer(models.Model):
|
||||
"""Add the Outlook OAuth authentication on the outgoing mail servers."""
|
||||
|
||||
_name = 'ir.mail_server'
|
||||
_inherit = ['ir.mail_server', 'microsoft.outlook.mixin']
|
||||
|
||||
_OUTLOOK_SCOPE = 'https://outlook.office.com/SMTP.Send'
|
||||
|
||||
smtp_authentication = fields.Selection(
|
||||
selection_add=[('outlook', 'Outlook OAuth Authentication')],
|
||||
ondelete={'outlook': 'set default'})
|
||||
|
||||
@api.depends('smtp_authentication')
|
||||
def _compute_is_microsoft_outlook_configured(self):
|
||||
outlook_servers = self.filtered(lambda server: server.smtp_authentication == 'outlook')
|
||||
(self - outlook_servers).is_microsoft_outlook_configured = False
|
||||
super(IrMailServer, outlook_servers)._compute_is_microsoft_outlook_configured()
|
||||
|
||||
def _compute_smtp_authentication_info(self):
|
||||
outlook_servers = self.filtered(lambda server: server.smtp_authentication == 'outlook')
|
||||
outlook_servers.smtp_authentication_info = _(
|
||||
'Connect your Outlook account with the OAuth Authentication process. \n'
|
||||
'By default, only a user with a matching email address will be able to use this server. '
|
||||
'To extend its use, you should set a "mail.default.from" system parameter.')
|
||||
super(IrMailServer, self - outlook_servers)._compute_smtp_authentication_info()
|
||||
|
||||
@api.constrains('smtp_authentication', 'smtp_pass', 'smtp_encryption', 'smtp_user')
|
||||
def _check_use_microsoft_outlook_service(self):
|
||||
outlook_servers = self.filtered(lambda server: server.smtp_authentication == 'outlook')
|
||||
for server in outlook_servers:
|
||||
if server.smtp_pass:
|
||||
raise UserError(_(
|
||||
'Please leave the password field empty for Outlook mail server %r. '
|
||||
'The OAuth process does not require it', server.name))
|
||||
|
||||
if server.smtp_encryption != 'starttls':
|
||||
raise UserError(_(
|
||||
'Incorrect Connection Security for Outlook mail server %r. '
|
||||
'Please set it to "TLS (STARTTLS)".', server.name))
|
||||
|
||||
if not server.smtp_user:
|
||||
raise UserError(_(
|
||||
'Please fill the "Username" field with your Outlook/Office365 username (your email address). '
|
||||
'This should be the same account as the one used for the Outlook OAuthentication Token.'))
|
||||
|
||||
@api.onchange('smtp_encryption')
|
||||
def _onchange_encryption(self):
|
||||
"""Do not change the SMTP configuration if it's a Outlook server
|
||||
|
||||
(e.g. the port which is already set)"""
|
||||
if self.smtp_authentication != 'outlook':
|
||||
super()._onchange_encryption()
|
||||
|
||||
@api.onchange('smtp_authentication')
|
||||
def _onchange_smtp_authentication_outlook(self):
|
||||
if self.smtp_authentication == 'outlook':
|
||||
self.smtp_host = 'smtp.outlook.com'
|
||||
self.smtp_encryption = 'starttls'
|
||||
self.smtp_port = 587
|
||||
else:
|
||||
self.microsoft_outlook_refresh_token = False
|
||||
self.microsoft_outlook_access_token = False
|
||||
self.microsoft_outlook_access_token_expiration = False
|
||||
|
||||
@api.onchange('smtp_user', 'smtp_authentication')
|
||||
def _on_change_smtp_user_outlook(self):
|
||||
"""The Outlook mail servers can only be used for the user personal email address."""
|
||||
if self.smtp_authentication == 'outlook':
|
||||
self.from_filter = self.smtp_user
|
||||
|
||||
def _smtp_login(self, connection, smtp_user, smtp_password):
|
||||
if len(self) == 1 and self.smtp_authentication == 'outlook':
|
||||
auth_string = self._generate_outlook_oauth2_string(smtp_user)
|
||||
oauth_param = base64.b64encode(auth_string.encode()).decode()
|
||||
connection.ehlo()
|
||||
connection.docmd('AUTH', f'XOAUTH2 {oauth_param}')
|
||||
else:
|
||||
super()._smtp_login(connection, smtp_user, smtp_password)
|
||||
|
|
@ -0,0 +1,194 @@
|
|||
# -*- 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/',
|
||||
)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
microsoft_outlook_client_identifier = fields.Char('Outlook Client Id', config_parameter='microsoft_outlook_client_id')
|
||||
microsoft_outlook_client_secret = fields.Char('Outlook Client Secret', config_parameter='microsoft_outlook_client_secret')
|
||||
Loading…
Add table
Add a link
Reference in a new issue