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.

202 lines
6.3 KiB

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