mirror of
https://github.com/bringout/oca-ocb-security.git
synced 2026-04-21 19:52:00 +02:00
19.0 vanilla
This commit is contained in:
parent
20ddc1b4a3
commit
c0efcc53f5
1162 changed files with 125577 additions and 105287 deletions
|
|
@ -1,4 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import auth_totp_device
|
||||
from . import res_config_settings
|
||||
from . import res_users
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, models
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
class Auth_TotpDevice(models.Model):
|
||||
_inherit = "auth_totp.device"
|
||||
|
||||
def unlink(self):
|
||||
""" Notify users when trusted devices are removed from their account. """
|
||||
removed_devices_by_user = self._classify_by_user()
|
||||
for user, removed_devices in removed_devices_by_user.items():
|
||||
user._notify_security_setting_update(
|
||||
_("Security Update: Device Removed"),
|
||||
_(
|
||||
"A trusted device has just been removed from your account: %(device_names)s",
|
||||
device_names=', '.join([device.name for device in removed_devices])
|
||||
),
|
||||
)
|
||||
|
||||
return super().unlink()
|
||||
|
||||
def _classify_by_user(self):
|
||||
devices_by_user = defaultdict(lambda: self.env['auth_totp.device'])
|
||||
for device in self:
|
||||
devices_by_user[device.user_id] |= device
|
||||
|
||||
return devices_by_user
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# 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 = self.auth_totp_policy or 'employee_required'
|
||||
else:
|
||||
self.auth_totp_policy = False
|
||||
|
||||
@api.model
|
||||
def get_values(self):
|
||||
res = super().get_values()
|
||||
res['auth_totp_enforce'] = bool(self.env['ir.config_parameter'].sudo().get_param('auth_totp.policy'))
|
||||
return res
|
||||
|
|
@ -1,15 +1,84 @@
|
|||
# -*- 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__)
|
||||
|
||||
|
||||
class Users(models.Model):
|
||||
class ResUsers(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
|
||||
if 'totp_secret' in vals:
|
||||
if vals.get('totp_secret'):
|
||||
self._notify_security_setting_update(
|
||||
_("Security Update: 2FA Activated"),
|
||||
_("Two-factor authentication has been activated on your account"),
|
||||
suggest_2fa=False,
|
||||
)
|
||||
else:
|
||||
self._notify_security_setting_update(
|
||||
_("Security Update: 2FA Deactivated"),
|
||||
_("Two-factor authentication has been deactivated on your account"),
|
||||
suggest_2fa=False,
|
||||
)
|
||||
|
||||
return res
|
||||
|
||||
def authenticate(self, credential, user_agent_env):
|
||||
"""Send an alert on new connection.
|
||||
|
||||
- 2FA enabled -> only for new device
|
||||
- Not enabled -> no alert
|
||||
"""
|
||||
auth_info = super().authenticate(credential, user_agent_env)
|
||||
self._notify_security_new_connection(auth_info)
|
||||
return auth_info
|
||||
|
||||
def _notify_security_new_connection(self, auth_info):
|
||||
user = self.env(user=auth_info['uid']).user
|
||||
|
||||
if request and user.email and user._mfa_type():
|
||||
# Check the `request` object to ensure that we will be able to get the
|
||||
# user information (like IP, user-agent, etc) and the cookie `td_id`.
|
||||
# (Can be unbounded if executed from a server action or a unit test.)
|
||||
|
||||
key = request.cookies.get('td_id')
|
||||
if not key or not request.env['auth_totp.device']._check_credentials_for_uid(
|
||||
scope="browser", key=key, uid=user.id):
|
||||
# 2FA enabled but not a trusted device
|
||||
user._notify_security_setting_update(
|
||||
subject=_('New Connection to your Account'),
|
||||
content=_('A new device was used to sign in to your account.'),
|
||||
)
|
||||
_logger.info("New device alert email sent for user <%s> to <%s>", user.login, user.email)
|
||||
|
||||
def _notify_security_setting_update_prepare_values(self, content, *, suggest_2fa=True, **kwargs):
|
||||
"""" Prepare rendering values for the 'mail.account_security_alert' qweb template
|
||||
|
||||
:param bool suggest_2fa:
|
||||
Whether or not to suggest the end-user to turn on 2FA authentication in the email sent.
|
||||
It will only suggest to turn on 2FA if not already turned on on the user's account. """
|
||||
|
||||
values = super()._notify_security_setting_update_prepare_values(content, **kwargs)
|
||||
values['suggest_2fa'] = suggest_2fa and not self.totp_enabled
|
||||
return values
|
||||
|
||||
def action_open_my_account_settings(self):
|
||||
action = {
|
||||
"name": _("Account Security"),
|
||||
"name": _("Security"),
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "res.users",
|
||||
"views": [[self.env.ref('auth_totp_mail.res_users_view_form').id, "form"]],
|
||||
|
|
@ -18,7 +87,7 @@ class Users(models.Model):
|
|||
return action
|
||||
|
||||
def get_totp_invite_url(self):
|
||||
return '/web#action=auth_totp_mail.action_activate_two_factor_authentication'
|
||||
return '/odoo/action-auth_totp_mail.action_activate_two_factor_authentication'
|
||||
|
||||
def action_totp_invite(self):
|
||||
invite_template = self.env.ref('auth_totp_mail.mail_template_totp_invite')
|
||||
|
|
@ -42,3 +111,99 @@ class Users(models.Model):
|
|||
', '.join(users_to_invite.mapped('name'))),
|
||||
}
|
||||
}
|
||||
|
||||
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' or \
|
||||
(ICP.get_param('auth_totp.policy') == 'employee_required' and self._is_internal()):
|
||||
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 _rpc_api_keys_only(self):
|
||||
return self._mfa_type() == 'totp_mail' or super()._rpc_api_keys_only()
|
||||
|
||||
def _check_credentials(self, credentials, env):
|
||||
if credentials['type'] == 'totp_mail':
|
||||
self._totp_rate_limit('code_check')
|
||||
user = self.sudo()
|
||||
key = user._get_totp_mail_key()
|
||||
match = TOTP(key).match(credentials['token'], window=3600, timestep=3600)
|
||||
if match is None:
|
||||
_logger.info("2FA check (mail): FAIL for %s %r", user, user.login)
|
||||
raise AccessDenied(_("Verification failed, please double-check the 6-digit code"))
|
||||
_logger.info("2FA check(mail): SUCCESS for %s %r", user, user.login)
|
||||
self._totp_rate_limit_purge('code_check')
|
||||
self._totp_rate_limit_purge('send_email')
|
||||
return {
|
||||
'uid': self.env.user.id,
|
||||
'auth_method': 'totp_mail',
|
||||
'mfa': 'default',
|
||||
}
|
||||
else:
|
||||
return super()._check_credentials(credentials, env)
|
||||
|
||||
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, locale=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.mail_template_totp_mail_code').sudo()
|
||||
context = {}
|
||||
if request:
|
||||
device = request.httprequest.user_agent.platform
|
||||
browser = request.httprequest.user_agent.browser
|
||||
context.update({
|
||||
'location': None,
|
||||
'device': device and device.capitalize() or None,
|
||||
'browser': browser and browser.capitalize() or None,
|
||||
'ip': request.httprequest.environ['REMOTE_ADDR'],
|
||||
})
|
||||
if request.geoip.city.name:
|
||||
context['location'] = f"{request.geoip.city.name}, {request.geoip.country_name}"
|
||||
|
||||
email_values = {
|
||||
'email_to': self.email,
|
||||
'email_cc': False,
|
||||
'auto_delete': True,
|
||||
'recipient_ids': [],
|
||||
'partner_ids': [],
|
||||
'scheduled_date': False,
|
||||
}
|
||||
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'
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue