Keychain: account manager for external systems (#644)
* Add keychain module
This commit is contained in:
parent
dc6e2b266e
commit
ad582ca028
|
|
@ -0,0 +1,240 @@
|
|||
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
|
||||
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
|
||||
:alt: License: AGPL-3
|
||||
|
||||
================
|
||||
Keychain Account
|
||||
================
|
||||
|
||||
This module allows you to store credentials of external systems.
|
||||
|
||||
* All the crendentials are stored in one place: easier to manage and to audit.
|
||||
* Multi-account made possible without effort.
|
||||
* Store additionnal data for each account.
|
||||
* Validation rules for additional data.
|
||||
* Have different account for different environments (prod / test / env / etc).
|
||||
|
||||
|
||||
By default, passwords are encrypted with a key stored in Odoo config.
|
||||
It's far from an ideal password storage setup, but it's way better
|
||||
than password in clear text in the database.
|
||||
It can be easily replaced by another system. See "Security" chapter below.
|
||||
|
||||
Accounts may be: market places (Amazon, Cdiscount, ...), carriers (Laposte, UPS, ...)
|
||||
or any third party system called from Odoo.
|
||||
|
||||
This module is aimed for developers.
|
||||
The logic to choose between accounts will be achieved in dependent modules.
|
||||
|
||||
|
||||
==========
|
||||
Uses cases
|
||||
==========
|
||||
|
||||
Possible use case for deliveries: you need multiple accounts for the same carrier.
|
||||
It can be for instance due to carrier restrictions (immutable sender address),
|
||||
or business rules (each warehouse use a different account).
|
||||
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
After the installation of this module, you need to add some entries
|
||||
in Odoo's config file: (etc/openerp.cfg)
|
||||
|
||||
> keychain_key = fyeMIx9XVPBBky5XZeLDxVc9dFKy7Uzas3AoyMarHPA=
|
||||
|
||||
You can generate keys with `python keychain/bin/generate_key.py`.
|
||||
|
||||
This key is used to encrypt account passwords.
|
||||
|
||||
If you plan to use environments, you should add a key per environment:
|
||||
|
||||
> keychain_key_dev = 8H_qFvwhxv6EeO9bZ8ww7BUymNt3xtQKYEq9rjAPtrc=
|
||||
|
||||
> keychain_key_prod = y5z-ETtXkVI_ADoFEZ5CHLvrNjwOPxsx-htSVbDbmRc=
|
||||
|
||||
keychain_key is used for encryption when no environment is set.
|
||||
|
||||
|
||||
Usage (for module dev)
|
||||
======================
|
||||
|
||||
|
||||
* Add this keychain as a dependency in __openerp__.py
|
||||
* Subclass `keychain.account` and add your module in namespaces: `(see after for the name of namespace )`
|
||||
|
||||
.. code:: python
|
||||
|
||||
class LaposteAccount(models.Model):
|
||||
_inherit = 'keychain.account'
|
||||
|
||||
namespace = fields.Selection(
|
||||
selection_add=[('roulier_laposte', 'Laposte')])
|
||||
|
||||
* Add the default data (as dict):
|
||||
|
||||
.. code:: python
|
||||
|
||||
class LaposteAccount(models.Model):
|
||||
# ...
|
||||
def _roulier_laposte_init_data(self):
|
||||
return {
|
||||
"agencyCode": "",
|
||||
"recommandationLevel": "R1"
|
||||
}
|
||||
|
||||
* Implement validation of user entered data:
|
||||
|
||||
.. code:: python
|
||||
|
||||
class LaposteAccount(models.Model):
|
||||
# ...
|
||||
def _roulier_laposte_validate_data(self, data):
|
||||
return len(data.get("agencyCode") > 3)
|
||||
|
||||
* In your code, fetch the account:
|
||||
|
||||
.. code:: python
|
||||
|
||||
import random
|
||||
|
||||
def get_auth(self):
|
||||
keychain = self.env['keychain.account']
|
||||
if self.env.user.has_group('stock.group_stock_user'):
|
||||
retrieve = keychain.suspend_security().retrieve
|
||||
else:
|
||||
retrieve = keychain.retrieve
|
||||
|
||||
accounts = retrieve(
|
||||
[['namespace', '=', 'roulier_laposte']])
|
||||
account = random.choice(accounts)
|
||||
return {
|
||||
'login': account.login,
|
||||
'password': account.get_password()
|
||||
}
|
||||
|
||||
|
||||
In this example, an account is randomly picked. Usually this is set according
|
||||
to rules specific for each client.
|
||||
|
||||
You have to restrict user access of your methods with suspend_security().
|
||||
|
||||
Warning: _init_data and _validate_data should be prefixed with your namespace!
|
||||
Choose python naming function compatible name.
|
||||
|
||||
Switching from prod to dev
|
||||
==========================
|
||||
|
||||
You may adopt one of the following strategies:
|
||||
|
||||
* store your dev accounts in production db using the dev key
|
||||
* import your dev accounts with Odoo builtin methods like a data.xml (in a dedicated module).
|
||||
* import your dev accounts with your own migration/cleanup script
|
||||
* etc.
|
||||
|
||||
Note: only the password field is unreadable without the proper key, login and data fields
|
||||
are available on all environments.
|
||||
|
||||
You may also use a same `technical_name` and different `environment` for choosing at runtime
|
||||
between accounts.
|
||||
|
||||
Usage (for user)
|
||||
================
|
||||
|
||||
Go to *settings / keychain*, create a record with the following
|
||||
|
||||
* Namespace: type of account (ie: Laposte)
|
||||
* Name: human readable label "Warehouse 1"
|
||||
* Technical Name: name used by a consumer module (like "warehouse_1")
|
||||
* Login: login of the account
|
||||
* Password_clear: For entering the password in clear text (not stored unencrypted)
|
||||
* Password: password encrypted, unreadable without the key (in config)
|
||||
* Data: a JSON string for additionnal values (additionnal config for the account, like: `{"agencyCode": "Lyon", "insuranceLevel": "R1"})`
|
||||
* Environment: usually prod or dev or blank (for all)
|
||||
|
||||
|
||||
|
||||
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
|
||||
:alt: Try me on Runbot
|
||||
:target: https://runbot.odoo-community.org/runbot/server-tools/9.0
|
||||
|
||||
|
||||
Known issues / Roadmap
|
||||
======================
|
||||
- Account inheritence is not supported out-of-the-box (like defining common settings for all environments)
|
||||
- Adapted to work with `server_environnement` modules
|
||||
- Key expiration or rotation should be done manually
|
||||
- Import passwords from data.xml
|
||||
|
||||
Security
|
||||
========
|
||||
|
||||
This discussion: https://github.com/OCA/server-tools/pull/644 may help you decide if this module is suitable for your needs or not.
|
||||
|
||||
Common sense: Odoo is not a safe place for storing sensitive data.
|
||||
But sometimes you don't have any other possibilities.
|
||||
This module is designed to store credentials of data like carrier account, smtp, api keys...
|
||||
but definitively not for credits cards number, medical records, etc.
|
||||
|
||||
|
||||
By default, passwords are stored encrypted in the db using
|
||||
symetric encryption `Fernet <https://cryptography.io/en/latest/fernet/>`_.
|
||||
The encryption key is stored in openerp.tools.config.
|
||||
|
||||
Threats even with this module installed:
|
||||
|
||||
- unauthorized Odoo user want to access data: access is rejected by Odoo security rules
|
||||
- authorized Odoo user try to access data with rpc api: he gets the passwords encrypted, he can't recover because the key and the decrypted password are not exposed through rpc
|
||||
- db is stolen: without the key it's currently pretty hard to recover the passwords
|
||||
- Odoo is compromised (malicious module or vulnerability): hacker has access to python and can do what he wants with Odoo: passwords of the current env can be easily decrypted
|
||||
- server is compromised: idem
|
||||
|
||||
If your dev server is compromised, hacker can't decrypt your prod passwords
|
||||
since you have different keys between dev and prod.
|
||||
|
||||
If you want something more secure: don't store any sensitive data in Odoo,
|
||||
use an external system as a proxy, you can still use this module
|
||||
for storing all other data related to your accounts.
|
||||
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues
|
||||
<https://github.com/OCA/server-tools/issues>`_. In case of trouble, please
|
||||
check there if your issue has already been reported. If you spotted it first,
|
||||
help us smashing it by providing a detailed and welcomed feedback.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
`Akretion <https://akretion.com>`_
|
||||
|
||||
|
||||
Contributors
|
||||
------------
|
||||
|
||||
* Raphaël Reverdy <raphael.reverdy@akretion.com>
|
||||
|
||||
Funders
|
||||
-------
|
||||
|
||||
The development of this module has been financially supported by:
|
||||
|
||||
* `Akretion <https://akretion.com>`_
|
||||
|
||||
Maintainer
|
||||
----------
|
||||
|
||||
.. image:: https://odoo-community.org/logo.png
|
||||
:alt: Odoo Community Association
|
||||
:target: https://odoo-community.org
|
||||
|
||||
This module is maintained by the OCA.
|
||||
|
||||
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.
|
||||
|
||||
To contribute to this module, please visit https://odoo-community.org.
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import models
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright <2016> Akretion
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
{
|
||||
"name": "Keychain",
|
||||
"summary": "Store accounts and credentials",
|
||||
"version": "9.0.1.0.0",
|
||||
"category": "Uncategorized",
|
||||
"website": "https://akretion.com/",
|
||||
"author": "Akretion, Odoo Community Association (OCA)",
|
||||
"license": "AGPL-3",
|
||||
"application": False,
|
||||
"installable": True,
|
||||
"external_dependencies": {
|
||||
"python": [
|
||||
'cryptography'],
|
||||
},
|
||||
"depends": [
|
||||
"base",
|
||||
],
|
||||
"data": [
|
||||
"security/ir.model.access.csv",
|
||||
'views/keychain_view.xml'
|
||||
],
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import keychain
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# © 2016 Akretion Mourad EL HADJ MIMOUNE, David BEAL, Raphaël REVERDY
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
from functools import wraps
|
||||
|
||||
import logging
|
||||
import json
|
||||
|
||||
from openerp import models, fields, api
|
||||
from openerp.exceptions import ValidationError
|
||||
from openerp.tools.config import config
|
||||
from openerp.tools.translate import _
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from cryptography.fernet import Fernet, MultiFernet, InvalidToken
|
||||
except ImportError as err:
|
||||
_logger.debug(err)
|
||||
|
||||
|
||||
def implemented_by_keychain(func):
|
||||
"""Call a prefixed function based on 'namespace'."""
|
||||
@wraps(func)
|
||||
def wrapper(cls, *args, **kwargs):
|
||||
fun_name = func.__name__
|
||||
fun = '_%s%s' % (cls.namespace, fun_name)
|
||||
if not hasattr(cls, fun):
|
||||
fun = '_default%s' % (fun_name)
|
||||
return getattr(cls, fun)(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
class KeychainAccount(models.Model):
|
||||
"""Manage all accounts of external systems in one place."""
|
||||
|
||||
_name = 'keychain.account'
|
||||
|
||||
name = fields.Char(required=True, help="Humain readable label")
|
||||
technical_name = fields.Char(
|
||||
required=True,
|
||||
help="Technical name. Must be unique")
|
||||
namespace = fields.Selection([], help="Type of account", required=True)
|
||||
environment = fields.Char(
|
||||
required=False,
|
||||
help="'prod', 'dev', etc. or empty (for all)"
|
||||
)
|
||||
login = fields.Char(help="Login")
|
||||
clear_password = fields.Char(
|
||||
help="Password. Leave empty if no changes",
|
||||
inverse='_inverse_set_password',
|
||||
compute='_compute_password',
|
||||
store=False)
|
||||
password = fields.Char(
|
||||
help="Password is derived from clear_password",
|
||||
readonly=True)
|
||||
data = fields.Text(help="Additionnal data as json")
|
||||
|
||||
def _compute_password(self):
|
||||
# Only needed in v8 for _description_searchable issues
|
||||
return True
|
||||
|
||||
def get_password(self):
|
||||
"""Password in clear text."""
|
||||
try:
|
||||
return self._decode_password(self.password)
|
||||
except Warning as warn:
|
||||
raise Warning(_(
|
||||
"%s \n"
|
||||
"Account: %s %s %s " % (
|
||||
warn,
|
||||
self.login, self.name, self.technical_name
|
||||
)
|
||||
))
|
||||
|
||||
def get_data(self):
|
||||
"""Data in dict form."""
|
||||
return self._parse_data(self.data)
|
||||
|
||||
@api.constrains('data')
|
||||
def _check_data(self):
|
||||
"""Ensure valid input in data field."""
|
||||
for account in self:
|
||||
if account.data:
|
||||
parsed = account._parse_data(account.data)
|
||||
if not account._validate_data(parsed):
|
||||
raise ValidationError(_("Data not valid"))
|
||||
|
||||
def _inverse_set_password(self):
|
||||
"""Encode password from clear text."""
|
||||
# inverse function
|
||||
for rec in self:
|
||||
rec.password = rec._encode_password(
|
||||
rec.clear_password, rec.environment)
|
||||
|
||||
@api.model
|
||||
def retrieve(self, domain):
|
||||
"""Search accounts for a given domain.
|
||||
|
||||
Environment is added by this function.
|
||||
Use this instead of search() to benefit from environment filtering.
|
||||
Use user.has_group() and suspend_security() before
|
||||
calling this method.
|
||||
"""
|
||||
domain.append(['environment', 'in', self._retrieve_env()])
|
||||
return self.search(domain)
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
"""At this time there is no namespace set."""
|
||||
if not vals.get('data') and not self.data:
|
||||
vals['data'] = self._serialize_data(self._init_data())
|
||||
return super(KeychainAccount, self).write(vals)
|
||||
|
||||
@implemented_by_keychain
|
||||
def _validate_data(self, data):
|
||||
pass
|
||||
|
||||
@implemented_by_keychain
|
||||
def _init_data(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _retrieve_env():
|
||||
"""Return the current environments.
|
||||
|
||||
You may override this function to fit your needs.
|
||||
|
||||
returns: a tuple like:
|
||||
('dev', 'test', False)
|
||||
Which means accounts for dev, test and blank (not set)
|
||||
Order is important: the first one is used for encryption.
|
||||
"""
|
||||
current = config.get('running_env') or False
|
||||
envs = [current]
|
||||
if False not in envs:
|
||||
envs.append(False)
|
||||
return envs
|
||||
|
||||
@staticmethod
|
||||
def _serialize_data(data):
|
||||
return json.dumps(data)
|
||||
|
||||
@staticmethod
|
||||
def _parse_data(data):
|
||||
try:
|
||||
return json.loads(data)
|
||||
except ValueError:
|
||||
raise ValidationError(_("Data should be a valid JSON"))
|
||||
|
||||
@classmethod
|
||||
def _encode_password(cls, data, env):
|
||||
cipher = cls._get_cipher(env)
|
||||
return cipher.encrypt(str((data or '').encode('UTF-8')))
|
||||
|
||||
@classmethod
|
||||
def _decode_password(cls, data):
|
||||
cipher = cls._get_cipher()
|
||||
try:
|
||||
return unicode(cipher.decrypt(str(data)), 'UTF-8')
|
||||
except InvalidToken:
|
||||
raise Warning(_(
|
||||
"Password has been encrypted with a different "
|
||||
"key. Unless you can recover the previous key, "
|
||||
"this password is unreadable."
|
||||
))
|
||||
|
||||
@classmethod
|
||||
def _get_cipher(cls, force_env=None):
|
||||
"""Return a cipher using the keys of environments.
|
||||
|
||||
force_env = name of the env key.
|
||||
Useful for encoding against one precise env
|
||||
"""
|
||||
def _get_keys(envs):
|
||||
suffixes = [
|
||||
'_%s' % env if env else ''
|
||||
for env in envs] # ('_dev', '')
|
||||
keys_name = [
|
||||
'keychain_key%s' % suf
|
||||
for suf in suffixes] # prefix it
|
||||
keys_str = [
|
||||
config.get(key)
|
||||
for key in keys_name] # fetch from config
|
||||
return [
|
||||
Fernet(key) for key in keys_str # build Fernet object
|
||||
if key and len(key) > 0 # remove False values
|
||||
]
|
||||
|
||||
if force_env:
|
||||
envs = [force_env]
|
||||
else:
|
||||
envs = cls._retrieve_env() # ex: ('dev', False)
|
||||
keys = _get_keys(envs)
|
||||
if len(keys) == 0:
|
||||
raise Warning(_(
|
||||
"No 'keychain_key_%s' entries found in config file. "
|
||||
"Use a key similar to: %s" % (envs[0], Fernet.generate_key())
|
||||
))
|
||||
return MultiFernet(keys)
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_keychain_account,access_keychain_account,model_keychain_account,,0,0,0,0
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
from . import test_keychain
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from openerp.tests.common import TransactionCase
|
||||
from openerp.tools.config import config
|
||||
from openerp.exceptions import ValidationError
|
||||
|
||||
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from cryptography.fernet import Fernet
|
||||
except ImportError as err:
|
||||
_logger.debug(err)
|
||||
|
||||
|
||||
class TestKeychain(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestKeychain, self).setUp()
|
||||
|
||||
self.keychain = self.env['keychain.account']
|
||||
config['keychain_key'] = Fernet.generate_key()
|
||||
|
||||
self.old_running_env = config['running_env']
|
||||
config['running_env'] = None
|
||||
|
||||
def _init_data(self):
|
||||
return {
|
||||
"c": True,
|
||||
"a": "b",
|
||||
"d": "",
|
||||
}
|
||||
|
||||
def _validate_data(self, data):
|
||||
return 'c' in data
|
||||
|
||||
keychain_clss = self.keychain.__class__
|
||||
keychain_clss._keychain_test_init_data = _init_data
|
||||
keychain_clss._keychain_test_validate_data = _validate_data
|
||||
|
||||
self.keychain._fields['namespace'].selection.append(
|
||||
('keychain_test', 'test')
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
config['running_env'] = self.old_running_env
|
||||
return super(TestKeychain, self).tearDown()
|
||||
|
||||
def _create_account(self):
|
||||
vals = {
|
||||
"name": "test",
|
||||
"namespace": "keychain_test",
|
||||
"login": "test",
|
||||
"technical_name": "keychain.test"
|
||||
}
|
||||
return self.keychain.create(vals)
|
||||
|
||||
def test_password(self):
|
||||
"""It should encrypt passwords."""
|
||||
account = self._create_account()
|
||||
passwords = ('', '12345', 'djkqfljfqm', u"""&é"'(§è!ç""")
|
||||
|
||||
for password in passwords:
|
||||
account.clear_password = password
|
||||
account._inverse_set_password()
|
||||
self.assertTrue(account.clear_password != account.password)
|
||||
self.assertEqual(account.get_password(), password)
|
||||
|
||||
def test_wrong_key(self):
|
||||
"""It should raise an exception when encoded key != decoded."""
|
||||
account = self._create_account()
|
||||
password = 'urieapocq'
|
||||
account.clear_password = password
|
||||
account._inverse_set_password()
|
||||
config['keychain_key'] = Fernet.generate_key()
|
||||
try:
|
||||
account.get_password()
|
||||
self.assertTrue(False, 'It should not work with another key')
|
||||
except Warning as err:
|
||||
self.assertTrue(True, 'It should raise a Warning')
|
||||
self.assertTrue(
|
||||
'has been encrypted with a diff' in str(err),
|
||||
'It should display the right msg')
|
||||
else:
|
||||
self.assertTrue(False, 'It should raise a Warning')
|
||||
|
||||
def test_no_key(self):
|
||||
"""It should raise an exception when no key is set."""
|
||||
account = self._create_account()
|
||||
del config.options['keychain_key']
|
||||
|
||||
with self.assertRaises(Warning) as err:
|
||||
account.clear_password = 'aiuepr'
|
||||
account._inverse_set_password()
|
||||
self.assertTrue(False, 'It should not work without key')
|
||||
self.assertTrue(
|
||||
'Use a key similar to' in str(err.exception),
|
||||
'It should display the right msg')
|
||||
|
||||
def test_badly_formatted_key(self):
|
||||
"""It should raise an exception when key is not acceptable format."""
|
||||
account = self._create_account()
|
||||
|
||||
config['keychain_key'] = ""
|
||||
with self.assertRaises(Warning):
|
||||
account.clear_password = 'aiuepr'
|
||||
account._inverse_set_password()
|
||||
self.assertTrue(False, 'It should not work missing formated key')
|
||||
|
||||
self.assertTrue(True, 'It shoud raise a ValueError')
|
||||
|
||||
def test_retrieve_env(self):
|
||||
"""Retrieve env should always return False at the end"""
|
||||
config['running_env'] = False
|
||||
self.assertListEqual(self.keychain._retrieve_env(), [False])
|
||||
|
||||
config['running_env'] = 'dev'
|
||||
self.assertListEqual(self.keychain._retrieve_env(), ['dev', False])
|
||||
|
||||
config['running_env'] = 'prod'
|
||||
self.assertListEqual(self.keychain._retrieve_env(), ['prod', False])
|
||||
|
||||
def test_multienv(self):
|
||||
"""Encrypt with dev, decrypt with dev."""
|
||||
account = self._create_account()
|
||||
config['keychain_key_dev'] = Fernet.generate_key()
|
||||
config['keychain_key_prod'] = Fernet.generate_key()
|
||||
config['running_env'] = 'dev'
|
||||
|
||||
account.clear_password = 'abc'
|
||||
account._inverse_set_password()
|
||||
self.assertEqual(
|
||||
account.get_password(),
|
||||
'abc', 'Should work with dev')
|
||||
|
||||
config['running_env'] = 'prod'
|
||||
with self.assertRaises(Warning):
|
||||
self.assertEqual(
|
||||
account.get_password(),
|
||||
'abc', 'Should not work with prod key')
|
||||
|
||||
def test_multienv_blank(self):
|
||||
"""Encrypt with blank, decrypt for all."""
|
||||
account = self._create_account()
|
||||
config['keychain_key'] = Fernet.generate_key()
|
||||
config['keychain_key_dev'] = Fernet.generate_key()
|
||||
config['keychain_key_prod'] = Fernet.generate_key()
|
||||
config['running_env'] = ''
|
||||
|
||||
account.clear_password = 'abc'
|
||||
account._inverse_set_password()
|
||||
self.assertEqual(
|
||||
account.get_password(),
|
||||
'abc', 'Should work with dev')
|
||||
|
||||
config['running_env'] = 'prod'
|
||||
self.assertEqual(
|
||||
account.get_password(),
|
||||
'abc', 'Should work with prod')
|
||||
|
||||
def test_multienv_force(self):
|
||||
"""Set the env on the record"""
|
||||
|
||||
account = self._create_account()
|
||||
account.environment = 'prod'
|
||||
|
||||
config['keychain_key'] = Fernet.generate_key()
|
||||
config['keychain_key_dev'] = Fernet.generate_key()
|
||||
config['keychain_key_prod'] = Fernet.generate_key()
|
||||
config['running_env'] = ''
|
||||
|
||||
account.clear_password = 'abc'
|
||||
account._inverse_set_password()
|
||||
|
||||
with self.assertRaises(Warning):
|
||||
self.assertEqual(
|
||||
account.get_password(),
|
||||
'abc', 'Should not work with dev')
|
||||
|
||||
config['running_env'] = 'prod'
|
||||
self.assertEqual(
|
||||
account.get_password(),
|
||||
'abc', 'Should work with prod')
|
||||
|
||||
def test_wrong_json(self):
|
||||
"""It should raise an exception when data is not valid json."""
|
||||
account = self._create_account()
|
||||
wrong_jsons = ("{'hi':'o'}", "{'oq", '[>}')
|
||||
for json in wrong_jsons:
|
||||
with self.assertRaises(ValidationError) as err:
|
||||
account.write({"data": json})
|
||||
self.assertTrue(
|
||||
False,
|
||||
'Should not validate baddly formatted json')
|
||||
self.assertTrue(
|
||||
'Data should be a valid JSON' in str(err.exception),
|
||||
'It should raise a ValidationError')
|
||||
|
||||
def test_invalid_json(self):
|
||||
"""It should raise an exception when data don't pass _validate_data."""
|
||||
account = self._create_account()
|
||||
invalid_jsons = ('{}', '{"hi": 1}')
|
||||
for json in invalid_jsons:
|
||||
with self.assertRaises(ValidationError) as err:
|
||||
account.write({"data": json})
|
||||
self.assertTrue(
|
||||
'Data not valid' in str(err.exception),
|
||||
'It should raise a ValidationError')
|
||||
|
||||
def test_valid_json(self):
|
||||
"""It should work with valid data."""
|
||||
account = self._create_account()
|
||||
valid_jsons = ('{"c": true}', '{"c": 1}', '{"a": "o", "c": "b"}')
|
||||
for json in valid_jsons:
|
||||
try:
|
||||
account.write({"data": json})
|
||||
self.assertTrue(True, 'Should validate json')
|
||||
except:
|
||||
self.assertTrue(False, 'It should validate a good json')
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="keychain_account_id">
|
||||
<field name="model">keychain.account</field>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<tree string="Accounts">
|
||||
<field name="namespace"/>
|
||||
<field name="name"/>
|
||||
<field name="technical_name" />
|
||||
<field name="login"/>
|
||||
<field name="environment"/>
|
||||
</tree>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="keychain_account_form">
|
||||
<field name="model">keychain.account</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Accounts form">
|
||||
<group>
|
||||
<field name="namespace"/>
|
||||
<field name="name" />
|
||||
<field name="technical_name" />
|
||||
<field name="environment"/>
|
||||
<field name="login"/>
|
||||
<field name="clear_password" />
|
||||
<field name="password" />
|
||||
<field name="data"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window" id="keychain_list_action">
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
|
||||
<field name="name">Accounts</field>
|
||||
<field name="res_model">keychain.account</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="keychain_menu" name="Keychain"
|
||||
parent="base.menu_config"
|
||||
action="keychain_list_action"/>
|
||||
</data>
|
||||
</openerp>
|
||||
Loading…
Reference in New Issue