You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
105 lines
4.1 KiB
105 lines
4.1 KiB
# -*- coding: utf-8 -*-
|
|
# Copyright 2016-2017 LasLabs Inc.
|
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
|
|
|
from collections import defaultdict
|
|
from uuid import uuid4
|
|
from odoo import _, api, fields, models
|
|
from odoo.exceptions import ValidationError
|
|
from odoo.http import request
|
|
from ..controllers.main import JsonSecureCookie
|
|
from ..exceptions import MfaLoginNeeded
|
|
|
|
|
|
class ResUsers(models.Model):
|
|
_inherit = 'res.users'
|
|
_mfa_uid_cache = defaultdict(set)
|
|
|
|
@classmethod
|
|
def _build_model(cls, pool, cr):
|
|
ModelCls = super(ResUsers, cls)._build_model(pool, cr)
|
|
ModelCls.SELF_WRITEABLE_FIELDS += ['mfa_enabled', 'authenticator_ids']
|
|
return ModelCls
|
|
|
|
mfa_enabled = fields.Boolean(string='MFA Enabled?')
|
|
authenticator_ids = fields.One2many(
|
|
comodel_name='res.users.authenticator',
|
|
inverse_name='user_id',
|
|
string='Authentication Apps/Devices',
|
|
help='To delete an authentication app, remove it from this list. To'
|
|
' add a new authentication app, please use the button to the'
|
|
' right. If the button is not present, you do not have the'
|
|
' permissions to do this.',
|
|
)
|
|
trusted_device_cookie_key = fields.Char(
|
|
compute='_compute_trusted_device_cookie_key',
|
|
store=True,
|
|
)
|
|
|
|
@api.multi
|
|
@api.depends('mfa_enabled')
|
|
def _compute_trusted_device_cookie_key(self):
|
|
for record in self:
|
|
if record.mfa_enabled:
|
|
record.trusted_device_cookie_key = uuid4()
|
|
else:
|
|
record.trusted_device_cookie_key = False
|
|
|
|
@api.multi
|
|
@api.constrains('mfa_enabled', 'authenticator_ids')
|
|
def _check_enabled_with_authenticator(self):
|
|
for record in self:
|
|
if record.mfa_enabled and not record.authenticator_ids:
|
|
raise ValidationError(_(
|
|
'You have MFA enabled but do not have any authentication'
|
|
' apps/devices set up. To keep from being locked out,'
|
|
' please add one before you activate this feature.'
|
|
))
|
|
|
|
@classmethod
|
|
def check(cls, db, uid, password):
|
|
"""Prevent auth caching for MFA users without active MFA session"""
|
|
if uid in cls._mfa_uid_cache[db]:
|
|
if not request or request.session.get('mfa_login_active') != uid:
|
|
cls._Users__uid_cache[db].pop(uid, None)
|
|
|
|
return super(ResUsers, cls).check(db, uid, password)
|
|
|
|
@api.model
|
|
def check_credentials(self, password):
|
|
"""Add MFA logic to core authentication process.
|
|
|
|
Overview:
|
|
* If user does not have MFA enabled, defer to parent logic.
|
|
* If user has MFA enabled and has gone through MFA login process
|
|
this session or has correct device cookie, defer to parent logic.
|
|
* If neither of these is true, call parent logic. If successful,
|
|
prevent auth while updating session to indicate that MFA login
|
|
process can now commence.
|
|
"""
|
|
if not self.env.user.mfa_enabled:
|
|
return super(ResUsers, self).check_credentials(password)
|
|
|
|
self._mfa_uid_cache[self.env.cr.dbname].add(self.env.uid)
|
|
|
|
if request:
|
|
if request.session.get('mfa_login_active') == self.env.uid:
|
|
return super(ResUsers, self).check_credentials(password)
|
|
|
|
cookie_key = 'trusted_devices_%d' % self.env.uid
|
|
device_cook = request.httprequest.cookies.get(cookie_key)
|
|
if device_cook:
|
|
secret = self.env.user.trusted_device_cookie_key
|
|
device_cook = JsonSecureCookie.unserialize(device_cook, secret)
|
|
if device_cook:
|
|
return super(ResUsers, self).check_credentials(password)
|
|
|
|
super(ResUsers, self).check_credentials(password)
|
|
if request:
|
|
request.session['mfa_login_needed'] = True
|
|
raise MfaLoginNeeded
|
|
|
|
@api.multi
|
|
def validate_mfa_confirmation_code(self, confirmation_code):
|
|
self.ensure_one()
|
|
return self.authenticator_ids.validate_conf_code(confirmation_code)
|