Browse Source
Keychain: account manager for external systems (#644)
Keychain: account manager for external systems (#644)
* Add keychain modulepull/780/head
Hpar
8 years ago
committed by
MonsieurB
9 changed files with 740 additions and 0 deletions
-
240keychain/README.rst
-
1keychain/__init__.py
-
25keychain/__openerp__.py
-
1keychain/models/__init__.py
-
200keychain/models/keychain.py
-
2keychain/security/ir.model.access.csv
-
1keychain/tests/__init__.py
-
220keychain/tests/test_keychain.py
-
50keychain/views/keychain_view.xml
@ -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> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue