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.

225 lines
7.0 KiB

  1. # -*- coding: utf-8 -*-
  2. # © 2016 Akretion Mourad EL HADJ MIMOUNE, David BEAL, Raphaël REVERDY
  3. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
  4. from functools import wraps
  5. import logging
  6. import json
  7. from openerp import models, fields, api
  8. from openerp.exceptions import ValidationError
  9. from openerp.tools.config import config
  10. from openerp.tools.translate import _
  11. try:
  12. from openerp.addons.server_environment import serv_config
  13. except ImportError: # server_environment not installed or configured
  14. serv_config = None
  15. _logger = logging.getLogger(__name__)
  16. try:
  17. from cryptography.fernet import Fernet, MultiFernet, InvalidToken
  18. except ImportError as err:
  19. _logger.debug(err)
  20. def implemented_by_keychain(func):
  21. """Call a prefixed function based on 'namespace'."""
  22. @wraps(func)
  23. def wrapper(cls, *args, **kwargs):
  24. fun_name = func.__name__
  25. fun = '_%s%s' % (cls.namespace, fun_name)
  26. if not hasattr(cls, fun):
  27. fun = '_default%s' % (fun_name)
  28. return getattr(cls, fun)(*args, **kwargs)
  29. return wrapper
  30. class KeychainAccount(models.Model):
  31. """Manage all accounts of external systems in one place."""
  32. _name = 'keychain.account'
  33. name = fields.Char(required=True, help="Humain readable label")
  34. technical_name = fields.Char(
  35. required=True,
  36. help="Technical name. Must be unique")
  37. namespace = fields.Selection([], help="Type of account", required=True)
  38. environment = fields.Char(
  39. required=False,
  40. help="'prod', 'dev', etc. or empty (for all)"
  41. )
  42. login = fields.Char(help="Login")
  43. clear_password = fields.Char(
  44. help="Password. Leave empty if no changes",
  45. inverse='_inverse_set_password',
  46. compute='_compute_password',
  47. store=False)
  48. password = fields.Char(
  49. help="Password is derived from clear_password",
  50. readonly=True)
  51. data = fields.Text(help="Additionnal data as json")
  52. def _compute_password(self):
  53. # Only needed in v8 for _description_searchable issues
  54. return True
  55. def _get_password(self):
  56. """Password in clear text."""
  57. try:
  58. return self._decode_password(self.password)
  59. except Warning as warn:
  60. raise Warning(_(
  61. "%s \n"
  62. "Account: %s %s %s " % (
  63. warn,
  64. self.login, self.name, self.technical_name
  65. )
  66. ))
  67. def get_data(self):
  68. """Data in dict form."""
  69. return self._parse_data(self.data)
  70. @api.constrains('data')
  71. def _check_data(self):
  72. """Ensure valid input in data field."""
  73. for account in self:
  74. if account.data:
  75. parsed = account._parse_data(account.data)
  76. if not account._validate_data(parsed):
  77. raise ValidationError(_("Data not valid"))
  78. def _inverse_set_password(self):
  79. """Encode password from clear text."""
  80. # inverse function
  81. for rec in self:
  82. rec.password = rec._encode_password(
  83. rec.clear_password, rec.environment)
  84. @api.model
  85. def retrieve(self, domain):
  86. """Search accounts for a given domain.
  87. Environment is added by this function.
  88. Use this instead of search() to benefit from environment filtering.
  89. Use user.has_group() and suspend_security() before
  90. calling this method.
  91. """
  92. domain.append(['environment', 'in', self._retrieve_env()])
  93. return self.search(domain)
  94. @api.multi
  95. def write(self, vals):
  96. """At this time there is no namespace set."""
  97. if not vals.get('data') and not self.data:
  98. vals['data'] = self._serialize_data(self._init_data())
  99. return super(KeychainAccount, self).write(vals)
  100. @implemented_by_keychain
  101. def _validate_data(self, data):
  102. pass
  103. @implemented_by_keychain
  104. def _init_data(self):
  105. pass
  106. @staticmethod
  107. def _retrieve_env():
  108. """Return the current environments.
  109. You may override this function to fit your needs.
  110. returns: a tuple like:
  111. ('dev', 'test', False)
  112. Which means accounts for dev, test and blank (not set)
  113. Order is important: the first one is used for encryption.
  114. """
  115. current = config.get('running_env') or False
  116. envs = [current]
  117. if False not in envs:
  118. envs.append(False)
  119. return envs
  120. @staticmethod
  121. def _serialize_data(data):
  122. return json.dumps(data)
  123. @staticmethod
  124. def _parse_data(data):
  125. try:
  126. return json.loads(data)
  127. except ValueError:
  128. raise ValidationError(_("Data should be a valid JSON"))
  129. @classmethod
  130. def _encode_password(cls, data, env):
  131. cipher = cls._get_cipher(env)
  132. return cipher.encrypt(str((data or '').encode('UTF-8')))
  133. @classmethod
  134. def _decode_password(cls, data):
  135. cipher = cls._get_cipher()
  136. try:
  137. return unicode(cipher.decrypt(str(data)), 'UTF-8')
  138. except InvalidToken:
  139. raise Warning(_(
  140. "Password has been encrypted with a different "
  141. "key. Unless you can recover the previous key, "
  142. "this password is unreadable."
  143. ))
  144. @classmethod
  145. def _get_cipher(cls, force_env=None):
  146. """Return a cipher using the keys of environments.
  147. force_env = name of the env key.
  148. Useful for encoding against one precise env
  149. """
  150. def _get_keys_main_config(envs):
  151. suffixes = [
  152. '_%s' % env if env else ''
  153. for env in envs] # ('_dev', '')
  154. keys_name = [
  155. 'keychain_key%s' % suf
  156. for suf in suffixes] # prefix it
  157. keys_str = [
  158. config.get(key)
  159. for key in keys_name] # fetch from config
  160. return [
  161. Fernet(key) for key in keys_str # build Fernet object
  162. if key and len(key) > 0 # remove False values
  163. ]
  164. def _get_keys_serv_config(envs):
  165. keys_name = [
  166. env for env in envs if env] # ignores empty env
  167. keys_str = [
  168. serv_config.get('keychain', key)
  169. for key in keys_name] # fetch from config
  170. return [
  171. Fernet(key) for key in keys_str # build Fernet object
  172. if key and len(key) > 0 # remove False values
  173. ]
  174. keys = []
  175. if force_env:
  176. envs = [force_env]
  177. else:
  178. envs = cls._retrieve_env() # ex: ('dev', False)
  179. if serv_config and serv_config.has_section('keychain'):
  180. keys = _get_keys_serv_config(envs)
  181. if not keys:
  182. keys = _get_keys_main_config(envs)
  183. if len(keys) == 0:
  184. raise Warning(_(
  185. "No 'keychain_key_%s' entries found in config file. "
  186. "Use a key similar to: %s" % (envs[0], Fernet.generate_key())
  187. ))
  188. return MultiFernet(keys)