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.

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