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.

147 lines
5.3 KiB

  1. # -*- coding: utf-8 -*-
  2. # Copyright 2017 Tecnativa - Jairo Llopis
  3. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
  4. import logging
  5. from contextlib import contextmanager
  6. from threading import current_thread
  7. from openerp import api, models, SUPERUSER_ID
  8. from openerp.exceptions import AccessDenied
  9. from openerp.service import wsgi_server
  10. _logger = logging.getLogger(__name__)
  11. class ResUsers(models.Model):
  12. _inherit = "res.users"
  13. # HACK https://github.com/odoo/odoo/issues/24183
  14. # TODO Remove in v12, and use normal odoo.http.request to get details
  15. def _register_hook(self, cr):
  16. """🐒-patch XML-RPC controller to know remote address."""
  17. original_fn = wsgi_server.application_unproxied
  18. def _patch(environ, start_response):
  19. current_thread().environ = environ
  20. return original_fn(environ, start_response)
  21. wsgi_server.application_unproxied = _patch
  22. # Helpers to track authentication attempts
  23. @classmethod
  24. @contextmanager
  25. def _auth_attempt(cls, login):
  26. """Start an authentication attempt and track its state."""
  27. try:
  28. # Check if this call is nested
  29. attempt_id = current_thread().auth_attempt_id
  30. except AttributeError:
  31. # Not nested; create a new attempt
  32. attempt_id = cls._auth_attempt_new(login)
  33. if not attempt_id:
  34. # No attempt was created, so there's nothing to do here
  35. yield
  36. return
  37. try:
  38. current_thread().auth_attempt_id = attempt_id
  39. result = "successful"
  40. try:
  41. yield
  42. except AccessDenied as error:
  43. result = getattr(error, "reason", "failed")
  44. raise
  45. finally:
  46. cls._auth_attempt_update({"result": result})
  47. finally:
  48. try:
  49. del current_thread().auth_attempt_id
  50. except AttributeError:
  51. pass # It was deleted already
  52. @classmethod
  53. def _auth_attempt_force_raise(cls, login, method):
  54. """Force a method to raise an AccessDenied on falsey return."""
  55. try:
  56. with cls._auth_attempt(login):
  57. result = method()
  58. if not result:
  59. # Force exception to record auth failure
  60. raise AccessDenied()
  61. except AccessDenied:
  62. pass # `_auth_attempt()` did the hard part already
  63. return result
  64. @classmethod
  65. def _auth_attempt_new(cls, login):
  66. """Store one authentication attempt, not knowing the result."""
  67. # Get the right remote address
  68. try:
  69. remote_addr = current_thread().environ["REMOTE_ADDR"]
  70. except (KeyError, AttributeError):
  71. remote_addr = False
  72. # Exit if it doesn't make sense to store this attempt
  73. if not remote_addr:
  74. return False
  75. # Use a separate cursor to keep changes always
  76. with cls.pool.cursor() as cr:
  77. env = api.Environment(cr, SUPERUSER_ID, {})
  78. attempt = env["res.authentication.attempt"].create({
  79. "login": login,
  80. "remote": remote_addr,
  81. })
  82. return attempt.id
  83. @classmethod
  84. def _auth_attempt_update(cls, values):
  85. """Update a given auth attempt if we still ignore its result."""
  86. auth_id = getattr(current_thread(), "auth_attempt_id", False)
  87. if not auth_id:
  88. return {} # No running auth attempt; nothing to do
  89. # Use a separate cursor to keep changes always
  90. with cls.pool.cursor() as cr:
  91. env = api.Environment(cr, SUPERUSER_ID, {})
  92. attempt = env["res.authentication.attempt"].browse(auth_id)
  93. # Update only on 1st call
  94. if not attempt.result:
  95. attempt.write(values)
  96. return attempt.copy_data()[0] if attempt else {}
  97. # Override all auth-related core methods
  98. def _login(self, db, login, password):
  99. return self._auth_attempt_force_raise(
  100. login,
  101. lambda: super(ResUsers, self)._login(db, login, password),
  102. )
  103. def authenticate(self, db, login, password, user_agent_env):
  104. return self._auth_attempt_force_raise(
  105. login,
  106. lambda: super(ResUsers, self).authenticate(
  107. db, login, password, user_agent_env),
  108. )
  109. @api.model
  110. def check_credentials(self, password):
  111. """This is the most important and specific auth check method.
  112. When we get here, it means that Odoo already checked the user exists
  113. in this database.
  114. Other auth methods usually plug here.
  115. """
  116. login = self.env.user.login
  117. with self._auth_attempt(login):
  118. # Update login, just in case we stored the UID before
  119. attempt = self._auth_attempt_update({"login": login})
  120. remote = attempt.get("remote")
  121. # Fail if the remote is banned
  122. trusted = self.env["res.authentication.attempt"]._trusted(
  123. remote,
  124. login,
  125. )
  126. if not trusted:
  127. error = AccessDenied()
  128. error.reason = "banned"
  129. raise error
  130. # Continue with other auth systems
  131. return super(ResUsers, self).check_credentials(password)