Add __access__ to Model

issue9903
review324601002
This commit is contained in:
Cédric Krier
2021-03-07 11:13:53 +01:00
parent b841ba8742
commit 04a0aea2ad
14 changed files with 246 additions and 38 deletions
+1
View File
@@ -1,3 +1,4 @@
* Add __access__ to Model
* Add route wrapper to allow null origin
* Add header grouping on report
* Protect trusted devices against brute force attack
+5
View File
@@ -17,6 +17,11 @@ Class attributes are:
It contains the a unique name to reference the model throughout the
platform.
.. attribute:: Model.__access__
A set that contains the names of relation field for which the access rights
are also checked for this model.
.. attribute:: Model.__rpc__
It contains a dictionary with method name as key and an instance of
+1
View File
@@ -29,6 +29,7 @@ CREATE TABLE ir_model_field (
ttype VARCHAR,
help TEXT,
module VARCHAR,
"access" BOOL,
PRIMARY KEY(id),
FOREIGN KEY (model) REFERENCES ir_model(id) ON DELETE CASCADE
);
+2 -1
View File
@@ -20,7 +20,8 @@ CREATE TABLE ir_model_field (
field_description VARCHAR,
ttype VARCHAR,
help TEXT,
module VARCHAR
module VARCHAR,
"access" BOOLEAN
);
+44 -9
View File
@@ -243,6 +243,15 @@ class ModelField(ModelSQL, ModelView):
depends=['module'])
module = fields.Char('Module',
help="Module in which this field is defined.")
access = fields.Boolean(
"Access",
states={
'invisible': ~Eval('relation'),
},
depends=['relation'],
help="If checked, the access right on the model of the field "
"is also tested against the relation of the field.")
_get_name_cache = Cache('ir.model.field.get_name')
@classmethod
@@ -273,6 +282,7 @@ class ModelField(ModelSQL, ModelView):
ir_model_field.relation.as_('relation'),
ir_model_field.module.as_('module'),
ir_model_field.help.as_('help'),
ir_model_field.access.as_('access'),
where=ir_model.model == model.__name__))
model_fields = {f['name']: f for f in cursor_dict(cursor)}
@@ -283,27 +293,32 @@ class ModelField(ModelSQL, ModelView):
relation = field.relation_name
else:
relation = None
access = field_name in model.__access__
if field_name not in model_fields:
cursor.execute(*ir_model_field.insert(
[ir_model_field.model, ir_model_field.name,
ir_model_field.field_description,
ir_model_field.ttype, ir_model_field.relation,
ir_model_field.help, ir_model_field.module],
ir_model_field.help, ir_model_field.module,
ir_model_field.access],
[[
model_id, field_name,
field.string,
field._type, relation,
field.help, module_name]]))
field.help, module_name,
access]]))
elif (model_fields[field_name]['field_description'] != field.string
or model_fields[field_name]['ttype'] != field._type
or model_fields[field_name]['relation'] != relation
or model_fields[field_name]['help'] != field.help):
or model_fields[field_name]['help'] != field.help
or model_fields[field_name]['access'] != access):
cursor.execute(*ir_model_field.update(
[ir_model_field.field_description,
ir_model_field.ttype, ir_model_field.relation,
ir_model_field.help],
[field.string, field._type, relation, field.help],
ir_model_field.help, ir_model_field.access],
[field.string, field._type, relation, field.help,
access],
where=(ir_model_field.id
== model_fields[field_name]['id'])))
@@ -520,6 +535,18 @@ class ModelAccess(DeactivableMixin, ModelSQL, ModelView):
else:
return access
def fill_models(Model, models):
if Model.__name__ in models:
return
models.append(Model.__name__)
for field_name in Model.__access__:
field = getattr(Model, field_name)
fill_models(field.get_target(), models)
model2models = defaultdict(list)
for model in models:
fill_models(pool.get(model), model2models[model])
all_models = list(set(sum(model2models.values(), [])))
default = {'read': True, 'write': True, 'create': True, 'delete': True}
access = dict((m, default) for m in models)
cursor.execute(*model_access.join(ir_model, 'LEFT',
@@ -542,16 +569,24 @@ class ModelAccess(DeactivableMixin, ModelSQL, ModelView):
Max(Case(
(model_access.perm_delete == Literal(True), 1),
else_=0)),
where=ir_model.model.in_(models)
where=ir_model.model.in_(all_models)
& (model_access.active == Literal(True))
& ((
(user_group.user == user)
& (group.active == Literal(True)))
| (model_access.group == Null)),
group_by=ir_model.model))
access.update(dict(
(m, {'read': r, 'write': w, 'create': c, 'delete': d})
for m, r, w, c, d in cursor.fetchall()))
raw_access = {
m: {'read': r, 'write': w, 'create': c, 'delete': d}
for m, r, w, c, d in cursor.fetchall()}
for model in models:
access[model] = {
perm: max(
(raw_access[m][perm] for m in model2models[model]
if m in raw_access),
default=True)
for perm in ['read', 'write', 'create', 'delete']}
for model, maccess in access.items():
cls._get_access_cache.set((user, model), maccess)
return access
+27 -2
View File
@@ -167,6 +167,25 @@ class Rule(ModelSQL, ModelView):
assert mode in cls.modes
model_names = []
model2field = defaultdict(list)
def update_model_names(Model, path=None):
if Model.__name__ in model_names:
return
model_names.append(Model.__name__)
if path:
model2field[Model.__name__].append(path)
for field_name in Model.__access__:
field = getattr(Model, field_name)
Target = field.get_target()
if path:
target_path = path + '.' + field_name
else:
target_path = field_name
update_model_names(Target, target_path)
update_model_names(pool.get(model_name))
cursor = transaction.connection.cursor()
user_id = transaction.user
# root user above constraint
@@ -179,7 +198,7 @@ class Rule(ModelSQL, ModelView):
).join(model,
condition=rule_group.model == model.id
).select(rule_table.id,
where=(model.model == model_name)
where=(model.model.in_(model_names))
& (getattr(rule_group, 'perm_%s' % mode) == Literal(True))
& (rule_group.id.in_(
rule_group_group.join(
@@ -198,7 +217,7 @@ class Rule(ModelSQL, ModelView):
cursor.execute(*rule_group.join(model,
condition=rule_group.model == model.id
).select(rule_group.id,
where=(model.model == model_name)
where=(model.model.in_(model_names))
& ~rule_group.id.in_(rule_table.select(rule_table.rule_group))
& rule_group.id.in_(rule_group_group.join(user_group,
condition=rule_group_group.group == user_group.group
@@ -215,6 +234,12 @@ class Rule(ModelSQL, ModelView):
assert rule.domain, ('Rule domain empty,'
'check if migration was done')
dom = decoder.decode(rule.domain)
target_model = rule.rule_group.model.model
if target_model in model2field:
target_dom = ['OR']
for field in model2field[target_model]:
target_dom.append((field, 'where', dom))
dom = target_dom
if rule.rule_group.global_p:
clause_global[rule.rule_group].append(dom)
else:
+2
View File
@@ -14,6 +14,8 @@ this repository contains the full copyright notices and license terms. -->
<field name="field_description" colspan="3"/>
<label name="help" yfill="1"/>
<field name="help" colspan="3"/>
<label name="access"/>
<field name="access"/>
<label name="module"/>
<field name="module"/>
</form>
+1
View File
@@ -7,5 +7,6 @@ this repository contains the full copyright notices and license terms. -->
<field name="ttype"/>
<field name="relation" expand="1"/>
<field name="field_description" expand="1"/>
<field name="access"/>
<field name="module"/>
</tree>
+1
View File
@@ -45,6 +45,7 @@ class Model(URLMixin, PoolBase, metaclass=ModelMeta):
'fields_get': RPC(cache=dict(days=1)),
'pre_validate': RPC(instantiate=0),
}
cls.__access__ = set()
# Copy fields and update depends
for attr in dir(cls):
+12
View File
@@ -23,8 +23,20 @@ class TestAccessRelate(ModelSQL):
value = fields.Integer("Value")
class TestAccessModel(TestAccess):
"Test Access from Model"
__name__ = 'test.access.model'
access = fields.Many2One('test.access', "Access")
@classmethod
def __setup__(cls):
super().__setup__()
cls.__access__.add('access')
def register(module):
Pool.register(
TestAccess,
TestAccessRelate,
TestAccessModel,
module=module, type_='model')
+15
View File
@@ -17,8 +17,23 @@ class TestRuleRelation(ModelSQL):
field = fields.Char("Field")
class TestRuleModel(ModelSQL):
"Test Rule from Model"
__name__ = 'test.rule.model'
__access__ = 'test.rule'
name = fields.Char("Name")
rule = fields.Many2One('test.rule', "Rule")
@classmethod
def __setup__(cls):
super().__setup__()
cls.__access__.add('rule')
def register(module):
Pool.register(
TestRule,
TestRuleRelation,
TestRuleModel,
module=module, type_='model')
+47 -24
View File
@@ -18,11 +18,13 @@ class _ModelAccessTestCase(unittest.TestCase):
def setUpClass(cls):
activate_module('tests')
model_name = model_access_name = 'test.access'
@property
def model(self):
pool = Pool()
Model = pool.get('ir.model')
model, = Model.search([('model', '=', 'test.access')])
model, = Model.search([('model', '=', self.model_access_name)])
return model
@property
@@ -42,7 +44,7 @@ class _ModelAccessTestCase(unittest.TestCase):
def test_access_empty(self):
"Test access without model access"
pool = Pool()
TestAccess = pool.get('test.access')
TestAccess = pool.get(self.model_name)
record, = TestAccess.create([{}])
self._assert(record)
@@ -52,7 +54,7 @@ class _ModelAccessTestCase(unittest.TestCase):
"Test access without group"
pool = Pool()
ModelAccess = pool.get('ir.model.access')
TestAccess = pool.get('test.access')
TestAccess = pool.get(self.model_name)
record, = TestAccess.create([{}])
ModelAccess.create([{
'model': self.model.id,
@@ -67,7 +69,7 @@ class _ModelAccessTestCase(unittest.TestCase):
"Test no access without group"
pool = Pool()
ModelAccess = pool.get('ir.model.access')
TestAccess = pool.get('test.access')
TestAccess = pool.get(self.model_name)
record, = TestAccess.create([{}])
ModelAccess.create([{
'model': self.model.id,
@@ -82,7 +84,7 @@ class _ModelAccessTestCase(unittest.TestCase):
"Test one access with groups"
pool = Pool()
ModelAccess = pool.get('ir.model.access')
TestAccess = pool.get('test.access')
TestAccess = pool.get(self.model_name)
record, = TestAccess.create([{}])
ModelAccess.create([{
'model': self.model.id,
@@ -102,7 +104,7 @@ class _ModelAccessTestCase(unittest.TestCase):
"Test one access without group"
pool = Pool()
ModelAccess = pool.get('ir.model.access')
TestAccess = pool.get('test.access')
TestAccess = pool.get(self.model_name)
record, = TestAccess.create([{}])
ModelAccess.create([{
'model': self.model.id,
@@ -122,7 +124,7 @@ class _ModelAccessTestCase(unittest.TestCase):
"Test all access with groups"
pool = Pool()
ModelAccess = pool.get('ir.model.access')
TestAccess = pool.get('test.access')
TestAccess = pool.get(self.model_name)
record, = TestAccess.create([{}])
ModelAccess.create([{
'model': self.model.id,
@@ -142,7 +144,7 @@ class _ModelAccessTestCase(unittest.TestCase):
"Test no access with groups"
pool = Pool()
ModelAccess = pool.get('ir.model.access')
TestAccess = pool.get('test.access')
TestAccess = pool.get(self.model_name)
record, = TestAccess.create([{}])
ModelAccess.create([{
'model': self.model.id,
@@ -162,7 +164,7 @@ class _ModelAccessTestCase(unittest.TestCase):
"Test one access with group"
pool = Pool()
ModelAccess = pool.get('ir.model.access')
TestAccess = pool.get('test.access')
TestAccess = pool.get(self.model_name)
record, = TestAccess.create([{}])
ModelAccess.create([{
'model': self.model.id,
@@ -177,7 +179,7 @@ class _ModelAccessTestCase(unittest.TestCase):
"Test no access with group"
pool = Pool()
ModelAccess = pool.get('ir.model.access')
TestAccess = pool.get('test.access')
TestAccess = pool.get(self.model_name)
record, = TestAccess.create([{}])
ModelAccess.create([{
'model': self.model.id,
@@ -193,7 +195,7 @@ class _ModelAccessTestCase(unittest.TestCase):
pool = Pool()
Group = pool.get('res.group')
ModelAccess = pool.get('ir.model.access')
TestAccess = pool.get('test.access')
TestAccess = pool.get(self.model_name)
record, = TestAccess.create([{}])
group, = Group.create([{'name': 'Test'}])
ModelAccess.create([{
@@ -215,7 +217,7 @@ class _ModelAccessTestCase(unittest.TestCase):
pool = Pool()
Group = pool.get('res.group')
ModelAccess = pool.get('ir.model.access')
TestAccess = pool.get('test.access')
TestAccess = pool.get(self.model_name)
record, = TestAccess.create([{}])
group, = Group.create([{'name': 'Test'}])
ModelAccess.create([{
@@ -237,7 +239,7 @@ class _ModelAccessTestCase(unittest.TestCase):
pool = Pool()
Group = pool.get('res.group')
ModelAccess = pool.get('ir.model.access')
TestAccess = pool.get('test.access')
TestAccess = pool.get(self.model_name)
record, = TestAccess.create([{}])
group, = Group.create([{'name': 'Test'}])
ModelAccess.create([{
@@ -259,13 +261,13 @@ class ModelAccessReadTestCase(_ModelAccessTestCase):
def _assert(self, record):
pool = Pool()
TestAccess = pool.get('test.access')
TestAccess = pool.get(self.model_name)
TestAccess.read([record.id], ['field1'])
TestAccess.search([])
def _assert_raises(self, record):
pool = Pool()
TestAccess = pool.get('test.access')
TestAccess = pool.get(self.model_name)
with self.assertRaises(AccessError):
TestAccess.read([record.id], ['field1'])
with self.assertRaises(AccessError):
@@ -275,7 +277,7 @@ class ModelAccessReadTestCase(_ModelAccessTestCase):
def test_access_relate_empty(self):
"Test access on search relate without model access"
pool = Pool()
TestAccess = pool.get('test.access')
TestAccess = pool.get(self.model_name)
record, = TestAccess.create([{}])
TestAccess.read([record.id], ['relate.value'])
@@ -286,7 +288,7 @@ class ModelAccessReadTestCase(_ModelAccessTestCase):
def test_access_relate(self):
"Test access on search relate"
pool = Pool()
TestAccess = pool.get('test.access')
TestAccess = pool.get(self.model_name)
Model = pool.get('ir.model')
ModelAccess = pool.get('ir.model.access')
record, = TestAccess.create([{}])
@@ -307,7 +309,7 @@ class ModelAccessReadTestCase(_ModelAccessTestCase):
def test_no_access_relate(self):
"Test no access on search relate"
pool = Pool()
TestAccess = pool.get('test.access')
TestAccess = pool.get(self.model_name)
Model = pool.get('ir.model')
ModelAccess = pool.get('ir.model.access')
record, = TestAccess.create([{}])
@@ -333,12 +335,12 @@ class ModelAccessWriteTestCase(_ModelAccessTestCase):
def _assert(self, record):
pool = Pool()
TestAccess = pool.get('test.access')
TestAccess = pool.get(self.model_name)
TestAccess.write([record], {})
def _assert_raises(self, record):
pool = Pool()
TestAccess = pool.get('test.access')
TestAccess = pool.get(self.model_name)
with self.assertRaises(AccessError):
TestAccess.write([record], {})
@@ -348,12 +350,12 @@ class ModelAccessCreateTestCase(_ModelAccessTestCase):
def _assert(self, record):
pool = Pool()
TestAccess = pool.get('test.access')
TestAccess = pool.get(self.model_name)
TestAccess.create([{}])
def _assert_raises(self, record):
pool = Pool()
TestAccess = pool.get('test.access')
TestAccess = pool.get(self.model_name)
with self.assertRaises(AccessError):
TestAccess.create([{}])
@@ -363,16 +365,35 @@ class ModelAccessDeleteTestCase(_ModelAccessTestCase):
def _assert(self, record):
pool = Pool()
TestAccess = pool.get('test.access')
TestAccess = pool.get(self.model_name)
TestAccess.delete([record])
def _assert_raises(self, record):
pool = Pool()
TestAccess = pool.get('test.access')
TestAccess = pool.get(self.model_name)
with self.assertRaises(AccessError):
TestAccess.delete([record])
class ModelAccessModelTestCase(_ModelAccessTestCase):
model_name = 'test.access.model'
_perm = 'perm_read'
def _assert(self, record):
pool = Pool()
TestAccess = pool.get(self.model_name)
TestAccess.read([record.id], ['field1'])
TestAccess.search([])
def _assert_raises(self, record):
pool = Pool()
TestAccess = pool.get(self.model_name)
with self.assertRaises(AccessError):
TestAccess.read([record.id], ['field1'])
with self.assertRaises(AccessError):
TestAccess.search([])
class _ModelFieldAccessTestCase(unittest.TestCase):
_perm = None
@@ -926,6 +947,8 @@ def suite():
).loadTestsFromTestCase(ModelAccessCreateTestCase))
suite_.addTests(unittest.TestLoader(
).loadTestsFromTestCase(ModelAccessDeleteTestCase))
suite_.addTests(unittest.TestLoader(
).loadTestsFromTestCase(ModelAccessModelTestCase))
suite_.addTests(unittest.TestLoader(
).loadTestsFromTestCase(ModelFieldAccessReadTestCase))
suite_.addTests(unittest.TestLoader(
+57
View File
@@ -623,6 +623,63 @@ class ModelRuleTestCase(unittest.TestCase):
value, = TestRule.read([non_deletable.id], ['_delete'])
self.assertEqual(value['_delete'], False)
@with_transaction()
def test_model_with_rule(self):
"Test model with rule"
pool = Pool()
TestRule = pool.get('test.rule')
TestRuleModel = pool.get('test.rule.model')
RuleGroup = pool.get('ir.rule.group')
Model = pool.get('ir.model')
model, = Model.search([('model', '=', 'test.rule')])
rule_group, = RuleGroup.create([{
'name': "Field different from foo",
'model': model.id,
'global_p': True,
'perm_read': True,
'perm_create': False,
'perm_write': False,
'perm_delete': False,
'rules': [('create', [{
'domain': json.dumps(
[('field', '!=', 'foo')]),
}])],
}])
rule, = TestRule.create([{'field': 'bar'}])
test, = TestRuleModel.create([{'rule': rule.id, 'name': 'foo'}])
TestRuleModel.read([test.id], ['name'])
@with_transaction()
def test_model_with_rule_fail(self):
"Test model with rule fail"
pool = Pool()
TestRule = pool.get('test.rule')
TestRuleModel = pool.get('test.rule.model')
RuleGroup = pool.get('ir.rule.group')
Model = pool.get('ir.model')
model, = Model.search([('model', '=', 'test.rule')])
rule_group, = RuleGroup.create([{
'name': "Field different from foo",
'model': model.id,
'global_p': True,
'perm_read': True,
'perm_create': False,
'perm_write': False,
'perm_delete': False,
'rules': [('create', [{
'domain': json.dumps(
[('field', '!=', 'foo')]),
}])],
}])
rule, = TestRule.create([{'field': 'foo'}])
test, = TestRuleModel.create([{'rule': rule.id, 'name': 'foo'}])
with self.assertRaisesRegex(AccessError, "Field different from foo"):
TestRuleModel.read([test.id], ['name'])
def suite():
suite_ = unittest.TestSuite()
+31 -2
View File
@@ -246,6 +246,23 @@ class ModuleTestCase(unittest.TestCase):
msg="Wrong '%s' type for _rec_name of %s'" % (
field._type, mname))
@with_transaction()
def test_model__access__(self):
"Test existing model __access__"
pool = Pool()
for mname, Model in pool.iterobject():
if not isregisteredby(Model, self.module):
continue
for field_name in Model.__access__:
self.assertIn(field_name, Model._fields.keys(),
msg="Wrong __access__ '%s' for %s" % (field_name, mname))
field = Model._fields[field_name]
Target = field.get_target()
self.assertTrue(
Target,
msg='Missing target for __access__ "%s" of %s' % (
field_name, mname))
@with_transaction()
def test_view(self):
'Test validity of all views of the module'
@@ -529,10 +546,22 @@ class ModuleTestCase(unittest.TestCase):
'Test missing default model access'
pool = Pool()
Access = pool.get('ir.model.access')
no_groups = {a.model.name for a in Access.search([
no_groups = {a.model.model for a in Access.search([
('group', '=', None),
])}
with_groups = {a.model.name for a in Access.search([
def has_access(Model, models):
if Model.__name__ in models:
return True
for field_name in Model.__access__:
Target = Model._fields[field_name].get_target()
if has_access(Target, models):
return True
for mname, Model in pool.iterobject():
if has_access(Model, no_groups):
no_groups.add(mname)
with_groups = {a.model.model for a in Access.search([
('group', '!=', None),
])}