In this viewtiverse, the heroes remove the context dependencies for `get_views`, from the views and python fields (such as domain). To reduce inconsistencies and the number of rpc. Current issues: * There may be inconsistencies in views at the JavaScript level. Some overrides modify the behavior of get_views or domains on fields via context keys, therefore by changing the action, the rendering may be different. However, these views are cached. However, the cache key (Javascript) does not reflect the entire context, and requires additional post-processing from the server. * Multiple rpc for the same rendering. get_views being dependent on the context, as soon as it changes, a new rpc is performed. In most cases, when JavaScript needs the same view, there is no change depending on the context, the rpc is useless. * Inconsistency when rendering subviews, some views could be different depending on the context, this context can be modified in the view itself via the context attributes. However, the JavaScript client does not redo an rpc for each change of these sub-contexts. Therefore the result may be inconsistent. Solution: Limit as much as possible the number of context keys provided when calling get_views, and use the context provided as a cache key. The authorized keys are 'lang' and '*_view_ref'. For the cache key, options are added in the get_views method. Instead of using the context, it is inserted into python expressions. This will be evaluated by JavaScript and thus avoids inconsistencies. task-3414108 task-3414068 closes odoo/odoo#135145 Related: odoo/enterprise#47584 Signed-off-by: Raphael Collet <rco@odoo.com>
301 lines
14 KiB
Python
301 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from lxml import etree
|
|
|
|
from odoo.exceptions import AccessError
|
|
from odoo.addons.base.tests.common import TransactionCaseWithUserDemo
|
|
from odoo.tests.common import TransactionCase
|
|
from odoo.tools.misc import mute_logger
|
|
from odoo import Command
|
|
|
|
# test group that demo user should not have
|
|
GROUP_SYSTEM = 'base.group_system'
|
|
|
|
|
|
class TestACL(TransactionCaseWithUserDemo):
|
|
|
|
def setUp(self):
|
|
super(TestACL, self).setUp()
|
|
self.erp_system_group = self.env.ref(GROUP_SYSTEM)
|
|
|
|
def _set_field_groups(self, model, field_name, groups):
|
|
field = model._fields[field_name]
|
|
self.patch(field, 'groups', groups)
|
|
|
|
def test_field_visibility_restriction(self):
|
|
"""Check that model-level ``groups`` parameter effectively restricts access to that
|
|
field for users who do not belong to one of the explicitly allowed groups"""
|
|
currency = self.env['res.currency'].with_user(self.user_demo)
|
|
|
|
# Add a view that adds a label for the field we are going to check
|
|
extension = self.env["ir.ui.view"].create({
|
|
"name": "Add separate label for decimal_places",
|
|
"model": "res.currency",
|
|
"inherit_id": self.env.ref("base.view_currency_form").id,
|
|
"arch": """
|
|
<data>
|
|
<field name="decimal_places" position="attributes">
|
|
<attribute name="nolabel">1</attribute>
|
|
</field>
|
|
<field name="decimal_places" position="before">
|
|
<label for="decimal_places"/>
|
|
</field>
|
|
</data>
|
|
""",
|
|
})
|
|
currency = currency.with_context(check_view_ids=extension.ids)
|
|
|
|
# Verify the test environment first
|
|
original_fields = currency.fields_get([])
|
|
with self.debug_mode():
|
|
# <group groups="base.group_no_one">
|
|
# <group string="Price Accuracy">
|
|
# <field name="rounding"/>
|
|
# <field name="decimal_places"/>
|
|
# </group>
|
|
form_view = currency.get_view(False, 'form')
|
|
view_arch = etree.fromstring(form_view.get('arch'))
|
|
has_group_system = self.user_demo.has_group(GROUP_SYSTEM)
|
|
self.assertFalse(has_group_system, "`demo` user should not belong to the restricted group before the test")
|
|
self.assertIn('decimal_places', original_fields, "'decimal_places' field must be properly visible before the test")
|
|
self.assertNotEqual(view_arch.xpath("//field[@name='decimal_places'][@nolabel='1']"), [],
|
|
"Field 'decimal_places' must be found in view definition before the test")
|
|
self.assertNotEqual(view_arch.xpath("//label[@for='decimal_places']"), [],
|
|
"Label for 'decimal_places' must be found in view definition before the test")
|
|
|
|
# restrict access to the field and check it's gone
|
|
self._set_field_groups(currency, 'decimal_places', GROUP_SYSTEM)
|
|
|
|
fields = currency.fields_get([])
|
|
form_view = currency.get_view(False, 'form')
|
|
view_arch = etree.fromstring(form_view.get('arch'))
|
|
self.assertNotIn('decimal_places', fields, "'decimal_places' field should be gone")
|
|
self.assertEqual(view_arch.xpath("//field[@name='decimal_places']"), [],
|
|
"Field 'decimal_places' must not be found in view definition")
|
|
self.assertEqual(view_arch.xpath("//label[@for='decimal_places']"), [],
|
|
"Label for 'decimal_places' must not be found in view definition")
|
|
|
|
# Make demo user a member of the restricted group and check that the field is back
|
|
self.erp_system_group.users += self.user_demo
|
|
has_group_system = self.user_demo.has_group(GROUP_SYSTEM)
|
|
fields = currency.fields_get([])
|
|
with self.debug_mode():
|
|
form_view = currency.get_view(False, 'form')
|
|
view_arch = etree.fromstring(form_view.get('arch'))
|
|
self.assertTrue(has_group_system, "`demo` user should now belong to the restricted group")
|
|
self.assertIn('decimal_places', fields, "'decimal_places' field must be properly visible again")
|
|
self.assertNotEqual(view_arch.xpath("//field[@name='decimal_places']"), [],
|
|
"Field 'decimal_places' must be found in view definition again")
|
|
self.assertNotEqual(view_arch.xpath("//label[@for='decimal_places']"), [],
|
|
"Label for 'decimal_places' must be found in view definition again")
|
|
|
|
@mute_logger('odoo.models')
|
|
def test_field_crud_restriction(self):
|
|
"Read/Write RPC access to restricted field should be forbidden"
|
|
partner = self.env['res.partner'].browse(1).with_user(self.user_demo)
|
|
|
|
# Verify the test environment first
|
|
has_group_system = self.user_demo.has_group(GROUP_SYSTEM)
|
|
self.assertFalse(has_group_system, "`demo` user should not belong to the restricted group")
|
|
self.assertTrue(partner.read(['bank_ids']))
|
|
self.assertTrue(partner.write({'bank_ids': []}))
|
|
|
|
# Now restrict access to the field and check it's forbidden
|
|
self._set_field_groups(partner, 'bank_ids', GROUP_SYSTEM)
|
|
|
|
with self.assertRaises(AccessError):
|
|
partner.search_fetch([], ['bank_ids'])
|
|
with self.assertRaises(AccessError):
|
|
partner.fetch(['bank_ids'])
|
|
with self.assertRaises(AccessError):
|
|
partner.read(['bank_ids'])
|
|
with self.assertRaises(AccessError):
|
|
partner.write({'bank_ids': []})
|
|
|
|
# Add the restricted group, and check that it works again
|
|
self.erp_system_group.users += self.user_demo
|
|
has_group_system = self.user_demo.has_group(GROUP_SYSTEM)
|
|
self.assertTrue(has_group_system, "`demo` user should now belong to the restricted group")
|
|
self.assertTrue(partner.read(['bank_ids']))
|
|
self.assertTrue(partner.write({'bank_ids': []}))
|
|
|
|
@mute_logger('odoo.models')
|
|
def test_fields_browse_restriction(self):
|
|
"""Test access to records having restricted fields"""
|
|
# Invalidate cache to avoid restricted value to be available
|
|
# in the cache
|
|
self.env.invalidate_all()
|
|
partner = self.env['res.partner'].with_user(self.user_demo)
|
|
self._set_field_groups(partner, 'email', GROUP_SYSTEM)
|
|
|
|
# accessing fields must no raise exceptions...
|
|
partner = partner.search([], limit=1)
|
|
partner.name
|
|
# ... except if they are restricted
|
|
with self.assertRaises(AccessError):
|
|
with mute_logger('odoo.models'):
|
|
partner.email
|
|
|
|
def test_view_create_edit_button(self):
|
|
""" Test form view Create, Edit, Delete button visibility based on access right of model.
|
|
Test the user with and without access in the same unit test / transaction
|
|
to test the views cache is properly working """
|
|
methods = ['create', 'edit', 'delete']
|
|
company = self.env['res.company'].with_user(self.user_demo)
|
|
company_view = company.get_view(False, 'form')
|
|
view_arch = etree.fromstring(company_view['arch'])
|
|
|
|
# demo not part of the group_system, create edit and delete must be False
|
|
for method in methods:
|
|
self.assertEqual(view_arch.get(method), 'False')
|
|
|
|
# demo part of the group_system, create edit and delete must not be specified
|
|
company = self.env['res.company'].with_user(self.env.ref("base.user_admin"))
|
|
company_view = company.get_view(False, 'form')
|
|
view_arch = etree.fromstring(company_view['arch'])
|
|
for method in methods:
|
|
self.assertIsNone(view_arch.get(method))
|
|
|
|
def test_m2o_field_create_edit(self):
|
|
""" Test many2one field Create and Edit option visibility based on access rights of relation field
|
|
Test the user with and without access in the same unit test / transaction
|
|
to test the views cache is properly working """
|
|
methods = ['create', 'write']
|
|
company = self.env['res.company'].with_user(self.user_demo)
|
|
company_view = company.get_view(False, 'form')
|
|
view_arch = etree.fromstring(company_view['arch'])
|
|
field_node = view_arch.xpath("//field[@name='currency_id']")
|
|
self.assertTrue(len(field_node), "currency_id field should be in company from view")
|
|
for method in methods:
|
|
self.assertEqual(field_node[0].get('can_' + method), 'False')
|
|
|
|
company = self.env['res.company'].with_user(self.env.ref("base.user_admin"))
|
|
company_view = company.get_view(False, 'form')
|
|
view_arch = etree.fromstring(company_view['arch'])
|
|
field_node = view_arch.xpath("//field[@name='currency_id']")
|
|
for method in methods:
|
|
self.assertEqual(field_node[0].get('can_' + method), 'True')
|
|
|
|
def test_get_views_fields(self):
|
|
""" Tests fields restricted to group_system are not passed when calling `get_views` as demo
|
|
but the same fields are well passed when calling `get_views` as admin"""
|
|
Partner = self.env['res.partner']
|
|
self._set_field_groups(Partner, 'email', GROUP_SYSTEM)
|
|
views = Partner.with_user(self.user_demo).get_views([(False, 'form')])
|
|
self.assertFalse('email' in views['models']['res.partner'])
|
|
views = Partner.with_user(self.env.ref("base.user_admin")).get_views([(False, 'form')])
|
|
self.assertTrue('email' in views['models']['res.partner'])
|
|
|
|
|
|
class TestIrRule(TransactionCaseWithUserDemo):
|
|
|
|
def test_ir_rule(self):
|
|
model_res_partner = self.env.ref('base.model_res_partner')
|
|
group_user = self.env.ref('base.group_user')
|
|
|
|
# create an ir_rule for the Employee group with an blank domain
|
|
rule1 = self.env['ir.rule'].create({
|
|
'name': 'test_rule1',
|
|
'model_id': model_res_partner.id,
|
|
'domain_force': False,
|
|
'groups': [Command.set(group_user.ids)],
|
|
})
|
|
|
|
# read as demo user the partners (one blank domain)
|
|
partners_demo = self.env['res.partner'].with_user(self.user_demo)
|
|
partners = partners_demo.search([])
|
|
self.assertTrue(partners, "Demo user should see some partner.")
|
|
|
|
# same with domain 1=1
|
|
rule1.domain_force = "[(1,'=',1)]"
|
|
partners = partners_demo.search([])
|
|
self.assertTrue(partners, "Demo user should see some partner.")
|
|
|
|
# same with domain []
|
|
rule1.domain_force = "[]"
|
|
partners = partners_demo.search([])
|
|
self.assertTrue(partners, "Demo user should see some partner.")
|
|
|
|
# create another ir_rule for the Employee group (to test multiple rules)
|
|
rule2 = self.env['ir.rule'].create({
|
|
'name': 'test_rule2',
|
|
'model_id': model_res_partner.id,
|
|
'domain_force': False,
|
|
'groups': [Command.set(group_user.ids)],
|
|
})
|
|
|
|
# read as demo user with domains [] and blank
|
|
partners = partners_demo.search([])
|
|
self.assertTrue(partners, "Demo user should see some partner.")
|
|
|
|
# same with domains 1=1 and blank
|
|
rule1.domain_force = "[(1,'=',1)]"
|
|
partners = partners_demo.search([])
|
|
self.assertTrue(partners, "Demo user should see some partner.")
|
|
|
|
# same with domains 1=1 and 1=1
|
|
rule2.domain_force = "[(1,'=',1)]"
|
|
partners = partners_demo.search([])
|
|
self.assertTrue(partners, "Demo user should see some partner.")
|
|
|
|
# create another ir_rule for the Employee group (to test multiple rules)
|
|
rule3 = self.env['ir.rule'].create({
|
|
'name': 'test_rule3',
|
|
'model_id': model_res_partner.id,
|
|
'domain_force': False,
|
|
'groups': [Command.set(group_user.ids)],
|
|
})
|
|
|
|
# read the partners as demo user
|
|
partners = partners_demo.search([])
|
|
self.assertTrue(partners, "Demo user should see some partner.")
|
|
|
|
# same with domains 1=1, 1=1 and 1=1
|
|
rule3.domain_force = "[(1,'=',1)]"
|
|
partners = partners_demo.search([])
|
|
self.assertTrue(partners, "Demo user should see some partner.")
|
|
|
|
# modify the global rule on res_company which triggers a recursive check
|
|
# of the rules on company
|
|
global_rule = self.env.ref('base.res_company_rule_employee')
|
|
global_rule.domain_force = "[('id','in', company_ids)]"
|
|
|
|
# read as demo user (exercising the global company rule)
|
|
partners = partners_demo.search([])
|
|
self.assertTrue(partners, "Demo user should see some partner.")
|
|
|
|
# Modify the ir_rule for employee to have a rule that fordids seeing any
|
|
# record. We use a domain with implicit AND operator for later tests on
|
|
# normalization.
|
|
rule2.domain_force = "[('id','=',False),('name','=',False)]"
|
|
|
|
# check that demo user still sees partners, because group-rules are OR'ed
|
|
partners = partners_demo.search([])
|
|
self.assertTrue(partners, "Demo user should see some partner.")
|
|
|
|
# create a new group with demo user in it, and a complex rule
|
|
group_test = self.env['res.groups'].create({
|
|
'name': 'Test Group',
|
|
'users': [Command.set(self.user_demo.ids)],
|
|
})
|
|
|
|
# add the rule to the new group, with a domain containing an implicit
|
|
# AND operator, which is more tricky because it will have to be
|
|
# normalized before combining it
|
|
rule3.write({
|
|
'domain_force': "[('name','!=',False),('id','!=',False)]",
|
|
'groups': [Command.set(group_test.ids)],
|
|
})
|
|
|
|
# read the partners again as demo user, which should give results
|
|
partners = partners_demo.search([])
|
|
self.assertTrue(partners, "Demo user should see partners even with the combined rules.")
|
|
|
|
# delete global domains (to combine only group domains)
|
|
self.env['ir.rule'].search([('groups', '=', False)]).unlink()
|
|
|
|
# read the partners as demo user (several group domains, no global domain)
|
|
partners = partners_demo.search([])
|
|
self.assertTrue(partners, "Demo user should see some partners.")
|