summaryrefslogtreecommitdiff
path: root/odoo
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2019-02-20 17:37:22 +0100
committerChristophe Simonis <chs@odoo.com>2019-02-20 17:37:22 +0100
commitdb1c7765cba0692cfc4eda34ead000b048da4dae (patch)
tree34e351e1714858f9b607e216f505b3e4ce453115 /odoo
parent7151f98358fc3cab13c1cc7400c5e0b58d662978 (diff)
parent2c495502aa37374f3748d9ff4ab7cf9e8d9a6d6e (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.py50
-rw-r--r--odoo/api.py11
-rw-r--r--odoo/fields.py22
-rw-r--r--odoo/models.py62
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``.