[IMP] base_changeset: overhaul security
parent
8dfc8caba4
commit
47fbaeda11
|
@ -88,7 +88,8 @@ class Base(models.AbstractModel):
|
||||||
{
|
{
|
||||||
field_name: value
|
field_name: value
|
||||||
for field_name, value in vals.items()
|
for field_name, value in vals.items()
|
||||||
if self._fields[field_name].required
|
if field_name in self._fields and
|
||||||
|
self._fields[field_name].required
|
||||||
and isinstance(
|
and isinstance(
|
||||||
self._fields[field_name], fields.Many2one,
|
self._fields[field_name], fields.Many2one,
|
||||||
)
|
)
|
||||||
|
|
|
@ -43,6 +43,13 @@ class ChangesetFieldRule(models.Model):
|
||||||
help="Use this rule only on records where this is true. "
|
help="Use this rule only on records where this is true. "
|
||||||
"Available variables: object, user",
|
"Available variables: object, user",
|
||||||
)
|
)
|
||||||
|
validator_group_ids = fields.Many2many(
|
||||||
|
'res.groups', 'changeset_field_rule_validator_group_rel',
|
||||||
|
string='Validator Groups', default=lambda self: self.env.ref(
|
||||||
|
'base_changeset.group_changeset_user',
|
||||||
|
raise_if_not_found=False,
|
||||||
|
) or self.env['res.groups'],
|
||||||
|
)
|
||||||
|
|
||||||
def init(self):
|
def init(self):
|
||||||
"""Ensure there is at most one rule with source_model_id NULL."""
|
"""Ensure there is at most one rule with source_model_id NULL."""
|
||||||
|
|
|
@ -397,8 +397,13 @@ class RecordChangesetChange(models.Model):
|
||||||
def _compute_user_can_validate_changeset(self):
|
def _compute_user_can_validate_changeset(self):
|
||||||
is_superuser = self.env.is_superuser()
|
is_superuser = self.env.is_superuser()
|
||||||
has_group = self.user_has_groups("base_changeset.group_changeset_user")
|
has_group = self.user_has_groups("base_changeset.group_changeset_user")
|
||||||
|
user_groups = self.env.user.groups_id
|
||||||
for rec in self:
|
for rec in self:
|
||||||
can_validate = rec._is_change_pending() and (is_superuser or has_group)
|
can_validate = rec._is_change_pending() and (
|
||||||
|
is_superuser
|
||||||
|
or rec.rule_id.validator_group_ids & user_groups
|
||||||
|
or has_group
|
||||||
|
)
|
||||||
if rec.rule_id.prevent_self_validation:
|
if rec.rule_id.prevent_self_validation:
|
||||||
can_validate = can_validate and rec.modified_by_id != self.env.user
|
can_validate = can_validate and rec.modified_by_id != self.env.user
|
||||||
rec.user_can_validate_changeset = can_validate
|
rec.user_can_validate_changeset = can_validate
|
||||||
|
|
|
@ -64,3 +64,12 @@ same that is passed in ``__changeset_rules_source_model``).
|
||||||
The source is used for the application of the rules, allowing to have a
|
The source is used for the application of the rules, allowing to have a
|
||||||
different rule for a different source. It is also stored on the changeset for
|
different rule for a different source. It is also stored on the changeset for
|
||||||
information.
|
information.
|
||||||
|
|
||||||
|
Notes on security
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Note that by default, changeset users see all changes on all configured
|
||||||
|
rules. This circumvents read restrictions on the original records, so if you
|
||||||
|
have restrictions on models with changeset rules, changeset users will still
|
||||||
|
see all changes of all records, and applying a change on an inaccessible record
|
||||||
|
will fail.
|
||||||
|
|
|
@ -17,10 +17,7 @@
|
||||||
<data noupdate="1">
|
<data noupdate="1">
|
||||||
<record id="group_changeset_manager" model="res.groups">
|
<record id="group_changeset_manager" model="res.groups">
|
||||||
<field name="users" eval="[(4, ref('base.user_root'))]" />
|
<field name="users" eval="[(4, ref('base.user_root'))]" />
|
||||||
</record>
|
<field name="implied_ids" eval="[(4, ref('group_changeset_user'))]" />
|
||||||
<record id="group_changeset_user" model="res.groups">
|
|
||||||
<field name="implied_ids" eval="[(4, ref('group_changeset_manager'))]" />
|
|
||||||
<field name="users" eval="[(4, ref('base.user_root'))]" />
|
|
||||||
</record>
|
</record>
|
||||||
</data>
|
</data>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
|
@ -16,4 +16,16 @@
|
||||||
name="domain_force"
|
name="domain_force"
|
||||||
>['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
|
>['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
|
||||||
</record>
|
</record>
|
||||||
|
<record id="rule_record_changeset_change_base_user" model="ir.rule">
|
||||||
|
<field name="name">Restrict changeset change access for users</field>
|
||||||
|
<field name="model_id" ref="model_record_changeset_change" />
|
||||||
|
<field name="groups" eval="[(4, ref('base.group_user'))]" />
|
||||||
|
<field name="domain_force">[('create_uid', '=', user.id)]</field>
|
||||||
|
</record>
|
||||||
|
<record id="rule_record_changeset_change_user" model="ir.rule">
|
||||||
|
<field name="name">Allow changeset change access for changeset users</field>
|
||||||
|
<field name="model_id" ref="model_record_changeset_change" />
|
||||||
|
<field name="groups" eval="[(4, ref('group_changeset_user'))]" />
|
||||||
|
<field name="domain_force">[('rule_id.validator_group_ids', 'in', user.groups_id.ids)]</field>
|
||||||
|
</record>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
|
@ -2,3 +2,4 @@ from . import test_changeset_flow
|
||||||
from . import test_changeset_field_type
|
from . import test_changeset_field_type
|
||||||
from . import test_changeset_origin
|
from . import test_changeset_origin
|
||||||
from . import test_changeset_field_rule
|
from . import test_changeset_field_rule
|
||||||
|
from . import test_changeset_security
|
||||||
|
|
|
@ -77,13 +77,19 @@ class TestChangesetFlow(ChangesetTestCommon, TransactionCase):
|
||||||
|
|
||||||
def test_create_new_changeset(self):
|
def test_create_new_changeset(self):
|
||||||
""" Create a new partner with a changeset """
|
""" Create a new partner with a changeset """
|
||||||
new = self.env["res.partner"].with_context(
|
new = (
|
||||||
test_record_changeset=True,
|
self.env["res.partner"]
|
||||||
).create({
|
.with_context(
|
||||||
"name": "partner",
|
test_record_changeset=True,
|
||||||
"street": "street",
|
)
|
||||||
"street2": "street2",
|
.create(
|
||||||
})
|
{
|
||||||
|
"name": "partner",
|
||||||
|
"street": "street",
|
||||||
|
"street2": "street2",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
self.assertEqual(new.count_pending_changesets, 1)
|
self.assertEqual(new.count_pending_changesets, 1)
|
||||||
self.assert_changeset(
|
self.assert_changeset(
|
||||||
new,
|
new,
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Copyright 2021 Hunki Enterprises BV (<https://hunki-enterprises.com>)
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from odoo.tests.common import TransactionCase
|
||||||
|
|
||||||
|
from .common import ChangesetTestCommon
|
||||||
|
|
||||||
|
|
||||||
|
class TestChangesetFlow(ChangesetTestCommon, TransactionCase):
|
||||||
|
""" Check that changesets don't leak information """
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.env['changeset.field.rule'].search([]).unlink()
|
||||||
|
self.rule = self.env['changeset.field.rule'].create({
|
||||||
|
'model_id': self.env.ref('base.model_ir_config_parameter').id,
|
||||||
|
'field_id': self.env.ref('base.field_ir_config_parameter__key').id,
|
||||||
|
'action': 'auto',
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_change_unprivileged_user(self):
|
||||||
|
"""
|
||||||
|
Check that unprivileged users can't see changesets they didn't create
|
||||||
|
"""
|
||||||
|
user = self.env.ref('base.user_demo')
|
||||||
|
self.env['ir.config_parameter'].with_context(
|
||||||
|
test_record_changeset=True,
|
||||||
|
).set_param('hello', 'world')
|
||||||
|
changeset = self.env['record.changeset.change'].search([
|
||||||
|
('rule_id', '=', self.rule.id),
|
||||||
|
])
|
||||||
|
self.assertTrue(changeset)
|
||||||
|
self.assertFalse(changeset.sudo(user).search([('id', '=', changeset.id)]))
|
|
@ -8,6 +8,7 @@
|
||||||
<field name="field_id" />
|
<field name="field_id" />
|
||||||
<field name="source_model_id" />
|
<field name="source_model_id" />
|
||||||
<field name="expression" />
|
<field name="expression" />
|
||||||
|
<field name="validator_group_ids" />
|
||||||
<field
|
<field
|
||||||
name="company_id"
|
name="company_id"
|
||||||
groups="base.group_multi_company"
|
groups="base.group_multi_company"
|
||||||
|
@ -42,6 +43,7 @@
|
||||||
<field name="source_model_id" widget="selection" />
|
<field name="source_model_id" widget="selection" />
|
||||||
<field name="prevent_self_validation" />
|
<field name="prevent_self_validation" />
|
||||||
<field name="expression" placeholder="True" />
|
<field name="expression" placeholder="True" />
|
||||||
|
<field name="validator_group_ids" widget="many2many_tags" />
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
</sheet>
|
</sheet>
|
||||||
|
@ -55,6 +57,7 @@
|
||||||
<field name="field_id" />
|
<field name="field_id" />
|
||||||
<field name="source_model_id" />
|
<field name="source_model_id" />
|
||||||
<field name="action" />
|
<field name="action" />
|
||||||
|
<field name="validator_group_ids" />
|
||||||
<group string="Group By" name="groupby">
|
<group string="Group By" name="groupby">
|
||||||
<filter
|
<filter
|
||||||
name="model_groupby"
|
name="model_groupby"
|
||||||
|
|
Loading…
Reference in New Issue