[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:
David Beguin
2021-08-30 21:05:12 +00:00
parent a3e1310222
commit 09f6ae5b95
12 changed files with 380 additions and 337 deletions
+1
View File
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from . import controllers
from . import models
from . import wizard
+3 -1
View File
@@ -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': [
+40
View File
@@ -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()}"
+3
View File
@@ -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
+5 -115
View File
@@ -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
+56
View File
@@ -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
+115
View File
@@ -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': &quot;[('id', '=', uid)]&quot;}" 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': &quot;[('id', '!=', uid)]&quot;}" 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': &quot;[('id', '=', uid)]&quot;}"
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>
-220
View File
@@ -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': &quot;[('id', '=', uid)]&quot;}" 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': &quot;[('id', '!=', uid)]&quot;}" 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': &quot;[('id', '=', uid)]&quot;}"
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&amp;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>
+4
View File
@@ -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&amp;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>