diff --git a/apps/application/tests/test_application_advanced_permissions.py b/apps/application/tests/test_application_advanced_permissions.py index 2cf21c4d5fd..0b3600bde6d 100644 --- a/apps/application/tests/test_application_advanced_permissions.py +++ b/apps/application/tests/test_application_advanced_permissions.py @@ -5,9 +5,10 @@ from application.views.application import ApplicationAPI from application.views.application_api_key import ApplicationApiKeyEnable +from common.auth.constants.group_constants import Group from common.auth.constants.operate_constants import Operate from common.auth.constants.permission_constants import PermissionConstants -from common.auth.struct.permission import Role +from common.auth.struct.permission import Permission, Role from common.exception.app_exception import AppUnauthorizedFailed @@ -20,13 +21,24 @@ def kwargs(self): def request_with_permission(self, permission_constant): permission = permission_constant.get_workspace_application_permission()(None, self.kwargs()) - permissions = {permission.get_resource_permission_key(permission.resource_id): permission.bit()} + return self.request_with_permissions(permission) + + def request_with_permissions(self, *permission_values, roles=None): + permissions = {} + for permission in permission_values: + permissions[permission.get_resource_permission_key(permission.resource_id)] = permission.bit() + resolved_roles = [Role("USER", self.workspace_id)] if roles is None else roles return SimpleNamespace( - auth=SimpleNamespace(roles=[Role("USER", self.workspace_id)], permissions=permissions), + auth=SimpleNamespace(roles=resolved_roles, permissions=permissions), user=SimpleNamespace(id=self.application_id, type="test"), META={"REMOTE_ADDR": "127.0.0.1"}, path="/test", - data={"enable_api_key": True}, + data={ + "enable_api_key": True, + "id_list": [self.application_id], + "clean_time": 30, + "file_clean_time": 7, + }, query_params={}, build_absolute_uri=lambda _: "http://localhost/", ) @@ -68,3 +80,41 @@ def test_mcp_permission_bits_exist_for_role_tree(self): PermissionConstants.APPLICATION_OVERVIEW_BACKEND_API_ACCESS.value.operate, Operate.BACKEND_API_ACCESS, ) + + def test_batch_clean_time_rejects_application_read_permission(self): + request = self.request_with_permissions( + PermissionConstants.APPLICATION_READ.get_workspace_permission()(None, self.kwargs()), + PermissionConstants.APPLICATION_CHAT_LOG_CLEAR_POLICY.get_workspace_application_permission()( + None, self.kwargs() + ), + roles=[], + ) + with ( + patch("application.views.application.ApplicationBatchOperateSerializer"), + patch("application.views.application.result.success"), + self.assertRaises(AppUnauthorizedFailed), + ): + ApplicationAPI.BatchCleanTime.put(ApplicationAPI.BatchCleanTime(), request, workspace_id=self.workspace_id) + + def test_batch_clean_time_accepts_dedicated_workspace_permission(self): + request = self.request_with_permissions( + Permission( + group=Group.APPLICATION, + sub_group=Group.APPLICATION, + operate=Operate.CLEAR_POLICY, + bit_index=42, + workspace_id=self.workspace_id, + ), + PermissionConstants.APPLICATION_CHAT_LOG_CLEAR_POLICY.get_workspace_application_permission()( + None, self.kwargs() + ), + roles=[], + ) + with ( + patch("application.views.application.ApplicationBatchOperateSerializer") as serializer, + patch("application.views.application.get_application_operation_object_batch", return_value=[]), + patch("application.views.application.result.success"), + ): + serializer.return_value.batch_clean_time.return_value = True + ApplicationAPI.BatchCleanTime.put(ApplicationAPI.BatchCleanTime(), request, workspace_id=self.workspace_id) + serializer.return_value.batch_clean_time.assert_called_once() diff --git a/apps/application/views/application.py b/apps/application/views/application.py index ab37b6edc4f..46379cfac4f 100644 --- a/apps/application/views/application.py +++ b/apps/application/views/application.py @@ -624,7 +624,7 @@ class BatchCleanTime(APIView): tags=[_("Application")], # type: ignore ) @has_permissions( - PermissionConstants.APPLICATION_READ.get_workspace_permission(), + PermissionConstants.APPLICATION_CLEAR_POLICY.get_workspace_permission(), RoleConstants.USER.get_workspace_role(), RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), ) diff --git a/apps/common/auth/constants/permission_constants.py b/apps/common/auth/constants/permission_constants.py index dc224e62cf2..51ecbcc6d3b 100644 --- a/apps/common/auth/constants/permission_constants.py +++ b/apps/common/auth/constants/permission_constants.py @@ -1688,6 +1688,16 @@ class PermissionConstants(Enum): ), ) + APPLICATION_CLEAR_POLICY = ( + Permission(group=Group.APPLICATION, sub_group=Group.APPLICATION, operate=Operate.CLEAR_POLICY, bit_index=42), + PermissionMeta( + role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER], + category=Category.WORKSPACE, + scope=[PermissionScopeConstants.WORKSPACE_RESOURCE, PermissionScopeConstants.WORKSPACE], + resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANAGE], + ), + ) + APPLICATION_RESOURCE_AUTHORIZATION = ( Permission(group=Group.APPLICATION, sub_group=Group.APPLICATION, operate=Operate.AUTH, bit_index=10), PermissionMeta( diff --git a/apps/system_manage/tests.py b/apps/system_manage/tests.py index f5e15c15c1b..3866389f841 100644 --- a/apps/system_manage/tests.py +++ b/apps/system_manage/tests.py @@ -5,6 +5,7 @@ from common.auth.constants.permission_constants import PermissionConstants from common.auth.struct.aggregate_permission import AggregatePermission +from common.auth.struct.permission import Permission from common.exception.app_exception import AppUnauthorizedFailed from system_manage.models import ChatUser, UserGroup, UserGroupRelation from system_manage.serializers.chat_user import ChatUserSerializer @@ -13,6 +14,9 @@ class ChatUserPermissionTests(SimpleTestCase): + workspace_id = "workspace-1" + resource_id = "resource-1" + @staticmethod def build_permissions(items, request, kwargs, compare): for item in items: @@ -20,6 +24,35 @@ def build_permissions(items, request, kwargs, compare): item(request, kwargs) return SimpleNamespace(hasPermission=lambda *args, **kwargs: True) + @classmethod + def build_extended_user_request(cls, permission_constant): + source = permission_constant.value + permission = Permission( + group=source.group, + sub_group=source.sub_group, + operate=source.operate, + bit_index=source.bit_index, + workspace_id=cls.workspace_id, + resource_id=cls.resource_id, + ) + return SimpleNamespace( + query_params={}, + data=[], + auth=SimpleNamespace( + roles={f"EXTENDS_USER:w:{cls.workspace_id}"}, + permissions={permission.get_resource_permission_key(cls.resource_id): permission.bit()}, + ), + ) + + @classmethod + def endpoint_kwargs(cls): + return { + "workspace_id": cls.workspace_id, + "resource_type": "APPLICATION", + "resource_id": cls.resource_id, + "user_group_id": "group-1", + } + def test_supported_resources_keep_existing_permissions(self): self.assertEqual( PermissionConstants.APPLICATION_CHAT_USER_READ, @@ -61,6 +94,52 @@ def test_list_page_and_group_endpoints_reject_tool_permission(self): resource_chat_user_group.ResourceChatUserGroup(), SimpleNamespace(), **kwargs ) + def test_extended_user_can_read_chat_users_and_groups(self): + request = self.build_extended_user_request(PermissionConstants.APPLICATION_CHAT_USER_READ) + kwargs = self.endpoint_kwargs() + group_kwargs = {key: value for key, value in kwargs.items() if key != "user_group_id"} + + with ( + patch("system_manage.views.resource_chat_user.ResourceChatUserQuerySerializer") as user_query, + patch("system_manage.views.resource_chat_user_group.ResourceChatUserGroupQuerySerializer") as group_query, + ): + user_query.return_value.list.return_value = [] + user_query.return_value.page.return_value = [] + group_query.return_value.list.return_value = [] + group_query.return_value.page.return_value = [] + + ResourceChatUser.get(ResourceChatUser(), request, **kwargs) + ResourceChatUser.Page.get(ResourceChatUser.Page(), request, **kwargs, current_page=1, page_size=20) + resource_chat_user_group.ResourceChatUserGroup.get( + resource_chat_user_group.ResourceChatUserGroup(), request, **group_kwargs + ) + resource_chat_user_group.ResourceChatUserGroup.Page.get( + resource_chat_user_group.ResourceChatUserGroup.Page(), + request, + **group_kwargs, + current_page=1, + page_size=20, + ) + + def test_extended_user_can_edit_chat_users_and_groups(self): + request = self.build_extended_user_request(PermissionConstants.APPLICATION_CHAT_USER_EDIT) + kwargs = self.endpoint_kwargs() + group_kwargs = {key: value for key, value in kwargs.items() if key != "user_group_id"} + + with ( + patch("system_manage.views.resource_chat_user.ResourceChatUserSerializer") as user_edit, + patch("system_manage.views.resource_chat_user_group.ResourceChatUserGroupSerializer") as group_edit, + ): + user_edit.return_value.edit.return_value = [] + group_edit.return_value.edit.return_value = [] + ResourceChatUser.put(ResourceChatUser(), request, **kwargs) + resource_chat_user_group.ResourceChatUserGroup.put( + resource_chat_user_group.ResourceChatUserGroup(), request, **group_kwargs + ) + + user_edit.return_value.edit.assert_called_once_with([]) + group_edit.return_value.edit.assert_called_once_with([]) + class ChatUserEditTests(TestCase): def test_status_and_password_changes_preserve_user_groups(self): diff --git a/apps/system_manage/views/resource_chat_user.py b/apps/system_manage/views/resource_chat_user.py index afb1f3b4c13..ab19cce8bc1 100644 --- a/apps/system_manage/views/resource_chat_user.py +++ b/apps/system_manage/views/resource_chat_user.py @@ -40,6 +40,32 @@ def get_chat_user_permission_constant(resource_type, operate): return PermissionConstants[f"{normalized}_CHAT_USER_{operate}"] +def get_chat_user_role_permissions(operate): + def get_permission(request, kwargs): + constant = get_chat_user_permission_constant(kwargs.get("resource_type"), operate).value + return Permission( + group=constant.group, + sub_group=constant.sub_group, + operate=constant.operate, + bit_index=constant.bit_index, + workspace_id=kwargs.get("workspace_id"), + resource_id=kwargs.get("resource_id"), + ) + + return [ + ViewPermission( + [RoleConstants.USER.get_workspace_role()], + [get_permission], + compare=CompareConstants.AND, + ), + ViewPermission( + [RoleConstants.EXTENDS_USER.get_workspace_role()], + [get_permission], + compare=CompareConstants.AND, + ), + ] + + class ResourceChatUser(APIView): authentication_classes = [TokenAuth] @@ -59,20 +85,7 @@ class ResourceChatUser(APIView): lambda r, kwargs: get_chat_user_permission_constant( kwargs.get("resource_type"), "READ" ).get_workspace_permission_workspace_manage_role(), - ViewPermission( - [RoleConstants.USER.get_workspace_role()], - [ - lambda r, kwargs: Permission( - group=get_chat_user_permission_constant(kwargs.get("resource_type"), "READ").value.group, - sub_group=get_chat_user_permission_constant(kwargs.get("resource_type"), "READ").value.sub_group, - operate=get_chat_user_permission_constant(kwargs.get("resource_type"), "READ").value.operate, - bit_index=get_chat_user_permission_constant(kwargs.get("resource_type"), "READ").value.bit_index, - workspace_id=kwargs.get("workspace_id"), - resource_id=kwargs.get("resource_id"), - ) - ], - compare=CompareConstants.AND, - ), + *get_chat_user_role_permissions("READ"), RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), ) def get(self, request: Request, workspace_id: str, resource_type: str, resource_id: str, user_group_id: str): @@ -107,20 +120,7 @@ def get(self, request: Request, workspace_id: str, resource_type: str, resource_ lambda r, kwargs: get_chat_user_permission_constant( kwargs.get("resource_type"), "EDIT" ).get_workspace_permission_workspace_manage_role(), - ViewPermission( - [RoleConstants.USER.get_workspace_role()], - [ - lambda r, kwargs: Permission( - group=get_chat_user_permission_constant(kwargs.get("resource_type"), "EDIT").value.group, - sub_group=get_chat_user_permission_constant(kwargs.get("resource_type"), "EDIT").value.sub_group, - operate=get_chat_user_permission_constant(kwargs.get("resource_type"), "EDIT").value.operate, - bit_index=get_chat_user_permission_constant(kwargs.get("resource_type"), "EDIT").value.bit_index, - workspace_id=kwargs.get("workspace_id"), - resource_id=kwargs.get("resource_id"), - ) - ], - compare=CompareConstants.AND, - ), + *get_chat_user_role_permissions("EDIT"), RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), ) def put(self, request: Request, workspace_id: str, resource_type, resource_id: str, user_group_id: str): @@ -154,24 +154,7 @@ class Page(APIView): lambda r, kwargs: get_chat_user_permission_constant( kwargs.get("resource_type"), "READ" ).get_workspace_permission_workspace_manage_role(), - ViewPermission( - [RoleConstants.USER.get_workspace_role()], - [ - lambda r, kwargs: Permission( - group=get_chat_user_permission_constant(kwargs.get("resource_type"), "READ").value.group, - sub_group=get_chat_user_permission_constant( - kwargs.get("resource_type"), "READ" - ).value.sub_group, - operate=get_chat_user_permission_constant(kwargs.get("resource_type"), "READ").value.operate, - bit_index=get_chat_user_permission_constant( - kwargs.get("resource_type"), "READ" - ).value.bit_index, - workspace_id=kwargs.get("workspace_id"), - resource_id=kwargs.get("resource_id"), - ) - ], - compare=CompareConstants.AND, - ), + *get_chat_user_role_permissions("READ"), RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), ) def get( diff --git a/apps/system_manage/views/resource_chat_user_group.py b/apps/system_manage/views/resource_chat_user_group.py index fa56710cd21..01911c3f3e7 100644 --- a/apps/system_manage/views/resource_chat_user_group.py +++ b/apps/system_manage/views/resource_chat_user_group.py @@ -5,18 +5,14 @@ from common.auth import TokenAuth from common.auth.authentication import has_permissions -from common.auth.constants.compare_constants import CompareConstants from common.auth.constants.role_constants import RoleConstants -from common.auth.struct.aggregate_permission import ViewPermission -from common.auth.struct.permission import Permission - from common.result import result from system_manage.api.resource_chat_user_group import ResourceChatUserGroupListAPI, ResourceChatUserGroupEditAPI from system_manage.serializers.resource_chat_user_group import ( ResourceChatUserGroupQuerySerializer, ResourceChatUserGroupSerializer, ) -from system_manage.views.resource_chat_user import get_chat_user_permission_constant +from system_manage.views.resource_chat_user import get_chat_user_permission_constant, get_chat_user_role_permissions class ResourceChatUserGroup(APIView): @@ -38,20 +34,7 @@ class ResourceChatUserGroup(APIView): lambda r, kwargs: get_chat_user_permission_constant( kwargs.get("resource_type"), "READ" ).get_workspace_permission_workspace_manage_role(), - ViewPermission( - [RoleConstants.USER.get_workspace_role()], - [ - lambda r, kwargs: Permission( - group=get_chat_user_permission_constant(kwargs.get("resource_type"), "READ").value.group, - sub_group=get_chat_user_permission_constant(kwargs.get("resource_type"), "READ").value.sub_group, - operate=get_chat_user_permission_constant(kwargs.get("resource_type"), "READ").value.operate, - bit_index=get_chat_user_permission_constant(kwargs.get("resource_type"), "READ").value.bit_index, - workspace_id=kwargs.get("workspace_id"), - resource_id=kwargs.get("resource_id"), - ) - ], - compare=CompareConstants.AND, - ), + *get_chat_user_role_permissions("READ"), RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), ) def get( @@ -89,20 +72,7 @@ def get( lambda r, kwargs: get_chat_user_permission_constant( kwargs.get("resource_type"), "EDIT" ).get_workspace_permission_workspace_manage_role(), - ViewPermission( - [RoleConstants.USER.get_workspace_role()], - [ - lambda r, kwargs: Permission( - group=get_chat_user_permission_constant(kwargs.get("resource_type"), "EDIT").value.group, - sub_group=get_chat_user_permission_constant(kwargs.get("resource_type"), "EDIT").value.sub_group, - operate=get_chat_user_permission_constant(kwargs.get("resource_type"), "EDIT").value.operate, - bit_index=get_chat_user_permission_constant(kwargs.get("resource_type"), "EDIT").value.bit_index, - workspace_id=kwargs.get("workspace_id"), - resource_id=kwargs.get("resource_id"), - ) - ], - compare=CompareConstants.AND, - ), + *get_chat_user_role_permissions("EDIT"), RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), ) def put(self, request: Request, workspace_id: str, resource_id: str, resource_type: str): @@ -135,24 +105,7 @@ class Page(APIView): lambda r, kwargs: get_chat_user_permission_constant( kwargs.get("resource_type"), "READ" ).get_workspace_permission_workspace_manage_role(), - ViewPermission( - [RoleConstants.USER.get_workspace_role()], - [ - lambda r, kwargs: Permission( - group=get_chat_user_permission_constant(kwargs.get("resource_type"), "READ").value.group, - sub_group=get_chat_user_permission_constant( - kwargs.get("resource_type"), "READ" - ).value.sub_group, - operate=get_chat_user_permission_constant(kwargs.get("resource_type"), "READ").value.operate, - bit_index=get_chat_user_permission_constant( - kwargs.get("resource_type"), "READ" - ).value.bit_index, - workspace_id=kwargs.get("workspace_id"), - resource_id=kwargs.get("resource_id"), - ) - ], - compare=CompareConstants.AND, - ), + *get_chat_user_role_permissions("READ"), RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), ) def get( diff --git a/ui/src/permission/core/data.ts b/ui/src/permission/core/data.ts index b5ad04e3eca..43d80cc8390 100644 --- a/ui/src/permission/core/data.ts +++ b/ui/src/permission/core/data.ts @@ -207,6 +207,7 @@ export const PermissionConstants = { APPLICATION_PUBLISH: p(Group.APPLICATION, Group.APPLICATION, Operate.PUBLISH, 7), APPLICATION_BATCH_DELETE: p(Group.APPLICATION, Group.APPLICATION, Operate.BATCH_DELETE, 8), APPLICATION_BATCH_MOVE: p(Group.APPLICATION, Group.APPLICATION, Operate.BATCH_MOVE, 9), + APPLICATION_CLEAR_POLICY: p(Group.APPLICATION, Group.APPLICATION, Operate.CLEAR_POLICY, 42), APPLICATION_RESOURCE_AUTHORIZATION: p(Group.APPLICATION, Group.APPLICATION, Operate.AUTH, 10), APPLICATION_RELATE_RESOURCE_VIEW: p(Group.APPLICATION, Group.APPLICATION, Operate.RELATE_VIEW, 11), APPLICATION_TRIGGER_READ: p(Group.APPLICATION, Group.APPLICATION, Operate.TRIGGER_READ, 12), diff --git a/ui/src/permission/modules/application/workspace.ts b/ui/src/permission/modules/application/workspace.ts index 073f15af009..c95933d83b5 100644 --- a/ui/src/permission/modules/application/workspace.ts +++ b/ui/src/permission/modules/application/workspace.ts @@ -28,7 +28,7 @@ const workspace = { /** * 工作空间「智能体」批量清除策略权限 */ - batchCleanStrategy: () => can(P.APPLICATION_CHAT_LOG_CLEAR_POLICY), + batchCleanStrategy: () => can(P.APPLICATION_CLEAR_POLICY), // —— 资源级(按应用 id)—— /**