mirror of
https://github.com/bringout/oca-ocb-security.git
synced 2026-04-22 11:52:07 +02:00
19.0 vanilla
This commit is contained in:
parent
20ddc1b4a3
commit
c0efcc53f5
1162 changed files with 125577 additions and 105287 deletions
|
|
@ -7,6 +7,8 @@ import logging
|
|||
import os
|
||||
import re
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.addons.base.models.res_users import check_identity
|
||||
from odoo.exceptions import AccessDenied, UserError
|
||||
|
|
@ -18,23 +20,30 @@ from odoo.addons.auth_totp.models.totp import TOTP, TOTP_SECRET_SIZE
|
|||
_logger = logging.getLogger(__name__)
|
||||
|
||||
compress = functools.partial(re.sub, r'\s', '')
|
||||
class Users(models.Model):
|
||||
|
||||
TOTP_RATE_LIMITS = {
|
||||
'send_email': (5, 3600),
|
||||
'code_check': (5, 3600),
|
||||
}
|
||||
|
||||
|
||||
class ResUsers(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
totp_secret = fields.Char(copy=False, groups=fields.NO_ACCESS, compute='_compute_totp_secret', inverse='_inverse_totp_secret', search='_search_totp_enable')
|
||||
totp_enabled = fields.Boolean(string="Two-factor authentication", compute='_compute_totp_enabled', search='_search_totp_enable')
|
||||
totp_secret = fields.Char(copy=False, groups=fields.NO_ACCESS, compute='_compute_totp_secret', inverse='_inverse_token')
|
||||
totp_last_counter = fields.Integer(copy=False, groups=fields.NO_ACCESS)
|
||||
totp_enabled = fields.Boolean(string="Two-factor authentication", compute='_compute_totp_enabled', search='_totp_enable_search')
|
||||
totp_trusted_device_ids = fields.One2many('auth_totp.device', 'user_id', string="Trusted Devices")
|
||||
|
||||
def init(self):
|
||||
super().init()
|
||||
if not sql.column_exists(self.env.cr, self._table, "totp_secret"):
|
||||
self.env.cr.execute("ALTER TABLE res_users ADD COLUMN totp_secret varchar")
|
||||
|
||||
@property
|
||||
def SELF_READABLE_FIELDS(self):
|
||||
return super().SELF_READABLE_FIELDS + ['totp_enabled', 'totp_trusted_device_ids']
|
||||
|
||||
def init(self):
|
||||
init_res = super().init()
|
||||
if not sql.column_exists(self.env.cr, self._table, "totp_secret"):
|
||||
self.env.cr.execute("ALTER TABLE res_users ADD COLUMN totp_secret varchar")
|
||||
return init_res
|
||||
|
||||
def _mfa_type(self):
|
||||
r = super()._mfa_type()
|
||||
if r is not None:
|
||||
|
|
@ -62,14 +71,29 @@ class Users(models.Model):
|
|||
def _get_session_token_fields(self):
|
||||
return super()._get_session_token_fields() | {'totp_secret'}
|
||||
|
||||
def _totp_check(self, code):
|
||||
sudo = self.sudo()
|
||||
key = base64.b32decode(sudo.totp_secret)
|
||||
match = TOTP(key).match(code)
|
||||
if match is None:
|
||||
_logger.info("2FA check: FAIL for %s %r", self, sudo.login)
|
||||
raise AccessDenied(_("Verification failed, please double-check the 6-digit code"))
|
||||
_logger.info("2FA check: SUCCESS for %s %r", self, sudo.login)
|
||||
def _check_credentials(self, credentials, env):
|
||||
if credentials['type'] == 'totp':
|
||||
self._totp_rate_limit('code_check')
|
||||
sudo = self.sudo()
|
||||
key = base64.b32decode(sudo.totp_secret)
|
||||
match = TOTP(key).match(credentials['token'])
|
||||
if match is None:
|
||||
_logger.info("2FA check: FAIL for %s %r", self, sudo.login)
|
||||
raise AccessDenied(_("Verification failed, please double-check the 6-digit code"))
|
||||
|
||||
if sudo.totp_last_counter and match <= sudo.totp_last_counter:
|
||||
_logger.warning("2FA check: REUSE for %s %r", self, sudo.login)
|
||||
raise AccessDenied(_("Verification failed, please use the latest 6-digit code"))
|
||||
|
||||
sudo.totp_last_counter = match
|
||||
_logger.info("2FA check: SUCCESS for %s %r", self, sudo.login)
|
||||
self._totp_rate_limit_purge('code_check')
|
||||
return {
|
||||
'uid': self.env.user.id,
|
||||
'auth_method': 'totp',
|
||||
'mfa': 'default',
|
||||
}
|
||||
return super()._check_credentials(credentials, env)
|
||||
|
||||
def _totp_try_setting(self, secret, code):
|
||||
if self.totp_enabled or self != self.env.user:
|
||||
|
|
@ -83,6 +107,7 @@ class Users(models.Model):
|
|||
return False
|
||||
|
||||
self.sudo().totp_secret = secret
|
||||
self.sudo().totp_last_counter = match
|
||||
if request:
|
||||
self.env.flush_all()
|
||||
# update session token so the user does not get logged out (cache cleared by change)
|
||||
|
|
@ -92,6 +117,40 @@ class Users(models.Model):
|
|||
_logger.info("2FA enable: SUCCESS for %s %r", self, self.login)
|
||||
return True
|
||||
|
||||
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[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),
|
||||
]
|
||||
count = RateLimitLog.search_count(domain)
|
||||
if count >= limit:
|
||||
descriptions = {
|
||||
'send_email': _('You reached the limit of authentication mails sent for your account, please try again later.'),
|
||||
'code_check': _('You reached the limit of code verifications for your account, please try again later.'),
|
||||
}
|
||||
description = descriptions[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"
|
||||
RateLimitLog = self.env['auth.totp.rate.limit.log'].sudo()
|
||||
RateLimitLog.search([
|
||||
('user_id', '=', self.id),
|
||||
('limit_type', '=', limit_type),
|
||||
]).unlink()
|
||||
|
||||
@check_identity
|
||||
def action_totp_disable(self):
|
||||
logins = ', '.join(map(repr, self.mapped('login')))
|
||||
|
|
@ -142,7 +201,7 @@ class Users(models.Model):
|
|||
'name': _("Two-Factor Authentication Activation"),
|
||||
'res_id': w.id,
|
||||
'views': [(False, 'form')],
|
||||
'context': self.env.context,
|
||||
'context': self.env.context | {'dialog_size': 'medium'},
|
||||
}
|
||||
|
||||
@check_identity
|
||||
|
|
@ -158,16 +217,20 @@ class Users(models.Model):
|
|||
return super().change_password(old_passwd, new_passwd)
|
||||
|
||||
def _compute_totp_secret(self):
|
||||
for user in self.filtered('id'):
|
||||
for user in self:
|
||||
if not user.id:
|
||||
user.totp_secret = user._origin.totp_secret
|
||||
continue
|
||||
self.env.cr.execute('SELECT totp_secret FROM res_users WHERE id=%s', (user.id,))
|
||||
user.totp_secret = self.env.cr.fetchone()[0]
|
||||
|
||||
def _inverse_totp_secret(self):
|
||||
for user in self.filtered('id'):
|
||||
def _inverse_token(self):
|
||||
self.sudo().totp_last_counter = False
|
||||
for user in self:
|
||||
secret = user.totp_secret if user.totp_secret else None
|
||||
self.env.cr.execute('UPDATE res_users SET totp_secret = %s WHERE id=%s', (secret, user.id))
|
||||
|
||||
def _search_totp_enable(self, operator, value):
|
||||
def _totp_enable_search(self, operator, value):
|
||||
value = not value if operator == '!=' else value
|
||||
if value:
|
||||
self.env.cr.execute("SELECT id FROM res_users WHERE totp_secret IS NOT NULL")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue