1# -*- coding: utf-8 -*-
2# Part of Odoo. See LICENSE file for full copyright and licensing details.
3
4from lxml import etree
5
6from odoo.exceptions import AccessError
7from odoo.addons.base.tests.common import TransactionCaseWithUserDemo
8from odoo.tests.common import TransactionCase
9from odoo.tools.misc import mute_logger
10
11# test group that demo user should not have
12GROUP_SYSTEM = 'base.group_system'
13
14
15class TestACL(TransactionCaseWithUserDemo):
16
17    def setUp(self):
18        super(TestACL, self).setUp()
19        self.erp_system_group = self.env.ref(GROUP_SYSTEM)
20
21    def _set_field_groups(self, model, field_name, groups):
22        field = model._fields[field_name]
23        self.patch(field, 'groups', groups)
24
25    def test_field_visibility_restriction(self):
26        """Check that model-level ``groups`` parameter effectively restricts access to that
27           field for users who do not belong to one of the explicitly allowed groups"""
28        currency = self.env['res.currency'].with_user(self.user_demo)
29
30        # Add a view that adds a label for the field we are going to check
31        extension = self.env["ir.ui.view"].create({
32            "name": "Add separate label for decimal_places",
33            "model": "res.currency",
34            "inherit_id": self.env.ref("base.view_currency_form").id,
35            "arch": """
36                <data>
37                    <field name="decimal_places" position="attributes">
38                        <attribute name="nolabel">1</attribute>
39                    </field>
40                    <field name="decimal_places" position="before">
41                        <label for="decimal_places"/>
42                    </field>
43                </data>
44            """,
45        })
46        currency = currency.with_context(check_view_ids=extension.ids)
47
48        # Verify the test environment first
49        original_fields = currency.fields_get([])
50        form_view = currency.fields_view_get(False, 'form')
51        view_arch = etree.fromstring(form_view.get('arch'))
52        has_group_system = self.user_demo.has_group(GROUP_SYSTEM)
53        self.assertFalse(has_group_system, "`demo` user should not belong to the restricted group before the test")
54        self.assertIn('decimal_places', original_fields, "'decimal_places' field must be properly visible before the test")
55        self.assertNotEqual(view_arch.xpath("//field[@name='decimal_places'][@nolabel='1']"), [],
56                             "Field 'decimal_places' must be found in view definition before the test")
57        self.assertNotEqual(view_arch.xpath("//label[@for='decimal_places']"), [],
58                             "Label for 'decimal_places' must be found in view definition before the test")
59
60        # restrict access to the field and check it's gone
61        self._set_field_groups(currency, 'decimal_places', GROUP_SYSTEM)
62
63        fields = currency.fields_get([])
64        form_view = currency.fields_view_get(False, 'form')
65        view_arch = etree.fromstring(form_view.get('arch'))
66        self.assertNotIn('decimal_places', fields, "'decimal_places' field should be gone")
67        self.assertEqual(view_arch.xpath("//field[@name='decimal_places']"), [],
68                          "Field 'decimal_places' must not be found in view definition")
69        self.assertEqual(view_arch.xpath("//label[@for='decimal_places']"), [],
70                          "Label for 'decimal_places' must not be found in view definition")
71
72        # Make demo user a member of the restricted group and check that the field is back
73        self.erp_system_group.users += self.user_demo
74        has_group_system = self.user_demo.has_group(GROUP_SYSTEM)
75        fields = currency.fields_get([])
76        form_view = currency.fields_view_get(False, 'form')
77        view_arch = etree.fromstring(form_view.get('arch'))
78        self.assertTrue(has_group_system, "`demo` user should now belong to the restricted group")
79        self.assertIn('decimal_places', fields, "'decimal_places' field must be properly visible again")
80        self.assertNotEqual(view_arch.xpath("//field[@name='decimal_places']"), [],
81                             "Field 'decimal_places' must be found in view definition again")
82        self.assertNotEqual(view_arch.xpath("//label[@for='decimal_places']"), [],
83                             "Label for 'decimal_places' must be found in view definition again")
84
85    @mute_logger('odoo.models')
86    def test_field_crud_restriction(self):
87        "Read/Write RPC access to restricted field should be forbidden"
88        partner = self.env['res.partner'].browse(1).with_user(self.user_demo)
89
90        # Verify the test environment first
91        has_group_system = self.user_demo.has_group(GROUP_SYSTEM)
92        self.assertFalse(has_group_system, "`demo` user should not belong to the restricted group")
93        self.assertTrue(partner.read(['bank_ids']))
94        self.assertTrue(partner.write({'bank_ids': []}))
95
96        # Now restrict access to the field and check it's forbidden
97        self._set_field_groups(partner, 'bank_ids', GROUP_SYSTEM)
98
99        with self.assertRaises(AccessError):
100            partner.read(['bank_ids'])
101        with self.assertRaises(AccessError):
102            partner.write({'bank_ids': []})
103
104        # Add the restricted group, and check that it works again
105        self.erp_system_group.users += self.user_demo
106        has_group_system = self.user_demo.has_group(GROUP_SYSTEM)
107        self.assertTrue(has_group_system, "`demo` user should now belong to the restricted group")
108        self.assertTrue(partner.read(['bank_ids']))
109        self.assertTrue(partner.write({'bank_ids': []}))
110
111    @mute_logger('odoo.models')
112    def test_fields_browse_restriction(self):
113        """Test access to records having restricted fields"""
114        # Invalidate cache to avoid restricted value to be available
115        # in the cache
116        self.user_demo.invalidate_cache()
117        partner = self.env['res.partner'].with_user(self.user_demo)
118        self._set_field_groups(partner, 'email', GROUP_SYSTEM)
119
120        # accessing fields must no raise exceptions...
121        partner = partner.search([], limit=1)
122        partner.name
123        # ... except if they are restricted
124        with self.assertRaises(AccessError):
125            with mute_logger('odoo.models'):
126                partner.email
127
128    def test_view_create_edit_button_invisibility(self):
129        """ Test form view Create, Edit, Delete button visibility based on access right of model"""
130        methods = ['create', 'edit', 'delete']
131        company = self.env['res.company'].with_user(self.user_demo)
132        company_view = company.fields_view_get(False, 'form')
133        view_arch = etree.fromstring(company_view['arch'])
134        for method in methods:
135            self.assertEqual(view_arch.get(method), 'false')
136
137    def test_view_create_edit_button_visibility(self):
138        """ Test form view Create, Edit, Delete button visibility based on access right of model"""
139        self.erp_system_group.users += self.user_demo
140        methods = ['create', 'edit', 'delete']
141        company = self.env['res.company'].with_user(self.user_demo)
142        company_view = company.fields_view_get(False, 'form')
143        view_arch = etree.fromstring(company_view['arch'])
144        for method in methods:
145            self.assertIsNone(view_arch.get(method))
146
147    def test_m2o_field_create_edit_invisibility(self):
148        """ Test many2one field Create and Edit option visibility based on access rights of relation field"""
149        methods = ['create', 'write']
150        company = self.env['res.company'].with_user(self.user_demo)
151        company_view = company.fields_view_get(False, 'form')
152        view_arch = etree.fromstring(company_view['arch'])
153        field_node = view_arch.xpath("//field[@name='currency_id']")
154        self.assertTrue(len(field_node), "currency_id field should be in company from view")
155        for method in methods:
156            self.assertEqual(field_node[0].get('can_' + method), 'false')
157
158    def test_m2o_field_create_edit_visibility(self):
159        """ Test many2one field Create and Edit option visibility based on access rights of relation field"""
160        self.erp_system_group.users += self.user_demo
161        methods = ['create', 'write']
162        company = self.env['res.company'].with_user(self.user_demo)
163        company_view = company.fields_view_get(False, 'form')
164        view_arch = etree.fromstring(company_view['arch'])
165        field_node = view_arch.xpath("//field[@name='currency_id']")
166        self.assertTrue(len(field_node), "currency_id field should be in company from view")
167        for method in methods:
168            self.assertEqual(field_node[0].get('can_' + method), 'true')
169
170
171class TestIrRule(TransactionCaseWithUserDemo):
172
173    def test_ir_rule(self):
174        model_res_partner = self.env.ref('base.model_res_partner')
175        group_user = self.env.ref('base.group_user')
176
177        # create an ir_rule for the Employee group with an blank domain
178        rule1 = self.env['ir.rule'].create({
179            'name': 'test_rule1',
180            'model_id': model_res_partner.id,
181            'domain_force': False,
182            'groups': [(6, 0, group_user.ids)],
183        })
184
185        # read as demo user the partners (one blank domain)
186        partners_demo = self.env['res.partner'].with_user(self.user_demo)
187        partners = partners_demo.search([])
188        self.assertTrue(partners, "Demo user should see some partner.")
189
190        # same with domain 1=1
191        rule1.domain_force = "[(1,'=',1)]"
192        partners = partners_demo.search([])
193        self.assertTrue(partners, "Demo user should see some partner.")
194
195        # same with domain []
196        rule1.domain_force = "[]"
197        partners = partners_demo.search([])
198        self.assertTrue(partners, "Demo user should see some partner.")
199
200        # create another ir_rule for the Employee group (to test multiple rules)
201        rule2 = self.env['ir.rule'].create({
202            'name': 'test_rule2',
203            'model_id': model_res_partner.id,
204            'domain_force': False,
205            'groups': [(6, 0, group_user.ids)],
206        })
207
208        # read as demo user with domains [] and blank
209        partners = partners_demo.search([])
210        self.assertTrue(partners, "Demo user should see some partner.")
211
212        # same with domains 1=1 and blank
213        rule1.domain_force = "[(1,'=',1)]"
214        partners = partners_demo.search([])
215        self.assertTrue(partners, "Demo user should see some partner.")
216
217        # same with domains 1=1 and 1=1
218        rule2.domain_force = "[(1,'=',1)]"
219        partners = partners_demo.search([])
220        self.assertTrue(partners, "Demo user should see some partner.")
221
222        # create another ir_rule for the Employee group (to test multiple rules)
223        rule3 = self.env['ir.rule'].create({
224            'name': 'test_rule3',
225            'model_id': model_res_partner.id,
226            'domain_force': False,
227            'groups': [(6, 0, group_user.ids)],
228        })
229
230        # read the partners as demo user
231        partners = partners_demo.search([])
232        self.assertTrue(partners, "Demo user should see some partner.")
233
234        # same with domains 1=1, 1=1 and 1=1
235        rule3.domain_force = "[(1,'=',1)]"
236        partners = partners_demo.search([])
237        self.assertTrue(partners, "Demo user should see some partner.")
238
239        # modify the global rule on res_company which triggers a recursive check
240        # of the rules on company
241        global_rule = self.env.ref('base.res_company_rule_employee')
242        global_rule.domain_force = "[('id','in', company_ids)]"
243
244        # read as demo user (exercising the global company rule)
245        partners = partners_demo.search([])
246        self.assertTrue(partners, "Demo user should see some partner.")
247
248        # Modify the ir_rule for employee to have a rule that fordids seeing any
249        # record. We use a domain with implicit AND operator for later tests on
250        # normalization.
251        rule2.domain_force = "[('id','=',False),('name','=',False)]"
252
253        # check that demo user still sees partners, because group-rules are OR'ed
254        partners = partners_demo.search([])
255        self.assertTrue(partners, "Demo user should see some partner.")
256
257        # create a new group with demo user in it, and a complex rule
258        group_test = self.env['res.groups'].create({
259            'name': 'Test Group',
260            'users': [(6, 0, self.user_demo.ids)],
261        })
262
263        # add the rule to the new group, with a domain containing an implicit
264        # AND operator, which is more tricky because it will have to be
265        # normalized before combining it
266        rule3.write({
267            'domain_force': "[('name','!=',False),('id','!=',False)]",
268            'groups': [(6, 0, group_test.ids)],
269        })
270
271        # read the partners again as demo user, which should give results
272        partners = partners_demo.search([])
273        self.assertTrue(partners, "Demo user should see partners even with the combined rules.")
274
275        # delete global domains (to combine only group domains)
276        self.env['ir.rule'].search([('groups', '=', False)]).unlink()
277
278        # read the partners as demo user (several group domains, no global domain)
279        partners = partners_demo.search([])
280        self.assertTrue(partners, "Demo user should see some partners.")
281