From 0da94d807d0e78b99f0d208bad327f560f1a39fe Mon Sep 17 00:00:00 2001 From: skoogi Date: Sat, 28 Dec 2019 14:28:07 +0800 Subject: [PATCH 1/8] refactor(user list): open user list for sub manager --- oneid_meta/models/mixin.py | 37 ++++++++++++++++++++++++--- oneid_meta/models/user.py | 30 +++++++++++++++++++--- siteapi/v1/tests/test_manager_perm.py | 35 +++++++++++++++++++++++++ siteapi/v1/views/user.py | 25 +++++++++++++++++- 4 files changed, 119 insertions(+), 8 deletions(-) create mode 100644 siteapi/v1/tests/test_manager_perm.py diff --git a/oneid_meta/models/mixin.py b/oneid_meta/models/mixin.py index ea8703b67..fced6d3a0 100644 --- a/oneid_meta/models/mixin.py +++ b/oneid_meta/models/mixin.py @@ -191,14 +191,45 @@ def upstream_uids(self): return chain([self.node_uid], res) + @classmethod + def get_upstream_uids(cls, node_uid): + ''' + 节点向上追溯的路径,包括该节点本身,包括终点节点,以node_uid形式返回 + ''' + key = f'oneid:node:{node_uid}:upstream' + res = cache.get(key) + if res is None: + node, _ = cls.retrieve_node(node_uid) + if node: + node_uids = [item.node_uid for item in node.path_up_to()] + cache.set(key, node_uids[1:]) + return node_uids + + return chain([node_uid], res) + @property def downstream_uids(self): ''' 节点以及其子孙节点,以node_uid形式返回 - 目前使用频次不大 ''' - for node in self.downstream: - yield node.node_uid + return self.get_downstream_uids(self.node_uid) + + @classmethod + def get_downstream_uids(cls, node_uid): + ''' + 节点以及其子孙节点,以node_uid形式返回 + TODO: 继续优化,从子节点的 downstream_uids 聚合 + TODO: 删除节点时,删除缓存 + ''' + key = f'oneid:node:{node_uid}:downstream' + res = cache.get(key) + if res is None: + node, _ = cls.retrieve_node(node_uid) + if node: + res = [item.node_uid for item in node.downstream] + cache.set(key, res[1:0]) + return res + return chain([node_uid], res) @property def downstream(self): diff --git a/oneid_meta/models/user.py b/oneid_meta/models/user.py index 05599d405..20c175bdf 100644 --- a/oneid_meta/models/user.py +++ b/oneid_meta/models/user.py @@ -380,7 +380,7 @@ def under_manage(self, user): def is_visible_to_manager(self, user): ''' 校验指定管理员是否可见此人 - TODO + TODO: ?? 什么情况下管理员可见,却不可管理 ''' return self.under_manage(user) @@ -399,17 +399,39 @@ def is_visible_to_employee(self, user): @property def manage_node_uids(self): ''' - 管理的节点(不包含下级) + 直接管理的节点(不包含下级) ''' res = set() for manager_group in self.manager_groups: - if manager_group.scope_subject == 2: + if manager_group.scope_subject == 2: # 指定节点、人 res.update(manager_group.nodes) continue - if manager_group.scope_subject == 1: + if manager_group.scope_subject == 1: # 所在节点 res.update(self.node_uids) return res + @property + def manage_user_uids(self): + ''' + 直接管理的人员(不包括从管理组继承而来的可管理的人) + ''' + res = set() + for manager_group in self.manager_groups: + if manager_group.scope_subject == 2: # 指定节点、人 + res.update(manager_group.users) + return res + + @property + def all_manage_node_uids(self): + ''' + 所有可管理的节点(包含直接管理的节点,及其下属节点) + ''' + + res = set() + for node_uid in self.manage_node_uids: + res.update(Node.get_downstream_uids(node_uid)) + return res + def check_password(self, password): ''' 校验密码是否正确 diff --git a/siteapi/v1/tests/test_manager_perm.py b/siteapi/v1/tests/test_manager_perm.py new file mode 100644 index 000000000..c4134572a --- /dev/null +++ b/siteapi/v1/tests/test_manager_perm.py @@ -0,0 +1,35 @@ +''' +tests for manager perm +''' +# pylint: disable=missing-docstring + +from siteapi.v1.tests import TestCase +from oneid_meta.models import User, Group, Dept + + +class ManagerPermTestCase(TestCase): + ''' + TODO: 复杂数据集供测试 + ''' + def setUp(self): + super(ManagerPermTestCase, self).setUp() + + employee1 = User.objects.create(username='employee1') + employee2 = User.objects.create(username='employee2') + employee3 = User.objects.create(username='employee3') + employee4 = User.objects.create(username='employee4') + + d_root = Dept.objects.get(uid='root') + d_dept_1 = Dept.objects.create(parent=d_root, uid='dept_1') + d_dept_2 = Dept.objects.create(parent=d_root, uid='dept_2') + d_dept_1_2 = Group.objects.create(parent=d_dept_1) + + g_role = Group.objects.get(uid='role') + g_role_1 = Group.objects.create(uid='role_1', parent=g_role) + g_role_2 = Group.objects.create(uid='role_2', parent=g_role) + g_role_1_2 = Group.objects.create(uid='role_1_2', parent=g_role_1) + + manager_group = Group.objects.get(uid='manager') + manager_group_1 = Group.objects.create(parent=manager_group, uid='manager_1') + manager_group_2 = Group.objects.create(parent=manager_group, uid='manager_2') + manager_group_3 = Group.objects.create(parent=manager_group, uid='manager_3') diff --git a/siteapi/v1/views/user.py b/siteapi/v1/views/user.py index a16a84735..7feb19cf3 100644 --- a/siteapi/v1/views/user.py +++ b/siteapi/v1/views/user.py @@ -16,11 +16,13 @@ ) from rest_framework.permissions import IsAuthenticated, SAFE_METHODS from django.db import transaction +from django.db.models import Q from django.core.exceptions import ObjectDoesNotExist from django.db.models import Q from oneid_meta.models import User, Group, Dept from oneid.permissions import ( IsAdminUser, + IsManagerUser, IsUserManager, UserEmployeeReadable, UserManagerReadable, @@ -37,11 +39,18 @@ class UserListCreateAPIView(generics.ListCreateAPIView): ''' 用户列表 [GET],[POST] + + :GET + - 主管理员可见全部 + - 子管理员可见管理范围内的指定人、指定节点及其子孙节点内的所有人 + :POST + - 主管理员可以创建用户 + - 拥有 system_user_create 权限的子管理员 ''' serializer_class = EmployeeSerializer pagination_class = DefaultListPaginator - read_permission_classes = [IsAuthenticated & IsAdminUser] + read_permission_classes = [IsAuthenticated & (IsAdminUser | IsManagerUser)] write_permission_classes = [IsAuthenticated & (IsAdminUser | CustomPerm('system_user_create'))] def get_permissions(self): @@ -64,6 +73,20 @@ def get_queryset(self): else: queryset = User.valid_objects.exclude(is_boss=True).exclude(username='admin').order_by('id') + user = self.request.user + if user.is_admin: + return queryset + + all_manage_node_uids = user.all_manage_node_uids + all_manage_dept_uids = [node_uid for node_uid in all_manage_node_uids if node_uid.startswith(Dept.NODE_PREFIX)] + all_manage_group_uids = [ + node_uid for node_uid in all_manage_node_uids if node_uid.startswith(Group.NODE_PREFIX) + ] + + queryset.filter( + Q(deptmember__owner__uid__in=all_manage_dept_uids) | Q(groupmember__owner__uid__in=all_manage_group_uids) + | Q(username__in=user.manage_user_uids)) + return queryset @transaction.atomic() From b8789ac2c7c1b6ef9e459e1dc46c26dc9494ff60 Mon Sep 17 00:00:00 2001 From: shiyueLongguikeji Date: Mon, 30 Dec 2019 14:33:31 +0800 Subject: [PATCH 2/8] =?UTF-8?q?fix(user=20list):=20=E5=AD=90=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=91=98=E5=8F=AF=E8=A7=81=E8=B4=A6=E6=88=B7=E5=8F=8A?= =?UTF-8?q?=E5=88=86=E7=BB=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- oneid_meta/models/mixin.py | 24 ++++++------ oneid_meta/models/user.py | 8 ++-- siteapi/v1/views/user.py | 25 ++++++++++-- test/tests/test_data.py | 2 +- .../test_group_manager_node_tree_view.py | 38 +++++++++++++++++++ 5 files changed, 77 insertions(+), 20 deletions(-) create mode 100644 test/tests/test_group_manager_node_tree_view.py diff --git a/oneid_meta/models/mixin.py b/oneid_meta/models/mixin.py index ea8703b67..fd802b435 100644 --- a/oneid_meta/models/mixin.py +++ b/oneid_meta/models/mixin.py @@ -12,7 +12,7 @@ class NodeVisibilityScope(models.Model): ''' 节点对员工的可见范围 ''' - class Meta: + class Meta: # pylint: disable=missing-class-docstring abstract = True VISIBILITY_SUBJECT = ( # 此处`对...可见`,为`对...开放`的意思,即使判定不可见,最终也有可能可以看到 @@ -41,7 +41,7 @@ def is_open_to_employee(self, user): if self.visibility == 3: node_uids = map(lambda node: node.uid, self.tree_front_walker()) # pylint: disable=no-member - return self.member_cls.valid_objects.filter(user=user, owner__uid__in=node_uids).exists() + return self.member_cls.valid_objects.filter(user=user, owner__uid__in=node_uids).exists() # pylint: disable=no-member if self.visibility == 4: if user.username in self.user_scope: # pylint: disable=no-member, unsupported-membership-test @@ -67,7 +67,7 @@ def is_open_to_manager(self, user): ''' 对管理员是否开放,由用户所在管理员组决定 ''' - return self.under_manage(user) + return self.under_manage(user) # pylint: disable=no-member def is_visible_to_manager(self, user): ''' @@ -78,11 +78,11 @@ def is_visible_to_manager(self, user): manage_node_uids = user.manage_node_uids - if set(self.upstream_uids) & manage_node_uids: + if set(self.upstream_uids) & manage_node_uids: # pylint: disable=no-member return True - for node in self.retrieve_nodes(manage_node_uids): - if self.node_uid in set(node.upstream_uids): + for node in self.retrieve_nodes(manage_node_uids): # pylint: disable=no-member + if self.node_uid in set(node.upstream_uids): # pylint: disable=no-member return True return False @@ -95,9 +95,9 @@ def refresh_visibility_scope(self): old_node_scope = set(self.node_scope) old_user_scope = set(self.user_scope) - valid_node_scope = set(node.node_uid for node in self.retrieve_nodes(old_node_scope)) + valid_node_scope = set(node.node_uid for node in self.retrieve_nodes(old_node_scope)) # pylint: disable=no-member - from oneid_meta.models import User + from oneid_meta.models import User # pylint: disable=import-outside-toplevel valid_user_scope = set(user.username for user in User.get_from_pks(old_user_scope, pk_name='username')) if valid_node_scope != old_node_scope: @@ -221,7 +221,7 @@ def retrieve_node(node_uid): ''' 通过node_uid 获取node及该节点类型 ''' - from oneid_meta.models import Dept, Group + from oneid_meta.models import Dept, Group # pylint: disable=import-outside-toplevel if node_uid.startswith(Dept.NODE_PREFIX): uid = node_uid.replace(Dept.NODE_PREFIX, '', 1) return Dept.valid_objects.filter(uid=uid).first(), 'dept' @@ -235,7 +235,7 @@ def retrieve_nodes(node_uids): ''' 通过node_uids 批量获取node ''' - from oneid_meta.models import Dept, Group + from oneid_meta.models import Dept, Group # pylint: disable=import-outside-toplevel dept_uids = set() group_uids = set() for node_uid in node_uids: @@ -271,9 +271,9 @@ def under_manage(self, user): upstream_uids = set(self.upstream_uids) for manager_group in user.manager_groups: if manager_group.scope_subject == 2: # 指定节点、人 - if upstream_uids & set(manager_group.nodes): + if self.node_uid in manager_group.nodes: return True - if manager_group.scope_subject == 1: # 所在节点 + if manager_group.scope_subject == 1: # 所在节点及下属节点 if upstream_uids & set(user.node_uids): return True return False diff --git a/oneid_meta/models/user.py b/oneid_meta/models/user.py index 05599d405..aa66730d3 100644 --- a/oneid_meta/models/user.py +++ b/oneid_meta/models/user.py @@ -368,7 +368,7 @@ def under_manage(self, user): self_all_node_uids = self.all_node_uids for manager_group in user.manager_groups: if manager_group.scope_subject == 2: # 指定节点、人 - if user.username in manager_group.users: + if self.username in manager_group.users: return True if self_all_node_uids & set(manager_group.nodes): return True @@ -399,14 +399,14 @@ def is_visible_to_employee(self, user): @property def manage_node_uids(self): ''' - 管理的节点(不包含下级) + 直接管理的节点(不包含下级) ''' res = set() for manager_group in self.manager_groups: - if manager_group.scope_subject == 2: + if manager_group.scope_subject == 2: # 指定节点、人 res.update(manager_group.nodes) continue - if manager_group.scope_subject == 1: + if manager_group.scope_subject == 1: # 所在节点 res.update(self.node_uids) return res diff --git a/siteapi/v1/views/user.py b/siteapi/v1/views/user.py index a16a84735..6b9155c87 100644 --- a/siteapi/v1/views/user.py +++ b/siteapi/v1/views/user.py @@ -16,11 +16,12 @@ ) from rest_framework.permissions import IsAuthenticated, SAFE_METHODS from django.db import transaction -from django.core.exceptions import ObjectDoesNotExist from django.db.models import Q +from django.core.exceptions import ObjectDoesNotExist from oneid_meta.models import User, Group, Dept from oneid.permissions import ( IsAdminUser, + IsManagerUser, IsUserManager, UserEmployeeReadable, UserManagerReadable, @@ -37,11 +38,18 @@ class UserListCreateAPIView(generics.ListCreateAPIView): ''' 用户列表 [GET],[POST] + + :GET + - 主管理员可见全部 + - 子管理员可见管理范围内的指定人、指定节点及其子孙节点内的所有人 + :POST + - 主管理员可以创建用户 + - 拥有 system_user_create 权限的子管理员 ''' serializer_class = EmployeeSerializer pagination_class = DefaultListPaginator - read_permission_classes = [IsAuthenticated & IsAdminUser] + read_permission_classes = [IsAuthenticated & (IsAdminUser | IsManagerUser)] write_permission_classes = [IsAuthenticated & (IsAdminUser | CustomPerm('system_user_create'))] def get_permissions(self): @@ -64,7 +72,18 @@ def get_queryset(self): else: queryset = User.valid_objects.exclude(is_boss=True).exclude(username='admin').order_by('id') - return queryset + user = self.request.user + if user.is_admin: + return queryset + + under_manage_user_ids = set() + + for item in queryset: + if item.under_manage(user): + under_manage_user_ids.add(item.username) + + under_manage_user_query_set = queryset.filter(username__in=under_manage_user_ids) + return under_manage_user_query_set @transaction.atomic() def create(self, request, *args, **kwargs): # pylint: disable=unused-argument diff --git a/test/tests/test_data.py b/test/tests/test_data.py index e9176fce2..7091c617b 100644 --- a/test/tests/test_data.py +++ b/test/tests/test_data.py @@ -83,4 +83,4 @@ def test_data_users(self): res2 = client.get(reverse('siteapi:user_list')) usernames = [i['user']['username'] for i in res2.json()['results']] expect = [str(j) for j in range(13899990001, 13899990011)] - self.assertEqual(usernames, expect) + self.assertEqual(usernames[:10], expect) diff --git a/test/tests/test_group_manager_node_tree_view.py b/test/tests/test_group_manager_node_tree_view.py new file mode 100644 index 000000000..ed222cd0a --- /dev/null +++ b/test/tests/test_group_manager_node_tree_view.py @@ -0,0 +1,38 @@ +# pylint: disable=missing-docstring +''' +test for api about node +''' +from django.urls import reverse +from siteapi.v1.tests import TestCase +from oneid_meta.models import ( + User, ) + + +class GroupManagerViewTestCase(TestCase): + def test_manager_two_perm_view(self): + ''' + 测试用户 13899990002(部门二admin)可见性 + 管理范围:部门一、部门二 + 其他权限:账号同步、创建应用、创建大类、公司配置、查看日志、创建用户 + 应用权限:应用一到十 + ''' + manager_two = User.objects.filter(username=13899990002).first() + client = self.login_as(manager_two) + + # 指定节点2个,分组管理只能看到2人 + res = client.get(reverse('siteapi:node_tree', args=('d_root', )), data={'user_required': True}) + self.assertEqual(res.json()['headcount'], 2) + + def test_manager_three_user_list_view(self): # pylint: disable=invalid-name + ''' + 部门三管理员(13899990003)权限: + 第一组权限:管理范围:部门一,角色三, 账号-部门二admin(13899990002)| 权限:账号同步、创建应用、创建大类 + 第二组权限:管理范围:部门一 | 权限:公司配置、查看日志、创建用户 + 编辑应用:应用一至十 + ''' + manager_three = User.objects.filter(username='13899990003').first() + client = self.login_as(manager_three) + + # user_list可见7人,包括部门一全部6人,用户13899990002 + res = client.get(reverse('siteapi:user_list')) + self.assertEqual(res.json()['count'], 7) From 9ca4780acdcc1580757d39c6262f9ead0cbeea90 Mon Sep 17 00:00:00 2001 From: shiyueLongguikeji Date: Mon, 30 Dec 2019 17:55:03 +0800 Subject: [PATCH 3/8] =?UTF-8?q?fix(user=5Flist):=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E4=BA=A4=E5=8F=89=E6=B5=8B=E8=AF=95=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_group_manager_node_tree_view.py | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/test/tests/test_group_manager_node_tree_view.py b/test/tests/test_group_manager_node_tree_view.py index ed222cd0a..d7320f8f1 100644 --- a/test/tests/test_group_manager_node_tree_view.py +++ b/test/tests/test_group_manager_node_tree_view.py @@ -21,18 +21,24 @@ def test_manager_two_perm_view(self): # 指定节点2个,分组管理只能看到2人 res = client.get(reverse('siteapi:node_tree', args=('d_root', )), data={'user_required': True}) + expect = ['13899990001', '13899990002'] self.assertEqual(res.json()['headcount'], 2) + self.assertEqual([j['username'] for i in res.json()['nodes'] for j in i['users']], expect) def test_manager_three_user_list_view(self): # pylint: disable=invalid-name ''' - 部门三管理员(13899990003)权限: - 第一组权限:管理范围:部门一,角色三, 账号-部门二admin(13899990002)| 权限:账号同步、创建应用、创建大类 - 第二组权限:管理范围:部门一 | 权限:公司配置、查看日志、创建用户 - 编辑应用:应用一至十 + 部门四管理员(13899990004)权限: + 第一组权限:管理范围:部门一、部门一admin、部门二admin | 权限:账号同步、创建应用、创建大类、公司配置、查看日志、创建用户 + 第二组权限:管理范围:部门二、部门三admin、部门四admin | 权限:应用一、应用二、应用三 ''' - manager_three = User.objects.filter(username='13899990003').first() - client = self.login_as(manager_three) + manager_four = User.objects.filter(username='13899990004').first() + client = self.login_as(manager_four) - # user_list可见7人,包括部门一全部6人,用户13899990002 + # user_list可见14人,包括部门一全部6人,部门二6人,部门三管理员,部门四管理员 res = client.get(reverse('siteapi:user_list')) - self.assertEqual(res.json()['count'], 7) + expect = [ + '13899990001', '13899990002', '13899990003', '13899990004', '13899990006', '13899990007', '13899990008', + '13899990009', '13899990010', '13899990011', '13899990012', '13899990013', '13899990014', '13899990015' + ] + self.assertEqual(res.json()['count'], 14) + self.assertEqual([i['user']['username'] for i in res.json()['results']], expect) From c3e2c08280af2dccbc4599a42bf301d0752cc024 Mon Sep 17 00:00:00 2001 From: shiyueLongguikeji Date: Mon, 30 Dec 2019 19:25:56 +0800 Subject: [PATCH 4/8] =?UTF-8?q?improvement(user=20list/user=20detail):=20?= =?UTF-8?q?=E5=8C=BA=E5=88=86=E5=8F=AF=E8=A7=81=E6=80=A7=E4=B8=8E=E5=8F=AF?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- oneid_meta/models/user.py | 20 ++++- siteapi/v1/views/user.py | 2 +- .../test_group_manager_node_tree_view.py | 87 +++++++++++++++++++ 3 files changed, 104 insertions(+), 5 deletions(-) diff --git a/oneid_meta/models/user.py b/oneid_meta/models/user.py index aa66730d3..d42a40b48 100644 --- a/oneid_meta/models/user.py +++ b/oneid_meta/models/user.py @@ -368,10 +368,11 @@ def under_manage(self, user): self_all_node_uids = self.all_node_uids for manager_group in user.manager_groups: if manager_group.scope_subject == 2: # 指定节点、人 + for item in self.nodes: + if item.under_manage(user): + return True if self.username in manager_group.users: return True - if self_all_node_uids & set(manager_group.nodes): - return True if manager_group.scope_subject == 1: # 所在节点 if self_all_node_uids & user.node_uids: return True @@ -380,9 +381,20 @@ def under_manage(self, user): def is_visible_to_manager(self, user): ''' 校验指定管理员是否可见此人 - TODO ''' - return self.under_manage(user) + if user.is_admin: + return True + self_all_node_uids = self.all_node_uids + for manager_group in user.manager_groups: + if manager_group.scope_subject == 2: # 指定节点、人 + if self.username in manager_group.users: + return True + if self_all_node_uids & set(manager_group.nodes): + return True + if manager_group.scope_subject == 1: # 所在节点 + if self_all_node_uids & user.node_uids: + return True + return False def is_visible_to_employee(self, user): ''' diff --git a/siteapi/v1/views/user.py b/siteapi/v1/views/user.py index 6b9155c87..6505bdb2d 100644 --- a/siteapi/v1/views/user.py +++ b/siteapi/v1/views/user.py @@ -79,7 +79,7 @@ def get_queryset(self): under_manage_user_ids = set() for item in queryset: - if item.under_manage(user): + if item.is_visible_to_manager(user): under_manage_user_ids.add(item.username) under_manage_user_query_set = queryset.filter(username__in=under_manage_user_ids) diff --git a/test/tests/test_group_manager_node_tree_view.py b/test/tests/test_group_manager_node_tree_view.py index d7320f8f1..56f7a719f 100644 --- a/test/tests/test_group_manager_node_tree_view.py +++ b/test/tests/test_group_manager_node_tree_view.py @@ -3,10 +3,85 @@ test for api about node ''' from django.urls import reverse +from rest_framework.status import HTTP_200_OK, HTTP_404_NOT_FOUND from siteapi.v1.tests import TestCase from oneid_meta.models import ( User, ) +USER_ONE = { + "avatar": + "", + "email": + "", + "employee_number": + "", + "gender": + 0, + "mobile": + "13899990001", + "name": + "部门一admin", + "position": + "", + "private_email": + "", + "username": + "13899990001", + "depts": + '', + "roles": + '', + "nodes": [{ + "node_uid": "g_wdexnpal", + "name": "wdexnpal", + "node_scope": [], + "user_scope": [], + "manager_group": { + "nodes": [], + "users": [], + "perms": [], + "apps": [''], + "scope_subject": 1 + }, + "users": [] + }, { + "node_uid": "d_bumenyi", + "name": "部门一(所有人可见)", + "node_scope": [], + "user_scope": [], + "users": [] + }], + "is_settled": + 'true', + "require_reset_password": + 'false', + "has_password": + 'true' +} +USER_SIX = { + "avatar": "", + "email": "", + "employee_number": "", + "gender": 0, + "mobile": "13899990006", + "name": "部门一一user", + "position": "", + "private_email": "", + "username": "13899990006", + "depts": '', + "roles": '', + "nodes": [{ + "node_uid": "d_bumenyiyi", + "name": "部门一(一)", + "node_scope": [], + "user_scope": [], + "users": [] + }], + "is_settled": 'true', + "require_reset_password": 'false', + "has_password": 'true' +} + class GroupManagerViewTestCase(TestCase): def test_manager_two_perm_view(self): @@ -42,3 +117,15 @@ def test_manager_three_user_list_view(self): # pylint: disable=invalid-name ] self.assertEqual(res.json()['count'], 14) self.assertEqual([i['user']['username'] for i in res.json()['results']], expect) + + # /user/13899990001, 可修改直接管理人员 + res = client.json_patch(reverse('siteapi:user_detail', args=('13899990001', )), data=USER_ONE) + self.assertEqual(res.status_code, HTTP_200_OK) + + # /user/13899990006, 不可修改间接管理人员,但是可见 + res = client.json_patch(reverse('siteapi:user_detail', args=('13899990006', )), data=USER_SIX) + self.assertEqual(res.status_code, HTTP_404_NOT_FOUND) + + # /user/13899990007, 不可删除间接管理人员,但是可见 + res = client.delete(reverse('siteapi:user_detail', args=('13899990007', ))) + self.assertEqual(res.status_code, HTTP_404_NOT_FOUND) From 5ccf596e6987ac8b0492cc7d7bf45b40e6e6a0d6 Mon Sep 17 00:00:00 2001 From: shiyueLongguikeji Date: Tue, 31 Dec 2019 15:57:35 +0800 Subject: [PATCH 5/8] =?UTF-8?q?improvement(user=5Flist):=20=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E5=88=86=E7=BB=84=E7=AE=A1=E7=90=86=E3=80=81=E8=B4=A6?= =?UTF-8?q?=E6=88=B7=E7=AE=A1=E7=90=86=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- oneid_meta/models/user.py | 16 ++++------ .../test_group_manager_node_tree_view.py | 32 ++++++++++++++++++- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/oneid_meta/models/user.py b/oneid_meta/models/user.py index df4cf5ab5..b19295662 100644 --- a/oneid_meta/models/user.py +++ b/oneid_meta/models/user.py @@ -368,8 +368,8 @@ def under_manage(self, user): self_all_node_uids = self.all_node_uids for manager_group in user.manager_groups: if manager_group.scope_subject == 2: # 指定节点、人 - for item in self.nodes: - if item.under_manage(user): + for node in self.nodes: + if node.under_manage(user): return True if self.username in manager_group.users: return True @@ -386,14 +386,10 @@ def is_visible_to_manager(self, user): return True self_all_node_uids = self.all_node_uids for manager_group in user.manager_groups: - if manager_group.scope_subject == 2: # 指定节点、人 - if self.username in manager_group.users: - return True - if self_all_node_uids & set(manager_group.nodes): - return True - if manager_group.scope_subject == 1: # 所在节点 - if self_all_node_uids & set(manager_group.nodes): - return True + if self.username in manager_group.users: + return True + if self_all_node_uids & set(manager_group.nodes): + return True return False def is_visible_to_employee(self, user): diff --git a/test/tests/test_group_manager_node_tree_view.py b/test/tests/test_group_manager_node_tree_view.py index 56f7a719f..04e03c402 100644 --- a/test/tests/test_group_manager_node_tree_view.py +++ b/test/tests/test_group_manager_node_tree_view.py @@ -3,7 +3,7 @@ test for api about node ''' from django.urls import reverse -from rest_framework.status import HTTP_200_OK, HTTP_404_NOT_FOUND +from rest_framework.status import HTTP_200_OK, HTTP_404_NOT_FOUND, HTTP_204_NO_CONTENT from siteapi.v1.tests import TestCase from oneid_meta.models import ( User, ) @@ -84,6 +84,36 @@ class GroupManagerViewTestCase(TestCase): + def test_manager_one_perm_view(self): + ''' + 测试用户13899990001(部门一admin)可见范围 + 管理范围:所在分组及下级 + 其他权限:空 + 应用权限:应用二 + ''' + manager_one = User.objects.filter(username=13899990001).first() + client = self.login_as(manager_one) + + # 分组管理可见本部门及下属6人 + res = client.get(reverse('siteapi:node_tree', args=('d_root', )), data={'user_required': True}) + self.assertEqual(res.json()['headcount'], 6) + expect = ['13899990006', '13899990007', '13899990008', '13899990009', '13899990010'] + self.assertEqual([j['username'] for i in res.json()['nodes'][0]['nodes'] for j in i['users']], expect) + + # 账户管理不可见,因为分组管理已经包括了可管理人员 + res = client.get(reverse('siteapi:user_list')) + self.assertEqual(res.json()['count'], 0) + expect = [] + self.assertEqual([i['user']['username'] for i in res.json()['results']], expect) + + # /user/13899990006, 可修改管理人员 + res = client.json_patch(reverse('siteapi:user_detail', args=('13899990006', )), data=USER_SIX) + self.assertEqual(res.status_code, HTTP_200_OK) + + # /user/13899990007, 可删除管理人员 + res = client.delete(reverse('siteapi:user_detail', args=('13899990007', ))) + self.assertEqual(res.status_code, HTTP_204_NO_CONTENT) + def test_manager_two_perm_view(self): ''' 测试用户 13899990002(部门二admin)可见性 From 3a2f2ed4a03dad2739d25c342cc5cc7f6ba08c47 Mon Sep 17 00:00:00 2001 From: shiyue1 Date: Mon, 24 Feb 2020 18:31:55 +0800 Subject: [PATCH 6/8] =?UTF-8?q?fix(view):=E5=8F=AF=E8=A7=81=E6=80=A7?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/tests/test_manager_view.py | 56 +-------------------------------- 1 file changed, 1 insertion(+), 55 deletions(-) diff --git a/test/tests/test_manager_view.py b/test/tests/test_manager_view.py index 5e39a4d81..1108af29b 100644 --- a/test/tests/test_manager_view.py +++ b/test/tests/test_manager_view.py @@ -304,61 +304,7 @@ def test_group_manager_three_view(self): 'remark': '' }, 'nodes': [ - { - 'info': { - 'dept_id': 7, - 'node_uid': 'd_bumenyiyi', - 'node_subject': 'dept', - 'uid': 'bumenyiyi', - 'name': '部门一(一)', - 'remark': '' - }, - 'nodes': [] - }, - { - 'info': { - 'dept_id': 8, - 'node_uid': 'd_bumenyier', - 'node_subject': 'dept', - 'uid': 'bumenyier', - 'name': '部门一(二)', - 'remark': '' - }, - 'nodes': [] - }, - { - 'info': { - 'dept_id': 9, - 'node_uid': 'd_bumenyisan', - 'node_subject': 'dept', - 'uid': 'bumenyisan', - 'name': '部门一(三)', - 'remark': '' - }, - 'nodes': [] - }, - { - 'info': { - 'dept_id': 10, - 'node_uid': 'd_bumenyisi', - 'node_subject': 'dept', - 'uid': 'bumenyisi', - 'name': '部门一(四)', - 'remark': '' - }, - 'nodes': [] - }, - { - 'info': { - 'dept_id': 11, - 'node_uid': 'd_bumenyiwu', - 'node_subject': 'dept', - 'uid': 'bumenyiwu', - 'name': '部门一(五)', - 'remark': '' - }, - 'nodes': [] - }, + ] }] } From 13f1ad339027bb92f0881a7fa781d5435e5f8bcd Mon Sep 17 00:00:00 2001 From: winfun Date: Tue, 25 Feb 2020 15:36:19 +0800 Subject: [PATCH 7/8] =?UTF-8?q?fix(user=5Flist):=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- oneid_meta/models/group.py | 2 +- oneid_meta/models/mixin.py | 61 ++++++++++++++++++++++++++++---------- oneid_meta/models/user.py | 46 +++++++++++++++++++++++----- siteapi/v1/views/user.py | 24 +++++++++++++-- 4 files changed, 106 insertions(+), 27 deletions(-) diff --git a/oneid_meta/models/group.py b/oneid_meta/models/group.py index c809f17e6..d6b3c65c0 100644 --- a/oneid_meta/models/group.py +++ b/oneid_meta/models/group.py @@ -147,7 +147,7 @@ def detail_serializer_cls(self): ''' 详情序列化类 ''' - from siteapi.v1.serializers.dept import DeptDetailSerializer + from siteapi.v1.serializers.dept import DeptDetailSerializer # pylint: disable=import-outside-toplevel return DeptDetailSerializer @property diff --git a/oneid_meta/models/mixin.py b/oneid_meta/models/mixin.py index ea8703b67..bf4359cd3 100644 --- a/oneid_meta/models/mixin.py +++ b/oneid_meta/models/mixin.py @@ -12,7 +12,7 @@ class NodeVisibilityScope(models.Model): ''' 节点对员工的可见范围 ''' - class Meta: + class Meta: # pylint: disable=missing-class-docstring abstract = True VISIBILITY_SUBJECT = ( # 此处`对...可见`,为`对...开放`的意思,即使判定不可见,最终也有可能可以看到 @@ -41,7 +41,7 @@ def is_open_to_employee(self, user): if self.visibility == 3: node_uids = map(lambda node: node.uid, self.tree_front_walker()) # pylint: disable=no-member - return self.member_cls.valid_objects.filter(user=user, owner__uid__in=node_uids).exists() + return self.member_cls.valid_objects.filter(user=user, owner__uid__in=node_uids).exists() # pylint: disable=no-member if self.visibility == 4: if user.username in self.user_scope: # pylint: disable=no-member, unsupported-membership-test @@ -67,7 +67,7 @@ def is_open_to_manager(self, user): ''' 对管理员是否开放,由用户所在管理员组决定 ''' - return self.under_manage(user) + return self.under_manage(user) # pylint: disable=no-member def is_visible_to_manager(self, user): ''' @@ -78,11 +78,11 @@ def is_visible_to_manager(self, user): manage_node_uids = user.manage_node_uids - if set(self.upstream_uids) & manage_node_uids: + if set(self.upstream_uids) & manage_node_uids: # pylint: disable=no-member return True - for node in self.retrieve_nodes(manage_node_uids): - if self.node_uid in set(node.upstream_uids): + for node in self.retrieve_nodes(manage_node_uids): # pylint: disable=no-member + if self.node_uid in set(node.upstream_uids): # pylint: disable=no-member return True return False @@ -95,9 +95,9 @@ def refresh_visibility_scope(self): old_node_scope = set(self.node_scope) old_user_scope = set(self.user_scope) - valid_node_scope = set(node.node_uid for node in self.retrieve_nodes(old_node_scope)) + valid_node_scope = set(node.node_uid for node in self.retrieve_nodes(old_node_scope)) # pylint: disable=no-member - from oneid_meta.models import User + from oneid_meta.models import User # pylint: disable=import-outside-toplevel valid_user_scope = set(user.username for user in User.get_from_pks(old_user_scope, pk_name='username')) if valid_node_scope != old_node_scope: @@ -191,14 +191,45 @@ def upstream_uids(self): return chain([self.node_uid], res) + @classmethod + def get_upstream_uids(cls, node_uid): + ''' + 节点向上追溯的路径,包括该节点本身,包括终点节点,以node_uid形式返回 + ''' + key = f'oneid:node:{node_uid}:upstream' + res = cache.get(key) + if res is None: + node, _ = cls.retrieve_node(node_uid) + if node: + node_uids = [item.node_uid for item in node.path_up_to()] + cache.set(key, node_uids[1:]) + return node_uids + + return chain([node_uid], res) + @property def downstream_uids(self): ''' 节点以及其子孙节点,以node_uid形式返回 - 目前使用频次不大 ''' - for node in self.downstream: - yield node.node_uid + return self.get_downstream_uids(self.node_uid) + + @classmethod + def get_downstream_uids(cls, node_uid): + ''' + 节点以及其子孙节点,以node_uid形式返回 + TODO: 继续优化,从子节点的 downstream_uids 聚合 + TODO: 删除节点时,删除缓存 + ''' + key = f'oneid:node:{node_uid}:downstream' + res = cache.get(key) + if res is None: + node, _ = cls.retrieve_node(node_uid) + if node: + res = [item.node_uid for item in node.downstream] + cache.set(key, res[1:0]) + return res + return chain([node_uid], res) @property def downstream(self): @@ -221,7 +252,7 @@ def retrieve_node(node_uid): ''' 通过node_uid 获取node及该节点类型 ''' - from oneid_meta.models import Dept, Group + from oneid_meta.models import Dept, Group # pylint: disable=import-outside-toplevel if node_uid.startswith(Dept.NODE_PREFIX): uid = node_uid.replace(Dept.NODE_PREFIX, '', 1) return Dept.valid_objects.filter(uid=uid).first(), 'dept' @@ -235,7 +266,7 @@ def retrieve_nodes(node_uids): ''' 通过node_uids 批量获取node ''' - from oneid_meta.models import Dept, Group + from oneid_meta.models import Dept, Group # pylint: disable=import-outside-toplevel dept_uids = set() group_uids = set() for node_uid in node_uids: @@ -271,9 +302,9 @@ def under_manage(self, user): upstream_uids = set(self.upstream_uids) for manager_group in user.manager_groups: if manager_group.scope_subject == 2: # 指定节点、人 - if upstream_uids & set(manager_group.nodes): + if self.node_uid in manager_group.nodes: return True - if manager_group.scope_subject == 1: # 所在节点 + if manager_group.scope_subject == 1: # 所在节点及下属节点 if upstream_uids & set(user.node_uids): return True return False diff --git a/oneid_meta/models/user.py b/oneid_meta/models/user.py index 05599d405..b19295662 100644 --- a/oneid_meta/models/user.py +++ b/oneid_meta/models/user.py @@ -368,9 +368,10 @@ def under_manage(self, user): self_all_node_uids = self.all_node_uids for manager_group in user.manager_groups: if manager_group.scope_subject == 2: # 指定节点、人 - if user.username in manager_group.users: - return True - if self_all_node_uids & set(manager_group.nodes): + for node in self.nodes: + if node.under_manage(user): + return True + if self.username in manager_group.users: return True if manager_group.scope_subject == 1: # 所在节点 if self_all_node_uids & user.node_uids: @@ -380,9 +381,16 @@ def under_manage(self, user): def is_visible_to_manager(self, user): ''' 校验指定管理员是否可见此人 - TODO ''' - return self.under_manage(user) + if user.is_admin: + return True + self_all_node_uids = self.all_node_uids + for manager_group in user.manager_groups: + if self.username in manager_group.users: + return True + if self_all_node_uids & set(manager_group.nodes): + return True + return False def is_visible_to_employee(self, user): ''' @@ -399,17 +407,39 @@ def is_visible_to_employee(self, user): @property def manage_node_uids(self): ''' - 管理的节点(不包含下级) + 直接管理的节点(不包含下级) ''' res = set() for manager_group in self.manager_groups: - if manager_group.scope_subject == 2: + if manager_group.scope_subject == 2: # 指定节点、人 res.update(manager_group.nodes) continue - if manager_group.scope_subject == 1: + if manager_group.scope_subject == 1: # 所在节点 res.update(self.node_uids) return res + @property + def manage_user_uids(self): + ''' + 直接管理的人员(不包括从管理组继承而来的可管理的人) + ''' + res = set() + for manager_group in self.manager_groups: + if manager_group.scope_subject == 2: # 指定节点、人 + res.update(manager_group.users) + return res + + @property + def all_manage_node_uids(self): + ''' + 所有可管理的节点(包含直接管理的节点,及其下属节点) + ''' + + res = set() + for node_uid in self.manage_node_uids: + res.update(Node.get_downstream_uids(node_uid)) + return res + def check_password(self, password): ''' 校验密码是否正确 diff --git a/siteapi/v1/views/user.py b/siteapi/v1/views/user.py index a16a84735..e328d019d 100644 --- a/siteapi/v1/views/user.py +++ b/siteapi/v1/views/user.py @@ -16,11 +16,12 @@ ) from rest_framework.permissions import IsAuthenticated, SAFE_METHODS from django.db import transaction -from django.core.exceptions import ObjectDoesNotExist from django.db.models import Q +from django.core.exceptions import ObjectDoesNotExist from oneid_meta.models import User, Group, Dept from oneid.permissions import ( IsAdminUser, + IsManagerUser, IsUserManager, UserEmployeeReadable, UserManagerReadable, @@ -37,11 +38,17 @@ class UserListCreateAPIView(generics.ListCreateAPIView): ''' 用户列表 [GET],[POST] + :GET + - 主管理员可见全部 + - 子管理员可见管理范围内的指定人、指定节点及其子孙节点内的所有人 + :POST + - 主管理员可以创建用户 + - 拥有 system_user_create 权限的子管理员 ''' serializer_class = EmployeeSerializer pagination_class = DefaultListPaginator - read_permission_classes = [IsAuthenticated & IsAdminUser] + read_permission_classes = [IsAuthenticated & (IsAdminUser | IsManagerUser)] write_permission_classes = [IsAuthenticated & (IsAdminUser | CustomPerm('system_user_create'))] def get_permissions(self): @@ -64,7 +71,18 @@ def get_queryset(self): else: queryset = User.valid_objects.exclude(is_boss=True).exclude(username='admin').order_by('id') - return queryset + user = self.request.user + if user.is_admin: + return queryset + + under_manage_user_ids = set() + + for item in queryset: + if item.is_visible_to_manager(user): + under_manage_user_ids.add(item.username) + + under_manage_user_query_set = queryset.filter(username__in=under_manage_user_ids) + return under_manage_user_query_set @transaction.atomic() def create(self, request, *args, **kwargs): # pylint: disable=unused-argument From 8d8a82abaf0725a7cd8cb5a8da46ce05ccc4cf21 Mon Sep 17 00:00:00 2001 From: winfun Date: Tue, 25 Feb 2020 17:30:09 +0800 Subject: [PATCH 8/8] =?UTF-8?q?feat(user=5Fview):=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/tests/test_manager_view.py | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/test/tests/test_manager_view.py b/test/tests/test_manager_view.py index 1108af29b..f002f1f4c 100644 --- a/test/tests/test_manager_view.py +++ b/test/tests/test_manager_view.py @@ -267,8 +267,9 @@ def test_group_manager_two_views(self): "require_reset_password": 'false', "has_password": 'true' } + # 不可修改非直接管理权限下人员 res = client.json_patch(reverse('siteapi:user_detail', args=('13899990006', )), data=patch_user_data) - self.assertEqual(res.status_code, HTTP_200_OK) + self.assertEqual(res.status_code, HTTP_404_NOT_FOUND) # 不可添加顶层部门 res = client.json_post(reverse('siteapi:node_child_node', args=('d_root', )), data=NEW_TOP_LEVEL_DEPT) @@ -303,9 +304,7 @@ def test_group_manager_three_view(self): 'name': '部门一(所有人可见)', 'remark': '' }, - 'nodes': [ - - ] + 'nodes': [] }] } self.assertEqual(expect, res_view_phonebook.json()) @@ -330,18 +329,7 @@ def test_group_manager_three_view(self): 'remark': '', 'accept_user': True }, - 'nodes': [{ - 'info': { - 'group_id': 27, - 'node_uid': 'g_juesesanyi', - 'node_subject': 'role', - 'uid': 'juesesanyi', - 'name': '角色三(一)', - 'remark': '', - 'accept_user': True - }, - 'nodes': [] - }] + 'nodes': [] }] } self.assertEqual(res.json(), expect) @@ -419,7 +407,7 @@ def test_group_manager_three_view(self): "has_password": 'true' } res = client.json_patch(reverse('siteapi:user_detail', args=('13899990006', )), data=patch_user_data) - self.assertEqual(res.status_code, HTTP_200_OK) + self.assertEqual(res.status_code, HTTP_404_NOT_FOUND) # 不可编辑管理范围外用户信息 patch_user_data = {