mirror of
https://github.com/bringout/oca-ocb-security.git
synced 2026-04-19 21:12:01 +02:00
19.0 vanilla
This commit is contained in:
parent
20ddc1b4a3
commit
c0efcc53f5
1162 changed files with 125577 additions and 105287 deletions
|
|
@ -6,3 +6,4 @@ from . import google_gmail_mixin
|
|||
from . import fetchmail_server
|
||||
from . import ir_mail_server
|
||||
from . import res_config_settings
|
||||
from . import res_users
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
# 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):
|
||||
|
|
@ -18,6 +19,12 @@ class FetchmailServer(models.Model):
|
|||
'need to accept the permission.')
|
||||
super(FetchmailServer, self - gmail_servers)._compute_server_type_info()
|
||||
|
||||
@api.constrains('server_type', 'is_ssl')
|
||||
def _check_use_google_gmail_service(self):
|
||||
for server in self:
|
||||
if server.server_type == 'gmail' and not server.is_ssl:
|
||||
raise UserError(_('SSL is required for server “%s”.', server.name))
|
||||
|
||||
@api.onchange('server_type', 'is_ssl', 'object_id')
|
||||
def onchange_server_type(self):
|
||||
"""Set the default configuration for a IMAP Gmail server."""
|
||||
|
|
@ -26,13 +33,12 @@ class FetchmailServer(models.Model):
|
|||
self.is_ssl = True
|
||||
self.port = 993
|
||||
else:
|
||||
self.google_gmail_authorization_code = False
|
||||
self.google_gmail_refresh_token = False
|
||||
self.google_gmail_access_token = False
|
||||
self.google_gmail_access_token_expiration = False
|
||||
super(FetchmailServer, self).onchange_server_type()
|
||||
super().onchange_server_type()
|
||||
|
||||
def _imap_login(self, connection):
|
||||
def _imap_login__(self, connection): # noqa: PLW3201
|
||||
"""Authenticate the IMAP connection.
|
||||
|
||||
If the mail server is Gmail, we use the OAuth2 authentication protocol.
|
||||
|
|
@ -43,7 +49,7 @@ class FetchmailServer(models.Model):
|
|||
connection.authenticate('XOAUTH2', lambda x: auth_string)
|
||||
connection.select('INBOX')
|
||||
else:
|
||||
super(FetchmailServer, self)._imap_login(connection)
|
||||
super()._imap_login__(connection)
|
||||
|
||||
def _get_connection_type(self):
|
||||
"""Return which connection must be used for this mail server (IMAP or POP).
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@ import logging
|
|||
import time
|
||||
import requests
|
||||
|
||||
from werkzeug.urls import url_encode, url_join
|
||||
from werkzeug.urls import url_encode
|
||||
|
||||
from odoo import _, api, fields, models, tools
|
||||
from odoo import _, fields, models, tools, release
|
||||
from odoo.exceptions import AccessError, UserError
|
||||
from odoo.tools.urls import urljoin as url_join
|
||||
from odoo.addons.google_gmail.tools import get_iap_error_message
|
||||
|
||||
GMAIL_TOKEN_REQUEST_TIMEOUT = 5
|
||||
|
||||
|
|
@ -21,34 +23,36 @@ _logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class GoogleGmailMixin(models.AbstractModel):
|
||||
|
||||
_name = 'google.gmail.mixin'
|
||||
|
||||
_description = 'Google Gmail Mixin'
|
||||
|
||||
_SERVICE_SCOPE = 'https://mail.google.com/'
|
||||
# The scope `https://mail.google.com/` is needed for SMTP and IMAP
|
||||
# https://developers.google.com/workspace/gmail/imap/xoauth2-protocol
|
||||
_SERVICE_SCOPE = 'https://mail.google.com/ https://www.googleapis.com/auth/userinfo.email'
|
||||
_DEFAULT_GMAIL_IAP_ENDPOINT = 'https://gmail.api.odoo.com'
|
||||
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
google_gmail_authorization_code = fields.Char(string='Authorization Code', groups='base.group_system', copy=False)
|
||||
google_gmail_refresh_token = fields.Char(string='Refresh Token', groups='base.group_system', copy=False)
|
||||
google_gmail_access_token = fields.Char(string='Access Token', groups='base.group_system', copy=False)
|
||||
google_gmail_access_token_expiration = fields.Integer(string='Access Token Expiration Timestamp', groups='base.group_system', copy=False)
|
||||
google_gmail_uri = fields.Char(compute='_compute_gmail_uri', string='URI', help='The URL to generate the authorization code from Google', groups='base.group_system')
|
||||
|
||||
@api.depends('google_gmail_authorization_code')
|
||||
def _compute_gmail_uri(self):
|
||||
Config = self.env['ir.config_parameter'].sudo()
|
||||
google_gmail_client_id = Config.get_param('google_gmail_client_id')
|
||||
google_gmail_client_secret = Config.get_param('google_gmail_client_secret')
|
||||
is_configured = google_gmail_client_id and google_gmail_client_secret
|
||||
base_url = self.get_base_url()
|
||||
|
||||
redirect_uri = url_join(base_url, '/google_gmail/confirm')
|
||||
|
||||
if not google_gmail_client_id or not google_gmail_client_secret:
|
||||
if not is_configured:
|
||||
self.google_gmail_uri = False
|
||||
else:
|
||||
for record in self:
|
||||
google_gmail_uri = 'https://accounts.google.com/o/oauth2/v2/auth?%s' % url_encode({
|
||||
'client_id': google_gmail_client_id,
|
||||
'redirect_uri': redirect_uri,
|
||||
'redirect_uri': url_join(base_url, '/google_gmail/confirm'),
|
||||
'response_type': 'code',
|
||||
'scope': self._SERVICE_SCOPE,
|
||||
# access_type and prompt needed to get a refresh token
|
||||
|
|
@ -71,15 +75,64 @@ class GoogleGmailMixin(models.AbstractModel):
|
|||
"""
|
||||
self.ensure_one()
|
||||
|
||||
if not self.env.user.has_group('base.group_system'):
|
||||
if not self.env.is_admin():
|
||||
raise AccessError(_('Only the administrator can link a Gmail mail server.'))
|
||||
|
||||
if not self.google_gmail_uri:
|
||||
email_normalized = tools.email_normalize(self[self._email_field])
|
||||
if not email_normalized:
|
||||
raise UserError(_('Please enter a valid email address.'))
|
||||
|
||||
Config = self.env['ir.config_parameter'].sudo()
|
||||
google_gmail_client_id = Config.get_param('google_gmail_client_id')
|
||||
google_gmail_client_secret = Config.get_param('google_gmail_client_secret')
|
||||
is_configured = google_gmail_client_id and google_gmail_client_secret
|
||||
|
||||
if not is_configured: # use IAP (see '/google_gmail/iap_confirm')
|
||||
if release.version_info[-1] != 'e':
|
||||
raise UserError(_('Please configure your Gmail credentials.'))
|
||||
|
||||
gmail_iap_endpoint = self.env['ir.config_parameter'].sudo().get_param(
|
||||
'mail.server.gmail.iap.endpoint',
|
||||
self._DEFAULT_GMAIL_IAP_ENDPOINT,
|
||||
)
|
||||
db_uuid = self.env['ir.config_parameter'].sudo().get_param('database.uuid')
|
||||
|
||||
# final callback URL that will receive the token from IAP
|
||||
callback_params = url_encode({
|
||||
'model': self._name,
|
||||
'rec_id': self.id,
|
||||
'csrf_token': self._get_gmail_csrf_token(),
|
||||
})
|
||||
callback_url = url_join(self.get_base_url(), f'/google_gmail/iap_confirm?{callback_params}')
|
||||
|
||||
iap_url = url_join(gmail_iap_endpoint, '/api/mail_oauth/1/gmail')
|
||||
try:
|
||||
response = requests.get(
|
||||
iap_url,
|
||||
params={'db_uuid': db_uuid, 'callback_url': callback_url},
|
||||
timeout=GMAIL_TOKEN_REQUEST_TIMEOUT)
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.RequestException as e:
|
||||
_logger.error('Can not contact IAP: %s.', e)
|
||||
raise UserError(_('Oops, we could not authenticate you. Please try again later.'))
|
||||
|
||||
response = response.json()
|
||||
if 'error' in response:
|
||||
self._raise_iap_error(response['error'])
|
||||
|
||||
# URL on IAP that will redirect to Gmail login page
|
||||
google_gmail_uri = response['url']
|
||||
|
||||
else:
|
||||
google_gmail_uri = self.google_gmail_uri
|
||||
|
||||
if not google_gmail_uri:
|
||||
raise UserError(_('Please configure your Gmail credentials.'))
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': self.google_gmail_uri,
|
||||
'url': google_gmail_uri,
|
||||
'target': 'self',
|
||||
}
|
||||
|
||||
def _fetch_gmail_refresh_token(self, authorization_code):
|
||||
|
|
@ -102,8 +155,14 @@ class GoogleGmailMixin(models.AbstractModel):
|
|||
:return:
|
||||
access_token, access_token_expiration
|
||||
"""
|
||||
response = self._fetch_gmail_token('refresh_token', refresh_token=refresh_token)
|
||||
Config = self.env['ir.config_parameter'].sudo()
|
||||
|
||||
google_gmail_client_id = Config.get_param('google_gmail_client_id')
|
||||
google_gmail_client_secret = Config.get_param('google_gmail_client_secret')
|
||||
if not google_gmail_client_id or not google_gmail_client_secret:
|
||||
return self._fetch_gmail_access_token_iap(refresh_token)
|
||||
|
||||
response = self._fetch_gmail_token('refresh_token', refresh_token=refresh_token)
|
||||
return (
|
||||
response['access_token'],
|
||||
int(time.time()) + response['expires_in'],
|
||||
|
|
@ -140,6 +199,40 @@ class GoogleGmailMixin(models.AbstractModel):
|
|||
|
||||
return response.json()
|
||||
|
||||
def _fetch_gmail_access_token_iap(self, refresh_token):
|
||||
"""Fetch the access token using IAP.
|
||||
|
||||
Make a HTTP request to IAP, that will make a HTTP request
|
||||
to the Gmail API and give us the result.
|
||||
|
||||
:return:
|
||||
access_token, access_token_expiration
|
||||
"""
|
||||
gmail_iap_endpoint = self.env['ir.config_parameter'].sudo().get_param(
|
||||
'mail.server.gmail.iap.endpoint',
|
||||
self.env['google.gmail.mixin']._DEFAULT_GMAIL_IAP_ENDPOINT,
|
||||
)
|
||||
db_uuid = self.env['ir.config_parameter'].sudo().get_param('database.uuid')
|
||||
|
||||
response = requests.get(
|
||||
url_join(gmail_iap_endpoint, '/api/mail_oauth/1/gmail_access_token'),
|
||||
params={'refresh_token': refresh_token, 'db_uuid': db_uuid},
|
||||
timeout=GMAIL_TOKEN_REQUEST_TIMEOUT,
|
||||
)
|
||||
|
||||
if not response.ok:
|
||||
_logger.error('Can not contact IAP: %s.', response.text)
|
||||
raise UserError(_('Oops, we could not authenticate you. Please try again later.'))
|
||||
|
||||
response = response.json()
|
||||
if 'error' in response:
|
||||
self._raise_iap_error(response['error'])
|
||||
|
||||
return response
|
||||
|
||||
def _raise_iap_error(self, error):
|
||||
raise UserError(get_iap_error_message(self.env, error))
|
||||
|
||||
def _generate_oauth2_string(self, user, refresh_token):
|
||||
"""Generate a OAuth2 string which can be used for authentication.
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from odoo import _, fields, models, api
|
|||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class IrMailServer(models.Model):
|
||||
class IrMail_Server(models.Model):
|
||||
"""Represents an SMTP server, able to send outgoing emails, with SSL and TLS capabilities."""
|
||||
|
||||
_name = 'ir.mail_server'
|
||||
|
|
@ -23,14 +23,14 @@ class IrMailServer(models.Model):
|
|||
'Connect your Gmail 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 - gmail_servers)._compute_smtp_authentication_info()
|
||||
super(IrMail_Server, self - gmail_servers)._compute_smtp_authentication_info()
|
||||
|
||||
@api.onchange('smtp_encryption')
|
||||
def _onchange_encryption(self):
|
||||
"""Do not change the SMTP configuration if it's a Gmail server
|
||||
(e.g. the port which is already set)"""
|
||||
if self.smtp_authentication != 'gmail':
|
||||
super(IrMailServer, self)._onchange_encryption()
|
||||
super()._onchange_encryption()
|
||||
|
||||
@api.onchange('smtp_authentication')
|
||||
def _onchange_smtp_authentication_gmail(self):
|
||||
|
|
@ -39,7 +39,6 @@ class IrMailServer(models.Model):
|
|||
self.smtp_encryption = 'starttls'
|
||||
self.smtp_port = 587
|
||||
else:
|
||||
self.google_gmail_authorization_code = False
|
||||
self.google_gmail_refresh_token = False
|
||||
self.google_gmail_access_token = False
|
||||
self.google_gmail_access_token_expiration = False
|
||||
|
|
@ -56,12 +55,12 @@ class IrMailServer(models.Model):
|
|||
for server in gmail_servers:
|
||||
if server.smtp_pass:
|
||||
raise UserError(_(
|
||||
'Please leave the password field empty for Gmail mail server %r. '
|
||||
'Please leave the password field empty for Gmail mail server “%s”. '
|
||||
'The OAuth process does not require it', server.name))
|
||||
|
||||
if server.smtp_encryption != 'starttls':
|
||||
raise UserError(_(
|
||||
'Incorrect Connection Security for Gmail mail server %r. '
|
||||
'Incorrect Connection Security for Gmail mail server “%s”. '
|
||||
'Please set it to "TLS (STARTTLS)".', server.name))
|
||||
|
||||
if not server.smtp_user:
|
||||
|
|
@ -69,11 +68,11 @@ class IrMailServer(models.Model):
|
|||
'Please fill the "Username" field with your Gmail username (your email address). '
|
||||
'This should be the same account as the one used for the Gmail OAuthentication Token.'))
|
||||
|
||||
def _smtp_login(self, connection, smtp_user, smtp_password):
|
||||
def _smtp_login__(self, connection, smtp_user, smtp_password): # noqa: PLW3201
|
||||
if len(self) == 1 and self.smtp_authentication == 'gmail':
|
||||
auth_string = self._generate_oauth2_string(smtp_user, self.google_gmail_refresh_token)
|
||||
oauth_param = base64.b64encode(auth_string.encode()).decode()
|
||||
connection.ehlo()
|
||||
connection.docmd('AUTH', f'XOAUTH2 {oauth_param}')
|
||||
else:
|
||||
super(IrMailServer, self)._smtp_login(connection, smtp_user, smtp_password)
|
||||
super()._smtp_login__(connection, smtp_user, smtp_password)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ResUsers(models.Model):
|
||||
_inherit = "res.users"
|
||||
|
||||
outgoing_mail_server_type = fields.Selection(
|
||||
selection_add=[("gmail", "Gmail")],
|
||||
ondelete={"gmail": "set default"},
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _get_mail_server_values(self, server_type):
|
||||
values = super()._get_mail_server_values(server_type)
|
||||
if server_type == "gmail":
|
||||
values |= {
|
||||
"smtp_host": "smtp.gmail.com",
|
||||
"smtp_authentication": "gmail",
|
||||
}
|
||||
return values
|
||||
|
||||
@api.model
|
||||
def _get_mail_server_setup_end_action(self, smtp_server):
|
||||
if smtp_server.smtp_authentication == "gmail":
|
||||
return smtp_server.sudo().open_google_gmail_uri()
|
||||
return super()._get_mail_server_setup_end_action(smtp_server)
|
||||
Loading…
Add table
Add a link
Reference in a new issue