diff --git a/apps/users/api/user_group.py b/apps/users/api/user_group.py index fd5d77333f3..629867d35d4 100644 --- a/apps/users/api/user_group.py +++ b/apps/users/api/user_group.py @@ -26,20 +26,22 @@ def get_response(): class DeleteUserGroupApi(APIMixin): @staticmethod def get_parameters(): - return [OpenApiParameter( - name='workspace_id', - type=OpenApiTypes.STR, - description=_('Workspace ID'), - required=True, - location=OpenApiParameter.PATH, # type: ignore - ), + return [ + OpenApiParameter( + name="workspace_id", + type=OpenApiTypes.STR, + description=_("Workspace ID"), + required=True, + location=OpenApiParameter.PATH, # type: ignore + ), OpenApiParameter( name="user_group_id", description=_("User Group ID"), type=OpenApiTypes.STR, location=OpenApiParameter.PATH, # type: ignore required=True, - )] + ), + ] @staticmethod def get_response(): @@ -60,12 +62,13 @@ def get_response(): def get_parameters(): return [ OpenApiParameter( - name='workspace_id', + name="workspace_id", type=OpenApiTypes.STR, - description=_('Workspace ID'), + description=_("Workspace ID"), required=True, location=OpenApiParameter.PATH, # type: ignore - )] + ) + ] class UserGroupListPageResponse(ResultSerializer): @@ -78,44 +81,51 @@ class UserGroupListPageApi(APIMixin): def get_parameters(): return [ OpenApiParameter( - name='workspace_id', + name="workspace_id", type=OpenApiTypes.STR, - description=_('Workspace ID'), + description=_("Workspace ID"), required=True, location=OpenApiParameter.PATH, # type: ignore ), OpenApiParameter( - name='user_group_id', + name="user_group_id", type=OpenApiTypes.STR, - description=_('Group ID'), + description=_("Group ID"), required=True, location=OpenApiParameter.PATH, # type: ignore ), OpenApiParameter( - name='current_page', + name="current_page", type=OpenApiTypes.INT, - description=_('Current page'), + description=_("Current page"), required=True, location=OpenApiParameter.PATH, # type: ignore ), OpenApiParameter( - name='page_size', + name="page_size", type=OpenApiTypes.INT, - description=_('Page size'), + description=_("Page size"), required=True, location=OpenApiParameter.PATH, # type: ignore ), OpenApiParameter( - name='username', + name="username", type=OpenApiTypes.STR, - description=_('Username'), + description=_("Username"), required=False, location=OpenApiParameter.QUERY, # type: ignore ), OpenApiParameter( - name='nick_name', + name="nick_name", type=OpenApiTypes.STR, - description=_('Nickname'), + description=_("Nickname"), + required=False, + location=OpenApiParameter.QUERY, # type: ignore + ), + OpenApiParameter( + name="role", + type=OpenApiTypes.STR, + description=_("Role"), required=False, location=OpenApiParameter.QUERY, # type: ignore ), @@ -127,11 +137,7 @@ def get_response(): class AddMemberRequest(serializers.Serializer): - user_ids = serializers.ListField( - child=serializers.CharField(required=True), - required=True, - label=_('User IDs') - ) + user_ids = serializers.ListField(child=serializers.CharField(required=True), required=True, label=_("User IDs")) class AddMemberApi(APIMixin): @@ -139,9 +145,9 @@ class AddMemberApi(APIMixin): def get_parameters(): return [ OpenApiParameter( - name='workspace_id', + name="workspace_id", type=OpenApiTypes.STR, - description=_('Workspace ID'), + description=_("Workspace ID"), required=True, location=OpenApiParameter.PATH, # type: ignore ), @@ -151,7 +157,8 @@ def get_parameters(): type=OpenApiTypes.STR, location=OpenApiParameter.PATH, # type: ignore required=True, - )] + ), + ] @staticmethod def get_request(): @@ -164,9 +171,7 @@ def get_response(): class RemoveMemberRequest(serializers.Serializer): group_relation_ids = serializers.ListField( - child=serializers.CharField(required=True), - required=True, - label=_('User group relation IDs') + child=serializers.CharField(required=True), required=True, label=_("User group relation IDs") ) @@ -175,9 +180,9 @@ class RemoveMemberApi(APIMixin): def get_parameters(): return [ OpenApiParameter( - name='workspace_id', + name="workspace_id", type=OpenApiTypes.STR, - description=_('Workspace ID'), + description=_("Workspace ID"), required=True, location=OpenApiParameter.PATH, # type: ignore ), @@ -187,7 +192,8 @@ def get_parameters(): type=OpenApiTypes.STR, location=OpenApiParameter.PATH, # type: ignore required=True, - )] + ), + ] @staticmethod def get_request(): diff --git a/apps/users/serializers/user_group.py b/apps/users/serializers/user_group.py index 70212566f2b..c903baadb4c 100644 --- a/apps/users/serializers/user_group.py +++ b/apps/users/serializers/user_group.py @@ -220,6 +220,7 @@ class Query(serializers.Serializer): group_id = serializers.CharField(required=True, label=_("Group ID")) username = serializers.CharField(required=False, label=_("Username"), allow_null=True) nick_name = serializers.CharField(required=False, label=_("Nick Name"), allow_null=True) + role = serializers.CharField(required=False, label=_("Role"), allow_null=True) source = serializers.CharField(required=False, label=_("Source"), allow_null=True) def is_valid(self, *, raise_exception=False): @@ -272,6 +273,7 @@ def get_query_set(self): group_id = self.data.get("group_id") username = self.data.get("username") nick_name = self.data.get("nick_name") + role = self.data.get("role") source = self.data.get("source") query_set = SystemUserGroupRelation.objects.filter(group_id=group_id).select_related("user") @@ -279,6 +281,16 @@ def get_query_set(self): query_set = query_set.filter(user__username__contains=username) if nick_name is not None: query_set = query_set.filter(user__nick_name__contains=nick_name) + if role is not None: + user_role_relation_model = DatabaseModelManage.get_model("workspace_user_role_mapping") + if user_role_relation_model is None: + return query_set.none() + role_user_ids = user_role_relation_model.objects.filter( + workspace_id=self.data.get("workspace_id"), + role__type=RoleConstants.USER.name, + role__role_name__icontains=role, + ).values("user_id") + query_set = query_set.filter(user_id__in=role_user_ids) if source is not None: query_set = query_set.filter(user__source=source) return query_set.order_by("-user__create_time") diff --git a/apps/users/tests/test_user_group.py b/apps/users/tests/test_user_group.py index 102610a3275..86dfd472239 100644 --- a/apps/users/tests/test_user_group.py +++ b/apps/users/tests/test_user_group.py @@ -3,7 +3,12 @@ from django.test import TestCase +from common.auth.constants.role_constants import RoleConstants +from common.database_model_manage.database_model_manage import DatabaseModelManage +from users.models import User from users.models.user_group import SystemUserGroup +from users.models.user_group import SystemUserGroupRelation +from users.serializers.user_group import UserGroupListPageSerializer class SystemUserGroupWorkspaceIsolationTests(TestCase): @@ -17,3 +22,51 @@ def test_same_group_name_rejected_in_same_workspace(self): SystemUserGroup.objects.create(id=uuid.uuid7(), name="test", workspace_id="default") with self.assertRaises(Exception): SystemUserGroup.objects.create(id=uuid.uuid7(), name="test", workspace_id="default") + + +class SystemUserGroupMemberRoleFilterTests(TestCase): + def test_filter_members_by_role(self): + DatabaseModelManage.init() + role_model = DatabaseModelManage.get_model("role_model") + user_role_relation_model = DatabaseModelManage.get_model("workspace_user_role_mapping") + if role_model is None or user_role_relation_model is None: + self.skipTest("Role search requires the enterprise role model") + + group = SystemUserGroup.objects.create(name="test-group", workspace_id="workspace-1") + target_user = User.objects.create( + username="target-user", + nick_name="Target User", + password="encrypted-password", + role=RoleConstants.USER.name, + ) + other_user = User.objects.create( + username="other-user", + nick_name="Other User", + password="encrypted-password", + role=RoleConstants.USER.name, + ) + SystemUserGroupRelation.objects.create(user=target_user, group=group) + SystemUserGroupRelation.objects.create(user=other_user, group=group) + + target_role = role_model.objects.create(role_name="知识库管理员", type=RoleConstants.USER.name) + other_role = role_model.objects.create(role_name="应用管理员", type=RoleConstants.USER.name) + user_role_relation_model.objects.create( + user=target_user, + role=target_role, + workspace_id=group.workspace_id, + ) + user_role_relation_model.objects.create( + user=other_user, + role=other_role, + workspace_id=group.workspace_id, + ) + + result = UserGroupListPageSerializer.Query( + data={ + "workspace_id": group.workspace_id, + "group_id": group.id, + "role": "知识库管理员", + } + ).page(1, 20) + + self.assertEqual(["target-user"], [member["username"] for member in result["records"]]) diff --git a/apps/users/views/system_user_group.py b/apps/users/views/system_user_group.py index 1280278a484..a787cb1ac87 100644 --- a/apps/users/views/system_user_group.py +++ b/apps/users/views/system_user_group.py @@ -190,6 +190,7 @@ def get(self, request: Request, workspace_id: str, user_group_id: str, current_p data={ "username": request.query_params.get("username", None), "nick_name": request.query_params.get("nick_name", None), + "role": request.query_params.get("role", None), "source": request.query_params.get("source", None), "group_id": user_group_id, "workspace_id": workspace_id, diff --git a/ui/src/components/business/resource-authorization-drawer/user-group/UserGroupMembersDrawer.vue b/ui/src/components/business/resource-authorization-drawer/user-group/UserGroupMembersDrawer.vue index 9589a7ece04..83a87324c92 100644 --- a/ui/src/components/business/resource-authorization-drawer/user-group/UserGroupMembersDrawer.vue +++ b/ui/src/components/business/resource-authorization-drawer/user-group/UserGroupMembersDrawer.vue @@ -3,21 +3,24 @@ import { computed, ref } from 'vue' import UserGroupsApi from '@/api/admin/system/user-groups' import { LOGIN_METHOD } from '@/api/enums' import { LOGIN_METHOD_LABELS } from '@/constants' +import { useStore } from '@/stores' import type { Dict, LoginMethod, OptionItem, SystemUserGroup, SystemUserGroupMember } from '@/api/types' defineOptions({ name: 'UserGroupMembersDrawer' }) /* 成员查询与分页 */ +const { auth } = useStore() const drawerVisible = ref(false) const currentGroup = ref() const loadingMembers = ref(false) const userGroupMembers = ref([]) const paginationConfig = ref({ currentPage: 1, pageSize: 20, total: 0 }) const memberSearchQuery = ref>() -const memberSearchFields: OptionItem[] = [ +const memberSearchFields = computed[]>(() => [ { label: '姓名', value: 'nick_name' }, { label: '用户名', value: 'username' }, -] + ...(auth.isEE || auth.isPE ? [{ label: '角色', value: 'role' }] : []), +]) /* 表头筛选 */ const selectedMemberSource = ref(null) @@ -115,7 +118,7 @@ defineExpose({ open }) {{ row.source === LOGIN_METHOD.LOCAL ? '系统用户' : LOGIN_METHOD_LABELS[row.source as LoginMethod] }} - +