Add module server_environment_data_encryption
This commit is contained in:
parent
76501485ed
commit
1a8a7f01bf
|
|
@ -0,0 +1 @@
|
||||||
|
from . import models
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
{
|
||||||
|
"name": "Server Environment Data Encryption",
|
||||||
|
"version": "12.0.0.0.1",
|
||||||
|
"category": "Tools",
|
||||||
|
"website": "https://akretion.com/",
|
||||||
|
"author": "Akretion, Odoo Community Association (OCA)",
|
||||||
|
"license": "AGPL-3",
|
||||||
|
"application": False,
|
||||||
|
"installable": True,
|
||||||
|
"depends": ["server_environment", "data_encryption"],
|
||||||
|
"data": [],
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
from . import server_env_mixin
|
||||||
|
|
@ -0,0 +1,165 @@
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from odoo import api, models, _
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
from odoo.tools.config import config
|
||||||
|
from lxml import etree
|
||||||
|
from odoo.osv.orm import setup_modifiers
|
||||||
|
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ServerEnvMixin(models.AbstractModel):
|
||||||
|
_inherit = "server.env.mixin"
|
||||||
|
|
||||||
|
def _compute_server_env_from_default(self, field_name, options):
|
||||||
|
"First return database encrypted value then default value"
|
||||||
|
self.ensure_one()
|
||||||
|
encrypted_data_name = "%s,%s" % (self._name, self.id)
|
||||||
|
env = self.env.context.get("environment", None)
|
||||||
|
vals = (
|
||||||
|
self.env["encrypted.data"]
|
||||||
|
.sudo()
|
||||||
|
._get_json(encrypted_data_name, env=env)
|
||||||
|
)
|
||||||
|
if vals.get(field_name):
|
||||||
|
self[field_name] = vals[field_name]
|
||||||
|
else:
|
||||||
|
return super()._compute_server_env_from_default(
|
||||||
|
field_name, options
|
||||||
|
)
|
||||||
|
|
||||||
|
def _inverse_server_env(self, field_name):
|
||||||
|
"""
|
||||||
|
When this module is installed, we store values into encrypted data
|
||||||
|
env instead of a default field in database (not env dependent).
|
||||||
|
"""
|
||||||
|
is_editable_field = self._server_env_is_editable_fieldname(field_name)
|
||||||
|
encrypted_data_obj = self.env["encrypted.data"].sudo()
|
||||||
|
env = self.env.context.get("environment", None)
|
||||||
|
for record in self:
|
||||||
|
if record[is_editable_field]:
|
||||||
|
encrypted_data_name = "%s,%s" % (record._name, record.id)
|
||||||
|
values = encrypted_data_obj._get_json(
|
||||||
|
encrypted_data_name, env=env
|
||||||
|
)
|
||||||
|
new_val = {field_name: record[field_name]}
|
||||||
|
values.update(new_val)
|
||||||
|
encrypted_data_obj._store_json(
|
||||||
|
encrypted_data_name, values, env=env
|
||||||
|
)
|
||||||
|
|
||||||
|
def action_change_env_data_encrypted_fields(self):
|
||||||
|
action_id = self.env.context.get("params", {}).get("action")
|
||||||
|
if not action_id:
|
||||||
|
# We don't know which action we are using... take one random.
|
||||||
|
action_id = self.env['ir.actions.act_window'].search(
|
||||||
|
[('res_model', '=', self._name)], limit=1).id
|
||||||
|
action = self.env["ir.actions.act_window"].browse(action_id).read()[0]
|
||||||
|
action["view_mode"] = "form"
|
||||||
|
action["res_id"] = self.id
|
||||||
|
views_form = []
|
||||||
|
for view_id, view_type in action.get("views", []):
|
||||||
|
if view_type == "form":
|
||||||
|
views_form.append((view_id, view_type))
|
||||||
|
action["views"] = views_form
|
||||||
|
return action
|
||||||
|
|
||||||
|
def _get_extra_environment_info_div(self, current_env, extra_envs):
|
||||||
|
button_div = "<div>"
|
||||||
|
button_string = _("Define values for ")
|
||||||
|
for environment in extra_envs:
|
||||||
|
button = """
|
||||||
|
<button name="action_change_env_data_encrypted_fields"
|
||||||
|
type="object" string="{}{}"
|
||||||
|
class="btn btn-lg btn-primary ml-2"
|
||||||
|
context="{}"/>
|
||||||
|
""".format(
|
||||||
|
button_string, environment, {"environment": environment}
|
||||||
|
)
|
||||||
|
button_div += "{}".format(button)
|
||||||
|
button_div += "</div>"
|
||||||
|
alert_string = _("Modify values for {} environment").format(
|
||||||
|
current_env
|
||||||
|
)
|
||||||
|
alert_type = (
|
||||||
|
current_env == config.get("running_env")
|
||||||
|
and "alert-info"
|
||||||
|
or "alert-warning"
|
||||||
|
)
|
||||||
|
elem = etree.fromstring(
|
||||||
|
"""
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<div class="alert lead {} text-center d-inline">
|
||||||
|
<strong>{}</strong>
|
||||||
|
</div>
|
||||||
|
{}
|
||||||
|
</div>
|
||||||
|
""".format(
|
||||||
|
alert_type, alert_string, button_div
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return elem
|
||||||
|
|
||||||
|
def _set_readonly_form_view(self, doc):
|
||||||
|
for field in doc.iter("field"):
|
||||||
|
env_fields = self._server_env_fields.keys()
|
||||||
|
field_name = field.get("name")
|
||||||
|
if field_name in env_fields:
|
||||||
|
continue
|
||||||
|
field.set("readonly", "1")
|
||||||
|
setup_modifiers(field, self.fields_get(field_name))
|
||||||
|
|
||||||
|
def _update_form_view_from_env(self, arch, view_type):
|
||||||
|
if view_type != "form":
|
||||||
|
return arch
|
||||||
|
current_env = self.env.context.get("environment") or config.get(
|
||||||
|
"running_env"
|
||||||
|
)
|
||||||
|
other_environments = [
|
||||||
|
key[15:]
|
||||||
|
for key, val in config.options.items()
|
||||||
|
if key.startswith("encryption_key_")
|
||||||
|
and val
|
||||||
|
and key[15:] != current_env
|
||||||
|
]
|
||||||
|
|
||||||
|
if not current_env:
|
||||||
|
raise ValidationError(
|
||||||
|
_(
|
||||||
|
"you need to define the running_env entry in your odoo "
|
||||||
|
"configuration file"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
doc = etree.XML(arch)
|
||||||
|
node = doc.xpath("//sheet")
|
||||||
|
if node:
|
||||||
|
node = node[0]
|
||||||
|
elem = self._get_extra_environment_info_div(
|
||||||
|
current_env, other_environments
|
||||||
|
)
|
||||||
|
node.insert(0, elem)
|
||||||
|
|
||||||
|
if current_env != config.get("running_env"):
|
||||||
|
self._set_readonly_form_view(doc)
|
||||||
|
arch = etree.tostring(doc, pretty_print=True, encoding="unicode")
|
||||||
|
else:
|
||||||
|
_logger.error(
|
||||||
|
"Missing sheet for form view on object {}".format(self._name)
|
||||||
|
)
|
||||||
|
return arch
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def fields_view_get(
|
||||||
|
self, view_id=None, view_type="form", toolbar=False, submenu=False
|
||||||
|
):
|
||||||
|
res = super().fields_view_get(
|
||||||
|
view_id=view_id,
|
||||||
|
view_type=view_type,
|
||||||
|
toolbar=toolbar,
|
||||||
|
submenu=submenu,
|
||||||
|
)
|
||||||
|
res["arch"] = self._update_form_view_from_env(res["arch"], view_type)
|
||||||
|
return res
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
In order to use this module properly, each environment should have their own encryption key
|
||||||
|
and the production environment should have the keys of all environments.
|
||||||
|
|
||||||
|
Example :
|
||||||
|
Development environment ::
|
||||||
|
|
||||||
|
[options]
|
||||||
|
running_env=dev
|
||||||
|
encryption_key_dev=XXX
|
||||||
|
|
||||||
|
Pre-production environment ::
|
||||||
|
|
||||||
|
[options]
|
||||||
|
running_env=preprod
|
||||||
|
encryption_key_preprod=YYY
|
||||||
|
|
||||||
|
Production environment ::
|
||||||
|
|
||||||
|
[options]
|
||||||
|
running_env=prod
|
||||||
|
encryption_key_dev=XXX
|
||||||
|
encryption_key_preprod=YYY
|
||||||
|
encryption_key_prod=ZZZ
|
||||||
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
* Florian da Costa <florian.dacosta@akretion.com>
|
||||||
|
* Sébastien Beau <sebastien.beau@akretion.com>
|
||||||
|
* Benoît Guillot <benoit.guillot@akretion.com>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
This module changes a little the behavior of server_environment modules.
|
||||||
|
When Odoo does not find the value of the field in the configuration file,
|
||||||
|
it will fallback on a Odoo encrypted field instead.
|
||||||
|
Also it allows you
|
||||||
|
to configure the environment dependent fields for all your environments
|
||||||
|
from the production server.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
from . import test_server_environment_data_encrypt
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
<form string="Test">
|
||||||
|
<header>
|
||||||
|
<button string="Reset Confirmation"
|
||||||
|
type="object" name="set_draft"
|
||||||
|
states="done"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<group col="4">
|
||||||
|
<field name="test" modifiers="{"required": true}"/>
|
||||||
|
<field name="test2" attrs="{'readonly': [('type_env_is_editable', '=', False)]}"
|
||||||
|
on_change="1" modifiers="{"readonly": [["type_env_is_editable", "=", false]]}"/>
|
||||||
|
<field name="date" modifiers="{"readonly": true}"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<form string="Test">
|
||||||
|
<header>
|
||||||
|
<button string="Reset Confirmation" type="object" name="set_draft" states="done"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<div class="alert lead alert-info text-center d-inline">
|
||||||
|
<strong>Modify values for test environment</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button name="action_change_env_data_encrypted_fields" type="object" string="Define values for preprod" class="btn btn-lg btn-primary ml-2" context="{'environment': 'preprod'}"/>
|
||||||
|
|
||||||
|
<button name="action_change_env_data_encrypted_fields" type="object" string="Define values for prod" class="btn btn-lg btn-primary ml-2" context="{'environment': 'prod'}"/>
|
||||||
|
</div>
|
||||||
|
</div><group col="4">
|
||||||
|
<field name="test" modifiers="{"required": true}"/>
|
||||||
|
<field name="test2" attrs="{'readonly': [('type_env_is_editable', '=', False)]}" on_change="1" modifiers="{"readonly": [["type_env_is_editable", "=", false]]}"/>
|
||||||
|
<field name="date" modifiers="{"readonly": true}"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<form string="Test">
|
||||||
|
<header>
|
||||||
|
<button string="Reset Confirmation" type="object" name="set_draft" states="done"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<div class="alert lead alert-warning text-center d-inline">
|
||||||
|
<strong>Modify values for prod environment</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button name="action_change_env_data_encrypted_fields" type="object" string="Define values for preprod" class="btn btn-lg btn-primary ml-2" context="{'environment': 'preprod'}"/>
|
||||||
|
|
||||||
|
<button name="action_change_env_data_encrypted_fields" type="object" string="Define values for test" class="btn btn-lg btn-primary ml-2" context="{'environment': 'test'}"/>
|
||||||
|
</div>
|
||||||
|
</div><group col="4">
|
||||||
|
<field name="test" modifiers="{"readonly": true}" readonly="1"/>
|
||||||
|
<field name="test2" attrs="{'readonly': [('type_env_is_editable', '=', False)]}" on_change="1" modifiers="{"readonly": true}" readonly="1"/>
|
||||||
|
<field name="date" modifiers="{"readonly": true}" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||||
|
|
||||||
|
from odoo.addons.data_encryption.tests.common import CommonDataEncrypted
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
class TestServerEnvDataEncrypted(CommonDataEncrypted):
|
||||||
|
|
||||||
|
# def test_store_data_no_superuser(self):
|
||||||
|
# self.env['server.env.mixin']._inverse_server_env('test')
|
||||||
|
# pass
|
||||||
|
|
||||||
|
def test_dynamic_view_current_env(self):
|
||||||
|
self.maxDiff = None
|
||||||
|
self.set_new_key_env("prod")
|
||||||
|
self.set_new_key_env("preprod")
|
||||||
|
mixin_obj = self.env["server.env.mixin"]
|
||||||
|
base_path = Path(__file__).parent / "fixtures" / "base.xml"
|
||||||
|
xml = base_path.read_text()
|
||||||
|
res_xml = mixin_obj._update_form_view_from_env(xml, "form")
|
||||||
|
expected_xml_path = Path(__file__).parent / "fixtures" / "res1.xml"
|
||||||
|
expected_xml = expected_xml_path.read_text()
|
||||||
|
self.assertEqual(res_xml, expected_xml)
|
||||||
|
|
||||||
|
def test_dynamic_view_other_env(self):
|
||||||
|
self.set_new_key_env("prod")
|
||||||
|
self.set_new_key_env("preprod")
|
||||||
|
mixin_obj = self.env["server.env.mixin"]
|
||||||
|
base_path = Path(__file__).parent / "fixtures" / "base.xml"
|
||||||
|
xml = base_path.read_text()
|
||||||
|
res_xml = mixin_obj.with_context(
|
||||||
|
environment="prod"
|
||||||
|
)._update_form_view_from_env(xml, "form")
|
||||||
|
expected_xml_path = Path(__file__).parent / "fixtures" / "res2.xml"
|
||||||
|
expected_xml = expected_xml_path.read_text()
|
||||||
|
self.assertEqual(res_xml, expected_xml)
|
||||||
Loading…
Reference in New Issue