res_users.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. import base64
  4. import functools
  5. import logging
  6. import os
  7. import re
  8. from odoo import _, api, fields, models
  9. from odoo.addons.base.models.res_users import check_identity
  10. from odoo.exceptions import AccessDenied, UserError
  11. from odoo.http import request
  12. from odoo.addons.auth_totp.models.totp import TOTP, TOTP_SECRET_SIZE
  13. _logger = logging.getLogger(__name__)
  14. compress = functools.partial(re.sub, r'\s', '')
  15. class Users(models.Model):
  16. _inherit = 'res.users'
  17. totp_secret = fields.Char(copy=False, groups=fields.NO_ACCESS)
  18. totp_enabled = fields.Boolean(string="Two-factor authentication", compute='_compute_totp_enabled')
  19. totp_trusted_device_ids = fields.One2many('auth_totp.device', 'user_id', string="Trusted Devices")
  20. @property
  21. def SELF_READABLE_FIELDS(self):
  22. return super().SELF_READABLE_FIELDS + ['totp_enabled', 'totp_trusted_device_ids']
  23. def _mfa_type(self):
  24. r = super()._mfa_type()
  25. if r is not None:
  26. return r
  27. if self.totp_enabled:
  28. return 'totp'
  29. def _mfa_url(self):
  30. r = super()._mfa_url()
  31. if r is not None:
  32. return r
  33. if self._mfa_type() == 'totp':
  34. return '/web/login/totp'
  35. @api.depends('totp_secret')
  36. def _compute_totp_enabled(self):
  37. for r, v in zip(self, self.sudo()):
  38. r.totp_enabled = bool(v.totp_secret)
  39. def _rpc_api_keys_only(self):
  40. # 2FA enabled means we can't allow password-based RPC
  41. self.ensure_one()
  42. return self.totp_enabled or super()._rpc_api_keys_only()
  43. def _get_session_token_fields(self):
  44. return super()._get_session_token_fields() | {'totp_secret'}
  45. def _totp_check(self, code):
  46. sudo = self.sudo()
  47. key = base64.b32decode(sudo.totp_secret)
  48. match = TOTP(key).match(code)
  49. if match is None:
  50. _logger.info("2FA check: FAIL for %s %r", self, self.login)
  51. raise AccessDenied(_("Verification failed, please double-check the 6-digit code"))
  52. _logger.info("2FA check: SUCCESS for %s %r", self, self.login)
  53. def _totp_try_setting(self, secret, code):
  54. if self.totp_enabled or self != self.env.user:
  55. _logger.info("2FA enable: REJECT for %s %r", self, self.login)
  56. return False
  57. secret = compress(secret).upper()
  58. match = TOTP(base64.b32decode(secret)).match(code)
  59. if match is None:
  60. _logger.info("2FA enable: REJECT CODE for %s %r", self, self.login)
  61. return False
  62. self.sudo().totp_secret = secret
  63. if request:
  64. self.env.flush_all()
  65. # update session token so the user does not get logged out (cache cleared by change)
  66. new_token = self.env.user._compute_session_token(request.session.sid)
  67. request.session.session_token = new_token
  68. _logger.info("2FA enable: SUCCESS for %s %r", self, self.login)
  69. return True
  70. @check_identity
  71. def action_totp_disable(self):
  72. logins = ', '.join(map(repr, self.mapped('login')))
  73. if not (self == self.env.user or self.env.user._is_admin() or self.env.su):
  74. _logger.info("2FA disable: REJECT for %s (%s) by uid #%s", self, logins, self.env.user.id)
  75. return False
  76. self.revoke_all_devices()
  77. self.sudo().write({'totp_secret': False})
  78. if request and self == self.env.user:
  79. self.env.flush_all()
  80. # update session token so the user does not get logged out (cache cleared by change)
  81. new_token = self.env.user._compute_session_token(request.session.sid)
  82. request.session.session_token = new_token
  83. _logger.info("2FA disable: SUCCESS for %s (%s) by uid #%s", self, logins, self.env.user.id)
  84. return {
  85. 'type': 'ir.actions.client',
  86. 'tag': 'display_notification',
  87. 'params': {
  88. 'type': 'warning',
  89. 'message': _("Two-factor authentication disabled for the following user(s): %s", ', '.join(self.mapped('name'))),
  90. 'next': {'type': 'ir.actions.act_window_close'},
  91. }
  92. }
  93. @check_identity
  94. def action_totp_enable_wizard(self):
  95. if self.env.user != self:
  96. raise UserError(_("Two-factor authentication can only be enabled for yourself"))
  97. if self.totp_enabled:
  98. raise UserError(_("Two-factor authentication already enabled"))
  99. secret_bytes_count = TOTP_SECRET_SIZE // 8
  100. secret = base64.b32encode(os.urandom(secret_bytes_count)).decode()
  101. # format secret in groups of 4 characters for readability
  102. secret = ' '.join(map(''.join, zip(*[iter(secret)]*4)))
  103. w = self.env['auth_totp.wizard'].create({
  104. 'user_id': self.id,
  105. 'secret': secret,
  106. })
  107. return {
  108. 'type': 'ir.actions.act_window',
  109. 'target': 'new',
  110. 'res_model': 'auth_totp.wizard',
  111. 'name': _("Two-Factor Authentication Activation"),
  112. 'res_id': w.id,
  113. 'views': [(False, 'form')],
  114. 'context': self.env.context,
  115. }
  116. @check_identity
  117. def revoke_all_devices(self):
  118. self._revoke_all_devices()
  119. def _revoke_all_devices(self):
  120. self.totp_trusted_device_ids._remove()
  121. @api.model
  122. def change_password(self, old_passwd, new_passwd):
  123. self.env.user._revoke_all_devices()
  124. return super().change_password(old_passwd, new_passwd)