fix: 收口管理端页面权限与工作流运行授权

- 页面选项接口改用所属页面权限并返回最小数据视图

- 统一校验工作流引用、租户、状态与定时任务执行主体

- 补充聊天记录权限迁移和权限隔离回归测试
This commit is contained in:
2026-08-07 12:51:21 +08:00
parent 6ad004da9b
commit d244a0404d
55 changed files with 3350 additions and 387 deletions

View File

@@ -0,0 +1,103 @@
package tech.easyflow.admin.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.testng.Assert;
import org.testng.annotations.Test;
import tech.easyflow.admin.controller.agent.AgentSessionController;
import tech.easyflow.admin.controller.ai.ChatHistoryController;
import tech.easyflow.admin.controller.ai.ModelController;
import tech.easyflow.admin.controller.ai.WorkFlowNodeController;
import tech.easyflow.admin.controller.ai.WorkflowController;
import tech.easyflow.admin.controller.dashboard.DashboardController;
import tech.easyflow.admin.controller.job.SysJobController;
import tech.easyflow.admin.controller.system.ApprovalFlowController;
import tech.easyflow.admin.controller.system.SysAccountController;
import tech.easyflow.admin.controller.system.SysRoleController;
import java.lang.reflect.Method;
import java.util.Arrays;
/**
* 管理端页面能力接口权限归属契约测试。
*/
public class PermissionIsolationContractTest {
/**
* 验证工作流设计器依赖的选项接口只要求工作流查询权限。
*/
@Test
public void workflowDesignerOptionsBelongToWorkflowPermission() {
assertMethodPermission(
WorkflowController.class,
"designerOptions",
"/api/v1/workflow/query"
);
assertMethodPermission(
WorkflowController.class,
"designerChildWorkflow",
"/api/v1/workflow/query"
);
assertMethodPermission(
WorkFlowNodeController.class,
"getChainParams",
"/api/v1/workflow/query"
);
}
/**
* 验证各管理页面的辅助能力接口使用页面自身权限。
*/
@Test
public void pageOptionsBelongToOwningPagePermissions() {
assertMethodPermission(ModelController.class, "gatewayConfig", "/api/v1/model/query");
assertMethodPermission(DashboardController.class, "agentOptions", "/api/v1/dashboard/query");
assertMethodPermission(SysJobController.class, "workflowOptions", "/api/v1/sysJob/save");
assertMethodPermission(SysJobController.class, "getNextTimes", "/api/v1/sysJob/save");
assertMethodPermission(ApprovalFlowController.class, "resourceScopeOptions", "/api/v1/approvalFlow/save");
assertMethodPermission(SysRoleController.class, "formOptions", "/api/v1/sysRole/query");
assertMethodPermission(SysAccountController.class, "formOptions", "/api/v1/sysAccount/save");
}
/**
* 验证 Agent 会话和聊天历史接口分别使用各自页面权限。
*/
@Test
public void agentSessionAndHistoryUseIndependentPermissions() {
assertClassPermission(AgentSessionController.class, "/api/v1/agent/session/query");
assertClassPermission(ChatHistoryController.class, "/api/v1/chatHistory/query");
}
/**
* 断言控制器方法只声明指定权限。
*
* @param controllerType 控制器类型
* @param methodName 方法名
* @param expectedPermission 期望权限
*/
private void assertMethodPermission(
Class<?> controllerType,
String methodName,
String expectedPermission) {
Method method = Arrays.stream(controllerType.getDeclaredMethods())
.filter(candidate -> methodName.equals(candidate.getName()))
.findFirst()
.orElseThrow(() -> new AssertionError("未找到控制器方法:" + methodName));
SaCheckPermission permission = method.getAnnotation(SaCheckPermission.class);
Assert.assertNotNull(permission, methodName + " 缺少权限注解");
Assert.assertEquals(permission.value(), new String[]{expectedPermission});
}
/**
* 断言控制器类只声明指定权限。
*
* @param controllerType 控制器类型
* @param expectedPermission 期望权限
*/
private void assertClassPermission(Class<?> controllerType, String expectedPermission) {
SaCheckPermission permission = controllerType.getAnnotation(SaCheckPermission.class);
Assert.assertNotNull(permission, controllerType.getSimpleName() + " 缺少权限注解");
Assert.assertEquals(permission.value(), new String[]{expectedPermission});
}
}

View File

@@ -6,6 +6,7 @@ import tech.easyflow.chatlog.domain.dto.ChatSessionPage;
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
import tech.easyflow.chatlog.domain.query.ChatSessionFilterQuery;
import tech.easyflow.chatlog.service.ChatHistoryManageService;
import tech.easyflow.agent.service.AgentOptionQueryService;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.system.service.CategoryPermissionService;
@@ -30,7 +31,8 @@ public class ChatHistoryControllerTest {
BigInteger accountId = BigInteger.valueOf(20);
ChatHistoryManageService service = mock(ChatHistoryManageService.class);
CategoryPermissionService permissionService = mock(CategoryPermissionService.class);
ChatHistoryController controller = new ChatHistoryController(service, permissionService);
ChatHistoryController controller = new ChatHistoryController(
service, permissionService, mock(AgentOptionQueryService.class));
ChatSessionFilterQuery query = new ChatSessionFilterQuery();
LoginAccount account = loginAccount(accountId);
when(permissionService.isSuperAdmin(account)).thenReturn(false);
@@ -54,7 +56,8 @@ public class ChatHistoryControllerTest {
BigInteger sessionId = BigInteger.valueOf(30);
ChatHistoryManageService service = mock(ChatHistoryManageService.class);
CategoryPermissionService permissionService = mock(CategoryPermissionService.class);
ChatHistoryController controller = new ChatHistoryController(service, permissionService);
ChatHistoryController controller = new ChatHistoryController(
service, permissionService, mock(AgentOptionQueryService.class));
LoginAccount account = loginAccount(accountId);
when(permissionService.isSuperAdmin(account)).thenReturn(true);
when(service.getAdminSession(accountId, true, sessionId)).thenReturn(new ChatSessionSummary());

View File

@@ -7,6 +7,7 @@ import org.testng.annotations.Test;
import tech.easyflow.ai.easyagentsflow.service.WorkflowRunningParameterResolver;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.service.WorkflowService;
import tech.easyflow.ai.service.WorkflowUsageAuthorizationService;
import tech.easyflow.common.constant.enums.EnumJobType;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
@@ -22,6 +23,7 @@ import java.util.Map;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
@@ -37,13 +39,19 @@ public class SysJobControllerTest {
BigInteger workflowId = BigInteger.valueOf(101);
SysJobService jobService = mock(SysJobService.class);
WorkflowService workflowService = mock(WorkflowService.class);
WorkflowUsageAuthorizationService workflowAuthorizationService =
mock(WorkflowUsageAuthorizationService.class);
ResourceAccessService resourceAccessService = mock(ResourceAccessService.class);
WorkflowRunningParameterResolver parameterResolver =
mock(WorkflowRunningParameterResolver.class);
Workflow workflow = new Workflow();
workflow.setId(workflowId);
workflow.setContent("{}");
when(workflowService.getById(workflowId)).thenReturn(workflow);
when(workflowAuthorizationService.requireUsableWorkflow(
org.mockito.ArgumentMatchers.eq(workflowId),
org.mockito.ArgumentMatchers.any(LoginAccount.class),
org.mockito.ArgumentMatchers.anyString()))
.thenReturn(workflow);
Parameter requiredParameter = mock(Parameter.class);
when(requiredParameter.isRequired()).thenReturn(true);
when(requiredParameter.getName()).thenReturn("user_input");
@@ -53,6 +61,7 @@ public class SysJobControllerTest {
SysJobController controller = new SysJobController(
jobService,
workflowService,
workflowAuthorizationService,
resourceAccessService,
parameterResolver
);
@@ -76,4 +85,66 @@ public class SysJobControllerTest {
Assert.assertTrue(exception.getMessage().contains("用户问题"));
}
}
/**
* 验证部分更新省略任务类型时仍按数据库中的工作流任务类型完成引用校验。
*/
@Test
public void shouldValidateMergedWorkflowReferenceOnPartialUpdate() {
BigInteger jobId = BigInteger.valueOf(201);
BigInteger oldWorkflowId = BigInteger.valueOf(301);
BigInteger newWorkflowId = BigInteger.valueOf(302);
SysJobService jobService = mock(SysJobService.class);
WorkflowService workflowService = mock(WorkflowService.class);
WorkflowUsageAuthorizationService workflowAuthorizationService =
mock(WorkflowUsageAuthorizationService.class);
ResourceAccessService resourceAccessService = mock(ResourceAccessService.class);
WorkflowRunningParameterResolver parameterResolver =
mock(WorkflowRunningParameterResolver.class);
SysJob existing = new SysJob();
existing.setId(jobId);
existing.setJobType(EnumJobType.TINY_FLOW.getCode());
existing.setJobParams(Map.of(
JobConstant.WORKFLOW_KEY, oldWorkflowId.toString(),
JobConstant.WORKFLOW_PARAMS_KEY, Map.of()
));
when(jobService.getById(jobId)).thenReturn(existing);
when(workflowAuthorizationService.requireUsableWorkflow(
org.mockito.ArgumentMatchers.eq(newWorkflowId),
org.mockito.ArgumentMatchers.any(LoginAccount.class),
org.mockito.ArgumentMatchers.anyString()))
.thenThrow(new BusinessException("无权限运行所选工作流"));
SysJob update = new SysJob();
update.setId(jobId);
update.setJobParams(Map.of(
JobConstant.WORKFLOW_KEY, newWorkflowId.toString(),
JobConstant.WORKFLOW_PARAMS_KEY, Map.of()
));
LoginAccount account = new LoginAccount();
account.setId(BigInteger.ONE);
SysJobController controller = new SysJobController(
jobService,
workflowService,
workflowAuthorizationService,
resourceAccessService,
parameterResolver
);
try (MockedStatic<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
saToken.when(SaTokenUtil::getLoginAccount).thenReturn(account);
BusinessException exception = Assert.expectThrows(
BusinessException.class,
() -> controller.onSaveOrUpdateBefore(update, false)
);
Assert.assertTrue(exception.getMessage().contains("无权限"));
verify(workflowAuthorizationService).requireUsableWorkflow(
org.mockito.ArgumentMatchers.eq(newWorkflowId),
org.mockito.ArgumentMatchers.eq(account),
org.mockito.ArgumentMatchers.anyString());
}
}
}

View File

@@ -10,6 +10,7 @@ import tech.easyflow.common.domain.Result;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.admin.controller.system.vo.SysAccountProfileVo;
import tech.easyflow.admin.service.system.SystemFormOptionService;
import tech.easyflow.system.entity.SysAccount;
import tech.easyflow.system.entity.SysRole;
import tech.easyflow.system.service.SysAccountService;
@@ -46,7 +47,8 @@ public class SysAccountControllerTest {
SysAccountController controller = new SysAccountController(
mock(SysAccountService.class),
mock(AuthCredentialKeyService.class),
mock(SysRoleService.class)
mock(SysRoleService.class),
mock(SystemFormOptionService.class)
);
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getParameter("keyword")).thenReturn(" search-user ");
@@ -74,7 +76,8 @@ public class SysAccountControllerTest {
SysAccountController controller = new SysAccountController(
mock(SysAccountService.class),
mock(AuthCredentialKeyService.class),
mock(SysRoleService.class)
mock(SysRoleService.class),
mock(SystemFormOptionService.class)
);
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getParameter("keyword")).thenReturn(" ");
@@ -93,7 +96,8 @@ public class SysAccountControllerTest {
SysAccountController controller = new SysAccountController(
mock(SysAccountService.class),
mock(AuthCredentialKeyService.class),
mock(SysRoleService.class)
mock(SysRoleService.class),
mock(SystemFormOptionService.class)
);
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getParameter("keyword")).thenReturn("a_b%c\\d");
@@ -118,7 +122,8 @@ public class SysAccountControllerTest {
SysAccountController controller = new SysAccountController(
accountService,
credentialKeyService,
roleService
roleService,
mock(SystemFormOptionService.class)
);
SysAccount entity = createAccount(selectedDeptId);
LoginAccount loginAccount = createLoginAccount(operatorId, tenantId, operatorDeptId);
@@ -167,7 +172,8 @@ public class SysAccountControllerTest {
SysAccountController controller = new SysAccountController(
accountService,
credentialKeyService,
roleService
roleService,
mock(SystemFormOptionService.class)
);
SysAccount entity = createAccount(BigInteger.valueOf(200));
entity.setRoleIds(List.of());
@@ -202,7 +208,8 @@ public class SysAccountControllerTest {
SysAccountController controller = new SysAccountController(
accountService,
credentialKeyService,
roleService
roleService,
mock(SystemFormOptionService.class)
);
SysAccount account = new SysAccount();
account.setId(accountId);
@@ -233,7 +240,8 @@ public class SysAccountControllerTest {
SysAccountController controller = new SysAccountController(
accountService,
credentialKeyService,
roleService
roleService,
mock(SystemFormOptionService.class)
);
SysAccount account = new SysAccount();
account.setId(accountId);

View File

@@ -0,0 +1,169 @@
package tech.easyflow.admin.service.ai;
import com.mybatisflex.core.query.QueryWrapper;
import com.easyagents.flow.core.parser.ChainParser;
import org.mockito.MockedStatic;
import org.testng.Assert;
import org.testng.annotations.Test;
import tech.easyflow.ai.easyagentsflow.service.WorkflowDatacenterContentService;
import tech.easyflow.ai.entity.Model;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.plugin.workflow.snapshot.WorkflowPluginSnapshotResolver;
import tech.easyflow.ai.service.DocumentCollectionService;
import tech.easyflow.ai.service.ModelService;
import tech.easyflow.ai.service.PluginItemService;
import tech.easyflow.ai.service.PluginService;
import tech.easyflow.ai.service.PluginVisibilityService;
import tech.easyflow.ai.service.WorkflowService;
import tech.easyflow.ai.service.WorkflowUsageAuthorizationService;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.datacenter.execution.service.DatacenterDatasetQueryService;
import tech.easyflow.datacenter.meta.entity.DatacenterSource;
import tech.easyflow.datacenter.meta.service.DatacenterDatasetRegistryService;
import tech.easyflow.datacenter.meta.service.DatacenterSourceService;
import tech.easyflow.system.service.ResourceAccessService;
import java.math.BigInteger;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
/**
* {@link WorkflowDesignerOptionService} 工作流引用权限测试。
*/
public class WorkflowDesignerOptionServiceTest {
/**
* 验证客户端提交候选列表之外的模型 ID 时服务端拒绝保存。
*/
@Test
public void shouldRejectModelOutsideSelectableOptions() {
ModelService modelService = mock(ModelService.class);
DocumentCollectionService knowledgeService = mock(DocumentCollectionService.class);
when(modelService.listByIds(any())).thenReturn(List.of());
when(knowledgeService.list(any(QueryWrapper.class))).thenReturn(List.of());
WorkflowDesignerOptionService service = createService(
modelService, knowledgeService, mock(DatacenterSourceService.class));
try (MockedStatic<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
saToken.when(SaTokenUtil::getLoginAccount).thenReturn(loginAccount());
BusinessException exception = Assert.expectThrows(
BusinessException.class,
() -> service.assertContentReferences(
"{\"nodes\":[{\"type\":\"llmNode\",\"data\":{\"llmId\":\"99\"}}]}")
);
Assert.assertTrue(exception.getMessage().contains("模型"));
}
}
/**
* 验证工作流数据节点不能引用其他租户的数据源。
*/
@Test
public void shouldRejectCrossTenantDataSource() {
ModelService modelService = mock(ModelService.class);
DocumentCollectionService knowledgeService = mock(DocumentCollectionService.class);
DatacenterSourceService sourceService = mock(DatacenterSourceService.class);
when(modelService.listSelectableModels(any(Model.class), eq(false), eq("id"), eq("desc")))
.thenReturn(List.of());
when(knowledgeService.list(any(QueryWrapper.class))).thenReturn(List.of());
DatacenterSource source = new DatacenterSource();
source.setId(BigInteger.valueOf(9));
source.setTenantId(BigInteger.valueOf(200));
when(sourceService.getById(BigInteger.valueOf(9))).thenReturn(source);
WorkflowDesignerOptionService service = createService(
modelService, knowledgeService, sourceService);
try (MockedStatic<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
saToken.when(SaTokenUtil::getLoginAccount).thenReturn(loginAccount());
BusinessException exception = Assert.expectThrows(
BusinessException.class,
() -> service.assertContentReferences("""
{"nodes":[{"type":"search-dataset-node","data":{
"datasetRef":{"sourceId":"9"}
}}]}
""")
);
Assert.assertTrue(exception.getMessage().contains("数据源"));
}
}
/**
* 验证子流程节点配置拒绝读取其他租户的工作流。
*/
@Test
public void shouldRejectCrossTenantChildWorkflow() {
WorkflowService workflowService = mock(WorkflowService.class);
Workflow workflow = new Workflow();
workflow.setId(BigInteger.valueOf(19));
workflow.setTenantId(BigInteger.valueOf(200));
when(workflowService.getById(BigInteger.valueOf(19))).thenReturn(workflow);
WorkflowDesignerOptionService service = createService(
mock(ModelService.class),
mock(DocumentCollectionService.class),
mock(DatacenterSourceService.class),
workflowService
);
try (MockedStatic<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
saToken.when(SaTokenUtil::getLoginAccount).thenReturn(loginAccount());
BusinessException exception = Assert.expectThrows(
BusinessException.class,
() -> service.getChildWorkflowNodeData(
BigInteger.valueOf(10),
BigInteger.valueOf(19))
);
Assert.assertTrue(exception.getMessage().contains("子流程"));
}
}
private WorkflowDesignerOptionService createService(
ModelService modelService,
DocumentCollectionService knowledgeService,
DatacenterSourceService sourceService) {
return createService(modelService, knowledgeService, sourceService, mock(WorkflowService.class));
}
private WorkflowDesignerOptionService createService(
ModelService modelService,
DocumentCollectionService knowledgeService,
DatacenterSourceService sourceService,
WorkflowService workflowService) {
ResourceAccessService resourceAccessService = mock(ResourceAccessService.class);
return new WorkflowDesignerOptionService(
modelService,
knowledgeService,
mock(PluginService.class),
mock(PluginItemService.class),
mock(PluginVisibilityService.class),
workflowService,
new WorkflowUsageAuthorizationService(workflowService, resourceAccessService),
mock(WorkflowPluginSnapshotResolver.class),
mock(ChainParser.class),
mock(WorkflowDatacenterContentService.class),
resourceAccessService,
sourceService,
mock(DatacenterDatasetRegistryService.class),
mock(DatacenterDatasetQueryService.class)
);
}
private LoginAccount loginAccount() {
LoginAccount account = new LoginAccount();
account.setId(BigInteger.ONE);
account.setTenantId(BigInteger.valueOf(100));
return account;
}
}