diff options
| author | Christophe Simonis <chs@odoo.com> | 2019-02-20 17:37:22 +0100 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2019-02-20 17:37:22 +0100 |
| commit | db1c7765cba0692cfc4eda34ead000b048da4dae (patch) | |
| tree | 34e351e1714858f9b607e216f505b3e4ce453115 /odoo | |
| parent | 7151f98358fc3cab13c1cc7400c5e0b58d662978 (diff) | |
| parent | 2c495502aa37374f3748d9ff4ab7cf9e8d9a6d6e (diff) | |
[MERGE] forward port branch saas-11.3 up to 2c495502aa3
Diffstat (limited to 'odoo')
| -rw-r--r-- | odoo/addons/test_new_api/tests/test_new_fields.py | 50 | ||||
| -rw-r--r-- | odoo/api.py | 11 | ||||
| -rw-r--r-- | odoo/fields.py | 22 | ||||
| -rw-r--r-- | odoo/models.py | 62 |
4 files changed, 118 insertions, 27 deletions
diff --git a/odoo/addons/test_new_api/tests/test_new_fields.py b/odoo/addons/test_new_api/tests/test_new_fields.py index 2dfe74a4769..6a7ed7daa30 100644 --- a/odoo/addons/test_new_api/tests/test_new_fields.py +++ b/odoo/addons/test_new_api/tests/test_new_fields.py @@ -369,6 +369,26 @@ class TestFields(common.TransactionCase): self.assertEqual(record.bar3, 'C') self.assertCountEqual(log, ['compute']) + def test_13_inverse_access(self): + """ test access rights on inverse fields """ + foo = self.env['test_new_api.category'].create({'name': 'Foo'}) + user = self.env['res.users'].create({'name': 'Foo', 'login': 'foo'}) + self.assertFalse(user.has_group('base.group_system')) + # add group on non-stored inverse field + self.patch(type(foo).display_name, 'groups', 'base.group_system') + with self.assertRaises(AccessError): + foo.sudo(user).display_name = 'Forbidden' + + def test_13_inverse_access(self): + """ test access rights on inverse fields """ + foo = self.env['test_new_api.category'].create({'name': 'Foo'}) + user = self.env['res.users'].create({'name': 'Foo', 'login': 'foo'}) + self.assertFalse(user.has_group('base.group_system')) + # add group on non-stored inverse field + self.patch(type(foo).display_name, 'groups', 'base.group_system') + with self.assertRaises(AccessError): + foo.sudo(user).display_name = 'Forbidden' + def test_14_search(self): """ test search on computed fields """ discussion = self.env.ref('test_new_api.discussion_0') @@ -771,6 +791,10 @@ class TestFields(common.TransactionCase): self.env['ir.property'].create({'name': 'foo', 'fields_id': field_tag_id.id, 'value': tag0, 'type': 'many2one'}) + # assumption: users don't have access to 'ir.property' + accesses = self.env['ir.model.access'].search([('model_id.model', '=', 'ir.property')]) + accesses.write(dict.fromkeys(['perm_read', 'perm_write', 'perm_create', 'perm_unlink'], False)) + # create/modify a record, and check the value for each user record = self.env['test_new_api.company'].create({ 'foo': 'main', @@ -824,6 +848,13 @@ class TestFields(common.TransactionCase): self.assertEqual(record.sudo(user1).foo, False) self.assertEqual(record.sudo(user2).foo, 'default') + # set field with 'force_company' in context + record.sudo(user0).with_context(force_company=company1.id).foo = 'beta' + record.invalidate_cache() + self.assertEqual(record.sudo(user0).foo, 'main') + self.assertEqual(record.sudo(user1).foo, 'beta') + self.assertEqual(record.sudo(user2).foo, 'default') + # create company record and attribute company_record = self.env['test_new_api.company'].create({'foo': 'ABC'}) attribute_record = self.env['test_new_api.company.attr'].create({ @@ -843,6 +874,25 @@ class TestFields(common.TransactionCase): self.assertEqual(attribute_record.bar, 'DEFDEF') self.assertFalse(self.env.has_todo()) + # add group on company-dependent field + self.assertFalse(user0.has_group('base.group_system')) + self.patch(type(record).foo, 'groups', 'base.group_system') + with self.assertRaises(AccessError): + record.sudo(user0).foo = 'forbidden' + + user0.write({'groups_id': [(4, self.env.ref('base.group_system').id)]}) + record.sudo(user0).foo = 'yes we can' + + # add ir.rule to prevent access on record + self.assertTrue(user0.has_group('base.group_user')) + rule = self.env['ir.rule'].create({ + 'model_id': self.env['ir.model']._get_id(record._name), + 'groups': [self.env.ref('base.group_user').id], + 'domain_force': str([('id', '!=', record.id)]), + }) + with self.assertRaises(AccessError): + record.sudo(user0).foo = 'forbidden' + def test_30_read(self): """ test computed fields as returned by read(). """ discussion = self.env.ref('test_new_api.discussion_0') diff --git a/odoo/api.py b/odoo/api.py index dffbc0e633a..a415461296e 100644 --- a/odoo/api.py +++ b/odoo/api.py @@ -55,7 +55,7 @@ from weakref import WeakSet from decorator import decorate, decorator from werkzeug.local import Local, release_local -from odoo.tools import frozendict, classproperty, StackMap +from odoo.tools import frozendict, classproperty, StackMap, pycompat from odoo.exceptions import CacheMiss _logger = logging.getLogger(__name__) @@ -1036,7 +1036,7 @@ class Cache(object): """ Return the value of ``field`` for ``record``. """ key = record.env.cache_key(field) try: - value = self._data[key][field][record.id] + value = self._data[key][field][record._ids[0]] except KeyError: raise CacheMiss(record, field) @@ -1045,7 +1045,12 @@ class Cache(object): def set(self, record, field, value): """ Set the value of ``field`` for ``record``. """ key = record.env.cache_key(field) - self._data[key][field][record.id] = value + self._data[key][field][record._ids[0]] = value + + def update(self, records, field, values): + """ Set the values of ``field`` for several ``records``. """ + key = records.env.cache_key(field) + self._data[key][field].update(pycompat.izip(records._ids, values)) def remove(self, record, field): """ Remove the value of ``field`` for ``record``. """ diff --git a/odoo/fields.py b/odoo/fields.py index f054bf5a90c..1efc86de888 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -643,13 +643,25 @@ class Field(MetaField('DummyField', (object,), {})): return model.env['ir.property'].get(self.name, self.model_name) def _compute_company_dependent(self, records): - Property = records.env['ir.property'] + # read property as superuser, as the current user may not have access + context = records.env.context + if 'force_company' not in context: + field_id = records.env['ir.model.fields']._get_id(self.model_name, self.name) + company = records.env['res.company']._company_default_get(self.model_name, field_id) + context = dict(context, force_company=company.id) + Property = records.env(user=SUPERUSER_ID, context=context)['ir.property'] values = Property.get_multi(self.name, self.model_name, records.ids) for record in records: record[self.name] = values.get(record.id) def _inverse_company_dependent(self, records): - Property = records.env['ir.property'] + # update property as superuser, as the current user may not have access + context = records.env.context + if 'force_company' not in context: + field_id = records.env['ir.model.fields']._get_id(self.model_name, self.name) + company = records.env['res.company']._company_default_get(self.model_name, field_id) + context = dict(context, force_company=company.id) + Property = records.env(user=SUPERUSER_ID, context=context)['ir.property'] values = { record.id: self.convert_to_write(record[self.name], record) for record in records @@ -2129,7 +2141,8 @@ class Many2one(_Relational): return () def convert_to_record(self, value, record): - return record.env[self.comodel_name]._browse(value, record.env, record._prefetch) + # use registry to avoid creating a recordset for the model + return record.env.registry[self.comodel_name]._browse(value, record.env, record._prefetch) def convert_to_read(self, value, record, use_name_get=True): if use_name_get and value: @@ -2249,7 +2262,8 @@ class _RelationalMulti(_Relational): raise ValueError("Wrong value for %s: %s" % (self, value)) def convert_to_record(self, value, record): - return record.env[self.comodel_name]._browse(value, record.env, record._prefetch) + # use registry to avoid creating a recordset for the model + return record.env.registry[self.comodel_name]._browse(value, record.env, record._prefetch) def convert_to_read(self, value, record, use_name_get=True): return value.ids diff --git a/odoo/models.py b/odoo/models.py index 2c3b475d31e..6423e18f314 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -2767,20 +2767,22 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): # retrieve results from records; this takes values from the cache and # computes remaining fields self = self.with_prefetch(self._prefetch.copy()) - data = {record: {'id': record.id} for record in self} - missing = set() + data = [(record, {'id': record._ids[0]}) for record in self] use_name_get = (load == '_classic_read') for name in (stored + inherited + computed): convert = self._fields[name].convert_to_read # restrict the prefetching of self's model to self; this avoids # computing fields on a larger recordset than self self._prefetch[self._name] = set(self._ids) - for record in self: + for record, vals in data: + # missing records have their vals empty + if not vals: + continue try: - data[record][name] = convert(record[name], record, use_name_get) + vals[name] = convert(record[name], record, use_name_get) except MissingError: - missing.add(record) - result = [data[record] for record in self if record not in missing] + vals.clear() + result = [vals for record, vals in data if vals] return result @@ -2875,36 +2877,46 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): res = 'pg_size_pretty(length(%s)::bigint)' % res return '%s as "%s"' % (res, col) + # selected fields are: 'id' followed by fields_pre qual_names = [qualify(name) for name in [self._fields['id']] + fields_pre] # determine the actual query to execute from_clause, where_clause, params = query.get_sql() query_str = "SELECT %s FROM %s WHERE %s" % (",".join(qual_names), from_clause, where_clause) - result = [] + # fetch one list of record values per field + field_values_list = [[] for name in qual_names] param_pos = params.index(param_ids) for sub_ids in cr.split_for_in_conditions(self.ids): params[param_pos] = tuple(sub_ids) cr.execute(query_str, params) - result.extend(cr.dictfetchall()) + for row in cr.fetchall(): + for values, val in pycompat.izip(field_values_list, row): + values.append(val) - ids = [vals['id'] for vals in result] + ids = field_values_list.pop(0) fetched = self.browse(ids) if ids: # translate the fields if necessary if context.get('lang'): - for field in fields_pre: + for field, values in pycompat.izip(fields_pre, field_values_list): if not field.inherited and callable(field.translate): name = field.name translate = field.get_trans_func(fetched) - for vals in result: - vals[name] = translate(vals['id'], vals[name]) + for index in range(len(ids)): + values[index] = translate(ids[index], values[index]) # store result in cache - for vals in result: - record = self.browse(vals.pop('id'), self._prefetch) - record._cache.update(record._convert_to_cache(vals, validate=False)) + target = self.browse([], self._prefetch) + for field, values in pycompat.izip(fields_pre, field_values_list): + convert = field.convert_to_cache + # Note that the target record passed to convert below is empty. + # This does not harm in practice, as it is only used in Monetary + # fields for rounding the value. As the value comes straight + # from the database, it is expected to be rounded already. + values = [convert(value, target, validate=False) for value in values] + self.env.cache.update(fetched, field, values) # determine the fields that must be processed now; # for the sake of simplicity, we ignore inherited fields @@ -3266,6 +3278,9 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): with self.env.protecting(protected_fields, self): # write stored fields with (low-level) method _write if store_vals or inverse_vals or inherited_vals: + # if log_access is enabled, this updates 'write_date' and + # 'write_uid' and check access rules, even when old_vals is + # empty self._write(store_vals) # update parent records (after possibly updating parent fields) @@ -3282,6 +3297,8 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): self.env[model_name].browse(parent_ids).write(parent_vals) if inverse_vals: + self.check_field_access_rights('write', list(inverse_vals)) + self.modified(set(inverse_vals) - set(store_vals)) # group fields by inverse method (to call it once), and order @@ -4612,7 +4629,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): # @classmethod - def _browse(cls, ids, env, prefetch=None): + def _browse(cls, ids, env, prefetch=None, add_prefetch=True): """ Create a recordset instance. :param ids: a tuple of record ids @@ -4625,7 +4642,8 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): if prefetch is None: prefetch = defaultdict(set) # {model_name: set(ids)} records._prefetch = prefetch - prefetch[cls._name].update(ids) + if add_prefetch: + prefetch[cls._name].update(ids) return records def browse(self, arg=None, prefetch=None): @@ -4664,9 +4682,13 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): """ Verifies that the current recorset holds a single record. Raises an exception otherwise. """ - if len(self) == 1: + try: + # unpack to ensure there is only one value is faster than len when true and + # has a significant impact as this check is largely called + _id, = self._ids return self - raise ValueError("Expected singleton: %s" % self) + except ValueError: + raise ValueError("Expected singleton: %s" % self) def with_env(self, env): """ Returns a new version of this recordset attached to the provided @@ -4932,7 +4954,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): def __iter__(self): """ Return an iterator over ``self``. """ for id in self._ids: - yield self._browse((id,), self.env, self._prefetch) + yield self._browse((id,), self.env, self._prefetch, False) def __contains__(self, item): """ Test whether ``item`` (record or field name) is an element of ``self``. |
