This commit juste moves the different part of code (class, views, data) in the correct file where they belong. Task-2487630 Part-of: odoo/odoo#71142
166 lines
6.2 KiB
Python
166 lines
6.2 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import base64
|
|
import functools
|
|
import logging
|
|
import os
|
|
import re
|
|
|
|
from odoo import _, api, fields, models
|
|
from odoo.addons.base.models.res_users import check_identity
|
|
from odoo.exceptions import AccessDenied, UserError
|
|
from odoo.http import request
|
|
|
|
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):
|
|
_inherit = 'res.users'
|
|
|
|
totp_secret = fields.Char(copy=False, groups=fields.NO_ACCESS)
|
|
totp_enabled = fields.Boolean(string="Two-factor authentication", compute='_compute_totp_enabled')
|
|
|
|
@property
|
|
def SELF_READABLE_FIELDS(self):
|
|
return super().SELF_READABLE_FIELDS + ['totp_enabled']
|
|
|
|
def _mfa_url(self):
|
|
r = super()._mfa_url()
|
|
if r is not None:
|
|
return r
|
|
if self.totp_enabled:
|
|
return '/web/login/totp'
|
|
|
|
@api.depends('totp_secret')
|
|
def _compute_totp_enabled(self):
|
|
for r, v in zip(self, self.sudo()):
|
|
r.totp_enabled = bool(v.totp_secret)
|
|
|
|
def action_open_my_account_settings(self):
|
|
action = {
|
|
"name": _("Account Security"),
|
|
"type": "ir.actions.act_window",
|
|
"res_model": "res.users",
|
|
"views": [[self.env.ref('auth_totp.res_users_view_form_security').id, "form"]],
|
|
"res_id": self.id,
|
|
}
|
|
return action
|
|
|
|
def _rpc_api_keys_only(self):
|
|
# 2FA enabled means we can't allow password-based RPC
|
|
self.ensure_one()
|
|
return self.totp_enabled or super()._rpc_api_keys_only()
|
|
|
|
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, self.login)
|
|
raise AccessDenied()
|
|
_logger.info("2FA check: SUCCESS for %s %r", self, self.login)
|
|
|
|
def _totp_try_setting(self, secret, code):
|
|
if self.totp_enabled or self != self.env.user:
|
|
_logger.info("2FA enable: REJECT for %s %r", self, self.login)
|
|
return False
|
|
|
|
secret = compress(secret).upper()
|
|
match = TOTP(base64.b32decode(secret)).match(code)
|
|
if match is None:
|
|
_logger.info("2FA enable: REJECT CODE for %s %r", self, self.login)
|
|
return False
|
|
|
|
self.sudo().totp_secret = secret
|
|
if request:
|
|
self.flush()
|
|
# update session token so the user does not get logged out (cache cleared by change)
|
|
new_token = self.env.user._compute_session_token(request.session.sid)
|
|
request.session.session_token = new_token
|
|
|
|
_logger.info("2FA enable: SUCCESS for %s %r", self, self.login)
|
|
return True
|
|
|
|
def get_totp_invite_url(self):
|
|
return '/web#action=auth_totp.action_activate_two_factor_authentication'
|
|
|
|
def action_totp_invite(self):
|
|
invite_template = self.env.ref('auth_totp.mail_template_totp_invite')
|
|
users_to_invite = self.sudo().filtered(lambda user: not user.totp_secret)
|
|
for user in users_to_invite:
|
|
email_values = {
|
|
'email_from': self.env.user.email_formatted,
|
|
'author_id': self.env.user.partner_id.id,
|
|
}
|
|
invite_template.send_mail(user.id, force_send=True, email_values=email_values,
|
|
notif_layout='mail.mail_notification_light')
|
|
|
|
# Display a confirmation toaster
|
|
return {
|
|
'type': 'ir.actions.client',
|
|
'tag': 'display_notification',
|
|
'params': {
|
|
'type': 'info',
|
|
'sticky': False,
|
|
'message': _("Invitation to use two-factor authentication sent for the following user(s): %s",
|
|
', '.join(users_to_invite.mapped('name'))),
|
|
}
|
|
}
|
|
|
|
@check_identity
|
|
def action_totp_disable(self):
|
|
logins = ', '.join(map(repr, self.mapped('login')))
|
|
if not (self == self.env.user or self.env.user._is_admin() or self.env.su):
|
|
_logger.info("2FA disable: REJECT for %s (%s) by uid #%s", self, logins, self.env.user.id)
|
|
return False
|
|
|
|
self.sudo().write({'totp_secret': False})
|
|
if request and self == self.env.user:
|
|
self.flush()
|
|
# update session token so the user does not get logged out (cache cleared by change)
|
|
new_token = self.env.user._compute_session_token(request.session.sid)
|
|
request.session.session_token = new_token
|
|
|
|
_logger.info("2FA disable: SUCCESS for %s (%s) by uid #%s", self, logins, self.env.user.id)
|
|
return {
|
|
'type': 'ir.actions.client',
|
|
'tag': 'display_notification',
|
|
'params': {
|
|
'type': 'warning',
|
|
'message': _("Two-factor authentication disabled for the following user(s): %s", ', '.join(self.mapped('name'))),
|
|
'next': {'type': 'ir.actions.act_window_close'},
|
|
}
|
|
}
|
|
|
|
@check_identity
|
|
def action_totp_enable_wizard(self):
|
|
if self.env.user != self:
|
|
raise UserError(_("Two-factor authentication can only be enabled for yourself"))
|
|
|
|
if self.totp_enabled:
|
|
raise UserError(_("Two-factor authentication already enabled"))
|
|
|
|
secret_bytes_count = TOTP_SECRET_SIZE // 8
|
|
secret = base64.b32encode(os.urandom(secret_bytes_count)).decode()
|
|
# format secret in groups of 4 characters for readability
|
|
secret = ' '.join(map(''.join, zip(*[iter(secret)]*4)))
|
|
w = self.env['auth_totp.wizard'].create({
|
|
'user_id': self.id,
|
|
'secret': secret,
|
|
})
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'target': 'new',
|
|
'res_model': 'auth_totp.wizard',
|
|
'name': _("Two-Factor Authentication Activation"),
|
|
'res_id': w.id,
|
|
'views': [(False, 'form')],
|
|
'context': self.env.context,
|
|
}
|