diff --git a/CHANGELOG b/CHANGELOG
index 4656895b4c..80e6f44341 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,4 @@
+* Add parent to group to inherit accesses
* Add __access__ to Model
* Add route wrapper to allow null origin
* Add header grouping on report
diff --git a/trytond/ir/model.py b/trytond/ir/model.py
index 2b7e469372..e9ab3a7ec6 100644
--- a/trytond/ir/model.py
+++ b/trytond/ir/model.py
@@ -523,7 +523,7 @@ class ModelAccess(DeactivableMixin, ModelSQL, ModelView):
user = Transaction().user
model_access = cls.__table__()
ir_model = Model.__table__()
- user_group = UserGroup.__table__()
+ user_group = UserGroup.user_group_all_table()
group = Group.__table__()
access = {}
@@ -729,7 +729,7 @@ class ModelFieldAccess(DeactivableMixin, ModelSQL, ModelView):
field_access = cls.__table__()
ir_model = Model.__table__()
model_field = ModelField.__table__()
- user_group = UserGroup.__table__()
+ user_group = UserGroup.user_group_all_table()
group = Group.__table__()
accesses = {}
diff --git a/trytond/ir/rule.py b/trytond/ir/rule.py
index 7041dbb899..b98f0bc0ab 100644
--- a/trytond/ir/rule.py
+++ b/trytond/ir/rule.py
@@ -143,6 +143,7 @@ class Rule(ModelSQL, ModelView):
user = EvalEnvironment(User(user_id), User)
return {
'user': user,
+ 'groups': User.get_groups()
}
@staticmethod
@@ -161,7 +162,7 @@ class Rule(ModelSQL, ModelView):
rule_table = cls.__table__()
rule_group = RuleGroup.__table__()
rule_group_group = RuleGroup_Group.__table__()
- user_group = User_Group.__table__()
+ user_group = User_Group.user_group_all_table()
model = Model.__table__()
transaction = Transaction()
diff --git a/trytond/res/group.py b/trytond/res/group.py
index c14f3b7a82..00ab650fc7 100644
--- a/trytond/res/group.py
+++ b/trytond/res/group.py
@@ -1,7 +1,12 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from itertools import chain
-from trytond.model import ModelView, ModelSQL, DeactivableMixin, fields, Unique
+
+from sql import With
+
+from trytond.model import ModelView, ModelSQL, DeactivableMixin, tree, fields
+from trytond.model import Unique
+from trytond.pool import Pool
from trytond.tools import grouped_slice
@@ -23,11 +28,14 @@ class MenuMany2Many(fields.Many2Many):
return res
-class Group(DeactivableMixin, ModelSQL, ModelView):
+class Group(DeactivableMixin, tree(), ModelSQL, ModelView):
"Group"
__name__ = "res.group"
name = fields.Char('Name', required=True, select=True, translate=True)
users = fields.Many2Many('res.user-res.group', 'group', 'user', 'Users')
+ parent = fields.Many2One(
+ 'res.group', "Parent",
+ help="The group to inherit accesses from.")
model_access = fields.One2Many('ir.model.access', 'group',
'Access Model')
field_access = fields.One2Many('ir.model.field.access', 'group',
@@ -50,6 +58,19 @@ class Group(DeactivableMixin, ModelSQL, ModelView):
]
cls._order.insert(0, ('name', 'ASC'))
+ @classmethod
+ def write(cls, *args):
+ super().write(*args)
+ pool = Pool()
+ # Restart the cache on the domain_get method
+ pool.get('ir.rule')._domain_get_cache.clear()
+ # Restart the cache for get_groups
+ pool.get('res.user')._get_groups_cache.clear()
+ # Restart the cache for model access and view
+ pool.get('ir.model.access')._get_access_cache.clear()
+ pool.get('ir.model.field.access')._get_access_cache.clear()
+ ModelView._fields_view_get_cache.clear()
+
@classmethod
def copy(cls, groups, default=None):
if default is None:
@@ -67,3 +88,14 @@ class Group(DeactivableMixin, ModelSQL, ModelView):
default['name'] = name
new_groups.extend(super(Group, cls).copy([group], default=default))
return new_groups
+
+ @classmethod
+ def group_parent_all_cte(cls):
+ group = cls.__table__()
+ parents = With('id', 'parent', recursive=True)
+ parents.query = group.select(group.id, group.parent)
+ parents.query |= group.select(group.id, group.id)
+ parents.query |= (group
+ .join(parents, condition=group.parent == parents.id)
+ .select(group.id, parents.parent))
+ return parents
diff --git a/trytond/res/ir.xml b/trytond/res/ir.xml
index 6b80fe87bc..c1a1ce6006 100644
--- a/trytond/res/ir.xml
+++ b/trytond/res/ir.xml
@@ -445,7 +445,7 @@ this repository contains the full copyright notices and license terms. -->
@@ -460,7 +460,7 @@ this repository contains the full copyright notices and license terms. -->
@@ -475,7 +475,7 @@ this repository contains the full copyright notices and license terms. -->
@@ -491,7 +491,7 @@ this repository contains the full copyright notices and license terms. -->
@@ -507,7 +507,7 @@ this repository contains the full copyright notices and license terms. -->
@@ -523,7 +523,7 @@ this repository contains the full copyright notices and license terms. -->
@@ -539,7 +539,7 @@ this repository contains the full copyright notices and license terms. -->
@@ -778,7 +778,7 @@ this repository contains the full copyright notices and license terms. -->
@@ -791,7 +791,7 @@ this repository contains the full copyright notices and license terms. -->
@@ -949,7 +949,7 @@ this repository contains the full copyright notices and license terms. -->
-
+
@@ -968,7 +968,7 @@ this repository contains the full copyright notices and license terms. -->
-
+
@@ -1006,7 +1006,7 @@ this repository contains the full copyright notices and license terms. -->
-
+
@@ -1025,7 +1025,7 @@ this repository contains the full copyright notices and license terms. -->
-
+
diff --git a/trytond/res/user.py b/trytond/res/user.py
index 8d338eaac6..9a75956ee8 100644
--- a/trytond/res/user.py
+++ b/trytond/res/user.py
@@ -24,7 +24,7 @@ from itertools import groupby
from operator import attrgetter
from ast import literal_eval
-from sql import Literal
+from sql import Literal, Null
from sql.functions import CurrentTimestamp
from sql.conditionals import Coalesce, Case
from sql.aggregate import Count
@@ -587,15 +587,22 @@ class User(DeactivableMixin, ModelSQL, ModelView):
return res
@classmethod
- def get_groups(cls):
+ def get_groups(cls, name=None):
'''
- Return a list of group ids for the user
+ Return a list of all group ids for the user
'''
user = Transaction().user
groups = cls._get_groups_cache.get(user)
if groups is not None:
return groups
- groups = cls.read([Transaction().user], ['groups'])[0]['groups']
+ pool = Pool()
+ UserGroup = pool.get('res.user-res.group')
+ cursor = Transaction().connection.cursor()
+ user_group = UserGroup.user_group_all_table()
+ cursor.execute(*user_group.select(
+ user_group.group,
+ where=user_group.user == user))
+ groups = [g for g, in cursor]
cls._get_groups_cache.set(user, groups)
return groups
@@ -969,6 +976,22 @@ class UserGroup(ModelSQL):
pool.get('ir.model.field.access')._get_access_cache.clear()
ModelView._fields_view_get_cache.clear()
+ @classmethod
+ def user_group_all_table(cls):
+ pool = Pool()
+ Group = pool.get('res.group')
+ user_group = cls.__table__()
+ group_parents = Group.group_parent_all_cte()
+
+ return (user_group
+ .join(group_parents,
+ condition=user_group.group == group_parents.id)
+ .select(
+ user_group.user.as_('user'),
+ group_parents.parent.as_('group'),
+ where=group_parents.parent != Null,
+ with_=group_parents))
+
class Warning_(ModelSQL, ModelView):
'User Warning'
diff --git a/trytond/res/view/group_form.xml b/trytond/res/view/group_form.xml
index 4837a7cfbb..c62568e1b1 100644
--- a/trytond/res/view/group_form.xml
+++ b/trytond/res/view/group_form.xml
@@ -6,6 +6,9 @@ this repository contains the full copyright notices and license terms. -->
+
+
+
diff --git a/trytond/res/view/group_list.xml b/trytond/res/view/group_list.xml
index b4d89538b1..a8329bd912 100644
--- a/trytond/res/view/group_list.xml
+++ b/trytond/res/view/group_list.xml
@@ -2,5 +2,6 @@
-
+
+
diff --git a/trytond/tests/test_access.py b/trytond/tests/test_access.py
index eebea8cfde..b9431115a6 100644
--- a/trytond/tests/test_access.py
+++ b/trytond/tests/test_access.py
@@ -255,6 +255,31 @@ class _ModelAccessTestCase(unittest.TestCase):
self._assert(record)
+ @with_transaction(context=_context)
+ def test_access_inherited_from_parent(self):
+ "Test access inherited from parent"
+ pool = Pool()
+ Group = pool.get('res.group')
+ ModelAccess = pool.get('ir.model.access')
+ TestAccess = pool.get('test.access')
+ record, = TestAccess.create([{}])
+ group, = Group.create([{'name': 'Test'}])
+ Group.write([self.group], {
+ 'parent': group.id,
+ })
+ ModelAccess.create([{
+ 'model': self.model.id,
+ 'group': self.group.id,
+ self._perm: False,
+ }])
+ ModelAccess.create([{
+ 'model': self.model.id,
+ 'group': group.id,
+ self._perm: True,
+ }])
+
+ self._assert(record)
+
class ModelAccessReadTestCase(_ModelAccessTestCase):
_perm = 'perm_read'
@@ -664,6 +689,32 @@ class _ModelFieldAccessTestCase(unittest.TestCase):
self._assert1(record)
self._assert2(record)
+ @with_transaction(context=_context)
+ def test_access_inherited_from_parent(self):
+ "Test no access with other group"
+ pool = Pool()
+ Group = pool.get('res.group')
+ FieldAccess = pool.get('ir.model.field.access')
+ TestAccess = pool.get('test.access')
+ record, = TestAccess.create([{}])
+ group, = Group.create([{'name': 'Test'}])
+ Group.write([self.group], {
+ 'parent': group.id,
+ })
+ FieldAccess.create([{
+ 'field': self.field1.id,
+ 'group': self.group.id,
+ self._perm: False,
+ }])
+ FieldAccess.create([{
+ 'field': self.field1.id,
+ 'group': group.id,
+ self._perm: True,
+ }])
+
+ self._assert1(record)
+ self._assert2(record)
+
@with_transaction(context=_context)
def test_two_access(self):
"Test two access"