From a08a0b6454e14716e6cf6f691047baefcb515b29 Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Tue, 1 Feb 2022 12:19:00 +0000 Subject: [PATCH] [ADD] auth_totp_mail: 2FA using code sent by email Add the possibility to force the two-factor authentication for all users, using a two-factor authentication by email when the 2FA using an Authenticator app is not configured for the user. Two possibilities: - Force the 2FA only for employee users using the system parameter `auth_totp.policy=employee_required` - Force the 2FA for all users, employees and portals, using the system parameter `auth_totp.policy=all_required` closes odoo/odoo#83750 Signed-off-by: Denis Ledoux (dle) --- .tx/config | 5 + addons/auth_totp/controllers/home.py | 7 +- addons/auth_totp/i18n/auth_totp.pot | 2 +- addons/auth_totp/models/res_users.py | 10 +- addons/auth_totp/models/totp.py | 6 +- addons/auth_totp_mail_enforce/__init__.py | 5 + addons/auth_totp_mail_enforce/__manifest__.py | 20 ++ .../controllers/__init__.py | 4 + .../controllers/home.py | 26 ++ .../data/mail_template_data.xml | 51 ++++ .../i18n/auth_totp_mail_enforce.pot | 229 ++++++++++++++++++ .../auth_totp_mail_enforce/models/__init__.py | 6 + .../models/auth_totp_rate_limit_log.py | 20 ++ .../models/res_config_settings.py | 32 +++ .../models/res_users.py | 146 +++++++++++ .../security/ir.model.access.csv | 2 + .../views/res_config_settings_views.xml | 33 +++ .../views/templates.xml | 28 +++ odoo/addons/base/models/res_users.py | 3 + 19 files changed, 626 insertions(+), 9 deletions(-) create mode 100644 addons/auth_totp_mail_enforce/__init__.py create mode 100644 addons/auth_totp_mail_enforce/__manifest__.py create mode 100644 addons/auth_totp_mail_enforce/controllers/__init__.py create mode 100644 addons/auth_totp_mail_enforce/controllers/home.py create mode 100644 addons/auth_totp_mail_enforce/data/mail_template_data.xml create mode 100755 addons/auth_totp_mail_enforce/i18n/auth_totp_mail_enforce.pot create mode 100644 addons/auth_totp_mail_enforce/models/__init__.py create mode 100644 addons/auth_totp_mail_enforce/models/auth_totp_rate_limit_log.py create mode 100644 addons/auth_totp_mail_enforce/models/res_config_settings.py create mode 100644 addons/auth_totp_mail_enforce/models/res_users.py create mode 100644 addons/auth_totp_mail_enforce/security/ir.model.access.csv create mode 100644 addons/auth_totp_mail_enforce/views/res_config_settings_views.xml create mode 100644 addons/auth_totp_mail_enforce/views/templates.xml diff --git a/.tx/config b/.tx/config index b9aac59bf20..1b15fc93e24 100644 --- a/.tx/config +++ b/.tx/config @@ -97,6 +97,11 @@ file_filter = addons/auth_totp_mail/i18n/.po source_file = addons/auth_totp_mail/i18n/auth_totp_mail.pot source_lang = en +[odoo-master.auth_totp_mail_enforce] +file_filter = addons/auth_totp_mail_enforce/i18n/.po +source_file = addons/auth_totp_mail_enforce/i18n/auth_totp_mail_enforce.pot +source_lang = en + [odoo-master.auth_totp_portal] file_filter = addons/auth_totp_portal/i18n/.po source_file = addons/auth_totp_portal/i18n/auth_totp_portal.pot diff --git a/addons/auth_totp/controllers/home.py b/addons/auth_totp/controllers/home.py index 1c6f2f879c1..71356088ba8 100644 --- a/addons/auth_totp/controllers/home.py +++ b/addons/auth_totp/controllers/home.py @@ -35,12 +35,12 @@ class Home(odoo.addons.web.controllers.main.Home): request.session.finalize() return request.redirect(self._login_redirect(request.session.uid, redirect=redirect)) - elif user and request.httprequest.method == 'POST': + elif user and request.httprequest.method == 'POST' and kwargs.get('totp_token'): try: with user._assert_can_auth(): user._totp_check(int(re.sub(r'\s', '', kwargs['totp_token']))) - except AccessDenied: - error = _("Verification failed, please double-check the 6-digit code") + except AccessDenied as e: + error = str(e) except ValueError: error = _("Invalid authentication code format.") else: @@ -66,6 +66,7 @@ class Home(odoo.addons.web.controllers.main.Home): return response return request.render('auth_totp.auth_totp_form', { + 'user': user, 'error': error, 'redirect': redirect, }) diff --git a/addons/auth_totp/i18n/auth_totp.pot b/addons/auth_totp/i18n/auth_totp.pot index 417e69be3e8..d67e0dea752 100644 --- a/addons/auth_totp/i18n/auth_totp.pot +++ b/addons/auth_totp/i18n/auth_totp.pot @@ -372,7 +372,7 @@ msgid "Verification Code" msgstr "" #. module: auth_totp -#: code:addons/auth_totp/controllers/home.py:0 +#: code:addons/auth_totp/models/res_users.py:0 #: code:addons/auth_totp/wizard/auth_totp_wizard.py:0 #, python-format msgid "Verification failed, please double-check the 6-digit code" diff --git a/addons/auth_totp/models/res_users.py b/addons/auth_totp/models/res_users.py index 1e133ff952b..f7b1227d860 100644 --- a/addons/auth_totp/models/res_users.py +++ b/addons/auth_totp/models/res_users.py @@ -28,12 +28,18 @@ class Users(models.Model): def SELF_READABLE_FIELDS(self): return super().SELF_READABLE_FIELDS + ['totp_enabled', 'totp_trusted_device_ids'] + def _mfa_type(self): + r = super()._mfa_type() + if r is not None: + return r + if self.totp_enabled: + return 'totp' def _mfa_url(self): r = super()._mfa_url() if r is not None: return r - if self.totp_enabled: + if self._mfa_type() == 'totp': return '/web/login/totp' @api.depends('totp_secret') @@ -55,7 +61,7 @@ class Users(models.Model): match = TOTP(key).match(code) if match is None: _logger.info("2FA check: FAIL for %s %r", self, self.login) - raise AccessDenied() + raise AccessDenied(_("Verification failed, please double-check the 6-digit code")) _logger.info("2FA check: SUCCESS for %s %r", self, self.login) def _totp_try_setting(self, secret, code): diff --git a/addons/auth_totp/models/totp.py b/addons/auth_totp/models/totp.py index 642fb8fcc81..86d8483d5ab 100644 --- a/addons/auth_totp/models/totp.py +++ b/addons/auth_totp/models/totp.py @@ -20,7 +20,7 @@ class TOTP: def __init__(self, key): self._key = key - def match(self, code, t=None, window=TIMESTEP): + def match(self, code, t=None, window=TIMESTEP, timestep=TIMESTEP): """ :param code: authenticator code to check against this key :param int t: current timestamp (seconds) @@ -32,8 +32,8 @@ class TOTP: if t is None: t = time.time() - low = int((t - window) / TIMESTEP) - high = int((t + window) / TIMESTEP) + 1 + low = int((t - window) / timestep) + high = int((t + window) / timestep) + 1 return next(( counter for counter in range(low, high) diff --git a/addons/auth_totp_mail_enforce/__init__.py b/addons/auth_totp_mail_enforce/__init__.py new file mode 100644 index 00000000000..7d34c7c054a --- /dev/null +++ b/addons/auth_totp_mail_enforce/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import controllers +from . import models diff --git a/addons/auth_totp_mail_enforce/__manifest__.py b/addons/auth_totp_mail_enforce/__manifest__.py new file mode 100644 index 00000000000..f0ec4fd473b --- /dev/null +++ b/addons/auth_totp_mail_enforce/__manifest__.py @@ -0,0 +1,20 @@ +{ + 'name': '2FA by mail', + 'description': """ +2FA by mail +=============== +Two-Factor authentication by sending a code to the user email inbox +when the 2FA using an authenticator app is not configured. +To enforce users to use a two-factor authentication by default, +and encourage users to configure their 2FA using an authenticator app. + """, + 'depends': ['auth_totp', 'mail'], + 'category': 'Extra Tools', + 'data': [ + 'data/mail_template_data.xml', + 'security/ir.model.access.csv', + 'views/res_config_settings_views.xml', + 'views/templates.xml', + ], + 'license': 'LGPL-3', +} diff --git a/addons/auth_totp_mail_enforce/controllers/__init__.py b/addons/auth_totp_mail_enforce/controllers/__init__.py new file mode 100644 index 00000000000..01adeead9c5 --- /dev/null +++ b/addons/auth_totp_mail_enforce/controllers/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import home diff --git a/addons/auth_totp_mail_enforce/controllers/home.py b/addons/auth_totp_mail_enforce/controllers/home.py new file mode 100644 index 00000000000..64f19470029 --- /dev/null +++ b/addons/auth_totp_mail_enforce/controllers/home.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +import odoo.addons.auth_totp.controllers.home + +from odoo import http +from odoo.exceptions import AccessDenied, UserError +from odoo.http import request + + +class Home(odoo.addons.auth_totp.controllers.home.Home): + @http.route() + def web_totp(self, redirect=None, **kwargs): + response = super().web_totp(redirect=redirect, **kwargs) + if response.status_code != 200 or response.qcontext['user']._mfa_type() != 'totp_mail': + # In case the response from the super is a redirection + # or the user has another TOTP method, we return the response from the call to super. + return response + assert request.session.pre_uid and not request.session.uid, \ + "The user must still be in the pre-authentication phase" + + # Send the email containing the code to the user inbox + try: + response.qcontext['user']._send_totp_mail_code() + except (AccessDenied, UserError) as e: + response.qcontext['error'] = str(e) + + return response diff --git a/addons/auth_totp_mail_enforce/data/mail_template_data.xml b/addons/auth_totp_mail_enforce/data/mail_template_data.xml new file mode 100644 index 00000000000..b1b68461ece --- /dev/null +++ b/addons/auth_totp_mail_enforce/data/mail_template_data.xml @@ -0,0 +1,51 @@ + + + + + TOTP for users: Authentication by email + + Your two-factor authentication code + {{ object.email_formatted }} + "{{ object.company_id.name }}" <{{ (object.company_id.email or user.email) }}> + {{ object.partner_id.lang }} + + +
+ Dear

+

Someone is trying to log in into your account with a new device.

+
    + N/A +
  • Location:
  • +
  • Device:
  • +
  • Browser:
  • +
  • IP address:
  • +
+

If this is you, please enter the following code to complete the login:

+ + + +
+ +
+ Please note that this code expires in . + +

+ If you did NOT initiate this log-in, + you should immediately change your password to ensure account security. +

+ +

+ We also strongly recommend enabling the two-factor authentication using an authenticator app to help secure your account. +

+ +

+ + Activate my two-factor authentication + +

+
+
+
+
+
diff --git a/addons/auth_totp_mail_enforce/i18n/auth_totp_mail_enforce.pot b/addons/auth_totp_mail_enforce/i18n/auth_totp_mail_enforce.pot new file mode 100755 index 00000000000..8043a1bad39 --- /dev/null +++ b/addons/auth_totp_mail_enforce/i18n/auth_totp_mail_enforce.pot @@ -0,0 +1,229 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * auth_totp_mail_enforce +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-02-01 12:13+0000\n" +"PO-Revision-Date: 2022-02-01 12:13+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: auth_totp_mail_enforce +#: model_terms:ir.ui.view,arch_db:auth_totp_mail_enforce.auth_totp_mail_form +msgid "" +".\n" +"
" +msgstr "" + +#. module: auth_totp_mail_enforce +#: model:mail.template,body_html:auth_totp_mail_enforce.mail_template_totp_mail_code +msgid "" +"
\n" +" Dear

\n" +"

Someone is trying to log in into your account with a new device.

\n" +"
    \n" +" N/A\n" +"
  • Location:
  • \n" +"
  • Device:
  • \n" +"
  • Browser:
  • \n" +"
  • IP address:
  • \n" +"
\n" +"

If this is you, please enter the following code to complete the login:

\n" +" \n" +" \n" +" \n" +"
\n" +" \n" +"
\n" +" Please note that this code expires in .\n" +"\n" +"

\n" +" If you did NOT initiate this log-in,\n" +" you should immediately change your password to ensure account security.\n" +"

\n" +"\n" +"

\n" +" We also strongly recommend enabling the two-factor authentication using an authenticator app to help secure your account.\n" +"

\n" +"\n" +"

\n" +" \n" +" Activate my two-factor authentication\n" +" \n" +"

\n" +"
\n" +" " +msgstr "" + +#. module: auth_totp_mail_enforce +#: model_terms:ir.ui.view,arch_db:auth_totp_mail_enforce.auth_totp_mail_form +msgid "" +"\n" +" To login, enter below the six-digit authentication code just sent via email to" +msgstr "" + +#. module: auth_totp_mail_enforce +#: model:ir.model.fields.selection,name:auth_totp_mail_enforce.selection__res_config_settings__auth_totp_policy__all_required +msgid "All users" +msgstr "" + +#. module: auth_totp_mail_enforce +#: code:addons/auth_totp_mail_enforce/models/res_users.py:0 +#, python-format +msgid "Cannot send email: user %s has no email address." +msgstr "" + +#. module: auth_totp_mail_enforce +#: model:ir.model.fields.selection,name:auth_totp_mail_enforce.selection__auth_totp_rate_limit_log__limit_type__code_check +msgid "Code Checking" +msgstr "" + +#. module: auth_totp_mail_enforce +#: model:ir.model,name:auth_totp_mail_enforce.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: auth_totp_mail_enforce +#: model:ir.model.fields,field_description:auth_totp_mail_enforce.field_auth_totp_rate_limit_log__create_uid +msgid "Created by" +msgstr "" + +#. module: auth_totp_mail_enforce +#: model:ir.model.fields,field_description:auth_totp_mail_enforce.field_auth_totp_rate_limit_log__create_date +msgid "Created on" +msgstr "" + +#. module: auth_totp_mail_enforce +#: model:ir.model.fields,field_description:auth_totp_mail_enforce.field_auth_totp_rate_limit_log__display_name +msgid "Display Name" +msgstr "" + +#. module: auth_totp_mail_enforce +#: model:ir.model.fields.selection,name:auth_totp_mail_enforce.selection__res_config_settings__auth_totp_policy__employee_required +msgid "Employees only" +msgstr "" + +#. module: auth_totp_mail_enforce +#: model_terms:ir.ui.view,arch_db:auth_totp_mail_enforce.res_config_settings_view_form +msgid "" +"Enforce the two-factor authentication by email for employees or for all users\n" +" (including portal users) if they didn't enable any other two-factor authentication\n" +" method." +msgstr "" + +#. module: auth_totp_mail_enforce +#: model:ir.model.fields,field_description:auth_totp_mail_enforce.field_res_config_settings__auth_totp_enforce +msgid "Enforce two-factor authentication" +msgstr "" + +#. module: auth_totp_mail_enforce +#: model:ir.model.fields,field_description:auth_totp_mail_enforce.field_auth_totp_rate_limit_log__id +msgid "ID" +msgstr "" + +#. module: auth_totp_mail_enforce +#: model:ir.model.fields,field_description:auth_totp_mail_enforce.field_auth_totp_rate_limit_log__ip +msgid "Ip" +msgstr "" + +#. module: auth_totp_mail_enforce +#: model:ir.model.fields,field_description:auth_totp_mail_enforce.field_auth_totp_rate_limit_log____last_update +msgid "Last Modified on" +msgstr "" + +#. module: auth_totp_mail_enforce +#: model:ir.model.fields,field_description:auth_totp_mail_enforce.field_auth_totp_rate_limit_log__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: auth_totp_mail_enforce +#: model:ir.model.fields,field_description:auth_totp_mail_enforce.field_auth_totp_rate_limit_log__write_date +msgid "Last Updated on" +msgstr "" + +#. module: auth_totp_mail_enforce +#: model_terms:ir.ui.view,arch_db:auth_totp_mail_enforce.auth_totp_mail_form +msgid "Learn More" +msgstr "" + +#. module: auth_totp_mail_enforce +#: model:ir.model.fields,field_description:auth_totp_mail_enforce.field_auth_totp_rate_limit_log__limit_type +msgid "Limit Type" +msgstr "" + +#. module: auth_totp_mail_enforce +#: model_terms:ir.ui.view,arch_db:auth_totp_mail_enforce.auth_totp_mail_form +msgid "Re-send email" +msgstr "" + +#. module: auth_totp_mail_enforce +#: model:ir.model.fields.selection,name:auth_totp_mail_enforce.selection__auth_totp_rate_limit_log__limit_type__send_email +msgid "Send Email" +msgstr "" + +#. module: auth_totp_mail_enforce +#: model:ir.model.fields,field_description:auth_totp_mail_enforce.field_auth_totp_rate_limit_log__source +msgid "Source" +msgstr "" + +#. module: auth_totp_mail_enforce +#: model:mail.template,name:auth_totp_mail_enforce.mail_template_totp_mail_code +msgid "TOTP for users: Authentication by email" +msgstr "" + +#. module: auth_totp_mail_enforce +#: model:ir.model,name:auth_totp_mail_enforce.model_auth_totp_rate_limit_log +msgid "TOTP rate limit logs" +msgstr "" + +#. module: auth_totp_mail_enforce +#: model:ir.model.fields,field_description:auth_totp_mail_enforce.field_res_config_settings__auth_totp_policy +msgid "Two-factor authentication enforcing policy" +msgstr "" + +#. module: auth_totp_mail_enforce +#: model:ir.model.fields,field_description:auth_totp_mail_enforce.field_auth_totp_rate_limit_log__user_id +msgid "User" +msgstr "" + +#. module: auth_totp_mail_enforce +#: model:ir.model,name:auth_totp_mail_enforce.model_res_users +msgid "Users" +msgstr "" + +#. module: auth_totp_mail_enforce +#: code:addons/auth_totp_mail_enforce/models/res_users.py:0 +#, python-format +msgid "Verification failed, please double-check the 6-digit code" +msgstr "" + +#. module: auth_totp_mail_enforce +#: model_terms:ir.ui.view,arch_db:auth_totp_mail_enforce.auth_totp_mail_form +msgid "" +"We strongly recommend enabling the two-factor authentication using an authenticator app to help secure your account.\n" +"
" +msgstr "" + +#. module: auth_totp_mail_enforce +#: code:addons/auth_totp_mail_enforce/models/res_users.py:0 +#, python-format +msgid "You reached the limit of authentication mails sent for your account" +msgstr "" + +#. module: auth_totp_mail_enforce +#: code:addons/auth_totp_mail_enforce/models/res_users.py:0 +#, python-format +msgid "You reached the limit of code verifications for your account" +msgstr "" + +#. module: auth_totp_mail_enforce +#: model:mail.template,subject:auth_totp_mail_enforce.mail_template_totp_mail_code +msgid "Your two-factor authentication code" +msgstr "" diff --git a/addons/auth_totp_mail_enforce/models/__init__.py b/addons/auth_totp_mail_enforce/models/__init__.py new file mode 100644 index 00000000000..e0bc2e99449 --- /dev/null +++ b/addons/auth_totp_mail_enforce/models/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import auth_totp_rate_limit_log +from . import res_config_settings +from . import res_users diff --git a/addons/auth_totp_mail_enforce/models/auth_totp_rate_limit_log.py b/addons/auth_totp_mail_enforce/models/auth_totp_rate_limit_log.py new file mode 100644 index 00000000000..ab5fe2745d8 --- /dev/null +++ b/addons/auth_totp_mail_enforce/models/auth_totp_rate_limit_log.py @@ -0,0 +1,20 @@ +from odoo import fields, models + + +class AuthTotpRateLimitLog(models.TransientModel): + _name = 'auth.totp.rate.limit.log' + _description = 'TOTP rate limit logs' + + def init(self): + self.env.cr.execute(""" + CREATE INDEX IF NOT EXISTS auth_totp_rate_limit_log_user_id_limit_type_create_date_idx + ON auth_totp_rate_limit_log(user_id, limit_type, create_date); + """) + + user_id = fields.Many2one('res.users', required=True, readonly=True) + scope = fields.Char(readonly=True) + ip = fields.Char(readonly=True) + limit_type = fields.Selection([ + ('send_email', 'Send Email'), + ('code_check', 'Code Checking'), + ], readonly=True) diff --git a/addons/auth_totp_mail_enforce/models/res_config_settings.py b/addons/auth_totp_mail_enforce/models/res_config_settings.py new file mode 100644 index 00000000000..7a84bf99b72 --- /dev/null +++ b/addons/auth_totp_mail_enforce/models/res_config_settings.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + auth_totp_enforce = fields.Boolean( + string="Enforce two-factor authentication", + ) + auth_totp_policy = fields.Selection([ + ('employee_required', 'Employees only'), + ('all_required', 'All users') + ], + string="Two-factor authentication enforcing policy", + config_parameter='auth_totp.policy', + ) + + @api.onchange('auth_totp_enforce') + def _onchange_auth_totp_enforce(self): + if self.auth_totp_enforce: + self.auth_totp_policy = 'employee_required' + else: + self.auth_totp_policy = False + + @api.model + def get_values(self): + res = super(ResConfigSettings, self).get_values() + res['auth_totp_enforce'] = bool(self.env['ir.config_parameter'].sudo().get_param('auth_totp.policy')) + return res diff --git a/addons/auth_totp_mail_enforce/models/res_users.py b/addons/auth_totp_mail_enforce/models/res_users.py new file mode 100644 index 00000000000..e79bb29fe93 --- /dev/null +++ b/addons/auth_totp_mail_enforce/models/res_users.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +import babel.dates +import logging + +from datetime import datetime, timedelta + +from odoo import _, models +from odoo.exceptions import AccessDenied, UserError +from odoo.http import request +from odoo.tools.misc import babel_locale_parse, hmac + +from odoo.addons.auth_totp.models.totp import hotp, TOTP + +_logger = logging.getLogger(__name__) + +TOTP_RATE_LIMITS = { + 'send_email': (10, 3600), + 'code_check': (10, 3600), +} + + +class Users(models.Model): + _inherit = 'res.users' + + def _mfa_type(self): + r = super()._mfa_type() + if r is not None: + return r + ICP = self.env['ir.config_parameter'].sudo() + otp_required = False + if ICP.get_param('auth_totp.policy') == 'all_required': + otp_required = True + elif ICP.get_param('auth_totp.policy') == 'employee_required' and self.has_group('base.group_user'): + otp_required = True + if otp_required: + return 'totp_mail' + + def _mfa_url(self): + r = super()._mfa_url() + if r is not None: + return r + if self._mfa_type() == 'totp_mail': + return '/web/login/totp' + + def _totp_check(self, code): + self._totp_rate_limit('code_check') + user = self.sudo() + if user._mfa_type() != 'totp_mail': + return super()._totp_check(code) + + key = self._get_totp_mail_key() + match = TOTP(key).match(code, window=3600, timestep=3600) + if match is None: + _logger.info("2FA check (mail): FAIL for %s %r", self, self.login) + raise AccessDenied(_("Verification failed, please double-check the 6-digit code")) + _logger.info("2FA check(mail): SUCCESS for %s %r", self, self.login) + self._totp_rate_limit_purge('code_check') + self._totp_rate_limit_purge('send_email') + return True + + def _get_totp_mail_key(self): + self.ensure_one() + return hmac(self.env(su=True), 'auth_totp_mail-code', (self.id, self.login, self.login_date)).encode() + + def _get_totp_mail_code(self): + self.ensure_one() + + key = self._get_totp_mail_key() + + now = datetime.now() + counter = int(datetime.timestamp(now) / 3600) + + code = hotp(key, counter) + expiration = timedelta(seconds=3600) + lang = babel_locale_parse(self.env.context.get('lang') or self.lang) + expiration = babel.dates.format_timedelta(expiration, lang) + + return str(code).zfill(6), expiration + + def _send_totp_mail_code(self): + self.ensure_one() + self._totp_rate_limit('send_email') + + if not self.email: + raise UserError(_("Cannot send email: user %s has no email address.", self.name)) + + template = self.env.ref('auth_totp_mail_enforce.mail_template_totp_mail_code').sudo() + context = {} + if request: + geoip = request.session.geoip + context.update({ + 'location': f"{geoip['city']}, {geoip['country_name']}" if geoip else None, + 'device': request.httprequest.user_agent.platform.capitalize(), + 'browser': request.httprequest.user_agent.browser.capitalize(), + 'ip': request.httprequest.environ['REMOTE_ADDR'], + }) + email_values = { + 'email_to': self.email, + 'email_cc': False, + 'auto_delete': True, + 'recipient_ids': [], + 'partner_ids': [], + 'scheduled_date': False, + } + with self.env.cr.savepoint(): + template.with_context(**context).send_mail( + self.id, force_send=True, raise_exception=True, email_values=email_values, email_layout_xmlid='mail.mail_notification_light' + ) + + def _totp_rate_limit(self, limit_type): + self.ensure_one() + assert request, "A request is required to be able to rate limit TOTP related actions" + limit, interval = TOTP_RATE_LIMITS.get(limit_type) + RateLimitLog = self.env['auth.totp.rate.limit.log'].sudo() + ip = request.httprequest.environ['REMOTE_ADDR'] + domain = [ + ('user_id', '=', self.id), + ('create_date', '>=', datetime.now() - timedelta(seconds=interval)), + ('limit_type', '=', limit_type), + ('ip', '=', ip), + ] + count = RateLimitLog.search_count(domain) + if count >= limit: + descriptions = { + 'send_email': _('You reached the limit of authentication mails sent for your account'), + 'code_check': _('You reached the limit of code verifications for your account'), + } + description = descriptions.get(limit_type) + raise AccessDenied(description) + RateLimitLog.create({ + 'user_id': self.id, + 'ip': ip, + 'limit_type': limit_type, + }) + + def _totp_rate_limit_purge(self, limit_type): + self.ensure_one() + assert request, "A request is required to be able to rate limit TOTP related actions" + ip = request.httprequest.environ['REMOTE_ADDR'] + RateLimitLog = self.env['auth.totp.rate.limit.log'].sudo() + RateLimitLog.search([ + ('user_id', '=', self.id), + ('limit_type', '=', limit_type), + ('ip', '=', ip), + ]).unlink() diff --git a/addons/auth_totp_mail_enforce/security/ir.model.access.csv b/addons/auth_totp_mail_enforce/security/ir.model.access.csv new file mode 100644 index 00000000000..ec646f60940 --- /dev/null +++ b/addons/auth_totp_mail_enforce/security/ir.model.access.csv @@ -0,0 +1,2 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +"access_auth_totp_rate_limit_log","access_auth_totp_rate_limit_log","model_auth_totp_rate_limit_log","base.group_user",0,0,0,0 diff --git a/addons/auth_totp_mail_enforce/views/res_config_settings_views.xml b/addons/auth_totp_mail_enforce/views/res_config_settings_views.xml new file mode 100644 index 00000000000..06a3b1a26b6 --- /dev/null +++ b/addons/auth_totp_mail_enforce/views/res_config_settings_views.xml @@ -0,0 +1,33 @@ + + + + + + res.config.settings.view.form.inherit.auth_totp_mail_enforce + res.config.settings + + + + +
+
+ +
+
+
+
+
+
+
+ +
+
diff --git a/addons/auth_totp_mail_enforce/views/templates.xml b/addons/auth_totp_mail_enforce/views/templates.xml new file mode 100644 index 00000000000..f95912309e2 --- /dev/null +++ b/addons/auth_totp_mail_enforce/views/templates.xml @@ -0,0 +1,28 @@ + + + diff --git a/odoo/addons/base/models/res_users.py b/odoo/addons/base/models/res_users.py index 25c92b3caf2..6ca9bd3c1ba 100644 --- a/odoo/addons/base/models/res_users.py +++ b/odoo/addons/base/models/res_users.py @@ -1034,6 +1034,9 @@ class Users(models.Model): if hasattr(self, 'check_credentials'): _logger.warning("The check_credentials method of res.users has been renamed _check_credentials. One of your installed modules defines one, but it will not be called anymore.") + def _mfa_type(self): + """ If an MFA method is enabled, returns its type as a string. """ + return def _mfa_url(self): """ If an MFA method is enabled, returns the URL for its second step. """