diff --git a/CHANGELOG b/CHANGELOG index 235bafaa44..4656895b4c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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 diff --git a/doc/ref/models/models.rst b/doc/ref/models/models.rst index 105ca11c89..f46da8526d 100644 --- a/doc/ref/models/models.rst +++ b/doc/ref/models/models.rst @@ -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 diff --git a/trytond/backend/postgresql/init.sql b/trytond/backend/postgresql/init.sql index f90ed1524d..184a6235c4 100644 --- a/trytond/backend/postgresql/init.sql +++ b/trytond/backend/postgresql/init.sql @@ -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 ); diff --git a/trytond/backend/sqlite/init.sql b/trytond/backend/sqlite/init.sql index f330bde314..fbca012772 100644 --- a/trytond/backend/sqlite/init.sql +++ b/trytond/backend/sqlite/init.sql @@ -20,7 +20,8 @@ CREATE TABLE ir_model_field ( field_description VARCHAR, ttype VARCHAR, help TEXT, - module VARCHAR + module VARCHAR, + "access" BOOLEAN ); diff --git a/trytond/ir/model.py b/trytond/ir/model.py index 0ebf8b588b..2b7e469372 100644 --- a/trytond/ir/model.py +++ b/trytond/ir/model.py @@ -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 diff --git a/trytond/ir/rule.py b/trytond/ir/rule.py index 88208e1869..7041dbb899 100644 --- a/trytond/ir/rule.py +++ b/trytond/ir/rule.py @@ -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: diff --git a/trytond/ir/view/model_field_form.xml b/trytond/ir/view/model_field_form.xml index 3149cce230..3f32b718aa 100644 --- a/trytond/ir/view/model_field_form.xml +++ b/trytond/ir/view/model_field_form.xml @@ -14,6 +14,8 @@ this repository contains the full copyright notices and license terms. -->