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