[MOV] auth_top: reorganise module to set content in proper place
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
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import controllers
|
||||
from . import models
|
||||
from . import wizard
|
||||
|
||||
@@ -18,10 +18,12 @@ can setup API keys to replace their main password.
|
||||
'category': 'Extra Tools',
|
||||
'auto_install': True,
|
||||
'data': [
|
||||
'data/ir_action_data.xml',
|
||||
'data/mail_templates_data.xml',
|
||||
'security/security.xml',
|
||||
'views/user_preferences.xml',
|
||||
'views/res_users_views.xml',
|
||||
'views/templates.xml',
|
||||
'wizard/auth_totp_wizard_views.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_tests': [
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Invite to user 2FA -->
|
||||
<record model="ir.actions.server" id="action_invite_totp">
|
||||
<field name="name">Invite to use two-factor authentication</field>
|
||||
<field name="model_id" ref="base.model_res_users"/>
|
||||
<field name="binding_model_id" ref="base.model_res_users"/>
|
||||
<field name="binding_view_types">list</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
action = records.action_totp_invite()
|
||||
</field>
|
||||
<field name="groups_id" eval="[(4, ref('base.group_erp_manager'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- Action called from the contextual menu -->
|
||||
<record model="ir.actions.server" id="action_disable_totp">
|
||||
<field name="name">Disable two-factor authentication</field>
|
||||
<field name="model_id" ref="base.model_res_users"/>
|
||||
<field name="binding_model_id" ref="base.model_res_users"/>
|
||||
<field name="binding_view_types">list</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
action = records.action_totp_disable()
|
||||
</field>
|
||||
<field name="groups_id" eval="[(4, ref('base.group_erp_manager'))]"/>
|
||||
</record>
|
||||
|
||||
<!--Action called when using the link in "Invite to use 2FA" mail-->
|
||||
<record model="ir.actions.server" id="action_activate_two_factor_authentication">
|
||||
<field name="name">Open two-factor authentication configuration</field>
|
||||
<field name="model_id" ref="base.model_res_users"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
user = env.user
|
||||
action = user.action_open_my_account_settings()
|
||||
</field>
|
||||
<field name="groups_id" eval="[(4, ref('base.group_user'))]"/>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -13,7 +13,9 @@
|
||||
<p style="margin: 0px; padding: 0px; font-size: 13px;">
|
||||
Dear ${object.partner_id.name}<br/><br/>
|
||||
${user.name} requested you activate two-factor authentication to protect your account.<br/><br/>
|
||||
Two-factor Authentication ("2FA") is a system of double authentication. The first one is done with your password and the second one with a code you get from a dedicated mobile app. Popular ones include Authy, Google Authenticator or the Microsoft Authenticator.
|
||||
Two-factor Authentication ("2FA") is a system of double authentication.
|
||||
The first one is done with your password and the second one with a code you get from a dedicated mobile app.
|
||||
Popular ones include Authy, Google Authenticator or the Microsoft Authenticator.
|
||||
|
||||
<p style="margin: 16px 0px 16px 0px; text-align: center;">
|
||||
<a href="${object.get_totp_invite_url()}"
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import ir_http
|
||||
from . import res_users
|
||||
from . import totp
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
import functools
|
||||
import hmac
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
import time
|
||||
|
||||
import werkzeug.urls
|
||||
|
||||
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, db_list
|
||||
from odoo.http import request
|
||||
|
||||
from odoo.addons.auth_totp.models.totp import TOTP, TOTP_SECRET_SIZE
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -165,111 +163,3 @@ class Users(models.Model):
|
||||
'views': [(False, 'form')],
|
||||
'context': self.env.context,
|
||||
}
|
||||
|
||||
|
||||
class TOTPWizard(models.TransientModel):
|
||||
_name = 'auth_totp.wizard'
|
||||
_description = "Two-Factor Setup Wizard"
|
||||
|
||||
user_id = fields.Many2one('res.users', required=True, readonly=True)
|
||||
secret = fields.Char(required=True, readonly=True)
|
||||
url = fields.Char(store=True, readonly=True, compute='_compute_qrcode')
|
||||
qrcode = fields.Binary(
|
||||
attachment=False, store=True, readonly=True,
|
||||
compute='_compute_qrcode',
|
||||
)
|
||||
code = fields.Char(string="Verification Code", size=7)
|
||||
|
||||
@api.depends('user_id.login', 'user_id.company_id.display_name', 'secret')
|
||||
def _compute_qrcode(self):
|
||||
# TODO: make "issuer" configurable through config parameter?
|
||||
global_issuer = request and request.httprequest.host.split(':', 1)[0]
|
||||
for w in self:
|
||||
issuer = global_issuer or w.user_id.company_id.display_name
|
||||
w.url = url = werkzeug.urls.url_unparse((
|
||||
'otpauth', 'totp',
|
||||
werkzeug.urls.url_quote(f'{issuer}:{w.user_id.login}', safe=':'),
|
||||
werkzeug.urls.url_encode({
|
||||
'secret': compress(w.secret),
|
||||
'issuer': issuer,
|
||||
# apparently a lowercase hash name is anathema to google
|
||||
# authenticator (error) and passlib (no token)
|
||||
'algorithm': ALGORITHM.upper(),
|
||||
'digits': DIGITS,
|
||||
'period': TIMESTEP,
|
||||
}), ''
|
||||
))
|
||||
|
||||
data = io.BytesIO()
|
||||
import qrcode
|
||||
qrcode.make(url.encode(), box_size=4).save(data, optimise=True, format='PNG')
|
||||
w.qrcode = base64.b64encode(data.getvalue()).decode()
|
||||
|
||||
@check_identity
|
||||
def enable(self):
|
||||
try:
|
||||
c = int(compress(self.code))
|
||||
except ValueError:
|
||||
raise UserError(_("The verification code should only contain numbers"))
|
||||
if self.user_id._totp_try_setting(self.secret, c):
|
||||
self.secret = '' # empty it, because why keep it until GC?
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'type': 'success',
|
||||
'message': _("Two-factor authentication is now enabled."),
|
||||
'next': {'type': 'ir.actions.act_window_close'},
|
||||
}
|
||||
}
|
||||
raise UserError(_('Verification failed, please double-check the 6-digit code'))
|
||||
|
||||
# 160 bits, as recommended by HOTP RFC 4226, section 4, R6.
|
||||
# Google Auth uses 80 bits by default but supports 160.
|
||||
TOTP_SECRET_SIZE = 160
|
||||
|
||||
# The algorithm (and key URI format) allows customising these parameters but
|
||||
# google authenticator doesn't support it
|
||||
# https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
||||
ALGORITHM = 'sha1'
|
||||
DIGITS = 6
|
||||
TIMESTEP = 30
|
||||
|
||||
class TOTP:
|
||||
def __init__(self, key):
|
||||
self._key = key
|
||||
|
||||
def match(self, code, t=None, window=TIMESTEP):
|
||||
"""
|
||||
:param code: authenticator code to check against this key
|
||||
:param int t: current timestamp (seconds)
|
||||
:param int window: fuzz window to account for slow fingers, network
|
||||
latency, desynchronised clocks, ..., every code
|
||||
valid between t-window an t+window is considered
|
||||
valid
|
||||
"""
|
||||
if t is None:
|
||||
t = time.time()
|
||||
|
||||
low = int((t - window) / TIMESTEP)
|
||||
high = int((t + window) / TIMESTEP) + 1
|
||||
|
||||
return next((
|
||||
counter for counter in range(low, high)
|
||||
if hotp(self._key, counter) == code
|
||||
), None)
|
||||
|
||||
def hotp(secret, counter):
|
||||
# C is the 64b counter encoded in big-endian
|
||||
C = struct.pack(">Q", counter)
|
||||
mac = hmac.new(secret, msg=C, digestmod=ALGORITHM).digest()
|
||||
# the data offset is the last nibble of the hash
|
||||
offset = mac[-1] & 0xF
|
||||
# code is the 4 bytes at the offset interpreted as a 31b big-endian uint
|
||||
# (31b to avoid sign concerns). This effectively limits digits to 9 and
|
||||
# hard-limits it to 10: each digit is normally worth 3.32 bits but the
|
||||
# 10th is only worth 1.1 (9 digits encode 29.9 bits).
|
||||
code = struct.unpack_from('>I', mac, offset)[0] & 0x7FFFFFFF
|
||||
r = code % (10 ** DIGITS)
|
||||
# NOTE: use text / bytes instead of int?
|
||||
return r
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import hmac
|
||||
import struct
|
||||
import time
|
||||
|
||||
# 160 bits, as recommended by HOTP RFC 4226, section 4, R6.
|
||||
# Google Auth uses 80 bits by default but supports 160.
|
||||
TOTP_SECRET_SIZE = 160
|
||||
|
||||
# The algorithm (and key URI format) allows customising these parameters but
|
||||
# google authenticator doesn't support it
|
||||
# https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
||||
ALGORITHM = 'sha1'
|
||||
DIGITS = 6
|
||||
TIMESTEP = 30
|
||||
|
||||
class TOTP:
|
||||
def __init__(self, key):
|
||||
self._key = key
|
||||
|
||||
def match(self, code, t=None, window=TIMESTEP):
|
||||
"""
|
||||
:param code: authenticator code to check against this key
|
||||
:param int t: current timestamp (seconds)
|
||||
:param int window: fuzz window to account for slow fingers, network
|
||||
latency, desynchronised clocks, ..., every code
|
||||
valid between t-window an t+window is considered
|
||||
valid
|
||||
"""
|
||||
if t is None:
|
||||
t = time.time()
|
||||
|
||||
low = int((t - window) / TIMESTEP)
|
||||
high = int((t + window) / TIMESTEP) + 1
|
||||
|
||||
return next((
|
||||
counter for counter in range(low, high)
|
||||
if hotp(self._key, counter) == code
|
||||
), None)
|
||||
|
||||
def hotp(secret, counter):
|
||||
# C is the 64b counter encoded in big-endian
|
||||
C = struct.pack(">Q", counter)
|
||||
mac = hmac.new(secret, msg=C, digestmod=ALGORITHM).digest()
|
||||
# the data offset is the last nibble of the hash
|
||||
offset = mac[-1] & 0xF
|
||||
# code is the 4 bytes at the offset interpreted as a 31b big-endian uint
|
||||
# (31b to avoid sign concerns). This effectively limits digits to 9 and
|
||||
# hard-limits it to 10: each digit is normally worth 3.32 bits but the
|
||||
# 10th is only worth 1.1 (9 digits encode 29.9 bits).
|
||||
code = struct.unpack_from('>I', mac, offset)[0] & 0x7FFFFFFF
|
||||
r = code % (10 ** DIGITS)
|
||||
# NOTE: use text / bytes instead of int?
|
||||
return r
|
||||
@@ -0,0 +1,115 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record model="ir.ui.view" id="res_users_view_search">
|
||||
<field name="name">res.users.view.search.inherit.auth.totp</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="base.view_users_search" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//search" position="inside">
|
||||
<separator/>
|
||||
<filter name="totp_enabled" string="Two-factor authentication Enabled" domain="[('totp_secret','!=',False)]"/>
|
||||
<separator/>
|
||||
<filter name="totp_disabled" string="Two-factor authentication Disabled" domain="[('totp_secret','=',False)]"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="view_totp_form">
|
||||
<field name="name">user form: add totp status</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="base.view_users_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//page[@name='preferences']" position="after">
|
||||
<page string="Account Security" name="security" attrs="{'invisible': [('id', '=', False)]}">
|
||||
<field name="totp_enabled" invisible="1"/>
|
||||
<!-- For own user, allow to activate the two-factor Authentication -->
|
||||
<group>
|
||||
<div>
|
||||
<div class="o_horizontal_separator d-flex align-items-center mt-0">Two-factor Authentication
|
||||
<div attrs="{'invisible': [('totp_enabled', '!=', False)]}">
|
||||
<button attrs="{'invisible': "[('id', '=', uid)]"}" name="action_totp_enable_wizard"
|
||||
disabled="1" type="object" class="fa fa-toggle-off o_auth_2fa_btn disabled" aria-label="Enable 2FA"></button>
|
||||
<button attrs="{'invisible': "[('id', '!=', uid)]"}" name="action_totp_enable_wizard"
|
||||
type="object" class="fa fa-toggle-off o_auth_2fa_btn disabled" aria-label="Enable 2FA"></button>
|
||||
<button groups="base.group_erp_manager" attrs="{'invisible': "[('id', '=', uid)]"}"
|
||||
name="action_totp_invite" string="Invite to use 2FA" type="object" class="btn btn-secondary"></button>
|
||||
</div>
|
||||
<button attrs="{'invisible': [('totp_enabled', '=', False)]}" name="action_totp_disable" type="object"
|
||||
class="fa fa-toggle-on o_auth_2fa_btn text-primary enabled" aria-label="Disable 2FA"></button>
|
||||
</div>
|
||||
<span attrs="{'invisible': [('totp_enabled', '!=', False)]}" class="text-muted">
|
||||
Two-factor Authentication ("2FA") is a system of double authentication.
|
||||
The first one is done with your password and the second one with a code you get from a dedicated mobile app.
|
||||
Popular ones include Authy, Google Authenticator or the Microsoft Authenticator.
|
||||
<a href="https://www.odoo.com/documentation/15.0/applications/general/auth/2fa.html"
|
||||
title="Learn More" target="_blank">Learn More</a>
|
||||
</span>
|
||||
<span attrs="{'invisible': [('totp_enabled', '=', False)]}" class="text-muted">Your account is protected!</span>
|
||||
</div>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="view_totp_field">
|
||||
<field name="name">users preference: totp</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="base.view_users_form_simple_modif"/>
|
||||
<field name="arch" type="xml">
|
||||
<group name="auth" position="after">
|
||||
<field name="totp_enabled" invisible="1"/>
|
||||
<group>
|
||||
<div>
|
||||
<div class="o_horizontal_separator mt-0">Two-factor Authentication
|
||||
<button attrs="{'invisible': [('totp_enabled', '!=', False)]}" name="action_totp_enable_wizard"
|
||||
type="object" class="fa fa-toggle-off o_auth_2fa_btn mb-1" aria-label="Enable 2FA"/>
|
||||
<button attrs="{'invisible': [('totp_enabled', '=', False)]}" name="action_totp_disable"
|
||||
type="object" class="fa fa-toggle-on o_auth_2fa_btn text-primary" aria-label="Disable 2FA"/>
|
||||
</div>
|
||||
<span attrs="{'invisible': [('totp_enabled', '!=', False)]}" class="text-muted">
|
||||
Two-factor Authentication ("2FA") is a system of double authentication.
|
||||
The first one is done with your password and the second one with a code you get from a dedicated mobile app.
|
||||
Popular ones include Authy, Google Authenticator or the Microsoft Authenticator.
|
||||
<a href="https://www.odoo.com/documentation/15.0/applications/general/auth/2fa.html"
|
||||
title="Learn More" target="_blank">Learn More</a>
|
||||
</span>
|
||||
<span attrs="{'invisible': [('totp_enabled', '=', False)]}" class="text-muted">Your account is protected!</span>
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- View used when coming from "invite to use 2FA" mail -->
|
||||
<record model="ir.ui.view" id="res_users_view_form_security">
|
||||
<field name="name">users preference: Account Security</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="base.view_users_form_simple_modif"/>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<form position="attributes">
|
||||
<attribute name='create'>0</attribute>
|
||||
<attribute name='edit'>0</attribute>
|
||||
<attribute name='delete'>0</attribute>
|
||||
</form>
|
||||
<h1 position="replace"/>
|
||||
<xpath expr="//field[@name='image_1920']" position="replace"/>
|
||||
<notebook position="replace">
|
||||
<header>
|
||||
</header>
|
||||
<sheet>$0</sheet>
|
||||
</notebook>
|
||||
<notebook position="before">
|
||||
<field name="image_1920" widget="image" class="oe_avatar" options="{'zoom': true, 'preview_image':'image_128'}"/>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" placeholder="Name" required="True" readonly="context.get('from_my_profile', False)"/>
|
||||
</h1>
|
||||
</div>
|
||||
</notebook>
|
||||
<page name="preferences_page" position="replace"></page>
|
||||
<footer position="replace"/>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -1,220 +0,0 @@
|
||||
<odoo>
|
||||
<record id="res_users_view_search" model="ir.ui.view">
|
||||
<field name="name">res.users.view.search.inherit.auth.totp</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="base.view_users_search" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//search" position="inside">
|
||||
<separator/>
|
||||
<filter name="totp_enabled" string="Two-factor authentication Enabled" domain="[('totp_secret','!=',False)]"/>
|
||||
<separator/>
|
||||
<filter name="totp_disabled" string="Two-factor authentication Disabled" domain="[('totp_secret','=',False)]"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="view_totp_form">
|
||||
<field name="name">user form: add totp status</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="base.view_users_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//page[@name='preferences']" position="after">
|
||||
<page string="Account Security" name="security" attrs="{'invisible': [('id', '=', False)]}">
|
||||
<field name="totp_enabled" invisible="1"/>
|
||||
<!-- For own user, allow to activate the two-factor Authentication -->
|
||||
<group>
|
||||
<div>
|
||||
<div class="o_horizontal_separator d-flex align-items-center mt-0">Two-factor Authentication
|
||||
<div attrs="{'invisible': [('totp_enabled', '!=', False)]}">
|
||||
<button attrs="{'invisible': "[('id', '=', uid)]"}" name="totp_enable_wizard"
|
||||
disabled="1" type="object" class="fa fa-toggle-off o_auth_2fa_btn disabled" aria-label="Enable 2FA"></button>
|
||||
<button attrs="{'invisible': "[('id', '!=', uid)]"}" name="totp_enable_wizard"
|
||||
type="object" class="fa fa-toggle-off o_auth_2fa_btn disabled" aria-label="Enable 2FA"></button>
|
||||
<button groups="base.group_erp_manager" attrs="{'invisible': "[('id', '=', uid)]"}"
|
||||
name="totp_invite" string="Invite to use 2FA" type="object" class="btn btn-primary"></button>
|
||||
</div>
|
||||
<button attrs="{'invisible': [('totp_enabled', '=', False)]}" name="totp_disable" type="object"
|
||||
class="fa fa-toggle-on o_auth_2fa_btn text-primary enabled" aria-label="Disable 2FA"></button>
|
||||
</div>
|
||||
<span attrs="{'invisible': [('totp_enabled', '!=', False)]}" class="text-muted">
|
||||
Two-factor Authentication ("2FA") is a system of double authentication. The first one is done with your password and the second one with a code you get from a dedicated mobile app. Popular ones include Authy, Google Authenticator or the Microsoft Authenticator.
|
||||
<a href="https://www.odoo.com/documentation/15.0/applications/general/auth/2fa.html"
|
||||
title="Learn More" target="_blank">Learn More</a>
|
||||
</span>
|
||||
<span attrs="{'invisible': [('totp_enabled', '=', False)]}" class="text-muted">Your account is protected!</span>
|
||||
</div>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.server" id="action_disable_totp">
|
||||
<field name="name">Disable two-factor authentication</field>
|
||||
<field name="model_id" ref="base.model_res_users"/>
|
||||
<field name="binding_model_id" ref="base.model_res_users"/>
|
||||
<field name="binding_view_types">list</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
action = records.action_totp_disable()
|
||||
</field>
|
||||
<field name="groups_id" eval="[(4, ref('base.group_erp_manager'))]"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="view_totp_wizard">
|
||||
<field name="name">auth_totp wizard</field>
|
||||
<field name="model">auth_totp.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<div class="o_auth_totp_enable_2FA container">
|
||||
<div class="mb-3 w-100">
|
||||
<h3 class="font-weight-bold">Authenticator App Setup</h3>
|
||||
<ul>
|
||||
<div class="d-md-none d-block">
|
||||
<li>
|
||||
<field class="text-wrap" name="url" widget="url" options="{'website_path': True}"
|
||||
text="Click on this link to open your authenticator app"/></li>
|
||||
</div>
|
||||
<li>
|
||||
<div class="d-flex align-items-center flex-wrap">
|
||||
<span class="d-md-none d-block">Or install an authenticator app</span>
|
||||
<span class="d-none d-md-block">Install an authenticator app on your mobile device</span>
|
||||
<div class="d-block d-md-none">
|
||||
<a href="https://play.google.com/store/search?q=authenticator&c=apps" class="mx-2" target="blank">
|
||||
<img alt="On Google Play" style="width: 24px;" src="/base_setup/static/src/img/logo_google_play.png"/>
|
||||
</a>
|
||||
<a href="http://appstore.com/2fa" class="mx-2" target="blank">
|
||||
<img alt="On Apple Store" style="width: 24px;" src="/base_setup/static/src/img/logo_apple_store.png"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<span class="text-muted">Popular ones include Authy, Google Authenticator or the Microsoft Authenticator.</span>
|
||||
<li>Look for an "Add an account" button</li>
|
||||
<li>
|
||||
<span class="d-none d-md-block">When requested to do so, scan the barcode below</span>
|
||||
<span class="d-block d-md-none">When requested to do so, copy the key below</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Desktop version -->
|
||||
<div class="text-center d-none d-md-block">
|
||||
<field name="qrcode" readonly="True" widget="image"/>
|
||||
|
||||
<h3 class="font-weight-bold"><a data-toggle="collapse"
|
||||
href="#collapseTotpSecret" role="button" aria-expanded="false"
|
||||
aria-controls="collapseTotpSecret">Cannot scan it?</a></h3>
|
||||
<div class="collapse" id="collapseTotpSecret">
|
||||
<field name="secret" widget="CopyClipboardChar" readonly="1" class="mb-3 pl-3"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Version -->
|
||||
<div class="text-center d-block d-md-none">
|
||||
<field name="secret" widget="CopyClipboardChar" readonly="1" class="mb-3 pl-3"/>
|
||||
</div>
|
||||
|
||||
<h3 class="font-weight-bold">Enter your six-digit code below</h3>
|
||||
<div class="mt-2">
|
||||
<label for="code" class="px-0">Verification Code</label>
|
||||
<div class="d-flex align-items-center">
|
||||
<field required="True" name="code" autocomplete="one-time-code" class="px-0 mr-2" placeholder="e.g. 123456"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button type="object" name="enable" class="btn btn-primary"
|
||||
string="Activate" data-hotkey="q"/>
|
||||
<button string="Cancel" special="cancel" data-hotkey="z"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="view_totp_field">
|
||||
<field name="name">users preference: totp</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="base.view_users_form_simple_modif"/>
|
||||
<field name="arch" type="xml">
|
||||
<group name="auth" position="after">
|
||||
<field name="totp_enabled" invisible="1"/>
|
||||
<group>
|
||||
<div>
|
||||
<div class="o_horizontal_separator mt-0">Two-factor Authentication
|
||||
<button attrs="{'invisible': [('totp_enabled', '!=', False)]}" name="totp_enable_wizard"
|
||||
type="object" class="fa fa-toggle-off o_auth_2fa_btn mb-1" aria-label="Enable 2FA"/>
|
||||
<button attrs="{'invisible': [('totp_enabled', '=', False)]}" name="totp_disable"
|
||||
type="object" class="fa fa-toggle-on o_auth_2fa_btn text-primary" aria-label="Disable 2FA"/>
|
||||
</div>
|
||||
<span attrs="{'invisible': [('totp_enabled', '!=', False)]}" class="text-muted">
|
||||
Two-factor Authentication ("2FA") is a system of double authentication. The first one is done with your password and the second one with a code you get from a dedicated mobile app. Popular ones include Authy, Google Authenticator or the Microsoft Authenticator.
|
||||
<a href="https://www.odoo.com/documentation/15.0/applications/general/auth/2fa.html"
|
||||
title="Learn More" target="_blank">Learn More</a>
|
||||
</span>
|
||||
<span attrs="{'invisible': [('totp_enabled', '=', False)]}" class="text-muted">Your account is protected!</span>
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Invite to user 2FA -->
|
||||
<record model="ir.actions.server" id="action_invite_totp">
|
||||
<field name="name">Invite to use two-factor authentication</field>
|
||||
<field name="model_id" ref="base.model_res_users"/>
|
||||
<field name="binding_model_id" ref="base.model_res_users"/>
|
||||
<field name="binding_view_types">list</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
action = records.action_totp_invite()
|
||||
</field>
|
||||
<field name="groups_id" eval="[(4, ref('base.group_erp_manager'))]"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="res_users_view_form_security">
|
||||
<field name="name">users preference: Account Security</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="base.view_users_form_simple_modif"/>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<form position="attributes">
|
||||
<attribute name='create'>0</attribute>
|
||||
<attribute name='edit'>0</attribute>
|
||||
<attribute name='delete'>0</attribute>
|
||||
</form>
|
||||
<h1 position="replace"/>
|
||||
<xpath expr="//field[@name='image_1920']" position="replace"/>
|
||||
<notebook position="replace">
|
||||
<header>
|
||||
</header>
|
||||
<sheet>$0</sheet>
|
||||
</notebook>
|
||||
<notebook position="before">
|
||||
<field name="image_1920" widget="image" class="oe_avatar" options="{'zoom': true, 'preview_image':'image_128'}"/>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" placeholder="Name" required="True" readonly="context.get('from_my_profile', False)"/>
|
||||
</h1>
|
||||
</div>
|
||||
</notebook>
|
||||
<page name="preferences_page" position="replace"></page>
|
||||
<footer position="replace"/>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.server" id="action_activate_two_factor_authentication">
|
||||
<field name="name">Open two-factor authentication configuration</field>
|
||||
<field name="model_id" ref="base.model_res_users"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
user = env.user
|
||||
action = user.action_open_my_account_settings()
|
||||
</field>
|
||||
<field name="groups_id" eval="[(4, ref('base.group_user'))]"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import auth_totp_wizard
|
||||
@@ -0,0 +1,74 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
import functools
|
||||
import io
|
||||
import qrcode
|
||||
import re
|
||||
import werkzeug.urls
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.addons.base.models.res_users import check_identity
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.http import request
|
||||
|
||||
from odoo.addons.auth_totp.models.totp import ALGORITHM, DIGITS, TIMESTEP
|
||||
|
||||
compress = functools.partial(re.sub, r'\s', '')
|
||||
|
||||
class TOTPWizard(models.TransientModel):
|
||||
_name = 'auth_totp.wizard'
|
||||
_description = "2-Factor Setup Wizard"
|
||||
|
||||
user_id = fields.Many2one('res.users', required=True, readonly=True)
|
||||
secret = fields.Char(required=True, readonly=True)
|
||||
url = fields.Char(store=True, readonly=True, compute='_compute_qrcode')
|
||||
qrcode = fields.Binary(
|
||||
attachment=False, store=True, readonly=True,
|
||||
compute='_compute_qrcode',
|
||||
)
|
||||
code = fields.Char(string="Verification Code", size=7)
|
||||
|
||||
@api.depends('user_id.login', 'user_id.company_id.display_name', 'secret')
|
||||
def _compute_qrcode(self):
|
||||
# TODO: make "issuer" configurable through config parameter?
|
||||
global_issuer = request and request.httprequest.host.split(':', 1)[0]
|
||||
for w in self:
|
||||
issuer = global_issuer or w.user_id.company_id.display_name
|
||||
w.url = url = werkzeug.urls.url_unparse((
|
||||
'otpauth', 'totp',
|
||||
werkzeug.urls.url_quote(f'{issuer}:{w.user_id.login}', safe=':'),
|
||||
werkzeug.urls.url_encode({
|
||||
'secret': compress(w.secret),
|
||||
'issuer': issuer,
|
||||
# apparently a lowercase hash name is anathema to google
|
||||
# authenticator (error) and passlib (no token)
|
||||
'algorithm': ALGORITHM.upper(),
|
||||
'digits': DIGITS,
|
||||
'period': TIMESTEP,
|
||||
}), ''
|
||||
))
|
||||
|
||||
data = io.BytesIO()
|
||||
qrcode.make(url.encode(), box_size=4).save(data, optimise=True, format='PNG')
|
||||
w.qrcode = base64.b64encode(data.getvalue()).decode()
|
||||
|
||||
@check_identity
|
||||
def enable(self):
|
||||
try:
|
||||
c = int(compress(self.code))
|
||||
except ValueError:
|
||||
raise UserError(_("The verification code should only contain numbers"))
|
||||
if self.user_id._totp_try_setting(self.secret, c):
|
||||
self.secret = '' # empty it, because why keep it until GC?
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'type': 'success',
|
||||
'message': _("2-Factor authentication is now enabled."),
|
||||
'next': {'type': 'ir.actions.act_window_close'},
|
||||
}
|
||||
}
|
||||
raise UserError(_('Verification failed, please double-check the 6-digit code'))
|
||||
@@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record model="ir.ui.view" id="view_totp_wizard">
|
||||
<field name="name">auth_totp wizard</field>
|
||||
<field name="model">auth_totp.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<div class="o_auth_totp_enable_2FA container">
|
||||
<div class="mb-3 w-100">
|
||||
<h3 class="font-weight-bold">Authenticator App Setup</h3>
|
||||
<ul>
|
||||
<div class="d-md-none d-block">
|
||||
<li>
|
||||
<field class="text-wrap" name="url" widget="url" options="{'website_path': True}"
|
||||
text="Click on this link to open your authenticator app"/></li>
|
||||
</div>
|
||||
<li>
|
||||
<div class="d-flex align-items-center flex-wrap">
|
||||
<span class="d-md-none d-block">Or install an authenticator app</span>
|
||||
<span class="d-none d-md-block">Install an authenticator app on your mobile device</span>
|
||||
<div class="d-block d-md-none">
|
||||
<a href="https://play.google.com/store/search?q=authenticator&c=apps" class="mx-2" target="blank">
|
||||
<img alt="On Google Play" style="width: 24px;" src="/base_setup/static/src/img/logo_google_play.png"/>
|
||||
</a>
|
||||
<a href="http://appstore.com/2fa" class="mx-2" target="blank">
|
||||
<img alt="On Apple Store" style="width: 24px;" src="/base_setup/static/src/img/logo_apple_store.png"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<span class="text-muted">Popular ones include Authy, Google Authenticator or the Microsoft Authenticator.</span>
|
||||
<li>Look for an "Add an account" button</li>
|
||||
<li>
|
||||
<span class="d-none d-md-block">When requested to do so, scan the barcode below</span>
|
||||
<span class="d-block d-md-none">When requested to do so, copy the key below</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Desktop version -->
|
||||
<div class="text-center d-none d-md-block">
|
||||
<field name="qrcode" readonly="True" widget="image"/>
|
||||
|
||||
<h3 class="font-weight-bold"><a data-toggle="collapse"
|
||||
href="#collapseTotpSecret" role="button" aria-expanded="false"
|
||||
aria-controls="collapseTotpSecret">Cannot scan it?</a></h3>
|
||||
<div class="collapse" id="collapseTotpSecret">
|
||||
<field name="secret" widget="CopyClipboardChar" readonly="1" class="mb-3 pl-3"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Version -->
|
||||
<div class="text-center d-block d-md-none">
|
||||
<field name="secret" widget="CopyClipboardChar" readonly="1" class="mb-3 pl-3"/>
|
||||
</div>
|
||||
|
||||
<h3 class="font-weight-bold">Enter your six-digit code below</h3>
|
||||
<div class="mt-2">
|
||||
<label for="code" class="px-0">Verification Code</label>
|
||||
<div class="d-flex align-items-center">
|
||||
<field required="True" name="code" autocomplete="off" class="px-0 mr-2" placeholder="e.g. 123456"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button type="object" name="enable" class="btn btn-primary"
|
||||
string="Activate" data-hotkey="q"/>
|
||||
<button string="Cancel" special="cancel" data-hotkey="z"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user