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

@@ -1,6 +1,7 @@
package tech.easyflow.admin.controller.agent;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import jakarta.servlet.http.HttpServletRequest;
@@ -230,6 +231,7 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
* @return SSE Emitter
*/
@PostMapping("chat")
@SaCheckPermission("/api/v1/agent/session/query")
public SseEmitter chat(@JsonBody AgentChatRequest request) {
return agentRunService.chat(request);
}
@@ -241,6 +243,7 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
* @return SSE Emitter
*/
@PostMapping("/chat/draft")
@SaCheckPermission("/api/v1/agent/save")
public SseEmitter chatDraft(@JsonBody AgentDraftChatRequest request) {
return agentRunService.chatDraft(request);
}
@@ -255,6 +258,9 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
* @return 上传结果
*/
@PostMapping(value = "/media/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@SaCheckPermission(value = {
"/api/v1/agent/session/query", "/api/v1/agent/save"
}, mode = SaMode.OR)
public Result<AgentMediaUploadView> uploadMedia(@RequestParam("file") MultipartFile file,
@RequestParam("mode") String mode,
@RequestParam("agentId") String agentId,
@@ -269,6 +275,9 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
* @return 操作结果
*/
@PostMapping("/media/delete")
@SaCheckPermission(value = {
"/api/v1/agent/session/query", "/api/v1/agent/save"
}, mode = SaMode.OR)
public Result<Void> deleteMedia(@JsonBody(value = "uploadId", required = true) String uploadId) {
agentMediaService.deleteUpload(uploadId, SaTokenUtil.getLoginAccount());
return Result.ok();
@@ -281,6 +290,9 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
* @return 图片响应
*/
@GetMapping("/media/content")
@SaCheckPermission(value = {
"/api/v1/agent/session/query", "/api/v1/agent/save"
}, mode = SaMode.OR)
@LogReporterDisabled
public ResponseEntity<byte[]> mediaContent(@RequestParam("reference") String reference) {
AgentMediaResource resource = agentMediaService.load(reference, SaTokenUtil.getLoginAccount());
@@ -303,6 +315,9 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
* @return 上传与读取状态
*/
@PostMapping(value = "/media/document/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@SaCheckPermission(value = {
"/api/v1/agent/session/query", "/api/v1/agent/save"
}, mode = SaMode.OR)
public Result<AgentDocumentUploadView> uploadDocument(@RequestParam("file") MultipartFile file,
@RequestParam("mode") String mode,
@RequestParam("agentId") String agentId,
@@ -320,6 +335,9 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
* @return 最新状态
*/
@GetMapping("/media/document/status")
@SaCheckPermission(value = {
"/api/v1/agent/session/query", "/api/v1/agent/save"
}, mode = SaMode.OR)
public Result<AgentDocumentUploadView> documentStatus(@RequestParam("uploadId") String uploadId) {
return Result.ok(agentDocumentService.status(uploadId, SaTokenUtil.getLoginAccount()));
}
@@ -331,6 +349,9 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
* @return 重试后的状态
*/
@PostMapping("/media/document/retry")
@SaCheckPermission(value = {
"/api/v1/agent/session/query", "/api/v1/agent/save"
}, mode = SaMode.OR)
public Result<AgentDocumentUploadView> retryDocument(
@JsonBody(value = "uploadId", required = true) String uploadId) {
return Result.ok(agentDocumentService.retry(uploadId, SaTokenUtil.getLoginAccount()));
@@ -343,6 +364,9 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
* @return 操作结果
*/
@PostMapping("/media/document/delete")
@SaCheckPermission(value = {
"/api/v1/agent/session/query", "/api/v1/agent/save"
}, mode = SaMode.OR)
public Result<Void> deleteDocument(
@JsonBody(value = "uploadId", required = true) String uploadId) {
agentDocumentService.deleteUpload(uploadId, SaTokenUtil.getLoginAccount());
@@ -356,6 +380,9 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
* @return 文档流
*/
@GetMapping("/media/document/content")
@SaCheckPermission(value = {
"/api/v1/agent/session/query", "/api/v1/agent/save"
}, mode = SaMode.OR)
@LogReporterDisabled
public ResponseEntity<StreamingResponseBody> documentContent(
@RequestParam("reference") String reference) {
@@ -384,6 +411,9 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
* @return 会话信息
*/
@PostMapping("/composer/session")
@SaCheckPermission(value = {
"/api/v1/agent/session/query", "/api/v1/agent/save"
}, mode = SaMode.OR)
public Result<AgentComposerSession> allocateComposerSession(
@JsonBody(value = "mode", required = true) String mode) {
return Result.ok(agentComposerDraftService.allocateSession(mode));
@@ -396,6 +426,9 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
* @return 保存后的草稿
*/
@PostMapping("/composer/draft/persist")
@SaCheckPermission(value = {
"/api/v1/agent/session/query", "/api/v1/agent/save"
}, mode = SaMode.OR)
public Result<AgentComposerDraft> saveComposerDraft(@JsonBody AgentComposerDraft draft) {
return Result.ok(agentComposerDraftService.save(draft, SaTokenUtil.getLoginAccount()));
}
@@ -409,6 +442,9 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
* @return 输入草稿
*/
@GetMapping("/composer/draft")
@SaCheckPermission(value = {
"/api/v1/agent/session/query", "/api/v1/agent/save"
}, mode = SaMode.OR)
public Result<AgentComposerDraft> getComposerDraft(@RequestParam("mode") String mode,
@RequestParam("agentId") String agentId,
@RequestParam(value = "sessionId", required = false) String sessionId) {
@@ -428,6 +464,9 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
* @return 操作结果
*/
@PostMapping("/composer/draft/delete")
@SaCheckPermission(value = {
"/api/v1/agent/session/query", "/api/v1/agent/save"
}, mode = SaMode.OR)
public Result<Void> deleteComposerDraft(@JsonBody(value = "mode", required = true) String mode,
@JsonBody(value = "agentId", required = true) String agentId,
@JsonBody(value = "sessionId", required = true) String sessionId,
@@ -446,6 +485,7 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
* @return 操作结果
*/
@PostMapping("/chat/draft/clear")
@SaCheckPermission("/api/v1/agent/save")
public Result<Void> clearDraftSession(@JsonBody(value = "sessionId", required = true) String sessionId) {
agentRunService.clearDraftSession(sessionId);
return Result.ok();
@@ -459,6 +499,9 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
* @return 操作结果
*/
@PostMapping("/run/approve")
@SaCheckPermission(value = {
"/api/v1/agent/session/query", "/api/v1/agent/save"
}, mode = SaMode.OR)
public Result<Void> approve(@JsonBody("requestId") String requestId,
@JsonBody(value = "resumeToken", required = true) String resumeToken) {
agentRunService.approve(requestId, resumeToken);
@@ -474,6 +517,9 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
* @return 操作结果
*/
@PostMapping("/run/reject")
@SaCheckPermission(value = {
"/api/v1/agent/session/query", "/api/v1/agent/save"
}, mode = SaMode.OR)
public Result<Void> reject(@JsonBody("requestId") String requestId,
@JsonBody(value = "resumeToken", required = true) String resumeToken,
@JsonBody("reason") String reason) {

View File

@@ -1,11 +1,15 @@
package tech.easyflow.admin.controller.agent;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.mybatisflex.core.keygen.impl.SnowFlakeIDKeyGenerator;
import org.springframework.web.bind.annotation.*;
import tech.easyflow.admin.dto.chatworkspace.ChatWorkspaceConversationView;
import tech.easyflow.admin.dto.chatworkspace.ChatWorkspaceSessionDetailView;
import tech.easyflow.admin.dto.chatworkspace.ChatWorkspaceSessionPage;
import tech.easyflow.admin.service.agent.AgentSessionService;
import tech.easyflow.agent.service.AgentOptionQueryService;
import tech.easyflow.agent.vo.AgentOptionView;
import tech.easyflow.agent.vo.AgentResourceOptionsView;
import tech.easyflow.chatlog.domain.dto.ChatHistoryPage;
import tech.easyflow.chatlog.domain.query.ChatPageQuery;
import tech.easyflow.common.domain.Result;
@@ -22,17 +26,42 @@ import java.util.List;
*/
@RestController
@RequestMapping("/api/v1/agent/session")
@SaCheckPermission("/api/v1/agent/session/query")
public class AgentSessionController {
private final AgentSessionService agentSessionService;
private final AgentOptionQueryService agentOptionQueryService;
/**
* 创建 Agent 管理端会话控制器。
*
* @param agentSessionService Agent 会话服务
* @param agentOptionQueryService Agent 安全选项服务
*/
public AgentSessionController(AgentSessionService agentSessionService) {
public AgentSessionController(AgentSessionService agentSessionService,
AgentOptionQueryService agentOptionQueryService) {
this.agentSessionService = agentSessionService;
this.agentOptionQueryService = agentOptionQueryService;
}
/**
* 查询正式聊天可使用的已发布 Agent。
*
* @return Agent 安全选项
*/
@GetMapping("/options")
public Result<List<AgentOptionView>> options() {
return Result.ok(agentOptionQueryService.listAgentOptions(true));
}
/**
* 查询正式聊天可附加的知识库。
*
* @return 知识库安全选项
*/
@GetMapping("/knowledgeOptions")
public Result<List<AgentResourceOptionsView.ResourceOption>> knowledgeOptions() {
return Result.ok(agentOptionQueryService.listKnowledgeOptions());
}
/**

View File

@@ -1,10 +1,13 @@
package tech.easyflow.admin.controller.ai;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.agent.service.AgentOptionQueryService;
import tech.easyflow.agent.vo.AgentOptionView;
import tech.easyflow.chatlog.domain.dto.ChatHistoryPage;
import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
import tech.easyflow.chatlog.domain.dto.ChatSessionPage;
@@ -23,21 +26,36 @@ import java.util.List;
@RestController
@RequestMapping("/api/v1/chatHistory")
@SaCheckPermission("/api/v1/chatHistory/query")
public class ChatHistoryController {
private final ChatHistoryManageService chatHistoryManageService;
private final CategoryPermissionService categoryPermissionService;
private final AgentOptionQueryService agentOptionQueryService;
/**
* 创建聊天历史控制器。
*
* @param chatHistoryManageService 聊天历史管理服务
* @param categoryPermissionService 账号权限服务
* @param agentOptionQueryService Agent 安全选项服务
*/
public ChatHistoryController(ChatHistoryManageService chatHistoryManageService,
CategoryPermissionService categoryPermissionService) {
CategoryPermissionService categoryPermissionService,
AgentOptionQueryService agentOptionQueryService) {
this.chatHistoryManageService = chatHistoryManageService;
this.categoryPermissionService = categoryPermissionService;
this.agentOptionQueryService = agentOptionQueryService;
}
/**
* 查询聊天记录筛选可使用的 Agent。
*
* @return Agent 安全选项
*/
@GetMapping("/agentOptions")
public Result<List<AgentOptionView>> agentOptions() {
return Result.ok(agentOptionQueryService.listAgentOptions(false));
}
/**

View File

@@ -13,6 +13,10 @@ import tech.easyflow.ai.entity.table.ModelTableDef;
import tech.easyflow.ai.mapper.ModelMapper;
import tech.easyflow.ai.service.AgentResourceReferenceService;
import tech.easyflow.ai.service.ModelService;
import tech.easyflow.admin.model.ai.ModelGatewayConfigView;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.system.entity.SysOption;
import tech.easyflow.system.service.SysOptionService;
import tech.easyflow.ai.service.capability.ModelCapabilityResolution;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.entity.LoginAccount;
@@ -40,6 +44,11 @@ import java.util.stream.Collectors;
@RequestMapping("/api/v1/model")
public class ModelController extends BaseCurdController<ModelService, Model> {
private static final String CHAT_PUBLISH_BASE_URL = "chat_publish_base_url";
@Autowired
private SysOptionService sysOptionService;
public ModelController(ModelService service) {
super(service);
}
@@ -47,6 +56,21 @@ public class ModelController extends BaseCurdController<ModelService, Model> {
@Autowired
ModelService modelService;
/**
* 查询模型统一网关页面所需的安全配置。
*
* @return 仅包含模型发布基础地址的配置
*/
@GetMapping("/gatewayConfig")
@SaCheckPermission("/api/v1/model/query")
public Result<ModelGatewayConfigView> gatewayConfig() {
SysOption option = sysOptionService.getByOptionKey(
CHAT_PUBLISH_BASE_URL,
SaTokenUtil.getLoginAccount().getTenantId()
);
return Result.ok(new ModelGatewayConfigView(option == null ? null : option.getValue()));
}
@Resource
ModelMapper modelMapper;
@Resource

View File

@@ -1,88 +1,45 @@
package tech.easyflow.admin.controller.ai;
import cn.hutool.core.util.IdUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.alibaba.fastjson2.JSONObject;
import com.easyagents.flow.core.chain.ChainDefinition;
import com.easyagents.flow.core.chain.Node;
import com.easyagents.flow.core.node.ConfirmNode;
import com.easyagents.flow.core.node.EndNode;
import com.easyagents.flow.core.node.StartNode;
import com.easyagents.flow.core.parser.ChainParser;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.ai.easyagentsflow.service.WorkflowDatacenterContentService;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.service.WorkflowService;
import tech.easyflow.admin.service.ai.WorkflowDesignerOptionService;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.exceptions.BusinessException;
import javax.annotation.Resource;
import java.util.List;
import java.math.BigInteger;
/**
* 工作流节点兼容接口。
*/
@RequestMapping("/api/v1/workflowNode")
@RestController
public class WorkFlowNodeController {
@Resource
private WorkflowService workflowService;
@Resource
private ChainParser chainParser;
@Resource
private WorkflowDatacenterContentService workflowDatacenterContentService;
private final WorkflowDesignerOptionService workflowDesignerOptionService;
@GetMapping("/getChainParams")
public Result<?> getChainParams(String currentId, String workflowId) {
if (workflowId.equals(currentId)) {
throw new BusinessException("工作流不能作为自身子节点");
}
JSONObject nodeData = new JSONObject();
Workflow workflow = workflowService.getById(workflowId);
if (workflow == null) {
throw new BusinessException("工作流不存在: " + workflowId);
}
nodeData.put("workflowId", workflow.getId());
nodeData.put("workflowName", workflow.getTitle());
ChainDefinition definition = chainParser.parse(workflowDatacenterContentService.prepareContent(workflow.getContent()));
List<Node> nodes = definition.getNodes();
JSONArray inputs = new JSONArray();
JSONArray outputs = new JSONArray();
for (Node node : nodes) {
if (node instanceof StartNode) {
inputs = JSON.parseArray(JSON.toJSONString(node.getParameters()));
handleArray(inputs);
}
if (node instanceof EndNode) {
outputs = JSON.parseArray(JSON.toJSONString(((EndNode) node).getOutputDefs()));
handleArray(outputs);
}
if (node instanceof ConfirmNode) {
throw new BusinessException("工作流存在【确认节点】,暂不支持作为子节点");
}
}
nodeData.put("parameters", inputs);
nodeData.put("outputDefs", outputs);
return Result.ok(nodeData);
/**
* 创建工作流节点兼容控制器。
*
* @param workflowDesignerOptionService 工作流设计器选项服务
*/
public WorkFlowNodeController(WorkflowDesignerOptionService workflowDesignerOptionService) {
this.workflowDesignerOptionService = workflowDesignerOptionService;
}
private void handleArray(JSONArray array) {
if (array != null) {
for (Object o : array) {
JSONObject obj = (JSONObject) o;
obj.put("id", IdUtil.simpleUUID());
obj.put("nameDisabled", true);
obj.put("dataTypeDisabled", true);
obj.put("deleteDisabled", true);
obj.put("addChildDisabled", true);
obj.put("refType", "ref");
JSONArray children = obj.getJSONArray("children");
if (children != null) {
handleArray(children);
}
}
}
/**
* 查询子流程输入输出定义。
*
* @param currentId 当前工作流 ID
* @param workflowId 子流程 ID
* @return 子流程节点配置
* @deprecated 请使用 {@code /api/v1/workflow/designer/childWorkflow}
*/
@Deprecated
@GetMapping("/getChainParams")
@SaCheckPermission("/api/v1/workflow/query")
public Result<JSONObject> getChainParams(BigInteger currentId, BigInteger workflowId) {
return Result.ok(workflowDesignerOptionService.getChildWorkflowNodeData(currentId, workflowId));
}
}

View File

@@ -4,6 +4,7 @@ import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.IdUtil;
import com.alibaba.fastjson2.JSONObject;
import com.mybatisflex.core.paginate.Page;
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
import com.mybatisflex.core.query.QueryWrapper;
@@ -14,6 +15,8 @@ import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
import tech.easyflow.admin.model.ai.WorkflowDesignerOptionsView;
import tech.easyflow.admin.service.ai.WorkflowDesignerOptionService;
import tech.easyflow.agent.entity.AgentToolBinding;
import tech.easyflow.agent.enums.AgentToolType;
import tech.easyflow.agent.service.AgentToolBindingService;
@@ -43,6 +46,7 @@ import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.common.web.jsonbody.JsonBody;
import tech.easyflow.datacenter.execution.model.DatasetRef;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceAction;
import tech.easyflow.system.enums.ResourceLookup;
@@ -101,12 +105,116 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
private AiResourceCreatorNameSupport aiResourceCreatorNameSupport;
@Resource
private WorkflowShareResourceAccessGrantProvider workflowShareGrantProvider;
@Resource
private WorkflowDesignerOptionService workflowDesignerOptionService;
public WorkflowController(WorkflowService service, ModelService modelService) {
super(service);
this.modelService = modelService;
}
/**
* 查询工作流设计器初始化所需的安全选项。
*
* @return 模型、知识库和代码引擎选项
*/
@GetMapping("/designer/options")
@SaCheckPermission("/api/v1/workflow/query")
public Result<WorkflowDesignerOptionsView> designerOptions() {
return Result.ok(workflowDesignerOptionService.listOptions(
codeEngineCapabilityService.listSupportedCodeEngines()));
}
/**
* 分页查询工作流设计器可用插件。
*
* @param pageNumber 页码
* @param pageSize 每页数量
* @return 插件安全选项分页
*/
@GetMapping("/designer/plugins")
@SaCheckPermission("/api/v1/workflow/query")
public Result<Page<WorkflowDesignerOptionsView.PluginOption>> designerPlugins(
Long pageNumber,
Long pageSize) {
return Result.ok(workflowDesignerOptionService.pagePlugins(pageNumber, pageSize));
}
/**
* 查询工作流插件节点配置。
*
* @param id 插件工具 ID
* @return 插件节点配置
*/
@GetMapping("/designer/pluginTinyFlow")
@SaCheckPermission("/api/v1/workflow/query")
public Result<?> designerPluginTinyFlow(BigInteger id) {
return Result.ok(workflowDesignerOptionService.getPluginTinyFlowData(id));
}
/**
* 查询工作流设计器的子流程节点配置。
*
* @param currentId 当前工作流 ID
* @param workflowId 子流程 ID
* @return 子流程输入输出定义
*/
@GetMapping("/designer/childWorkflow")
@SaCheckPermission("/api/v1/workflow/query")
public Result<JSONObject> designerChildWorkflow(BigInteger currentId, BigInteger workflowId) {
return Result.ok(workflowDesignerOptionService.getChildWorkflowNodeData(currentId, workflowId));
}
/**
* 查询工作流数据节点可见的数据源。
*
* @return 数据源安全选项
*/
@GetMapping("/designer/dataSources")
@SaCheckPermission("/api/v1/workflow/query")
public Result<List<WorkflowDesignerOptionsView.DataSourceOption>> designerDataSources() {
return Result.ok(workflowDesignerOptionService.listDataSources());
}
/**
* 查询工作流数据节点的数据目录。
*
* @param sourceId 数据源 ID
* @return 目录安全选项
*/
@GetMapping("/designer/catalogs")
@SaCheckPermission("/api/v1/workflow/query")
public Result<List<WorkflowDesignerOptionsView.CatalogOption>> designerCatalogs(BigInteger sourceId) {
return Result.ok(workflowDesignerOptionService.listCatalogs(sourceId));
}
/**
* 查询工作流数据节点的已接入数据集。
*
* @param sourceId 数据源 ID
* @param catalogId 目录 ID
* @return 数据集安全选项
*/
@GetMapping("/designer/managedTables")
@SaCheckPermission("/api/v1/workflow/query")
public Result<List<WorkflowDesignerOptionsView.DatasetOption>> designerManagedTables(
BigInteger sourceId,
BigInteger catalogId) {
return Result.ok(workflowDesignerOptionService.listManagedDatasets(sourceId, catalogId));
}
/**
* 查询工作流数据节点的数据集结构。
*
* @param datasetRef 数据集引用
* @return 数据集结构安全视图
*/
@GetMapping("/designer/schema")
@SaCheckPermission("/api/v1/workflow/query")
public Result<WorkflowDesignerOptionsView.DatasetSchemaOption> designerSchema(DatasetRef datasetRef) {
return Result.ok(workflowDesignerOptionService.getDatasetSchema(datasetRef));
}
/**
* 节点单独运行
*/
@@ -128,6 +236,9 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
if (workflow == null) {
return Result.fail(1, "工作流不存在");
}
workflowCheckService.checkOrThrow(
workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
workflowDesignerOptionService.assertContentReferences(workflow.getContent());
if (variables == null) {
variables = new HashMap<>();
}
@@ -161,6 +272,7 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
throw new RuntimeException("工作流不存在");
}
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
workflowDesignerOptionService.assertContentReferences(workflow.getContent());
variables = workflowRunningParameterResolver.normalizeRuntimeVariables(workflow.getContent(), variables);
if (StpUtil.isLogin()) {
variables.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
@@ -252,6 +364,7 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
return Result.fail(1, "can not find the workflow by id: " + id);
}
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
workflowDesignerOptionService.assertContentReferences(workflow.getContent());
Map<String, Object> res = workflowRunningParameterResolver.buildRunningParametersView(workflow);
if (res == null) {
return Result.fail(2, "节点配置错误,请检查! ");
@@ -431,6 +544,7 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
workflow.setAlias(IdUtil.fastSimpleUUID());
workflow.setRevision(0);
commonFiled(workflow, account.getId(), account.getTenantId(), account.getDeptId());
workflowDesignerOptionService.assertContentReferences(workflow.getContent());
service.save(workflow);
return Result.ok();
}
@@ -497,6 +611,7 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
}
if (StringUtils.hasLength(entity.getContent())) {
workflowCheckService.checkOrThrow(entity.getContent(), WorkflowCheckStage.SAVE, entity.getId());
workflowDesignerOptionService.assertContentReferences(entity.getContent());
}
String alias = entity.getAlias();

View File

@@ -6,6 +6,7 @@ import tech.easyflow.common.dict.Dict;
import tech.easyflow.common.dict.DictItem;
import tech.easyflow.common.dict.DictLoader;
import tech.easyflow.common.dict.DictManager;
import tech.easyflow.common.web.exceptions.BusinessException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -16,18 +17,30 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* 提供已注册静态字典的查询接口。
*/
@RestController
@RequestMapping("/api/v1/dict/")
public class DictController {
@Resource
DictManager dictManager;
private DictManager dictManager;
/**
* 查询指定静态字典的选项。
*
* @param code 字典编码
* @param keyword 搜索关键字
* @param request HTTP 请求
* @return 字典选项
* @throws BusinessException 字典未注册时抛出
*/
@GetMapping("/items/{code}")
public Result<List<DictItem>> items(@PathVariable("code") String code, String keyword, HttpServletRequest request) {
DictLoader loader = dictManager.getLoader(code);
if (loader == null) {
return Result.ok(Collections.emptyList());
throw new BusinessException("字典不存在或不支持公共查询");
}
Map<String, String[]> parameterMap = request.getParameterMap();
Dict dict = loader.load(keyword, parameterMap);

View File

@@ -10,6 +10,8 @@ import tech.easyflow.admin.model.dashboard.DashboardOverviewVo;
import tech.easyflow.admin.model.dashboard.DashboardUserRankItemVo;
import tech.easyflow.admin.model.dashboard.DashboardUserRankQuery;
import tech.easyflow.admin.service.dashboard.DashboardService;
import tech.easyflow.agent.service.AgentOptionQueryService;
import tech.easyflow.agent.vo.AgentOptionView;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.satoken.util.SaTokenUtil;
@@ -28,9 +30,29 @@ import java.util.List;
public class DashboardController {
private final DashboardService dashboardService;
private final AgentOptionQueryService agentOptionQueryService;
public DashboardController(DashboardService dashboardService) {
/**
* 创建管理端工作台控制器。
*
* @param dashboardService 工作台统计服务
* @param agentOptionQueryService Agent 安全选项服务
*/
public DashboardController(DashboardService dashboardService,
AgentOptionQueryService agentOptionQueryService) {
this.dashboardService = dashboardService;
this.agentOptionQueryService = agentOptionQueryService;
}
/**
* 查询工作台筛选可使用的 Agent。
*
* @return Agent 安全选项
*/
@GetMapping("/agentOptions")
@SaCheckPermission("/api/v1/dashboard/query")
public Result<List<AgentOptionView>> agentOptions() {
return Result.ok(agentOptionQueryService.listAgentOptions(false));
}
@GetMapping("/overview")

View File

@@ -3,6 +3,7 @@ package tech.easyflow.admin.controller.job;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.date.DateUtil;
import com.easyagents.flow.core.chain.Parameter;
import com.mybatisflex.core.query.QueryWrapper;
import org.quartz.CronExpression;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
@@ -11,6 +12,9 @@ import org.springframework.web.bind.annotation.RestController;
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.admin.model.SysJobWorkflowOptionView;
import tech.easyflow.common.constant.enums.EnumDataStatus;
import tech.easyflow.common.constant.enums.EnumJobType;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.entity.LoginAccount;
@@ -33,6 +37,7 @@ import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* 系统任务表 控制层。
@@ -47,6 +52,9 @@ public class SysJobController extends BaseCurdController<SysJobService, SysJob>
/** 工作流服务。 */
private final WorkflowService workflowService;
/** 工作流使用权限校验服务。 */
private final WorkflowUsageAuthorizationService workflowUsageAuthorizationService;
/** 资源访问控制服务。 */
private final ResourceAccessService resourceAccessService;
@@ -58,15 +66,18 @@ public class SysJobController extends BaseCurdController<SysJobService, SysJob>
*
* @param service 定时任务服务
* @param workflowService 工作流服务
* @param workflowUsageAuthorizationService 工作流使用权限校验服务
* @param resourceAccessService 资源访问控制服务
* @param workflowRunningParameterResolver 工作流运行参数解析器
*/
public SysJobController(SysJobService service,
WorkflowService workflowService,
WorkflowUsageAuthorizationService workflowUsageAuthorizationService,
ResourceAccessService resourceAccessService,
WorkflowRunningParameterResolver workflowRunningParameterResolver) {
super(service);
this.workflowService = workflowService;
this.workflowUsageAuthorizationService = workflowUsageAuthorizationService;
this.resourceAccessService = resourceAccessService;
this.workflowRunningParameterResolver = workflowRunningParameterResolver;
}
@@ -75,6 +86,9 @@ public class SysJobController extends BaseCurdController<SysJobService, SysJob>
@SaCheckPermission("/api/v1/sysJob/save")
@LogRecord("启动定时任务")
public Result<Void> start(BigInteger id) {
LoginAccount account = SaTokenUtil.getLoginAccount();
SysJob job = requireExistingJob(id);
validateWorkflowReference(job, account);
service.startJob(id);
return Result.ok();
}
@@ -88,6 +102,7 @@ public class SysJobController extends BaseCurdController<SysJobService, SysJob>
}
@GetMapping("/getNextTimes")
@SaCheckPermission("/api/v1/sysJob/save")
public Result<List<String>> getNextTimes(String cronExpression) throws Exception{
CronExpression ex = new CronExpression(cronExpression);
List<String> times = new ArrayList<>();
@@ -100,16 +115,72 @@ public class SysJobController extends BaseCurdController<SysJobService, SysJob>
return Result.ok(times);
}
/**
* 查询当前账号可用于定时任务的工作流。
*
* @return 工作流安全选项
*/
@GetMapping("/workflowOptions")
@SaCheckPermission("/api/v1/sysJob/save")
public Result<List<SysJobWorkflowOptionView>> workflowOptions() {
LoginAccount account = SaTokenUtil.getLoginAccount();
List<SysJobWorkflowOptionView> options = workflowService.list(QueryWrapper.create()
.eq(Workflow::getTenantId, account.getTenantId())
.eq(Workflow::getStatus, EnumDataStatus.AVAILABLE.getCode())
.orderBy(Workflow::getModified, false))
.stream()
.filter(workflow -> Objects.equals(workflow.getTenantId(), account.getTenantId()))
.filter(workflow -> resourceAccessService.canAccess(
account,
CategoryResourceType.WORKFLOW,
workflow,
ResourceAction.USE))
.map(workflow -> new SysJobWorkflowOptionView(
workflow.getId(),
workflow.getTitle(),
workflow.getDescription()))
.toList();
return Result.ok(options);
}
/**
* 查询定时任务所选工作流的运行参数。
*
* @param id 工作流 ID
* @return 工作流运行参数
* @throws BusinessException 工作流不存在或无运行权限时抛出
*/
@GetMapping("/workflowRunningParameters")
@SaCheckPermission("/api/v1/sysJob/save")
public Result<Map<String, Object>> workflowRunningParameters(BigInteger id) {
Workflow workflow = workflowUsageAuthorizationService.requireUsableWorkflow(
id,
SaTokenUtil.getLoginAccount(),
"工作流不存在、已禁用或无权运行");
Map<String, Object> result = workflowRunningParameterResolver.buildRunningParametersView(workflow);
if (result == null) {
throw new BusinessException("工作流参数配置无效,请检查工作流后重试");
}
return Result.ok(result);
}
@Override
protected Result onSaveOrUpdateBefore(SysJob entity, boolean isSave) {
if (entity == null) {
throw new BusinessException("定时任务不能为空");
}
LoginAccount loginUser = SaTokenUtil.getLoginAccount();
SysJob effectiveEntity = entity;
if (isSave) {
commonFiled(entity,loginUser.getId(),loginUser.getTenantId(), loginUser.getDeptId());
} else {
SysJob existing = requireExistingJob(entity.getId());
preserveServerControlledFields(entity, existing);
effectiveEntity = mergeForValidation(entity, existing);
entity.setModified(new Date());
entity.setModifiedBy(loginUser.getId());
}
validateWorkflowReference(entity);
validateWorkflowReference(effectiveEntity, loginUser);
return super.onSaveOrUpdateBefore(entity, isSave);
}
@@ -117,27 +188,71 @@ public class SysJobController extends BaseCurdController<SysJobService, SysJob>
* 校验工作流类型任务引用的工作流可被当前用户运行。
*
* @param entity 待保存的定时任务
* @param account 当前账号
* @throws BusinessException 工作流不存在、参数非法或无运行权限时抛出
*/
private void validateWorkflowReference(SysJob entity) {
private void validateWorkflowReference(SysJob entity, LoginAccount account) {
if (entity == null
|| !Integer.valueOf(EnumJobType.TINY_FLOW.getCode()).equals(entity.getJobType())) {
return;
}
BigInteger workflowId = SysJobWorkflowReferenceSupport.requireWorkflowId(entity);
Workflow workflow = workflowService.getById(workflowId);
if (workflow == null) {
throw new BusinessException("工作流不存在,请重新选择");
}
resourceAccessService.assertAccess(
CategoryResourceType.WORKFLOW,
workflow,
ResourceAction.USE,
"无权限运行所选工作流"
);
Workflow workflow = workflowUsageAuthorizationService.requireUsableWorkflow(
workflowId,
account,
"工作流不存在、已禁用或无权运行");
validateRequiredWorkflowParams(entity, workflow);
}
/**
* 获取当前租户内存在的定时任务。
*
* @param id 定时任务 ID
* @return 已存在的定时任务
* @throws BusinessException ID 缺失或任务不存在时抛出
*/
private SysJob requireExistingJob(BigInteger id) {
if (id == null) {
throw new BusinessException("定时任务ID不能为空");
}
SysJob existing = service.getById(id);
if (existing == null) {
throw new BusinessException("定时任务不存在");
}
return existing;
}
/**
* 保留更新请求不能修改的服务端控制字段。
*
* @param entity 更新请求
* @param existing 数据库中的定时任务
*/
private void preserveServerControlledFields(SysJob entity, SysJob existing) {
entity.setTenantId(existing.getTenantId());
entity.setDeptId(existing.getDeptId());
entity.setCreated(existing.getCreated());
entity.setCreatedBy(existing.getCreatedBy());
}
/**
* 合并部分更新请求与原记录,生成用于权限和参数校验的有效任务状态。
*
* @param entity 更新请求
* @param existing 数据库中的定时任务
* @return 合并后的校验对象
*/
private SysJob mergeForValidation(SysJob entity, SysJob existing) {
SysJob effective = new SysJob();
effective.setJobType(entity.getJobType() == null
? existing.getJobType()
: entity.getJobType());
effective.setJobParams(entity.getJobParams() == null
? existing.getJobParams()
: entity.getJobParams());
return effective;
}
/**
* 校验定时任务已填写工作流的全部必填运行参数。
*

View File

@@ -10,6 +10,8 @@ import tech.easyflow.common.domain.Result;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.common.web.jsonbody.JsonBody;
import tech.easyflow.admin.model.SystemFormOptionsView;
import tech.easyflow.admin.service.system.SystemFormOptionService;
import tech.easyflow.approval.entity.vo.ApprovalAssigneeOptionVo;
import tech.easyflow.approval.entity.vo.ApprovalFlowDetailVo;
import tech.easyflow.approval.entity.vo.ApprovalFlowPageVo;
@@ -36,6 +38,20 @@ public class ApprovalFlowController {
@Resource
private ApprovalAssigneeService approvalAssigneeService;
@Resource
private SystemFormOptionService systemFormOptionService;
/**
* 查询审批流程配置所需的资源范围选项。
*
* @return 非 Bot 分类和部门树
*/
@GetMapping("/resourceScopeOptions")
@SaCheckPermission("/api/v1/approvalFlow/save")
public Result<SystemFormOptionsView.ApprovalResourceScopeOptions> resourceScopeOptions() {
assertSuperAdmin();
return Result.ok(systemFormOptionService.approvalResourceScopeOptions());
}
/**
* 分页查询审批流程。
@@ -102,6 +118,9 @@ public class ApprovalFlowController {
@SaCheckPermission("/api/v1/approvalFlow/save")
public Result<BigInteger> save(@JsonBody ApprovalFlowDetailVo request) {
assertSuperAdmin();
systemFormOptionService.validateApprovalScopes(
request == null ? null : request.getResourceType(),
request == null ? null : request.getScopes());
BigInteger operatorId = SaTokenUtil.getLoginAccount().getId();
return Result.ok(approvalFlowService.saveFlow(request, operatorId));
}
@@ -116,6 +135,9 @@ public class ApprovalFlowController {
@SaCheckPermission("/api/v1/approvalFlow/save")
public Result<Void> update(@JsonBody ApprovalFlowDetailVo request) {
assertSuperAdmin();
systemFormOptionService.validateApprovalScopes(
request == null ? null : request.getResourceType(),
request == null ? null : request.getScopes());
BigInteger operatorId = SaTokenUtil.getLoginAccount().getId();
approvalFlowService.updateFlow(request, operatorId);
return Result.ok();

View File

@@ -31,6 +31,8 @@ import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.jsonbody.JsonBody;
import tech.easyflow.log.annotation.LogRecord;
import tech.easyflow.admin.controller.system.vo.SysAccountProfileVo;
import tech.easyflow.admin.model.SystemFormOptionsView;
import tech.easyflow.admin.service.system.SystemFormOptionService;
import tech.easyflow.system.entity.SysAccount;
import tech.easyflow.system.entity.SysRole;
import tech.easyflow.system.entity.vo.SysAccountBatchActionResultVo;
@@ -43,8 +45,8 @@ import javax.annotation.Resource;
import java.io.Serializable;
import java.math.BigInteger;
import java.net.URLEncoder;
import java.util.Collection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
@@ -71,6 +73,7 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
private final AuthCredentialKeyService credentialKeyService;
private final SysRoleService sysRoleService;
private final SystemFormOptionService systemFormOptionService;
@Resource
private AuthService authService;
@@ -80,13 +83,16 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
* @param service 用户服务
* @param credentialKeyService 凭证密钥服务
* @param sysRoleService 角色服务
* @param systemFormOptionService 用户表单安全选项服务
*/
public SysAccountController(SysAccountService service,
AuthCredentialKeyService credentialKeyService,
SysRoleService sysRoleService) {
SysRoleService sysRoleService,
SystemFormOptionService systemFormOptionService) {
super(service);
this.credentialKeyService = credentialKeyService;
this.sysRoleService = sysRoleService;
this.systemFormOptionService = systemFormOptionService;
}
/**
@@ -188,6 +194,7 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
@Override
protected Result onSaveOrUpdateBefore(SysAccount entity, boolean isSave) {
systemFormOptionService.validateAccountReferences(entity);
LoginAccount loginUser = SaTokenUtil.getLoginAccount();
if (isSave) {
// 查询用户名是否存在
@@ -233,6 +240,17 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
return null;
}
/**
* 查询账号表单所需的部门、角色和岗位选项。
*
* @return 账号表单安全选项
*/
@GetMapping("/formOptions")
@SaCheckPermission("/api/v1/sysAccount/save")
public Result<SystemFormOptionsView.AccountFormOptions> formOptions() {
return Result.ok(systemFormOptionService.accountFormOptions());
}
/**
* 填充账号创建的公共字段。
*

View File

@@ -9,6 +9,7 @@ import tech.easyflow.common.domain.Result;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.common.web.jsonbody.JsonBody;
import tech.easyflow.admin.service.system.SystemFormOptionService;
import tech.easyflow.system.entity.vo.SysRoleCategoryScopeDetailVo;
import tech.easyflow.system.service.CategoryPermissionService;
import tech.easyflow.system.service.SysRoleCategoryScopeService;
@@ -25,6 +26,8 @@ public class SysRoleCategoryScopeController {
@Resource
private CategoryPermissionService categoryPermissionService;
@Resource
private SystemFormOptionService systemFormOptionService;
@GetMapping("/detail")
@SaCheckPermission("/api/v1/sysRole/query")
@@ -41,6 +44,7 @@ public class SysRoleCategoryScopeController {
if (request == null || request.getRoleId() == null) {
throw new BusinessException("角色ID不能为空");
}
systemFormOptionService.validateCategoryScopes(request.getScopes());
BigInteger operatorId = SaTokenUtil.getLoginAccount().getId();
sysRoleCategoryScopeService.saveRoleScopes(request.getRoleId(), request.getScopes(), operatorId);
return Result.ok();

View File

@@ -9,6 +9,8 @@ import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.jsonbody.JsonBody;
import tech.easyflow.admin.model.SystemFormOptionsView;
import tech.easyflow.admin.service.system.SystemFormOptionService;
import tech.easyflow.system.entity.SysRole;
import tech.easyflow.system.entity.SysRoleDept;
import tech.easyflow.system.entity.SysRoleMenu;
@@ -38,11 +40,24 @@ public class SysRoleController extends BaseCurdController<SysRoleService, SysRol
private SysRoleMenuService sysRoleMenuService;
@Resource
private SysRoleDeptService sysRoleDeptService;
@Resource
private SystemFormOptionService systemFormOptionService;
public SysRoleController(SysRoleService service) {
super(service);
}
/**
* 查询角色表单所需的菜单和非 Bot 分类选项。
*
* @return 角色表单安全选项
*/
@GetMapping("formOptions")
@SaCheckPermission("/api/v1/sysRole/query")
public Result<SystemFormOptionsView.RoleFormOptions> formOptions() {
return Result.ok(systemFormOptionService.roleFormOptions());
}
@PostMapping("saveRoleMenu/{roleId}")
@SaCheckPermission("/api/v1/sysRole/save")
@Deprecated
@@ -85,6 +100,7 @@ public class SysRoleController extends BaseCurdController<SysRoleService, SysRol
if (entity.getId() == null) {
commonFiled(entity, loginUser.getId(), loginUser.getTenantId(), loginUser.getDeptId());
}
systemFormOptionService.validateRoleReferences(entity);
service.saveRole(entity);
return Result.ok(entity.getId());
}

View File

@@ -0,0 +1,20 @@
package tech.easyflow.admin.model;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import java.math.BigInteger;
/**
* 定时任务可运行的工作流安全选项。
*
* @param id 工作流 ID
* @param title 工作流标题
* @param description 工作流描述
*/
public record SysJobWorkflowOptionView(
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
String title,
String description
) {
}

View File

@@ -0,0 +1,123 @@
package tech.easyflow.admin.model;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import java.math.BigInteger;
import java.util.List;
import java.util.Map;
/**
* 管理端系统表单所需的安全选项视图。
*/
public final class SystemFormOptionsView {
private SystemFormOptionsView() {
}
/**
* 审批流程资源范围选项。
*
* @param categories 按资源类型分组的分类选项
* @param departments 部门树
*/
public record ApprovalResourceScopeOptions(
Map<String, List<CategoryOption>> categories,
List<DepartmentOption> departments
) {
}
/**
* 角色表单选项。
*
* @param menus 菜单树
* @param categories 按资源类型分组的非 Bot 分类选项
*/
public record RoleFormOptions(
List<MenuOption> menus,
Map<String, List<CategoryOption>> categories
) {
}
/**
* 账号表单选项。
*
* @param departments 部门树
* @param roles 可用角色
* @param positions 可用岗位
*/
public record AccountFormOptions(
List<DepartmentOption> departments,
List<RoleOption> roles,
List<PositionOption> positions
) {
}
/**
* 分类安全选项。
*
* @param id 分类 ID
* @param categoryName 分类名称
*/
public record CategoryOption(
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
String categoryName
) {
}
/**
* 部门树安全选项。
*
* @param id 部门 ID
* @param parentId 上级部门 ID
* @param deptName 部门名称
* @param children 下级部门
*/
public record DepartmentOption(
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
@JsonSerialize(using = ToStringSerializer.class) BigInteger parentId,
String deptName,
List<DepartmentOption> children
) {
}
/**
* 菜单树安全选项。
*
* @param id 菜单 ID
* @param parentId 上级菜单 ID
* @param menuTitle 菜单标题
* @param children 下级菜单
*/
public record MenuOption(
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
@JsonSerialize(using = ToStringSerializer.class) BigInteger parentId,
String menuTitle,
List<MenuOption> children
) {
}
/**
* 角色安全选项。
*
* @param id 角色 ID
* @param roleName 角色名称
*/
public record RoleOption(
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
String roleName
) {
}
/**
* 岗位安全选项。
*
* @param id 岗位 ID
* @param positionName 岗位名称
*/
public record PositionOption(
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
String positionName
) {
}
}

View File

@@ -0,0 +1,9 @@
package tech.easyflow.admin.model.ai;
/**
* 模型统一网关页面所需的安全配置。
*
* @param publishBaseUrl 模型发布基础地址
*/
public record ModelGatewayConfigView(String publishBaseUrl) {
}

View File

@@ -0,0 +1,172 @@
package tech.easyflow.admin.model.ai;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import java.math.BigInteger;
import java.util.List;
import java.util.Map;
/**
* 工作流设计器所需的安全选项视图。
*
* @param models 模型选项
* @param knowledges 知识库选项
* @param codeEngines 代码执行引擎选项
*/
public record WorkflowDesignerOptionsView(
List<ModelOption> models,
List<KnowledgeOption> knowledges,
List<Map<String, Object>> codeEngines
) {
/**
* 模型安全选项。
*
* @param id 模型 ID
* @param title 模型标题
* @param description 模型描述
* @param modelProvider 供应商安全摘要
*/
public record ModelOption(
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
String title,
String description,
ProviderOption modelProvider
) {
}
/**
* 模型供应商安全摘要。
*
* @param providerName 供应商名称
* @param providerType 供应商类型
* @param icon 供应商图标
*/
public record ProviderOption(String providerName, String providerType, String icon) {
}
/**
* 知识库安全选项。
*
* @param id 知识库 ID
* @param title 知识库标题
* @param description 知识库描述
*/
public record KnowledgeOption(
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
String title,
String description
) {
}
/**
* 插件安全选项。
*
* @param id 插件 ID
* @param name 插件名称
* @param description 插件描述
* @param icon 插件图标
* @param tools 可用工具
*/
public record PluginOption(
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
String name,
String description,
String icon,
List<PluginToolOption> tools
) {
}
/**
* 插件工具安全选项。
*
* @param id 工具 ID
* @param name 工具名称
* @param description 工具描述
*/
public record PluginToolOption(
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
String name,
String description
) {
}
/**
* 数据源安全选项。
*
* @param id 数据源 ID
* @param sourceName 数据源名称
* @param sourceType 数据源类型
*/
public record DataSourceOption(
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
String sourceName,
String sourceType
) {
}
/**
* 数据目录安全选项。
*
* @param id 目录 ID
* @param sourceId 数据源 ID
* @param catalogName 目录名称
* @param catalogDesc 目录描述
*/
public record CatalogOption(
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
@JsonSerialize(using = ToStringSerializer.class) BigInteger sourceId,
String catalogName,
String catalogDesc
) {
}
/**
* 已接入数据集安全选项。
*
* @param id 数据集 ID
* @param sourceId 数据源 ID
* @param catalogId 目录 ID
* @param tableName 数据表名称
* @param tableDesc 数据表描述
*/
public record DatasetOption(
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
@JsonSerialize(using = ToStringSerializer.class) BigInteger sourceId,
@JsonSerialize(using = ToStringSerializer.class) BigInteger catalogId,
String tableName,
String tableDesc
) {
}
/**
* 数据集字段安全视图。
*
* @param fieldName 字段名称
* @param fieldDesc 字段描述
* @param jdbcType JDBC 类型
* @param fieldType 业务字段类型
*/
public record DatasetFieldOption(
String fieldName,
String fieldDesc,
String jdbcType,
Integer fieldType
) {
}
/**
* 数据集结构安全视图。
*
* @param tableName 数据表名称
* @param tableDesc 数据表描述
* @param fields 字段列表
*/
public record DatasetSchemaOption(
String tableName,
String tableDesc,
List<DatasetFieldOption> fields
) {
}
}

View File

@@ -0,0 +1,657 @@
package tech.easyflow.admin.service.ai;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.easyagents.flow.core.chain.ChainDefinition;
import com.easyagents.flow.core.chain.Node;
import com.easyagents.flow.core.node.ConfirmNode;
import com.easyagents.flow.core.node.EndNode;
import com.easyagents.flow.core.node.StartNode;
import com.easyagents.flow.core.parser.ChainParser;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.stereotype.Service;
import tech.easyflow.admin.model.ai.WorkflowDesignerOptionsView;
import tech.easyflow.ai.easyagentsflow.service.WorkflowDatacenterContentService;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.entity.Model;
import tech.easyflow.ai.entity.ModelProvider;
import tech.easyflow.ai.entity.Plugin;
import tech.easyflow.ai.entity.PluginItem;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.enums.PluginType;
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.entity.DatacenterTable;
import tech.easyflow.datacenter.entity.DatacenterTableField;
import tech.easyflow.datacenter.execution.model.DatacenterSchemaResponse;
import tech.easyflow.datacenter.execution.model.DatasetRef;
import tech.easyflow.datacenter.execution.service.DatacenterDatasetQueryService;
import tech.easyflow.datacenter.meta.entity.DatacenterSource;
import tech.easyflow.datacenter.meta.enums.DatacenterSourceType;
import tech.easyflow.datacenter.meta.model.DatacenterCatalogMeta;
import tech.easyflow.datacenter.meta.service.DatacenterDatasetRegistryService;
import tech.easyflow.datacenter.meta.service.DatacenterSourceService;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceAction;
import tech.easyflow.system.service.ResourceAccessService;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* 查询工作流设计器所需的场景化安全选项。
*/
@Service
public class WorkflowDesignerOptionService {
private final ModelService modelService;
private final DocumentCollectionService documentCollectionService;
private final PluginService pluginService;
private final PluginItemService pluginItemService;
private final PluginVisibilityService pluginVisibilityService;
private final WorkflowService workflowService;
private final WorkflowUsageAuthorizationService workflowUsageAuthorizationService;
private final WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver;
private final ChainParser chainParser;
private final WorkflowDatacenterContentService workflowDatacenterContentService;
private final ResourceAccessService resourceAccessService;
private final DatacenterSourceService datacenterSourceService;
private final DatacenterDatasetRegistryService datacenterDatasetRegistryService;
private final DatacenterDatasetQueryService datacenterDatasetQueryService;
/**
* 创建工作流设计器选项服务。
*
* @param modelService 模型服务
* @param documentCollectionService 知识库服务
* @param pluginService 插件服务
* @param pluginItemService 插件工具服务
* @param pluginVisibilityService 插件可见性服务
* @param workflowService 工作流服务
* @param workflowUsageAuthorizationService 工作流使用权限校验服务
* @param workflowPluginSnapshotResolver 工作流插件快照解析器
* @param chainParser 工作流解析器
* @param workflowDatacenterContentService 工作流数据中心内容服务
* @param resourceAccessService 资源访问服务
* @param datacenterSourceService 数据源服务
* @param datacenterDatasetRegistryService 数据集注册服务
* @param datacenterDatasetQueryService 数据集查询服务
*/
public WorkflowDesignerOptionService(
ModelService modelService,
DocumentCollectionService documentCollectionService,
PluginService pluginService,
PluginItemService pluginItemService,
PluginVisibilityService pluginVisibilityService,
WorkflowService workflowService,
WorkflowUsageAuthorizationService workflowUsageAuthorizationService,
WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver,
ChainParser chainParser,
WorkflowDatacenterContentService workflowDatacenterContentService,
ResourceAccessService resourceAccessService,
DatacenterSourceService datacenterSourceService,
DatacenterDatasetRegistryService datacenterDatasetRegistryService,
DatacenterDatasetQueryService datacenterDatasetQueryService) {
this.modelService = modelService;
this.documentCollectionService = documentCollectionService;
this.pluginService = pluginService;
this.pluginItemService = pluginItemService;
this.pluginVisibilityService = pluginVisibilityService;
this.workflowService = workflowService;
this.workflowUsageAuthorizationService = workflowUsageAuthorizationService;
this.workflowPluginSnapshotResolver = workflowPluginSnapshotResolver;
this.chainParser = chainParser;
this.workflowDatacenterContentService = workflowDatacenterContentService;
this.resourceAccessService = resourceAccessService;
this.datacenterSourceService = datacenterSourceService;
this.datacenterDatasetRegistryService = datacenterDatasetRegistryService;
this.datacenterDatasetQueryService = datacenterDatasetQueryService;
}
/**
* 查询设计器初始化所需的模型和知识库。
*
* @param codeEngines 代码执行引擎选项
* @return 设计器初始化选项
*/
public WorkflowDesignerOptionsView listOptions(List<Map<String, Object>> codeEngines) {
LoginAccount account = requireAccount();
return new WorkflowDesignerOptionsView(
listModelOptions(account),
listKnowledgeOptions(account),
codeEngines == null ? List.of() : codeEngines
);
}
/**
* 校验工作流内容引用的场景资源是否仍允许当前账号使用。
*
* @param content 工作流内容
* @throws BusinessException 内容引用越权、跨租户或资源失效时抛出
*/
public void assertContentReferences(String content) {
if (content == null || content.isBlank()) {
return;
}
JSONObject root;
try {
root = JSON.parseObject(content);
} catch (Exception exception) {
throw new BusinessException("工作流内容不是合法JSON");
}
JSONArray nodes = root.getJSONArray("nodes");
if (nodes == null || nodes.isEmpty()) {
return;
}
LoginAccount account = requireAccount();
Set<BigInteger> modelIds = new HashSet<>();
Set<BigInteger> knowledgeIds = new HashSet<>();
Set<BigInteger> checkedPluginItemIds = new HashSet<>();
Set<BigInteger> checkedWorkflowIds = new HashSet<>();
Set<BigInteger> checkedSourceIds = new HashSet<>();
Set<String> checkedTableIds = new HashSet<>();
for (int index = 0; index < nodes.size(); index++) {
JSONObject node = nodes.getJSONObject(index);
if (node == null) {
continue;
}
JSONObject data = node.getJSONObject("data");
if (data == null) {
continue;
}
String nodeType = data.getString("type");
if (nodeType == null || nodeType.isBlank()) {
nodeType = node.getString("type");
}
if ("llmNode".equals(nodeType)) {
addReferenceId(modelIds, readReferenceId(data, "llmId", "模型"));
} else if ("knowledgeNode".equals(nodeType)) {
addReferenceId(knowledgeIds, readReferenceId(data, "knowledgeId", "知识库"));
} else if ("plugin-node".equals(nodeType)) {
BigInteger pluginItemId = readReferenceId(data, "pluginId", "插件工具");
if (pluginItemId != null && checkedPluginItemIds.add(pluginItemId)) {
getPluginTinyFlowData(pluginItemId);
}
} else if ("workflow-node".equals(nodeType)) {
assertWorkflowReference(readReferenceId(data, "workflowId", "子流程"),
account, checkedWorkflowIds);
}
assertDatasetReference(data, account, checkedSourceIds, checkedTableIds);
}
assertModelReferences(modelIds, account);
assertKnowledgeReferences(knowledgeIds, account);
}
/**
* 分页查询当前账号可用于工作流的插件。
*
* @param pageNumber 页码
* @param pageSize 每页数量
* @return 插件安全选项分页
*/
public Page<WorkflowDesignerOptionsView.PluginOption> pagePlugins(Long pageNumber, Long pageSize) {
LoginAccount account = requireAccount();
QueryWrapper wrapper = QueryWrapper.create()
.eq(Plugin::getTenantId, account.getTenantId().longValue())
.orderBy(Plugin::getCreated, false);
List<Plugin> plugins = pluginService.getMapper().selectListWithRelationsByQuery(wrapper);
List<Plugin> availablePlugins = pluginService.preparePluginsForCurrentUser(plugins, false, true);
List<WorkflowDesignerOptionsView.PluginOption> options = availablePlugins.stream()
.map(this::toPluginOption)
.toList();
long actualPageNumber = pageNumber == null || pageNumber < 1 ? 1L : pageNumber;
long actualPageSize = pageSize == null || pageSize < 1 ? 10L : Math.min(pageSize, 100L);
int fromIndex = Math.toIntExact(Math.min(options.size(), (actualPageNumber - 1) * actualPageSize));
int toIndex = Math.toIntExact(Math.min(options.size(), fromIndex + actualPageSize));
return new Page<>(
options.subList(fromIndex, toIndex),
actualPageNumber,
actualPageSize,
options.size()
);
}
/**
* 查询一个插件工具的工作流节点安全配置。
*
* @param pluginItemId 插件工具 ID
* @return 节点配置
* @throws BusinessException 插件工具不存在或不可用时抛出
*/
public JSONObject getPluginTinyFlowData(BigInteger pluginItemId) {
if (pluginItemId == null) {
throw new BusinessException("插件工具不能为空");
}
PluginItem record = pluginItemService.getById(pluginItemId);
if (record == null || !Integer.valueOf(1).equals(record.getStatus())) {
throw new BusinessException("插件工具不存在或已禁用");
}
Plugin plugin = pluginService.getById(record.getPluginId());
if (plugin == null) {
throw new BusinessException("插件不存在");
}
LoginAccount account = requireAccount();
if (!Objects.equals(plugin.getTenantId(), account.getTenantId().longValue())) {
throw new BusinessException("无权限访问插件");
}
pluginVisibilityService.assertPluginVisible(plugin.getCreatedBy(), plugin.getId(), "无权限访问插件");
Plugin preparedPlugin = pluginService.preparePluginForCurrentUser(plugin);
if (Boolean.FALSE.equals(preparedPlugin.getAvailable())) {
throw new BusinessException(preparedPlugin.getReasonMessage());
}
JSONObject nodeData = new JSONObject();
nodeData.put("pluginId", record.getId().toString());
nodeData.put("pluginName", record.getName());
nodeData.put("pluginType", preparedPlugin.getType());
nodeData.put("workflowId", preparedPlugin.getWorkflowId());
nodeData.put("workflowTitle", preparedPlugin.getWorkflowTitle());
nodeData.put("available", preparedPlugin.getAvailable());
nodeData.put("reasonCode", preparedPlugin.getReasonCode());
nodeData.put("reasonMessage", preparedPlugin.getReasonMessage());
nodeData.put("parameters", parseSchemaArray(record.getInputData()));
nodeData.put("outputDefs", parseSchemaArray(record.getOutputData()));
nodeData.put("schemaHash", resolveSchemaHash(record, preparedPlugin));
return nodeData;
}
/**
* 查询子流程节点所需的输入输出定义。
*
* @param currentWorkflowId 当前工作流 ID
* @param childWorkflowId 子流程 ID
* @return 子流程节点配置
* @throws BusinessException 子流程不存在、不可用或无权使用时抛出
*/
public JSONObject getChildWorkflowNodeData(
BigInteger currentWorkflowId,
BigInteger childWorkflowId) {
if (childWorkflowId == null) {
throw new BusinessException("子流程不能为空");
}
if (Objects.equals(childWorkflowId, currentWorkflowId)) {
throw new BusinessException("工作流不能作为自身子节点");
}
LoginAccount account = requireAccount();
Workflow workflow = workflowUsageAuthorizationService.requireUsableWorkflow(
childWorkflowId,
account,
"子流程不存在、已禁用或无权使用");
assertContentReferences(workflow.getContent());
ChainDefinition definition = chainParser.parse(
workflowDatacenterContentService.prepareContent(workflow.getContent()));
JSONArray inputs = new JSONArray();
JSONArray outputs = new JSONArray();
for (Node node : definition.getNodes()) {
if (node instanceof StartNode) {
inputs = JSON.parseArray(JSON.toJSONString(node.getParameters()));
decorateChildWorkflowSchemaArray(inputs);
}
if (node instanceof EndNode endNode) {
outputs = JSON.parseArray(JSON.toJSONString(endNode.getOutputDefs()));
decorateChildWorkflowSchemaArray(outputs);
}
if (node instanceof ConfirmNode) {
throw new BusinessException("工作流存在【确认节点】,暂不支持作为子节点");
}
}
JSONObject nodeData = new JSONObject();
nodeData.put("workflowId", workflow.getId());
nodeData.put("workflowName", workflow.getTitle());
nodeData.put("parameters", inputs);
nodeData.put("outputDefs", outputs);
return nodeData;
}
/**
* 查询当前租户可用于工作流的数据源安全选项。
*
* @return 数据源安全选项
*/
public List<WorkflowDesignerOptionsView.DataSourceOption> listDataSources() {
LoginAccount account = requireAccount();
datacenterDatasetRegistryService.ensureBuiltinSource(DatacenterSourceType.PROJECT_MYSQL, account);
return datacenterSourceService.list(QueryWrapper.create()
.eq(DatacenterSource::getTenantId, account.getTenantId())
.orderBy(DatacenterSource::getModified, false))
.stream()
.map(source -> new WorkflowDesignerOptionsView.DataSourceOption(
source.getId(), source.getSourceName(), source.getSourceType()))
.toList();
}
/**
* 查询一个数据源的目录安全选项。
*
* @param sourceId 数据源 ID
* @return 目录安全选项
*/
public List<WorkflowDesignerOptionsView.CatalogOption> listCatalogs(BigInteger sourceId) {
LoginAccount account = requireAccount();
requireTenantSource(sourceId, account);
return datacenterSourceService.listCatalogs(sourceId, account).stream()
.map(this::toCatalogOption)
.toList();
}
/**
* 查询一个数据源目录下已接入的数据集。
*
* @param sourceId 数据源 ID
* @param catalogId 目录 ID
* @return 数据集安全选项
*/
public List<WorkflowDesignerOptionsView.DatasetOption> listManagedDatasets(
BigInteger sourceId,
BigInteger catalogId) {
LoginAccount account = requireAccount();
requireTenantSource(sourceId, account);
return datacenterDatasetRegistryService.listManagedTables(sourceId, catalogId).stream()
.filter(table -> Objects.equals(table.getTenantId(), account.getTenantId()))
.filter(table -> Objects.equals(table.getSourceId(), sourceId))
.filter(table -> catalogId == null || Objects.equals(table.getCatalogId(), catalogId))
.map(this::toDatasetOption)
.toList();
}
/**
* 查询工作流数据节点所需的数据集结构。
*
* @param datasetRef 数据集引用
* @return 数据集结构安全视图
*/
public WorkflowDesignerOptionsView.DatasetSchemaOption getDatasetSchema(DatasetRef datasetRef) {
if (datasetRef == null || datasetRef.getSourceId() == null || datasetRef.getTableId() == null) {
throw new BusinessException("数据集引用不完整");
}
LoginAccount account = requireAccount();
requireTenantSource(datasetRef.getSourceId(), account);
DatacenterTable table = datacenterDatasetRegistryService.getTableWithFields(datasetRef.getTableId());
if (table == null
|| !Objects.equals(table.getTenantId(), account.getTenantId())
|| !Objects.equals(table.getSourceId(), datasetRef.getSourceId())) {
throw new BusinessException("数据集不存在或无权访问");
}
DatacenterSchemaResponse schema = datacenterDatasetQueryService.getSchema(datasetRef);
List<DatacenterTableField> fields = schema == null || schema.getFields() == null
? Collections.emptyList()
: schema.getFields();
return new WorkflowDesignerOptionsView.DatasetSchemaOption(
table.getTableName(),
table.getTableDesc(),
fields.stream()
.map(field -> new WorkflowDesignerOptionsView.DatasetFieldOption(
field.getFieldName(),
field.getFieldDesc(),
field.getJdbcType(),
field.getFieldType()
))
.toList()
);
}
private List<WorkflowDesignerOptionsView.ModelOption> listModelOptions(LoginAccount account) {
Model query = new Model();
query.setTenantId(account.getTenantId());
query.setModelType(Model.MODEL_TYPES[0]);
return modelService.listSelectableModels(query, false, "id", "desc").stream()
.filter(model -> Objects.equals(model.getTenantId(), account.getTenantId()))
.map(model -> {
ModelProvider provider = model.getModelProvider();
WorkflowDesignerOptionsView.ProviderOption providerOption = provider == null
? null
: new WorkflowDesignerOptionsView.ProviderOption(
provider.getProviderName(),
provider.getProviderType(),
provider.getIcon()
);
return new WorkflowDesignerOptionsView.ModelOption(
model.getId(),
model.getTitle(),
model.getDescription(),
providerOption
);
})
.toList();
}
private List<WorkflowDesignerOptionsView.KnowledgeOption> listKnowledgeOptions(LoginAccount account) {
return documentCollectionService.list(QueryWrapper.create()
.eq(DocumentCollection::getTenantId, account.getTenantId())
.orderBy(DocumentCollection::getModified, false))
.stream()
.filter(item -> resourceAccessService.canAccess(
account, CategoryResourceType.KNOWLEDGE, item, ResourceAction.USE))
.map(item -> new WorkflowDesignerOptionsView.KnowledgeOption(
item.getId(), item.getTitle(), item.getDescription()))
.toList();
}
private void addReferenceId(Set<BigInteger> resourceIds, BigInteger resourceId) {
if (resourceId != null) {
resourceIds.add(resourceId);
}
}
private void assertModelReferences(Set<BigInteger> modelIds, LoginAccount account) {
if (modelIds.isEmpty()) {
return;
}
List<Model> models = modelService.listByIds(modelIds);
boolean valid = models.size() == modelIds.size()
&& models.stream().allMatch(model ->
modelIds.contains(model.getId())
&& Objects.equals(model.getTenantId(), account.getTenantId())
&& Model.MODEL_TYPES[0].equals(model.getModelType()));
if (!valid) {
throw new BusinessException("模型不存在、已失效或无权使用");
}
}
private void assertKnowledgeReferences(Set<BigInteger> knowledgeIds, LoginAccount account) {
if (knowledgeIds.isEmpty()) {
return;
}
List<DocumentCollection> knowledges = documentCollectionService.listByIds(knowledgeIds);
boolean valid = knowledges.size() == knowledgeIds.size()
&& knowledges.stream().allMatch(knowledge ->
knowledgeIds.contains(knowledge.getId())
&& Objects.equals(knowledge.getTenantId(), account.getTenantId())
&& resourceAccessService.canAccess(
account, CategoryResourceType.KNOWLEDGE, knowledge, ResourceAction.USE));
if (!valid) {
throw new BusinessException("知识库不存在、已失效或无权使用");
}
}
private void assertWorkflowReference(
BigInteger workflowId,
LoginAccount account,
Set<BigInteger> checkedWorkflowIds) {
if (workflowId == null || !checkedWorkflowIds.add(workflowId)) {
return;
}
workflowUsageAuthorizationService.requireUsableWorkflow(
workflowId,
account,
"子流程不存在、已禁用或无权使用");
}
private void assertDatasetReference(
JSONObject data,
LoginAccount account,
Set<BigInteger> checkedSourceIds,
Set<String> checkedTableIds) {
JSONObject datasetRef = data.getJSONObject("datasetRef");
if (datasetRef == null) {
return;
}
BigInteger sourceId = readReferenceId(datasetRef, "sourceId", "数据源");
if (sourceId != null && checkedSourceIds.add(sourceId)) {
requireTenantSource(sourceId, account);
}
BigInteger tableId = readReferenceId(datasetRef, "tableId", "数据集");
if (tableId == null) {
return;
}
if (sourceId == null) {
throw new BusinessException("数据集缺少数据源引用");
}
String checkedKey = sourceId + ":" + tableId;
if (!checkedTableIds.add(checkedKey)) {
return;
}
DatacenterTable table = datacenterDatasetRegistryService.getTableWithFields(tableId);
if (table == null
|| !Objects.equals(table.getTenantId(), account.getTenantId())
|| !Objects.equals(table.getSourceId(), sourceId)) {
throw new BusinessException("数据集不存在或无权使用");
}
}
private BigInteger readReferenceId(JSONObject data, String key, String resourceName) {
Object value = data.get(key);
if (value == null || String.valueOf(value).isBlank()) {
return null;
}
try {
return new BigInteger(String.valueOf(value));
} catch (NumberFormatException exception) {
throw new BusinessException(resourceName + "引用格式无效");
}
}
private WorkflowDesignerOptionsView.PluginOption toPluginOption(Plugin plugin) {
List<WorkflowDesignerOptionsView.PluginToolOption> tools =
plugin.getTools() == null ? List.of() : plugin.getTools().stream()
.filter(tool -> Integer.valueOf(1).equals(tool.getStatus()))
.map(tool -> new WorkflowDesignerOptionsView.PluginToolOption(
tool.getId(), tool.getName(), tool.getDescription()))
.toList();
return new WorkflowDesignerOptionsView.PluginOption(
plugin.getId(),
plugin.getName(),
plugin.getDescription(),
plugin.getIcon(),
tools
);
}
private JSONArray parseSchemaArray(String content) {
if (content == null || content.isBlank()) {
return new JSONArray();
}
JSONArray array = JSON.parseArray(content);
decorateSchemaArray(array);
return array;
}
private void decorateSchemaArray(JSONArray array) {
for (Object item : array) {
if (!(item instanceof JSONObject value)) {
continue;
}
value.put("id", cn.hutool.core.util.IdUtil.simpleUUID());
value.put("nameDisabled", true);
value.put("dataTypeDisabled", true);
value.put("deleteDisabled", true);
value.put("addChildDisabled", true);
JSONArray children = value.getJSONArray("children");
if (children != null) {
decorateSchemaArray(children);
}
}
}
/**
* 为子流程参数补充设计器只读元数据。
*
* @param array 子流程参数定义
*/
private void decorateChildWorkflowSchemaArray(JSONArray array) {
for (Object item : array) {
if (!(item instanceof JSONObject value)) {
continue;
}
value.put("id", cn.hutool.core.util.IdUtil.simpleUUID());
value.put("nameDisabled", true);
value.put("dataTypeDisabled", true);
value.put("deleteDisabled", true);
value.put("addChildDisabled", true);
value.put("refType", "ref");
JSONArray children = value.getJSONArray("children");
if (children != null) {
decorateChildWorkflowSchemaArray(children);
}
}
}
private String resolveSchemaHash(PluginItem record, Plugin plugin) {
if (record.getSchemaHash() != null && !record.getSchemaHash().isBlank()) {
return record.getSchemaHash();
}
if (!PluginType.isWorkflow(plugin.getType()) || plugin.getWorkflowId() == null) {
return null;
}
Workflow workflow = workflowService.getPublishedById(plugin.getWorkflowId());
return workflow == null ? null : workflowPluginSnapshotResolver.resolveSchemaHash(workflow);
}
private WorkflowDesignerOptionsView.CatalogOption toCatalogOption(DatacenterCatalogMeta catalog) {
return new WorkflowDesignerOptionsView.CatalogOption(
catalog.getId(),
catalog.getSourceId(),
catalog.getCatalogName(),
catalog.getCatalogDesc()
);
}
private WorkflowDesignerOptionsView.DatasetOption toDatasetOption(DatacenterTable table) {
return new WorkflowDesignerOptionsView.DatasetOption(
table.getId(),
table.getSourceId(),
table.getCatalogId(),
table.getTableName(),
table.getTableDesc()
);
}
private DatacenterSource requireTenantSource(BigInteger sourceId, LoginAccount account) {
if (sourceId == null) {
throw new BusinessException("数据源不能为空");
}
DatacenterSource source = datacenterSourceService.getById(sourceId);
if (source == null || !Objects.equals(source.getTenantId(), account.getTenantId())) {
throw new BusinessException("数据源不存在或无权访问");
}
return source;
}
private LoginAccount requireAccount() {
LoginAccount account = SaTokenUtil.getLoginAccount();
if (account == null || account.getId() == null || account.getTenantId() == null) {
throw new BusinessException("当前登录状态失效,请重新登录后再试");
}
return account;
}
}

View File

@@ -0,0 +1,510 @@
package tech.easyflow.admin.service.system;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.stereotype.Service;
import tech.easyflow.admin.model.SystemFormOptionsView;
import tech.easyflow.approval.entity.vo.ApprovalFlowScopeVo;
import tech.easyflow.approval.enums.ApprovalScopeType;
import tech.easyflow.agent.entity.AgentCategory;
import tech.easyflow.agent.service.AgentCategoryService;
import tech.easyflow.ai.entity.DocumentCollectionCategory;
import tech.easyflow.ai.entity.PluginCategory;
import tech.easyflow.ai.entity.ResourceCategory;
import tech.easyflow.ai.entity.WorkflowCategory;
import tech.easyflow.ai.service.DocumentCollectionCategoryService;
import tech.easyflow.ai.service.PluginCategoryService;
import tech.easyflow.ai.service.ResourceCategoryService;
import tech.easyflow.ai.service.WorkflowCategoryService;
import tech.easyflow.common.constant.enums.EnumDataStatus;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.entity.SysAccount;
import tech.easyflow.system.entity.SysDept;
import tech.easyflow.system.entity.SysMenu;
import tech.easyflow.system.entity.SysPosition;
import tech.easyflow.system.entity.SysRole;
import tech.easyflow.system.entity.vo.SysRoleCategoryScopeItemVo;
import tech.easyflow.system.service.SysDeptService;
import tech.easyflow.system.service.SysMenuService;
import tech.easyflow.system.service.SysPositionService;
import tech.easyflow.system.service.SysRoleService;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
/**
* 查询并校验系统管理表单所需的安全选项。
*/
@Service
public class SystemFormOptionService {
private final AgentCategoryService agentCategoryService;
private final WorkflowCategoryService workflowCategoryService;
private final DocumentCollectionCategoryService documentCollectionCategoryService;
private final PluginCategoryService pluginCategoryService;
private final ResourceCategoryService resourceCategoryService;
private final SysDeptService sysDeptService;
private final SysMenuService sysMenuService;
private final SysRoleService sysRoleService;
private final SysPositionService sysPositionService;
/**
* 创建系统表单选项服务。
*
* @param agentCategoryService Agent 分类服务
* @param workflowCategoryService 工作流分类服务
* @param documentCollectionCategoryService 知识库分类服务
* @param pluginCategoryService 插件分类服务
* @param resourceCategoryService 素材分类服务
* @param sysDeptService 部门服务
* @param sysMenuService 菜单服务
* @param sysRoleService 角色服务
* @param sysPositionService 岗位服务
*/
public SystemFormOptionService(
AgentCategoryService agentCategoryService,
WorkflowCategoryService workflowCategoryService,
DocumentCollectionCategoryService documentCollectionCategoryService,
PluginCategoryService pluginCategoryService,
ResourceCategoryService resourceCategoryService,
SysDeptService sysDeptService,
SysMenuService sysMenuService,
SysRoleService sysRoleService,
SysPositionService sysPositionService) {
this.agentCategoryService = agentCategoryService;
this.workflowCategoryService = workflowCategoryService;
this.documentCollectionCategoryService = documentCollectionCategoryService;
this.pluginCategoryService = pluginCategoryService;
this.resourceCategoryService = resourceCategoryService;
this.sysDeptService = sysDeptService;
this.sysMenuService = sysMenuService;
this.sysRoleService = sysRoleService;
this.sysPositionService = sysPositionService;
}
/**
* 查询审批流程资源范围选项。
*
* @return 审批流程资源范围选项
*/
public SystemFormOptionsView.ApprovalResourceScopeOptions approvalResourceScopeOptions() {
Map<String, List<SystemFormOptionsView.CategoryOption>> categories = new LinkedHashMap<>();
categories.put("AGENT", listAgentCategories());
categories.put("WORKFLOW", listWorkflowCategories());
categories.put("KNOWLEDGE", listKnowledgeCategories());
return new SystemFormOptionsView.ApprovalResourceScopeOptions(
categories,
listDepartments()
);
}
/**
* 查询角色管理表单选项。
*
* @return 角色管理表单选项
*/
public SystemFormOptionsView.RoleFormOptions roleFormOptions() {
Map<String, List<SystemFormOptionsView.CategoryOption>> categories = new LinkedHashMap<>();
categories.put("AGENT", listAgentCategories());
categories.put("PLUGIN", listPluginCategories());
categories.put("WORKFLOW", listWorkflowCategories());
categories.put("KNOWLEDGE", listKnowledgeCategories());
categories.put("RESOURCE", listResourceCategories());
return new SystemFormOptionsView.RoleFormOptions(
listMenus(),
categories
);
}
/**
* 查询账号管理表单选项。
*
* @return 账号管理表单选项
*/
public SystemFormOptionsView.AccountFormOptions accountFormOptions() {
BigInteger tenantId = requireAccount().getTenantId();
List<SystemFormOptionsView.RoleOption> roles = sysRoleService.list(QueryWrapper.create()
.eq(SysRole::getTenantId, tenantId)
.eq(SysRole::getStatus, EnumDataStatus.AVAILABLE.getCode())
.orderBy(SysRole::getId, true))
.stream()
.map(role -> new SystemFormOptionsView.RoleOption(role.getId(), role.getRoleName()))
.toList();
List<SystemFormOptionsView.PositionOption> positions = sysPositionService.list(QueryWrapper.create()
.eq(SysPosition::getTenantId, tenantId)
.eq(SysPosition::getStatus, EnumDataStatus.AVAILABLE.getCode())
.orderBy(SysPosition::getSortNo, true))
.stream()
.map(position -> new SystemFormOptionsView.PositionOption(
position.getId(), position.getPositionName()))
.toList();
return new SystemFormOptionsView.AccountFormOptions(
listDepartments(),
roles,
positions
);
}
/**
* 校验账号表单引用的部门、角色和岗位。
*
* @param account 账号表单
* @throws BusinessException 引用不存在或已禁用时抛出
*/
public void validateAccountReferences(SysAccount account) {
if (account == null) {
throw new BusinessException("账号信息不能为空");
}
if (account.getDeptId() == null) {
throw new BusinessException("部门不能为空");
}
SysDept dept = sysDeptService.getById(account.getDeptId());
BigInteger tenantId = requireAccount().getTenantId();
if (dept == null
|| !Objects.equals(dept.getTenantId(), tenantId)
|| !EnumDataStatus.AVAILABLE.getCode().equals(dept.getStatus())) {
throw new BusinessException("部门不存在或已禁用");
}
assertAvailableIds(
account.getRoleIds(),
sysRoleService::listByIds,
SysRole::getId,
SysRole::getStatus,
SysRole::getTenantId,
tenantId,
"角色"
);
if (account.getPositionIds() != null && !account.getPositionIds().isEmpty()) {
assertAvailableIds(
account.getPositionIds(),
sysPositionService::listByIds,
SysPosition::getId,
SysPosition::getStatus,
SysPosition::getTenantId,
tenantId,
"岗位"
);
}
}
/**
* 校验角色引用的菜单和部门。
*
* @param role 角色表单
* @throws BusinessException 引用不存在时抛出
*/
public void validateRoleReferences(SysRole role) {
if (role == null) {
throw new BusinessException("角色信息不能为空");
}
assertExistingIds(role.getMenuIds(), sysMenuService::listByIds, SysMenu::getId, "菜单");
if (role.getDeptIds() != null && !role.getDeptIds().isEmpty()) {
assertTenantIds(
role.getDeptIds(),
sysDeptService::listByIds,
SysDept::getId,
SysDept::getTenantId,
requireAccount().getTenantId(),
"部门");
}
}
/**
* 校验角色分类授权中提交的非 Bot 分类 ID。
*
* @param scopes 分类权限范围
* @throws BusinessException 分类类型或分类 ID 非法时抛出
*/
public void validateCategoryScopes(List<SysRoleCategoryScopeItemVo> scopes) {
if (scopes == null) {
return;
}
for (SysRoleCategoryScopeItemVo scope : scopes) {
if (scope == null || scope.getCategoryIds() == null || scope.getCategoryIds().isEmpty()) {
continue;
}
switch (String.valueOf(scope.getResourceType()).toUpperCase()) {
case "AGENT" -> assertTenantIds(
scope.getCategoryIds(),
agentCategoryService::listByIds,
AgentCategory::getId,
AgentCategory::getTenantId,
requireAccount().getTenantId(),
"Agent 分类");
case "PLUGIN" -> assertExistingIds(
scope.getCategoryIds(), pluginCategoryService::listByIds, PluginCategory::getId, "插件分类");
case "WORKFLOW" -> assertExistingIds(
scope.getCategoryIds(), workflowCategoryService::listByIds, WorkflowCategory::getId, "工作流分类");
case "KNOWLEDGE" -> assertExistingIds(
scope.getCategoryIds(),
documentCollectionCategoryService::listByIds,
DocumentCollectionCategory::getId,
"知识库分类");
case "RESOURCE" -> assertExistingIds(
scope.getCategoryIds(), resourceCategoryService::listByIds, ResourceCategory::getId, "素材分类");
case "BOT" -> throw new BusinessException("Bot 分类授权已停止维护");
default -> throw new BusinessException("不支持的分类资源类型");
}
}
}
/**
* 校验审批流程提交的分类和部门范围。
*
* @param resourceType 审批资源类型
* @param scopes 审批范围
* @throws BusinessException 范围引用不存在或资源类型不匹配时抛出
*/
public void validateApprovalScopes(String resourceType, List<ApprovalFlowScopeVo> scopes) {
if (scopes == null) {
return;
}
for (ApprovalFlowScopeVo scope : scopes) {
if (scope == null || scope.getScopeValue() == null) {
continue;
}
String scopeType = String.valueOf(scope.getScopeType()).toUpperCase();
if (ApprovalScopeType.DEPT.getCode().equals(scopeType)) {
assertTenantIds(
List.of(scope.getScopeValue()),
sysDeptService::listByIds,
SysDept::getId,
SysDept::getTenantId,
requireAccount().getTenantId(),
"部门");
continue;
}
if (!ApprovalScopeType.CATEGORY.getCode().equals(scopeType)) {
throw new BusinessException("不支持的审批范围类型");
}
switch (String.valueOf(resourceType).toUpperCase()) {
case "AGENT" -> assertTenantIds(
List.of(scope.getScopeValue()),
agentCategoryService::listByIds,
AgentCategory::getId,
AgentCategory::getTenantId,
requireAccount().getTenantId(),
"Agent 分类");
case "WORKFLOW" -> assertExistingIds(
List.of(scope.getScopeValue()),
workflowCategoryService::listByIds,
WorkflowCategory::getId,
"工作流分类");
case "KNOWLEDGE" -> assertExistingIds(
List.of(scope.getScopeValue()),
documentCollectionCategoryService::listByIds,
DocumentCollectionCategory::getId,
"知识库分类");
default -> throw new BusinessException("当前资源类型不支持分类审批范围");
}
}
}
private List<SystemFormOptionsView.DepartmentOption> listDepartments() {
BigInteger tenantId = requireAccount().getTenantId();
List<SysDept> departments = sysDeptService.list(QueryWrapper.create()
.eq(SysDept::getTenantId, tenantId)
.eq(SysDept::getStatus, EnumDataStatus.AVAILABLE.getCode())
.orderBy(SysDept::getSortNo, true));
return buildDepartmentTree(departments);
}
private List<SystemFormOptionsView.MenuOption> listMenus() {
List<SysMenu> menus = sysMenuService.list(QueryWrapper.create()
.orderBy(SysMenu::getSortNo, true));
return buildMenuTree(menus);
}
private List<SystemFormOptionsView.CategoryOption> listAgentCategories() {
BigInteger tenantId = requireAccount().getTenantId();
return agentCategoryService.list(QueryWrapper.create()
.eq(AgentCategory::getTenantId, tenantId)
.eq(AgentCategory::getStatus, EnumDataStatus.AVAILABLE.getCode())
.orderBy(AgentCategory::getSortNo, true))
.stream()
.map(category -> new SystemFormOptionsView.CategoryOption(
category.getId(), category.getCategoryName()))
.toList();
}
private List<SystemFormOptionsView.CategoryOption> listWorkflowCategories() {
return workflowCategoryService.list(QueryWrapper.create()
.orderBy(WorkflowCategory::getSortNo, true))
.stream()
.map(category -> new SystemFormOptionsView.CategoryOption(
category.getId(), category.getCategoryName()))
.toList();
}
private List<SystemFormOptionsView.CategoryOption> listKnowledgeCategories() {
return documentCollectionCategoryService.list(QueryWrapper.create()
.orderBy(DocumentCollectionCategory::getSortNo, true))
.stream()
.map(category -> new SystemFormOptionsView.CategoryOption(
category.getId(), category.getCategoryName()))
.toList();
}
private List<SystemFormOptionsView.CategoryOption> listPluginCategories() {
return pluginCategoryService.list(QueryWrapper.create()
.orderBy(PluginCategory::getId, true))
.stream()
.map(category -> new SystemFormOptionsView.CategoryOption(
category.getId(), category.getName()))
.toList();
}
private List<SystemFormOptionsView.CategoryOption> listResourceCategories() {
return resourceCategoryService.list(QueryWrapper.create()
.orderBy(ResourceCategory::getSortNo, true))
.stream()
.map(category -> new SystemFormOptionsView.CategoryOption(
category.getId(), category.getCategoryName()))
.toList();
}
private List<SystemFormOptionsView.DepartmentOption> buildDepartmentTree(List<SysDept> departments) {
Set<BigInteger> ids = new HashSet<>();
departments.forEach(item -> ids.add(item.getId()));
Map<BigInteger, List<SysDept>> children = new LinkedHashMap<>();
List<SysDept> roots = new ArrayList<>();
for (SysDept department : departments) {
BigInteger parentId = department.getParentId();
if (parentId == null || BigInteger.ZERO.equals(parentId) || !ids.contains(parentId)) {
roots.add(department);
} else {
children.computeIfAbsent(parentId, ignored -> new ArrayList<>()).add(department);
}
}
return roots.stream().map(item -> toDepartmentOption(item, children)).toList();
}
private SystemFormOptionsView.DepartmentOption toDepartmentOption(
SysDept department,
Map<BigInteger, List<SysDept>> children) {
return new SystemFormOptionsView.DepartmentOption(
department.getId(),
department.getParentId(),
department.getDeptName(),
children.getOrDefault(department.getId(), List.of()).stream()
.map(item -> toDepartmentOption(item, children))
.toList()
);
}
private List<SystemFormOptionsView.MenuOption> buildMenuTree(List<SysMenu> menus) {
Set<BigInteger> ids = new HashSet<>();
menus.forEach(item -> ids.add(item.getId()));
Map<BigInteger, List<SysMenu>> children = new LinkedHashMap<>();
List<SysMenu> roots = new ArrayList<>();
for (SysMenu menu : menus) {
BigInteger parentId = menu.getParentId();
if (parentId == null || BigInteger.ZERO.equals(parentId) || !ids.contains(parentId)) {
roots.add(menu);
} else {
children.computeIfAbsent(parentId, ignored -> new ArrayList<>()).add(menu);
}
}
return roots.stream().map(item -> toMenuOption(item, children)).toList();
}
private SystemFormOptionsView.MenuOption toMenuOption(
SysMenu menu,
Map<BigInteger, List<SysMenu>> children) {
return new SystemFormOptionsView.MenuOption(
menu.getId(),
menu.getParentId(),
menu.getMenuTitle(),
children.getOrDefault(menu.getId(), List.of()).stream()
.map(item -> toMenuOption(item, children))
.toList()
);
}
private <T> void assertAvailableIds(
Collection<BigInteger> rawIds,
Function<Collection<BigInteger>, List<T>> loader,
Function<T, BigInteger> idGetter,
Function<T, Integer> statusGetter,
Function<T, BigInteger> tenantGetter,
BigInteger tenantId,
String label) {
Set<BigInteger> ids = normalizeIds(rawIds);
if (ids.isEmpty()) {
throw new BusinessException(label + "不能为空");
}
List<T> records = loader.apply(ids);
boolean valid = records.size() == ids.size()
&& records.stream().allMatch(item ->
ids.contains(idGetter.apply(item))
&& EnumDataStatus.AVAILABLE.getCode().equals(statusGetter.apply(item))
&& Objects.equals(tenantGetter.apply(item), tenantId));
if (!valid) {
throw new BusinessException(label + "不存在或已禁用");
}
}
private <T> void assertExistingIds(
Collection<BigInteger> rawIds,
Function<Collection<BigInteger>, List<T>> loader,
Function<T, BigInteger> idGetter,
String label) {
Set<BigInteger> ids = normalizeIds(rawIds);
if (ids.isEmpty()) {
return;
}
List<T> records = loader.apply(ids);
boolean valid = records.size() == ids.size()
&& records.stream().allMatch(item -> ids.contains(idGetter.apply(item)));
if (!valid) {
throw new BusinessException(label + "不存在或无权访问");
}
}
private <T> void assertTenantIds(
Collection<BigInteger> rawIds,
Function<Collection<BigInteger>, List<T>> loader,
Function<T, BigInteger> idGetter,
Function<T, BigInteger> tenantGetter,
BigInteger tenantId,
String label) {
Set<BigInteger> ids = normalizeIds(rawIds);
if (ids.isEmpty()) {
return;
}
List<T> records = loader.apply(ids);
boolean valid = records.size() == ids.size()
&& records.stream().allMatch(item ->
ids.contains(idGetter.apply(item))
&& Objects.equals(tenantGetter.apply(item), tenantId));
if (!valid) {
throw new BusinessException(label + "不存在或无权访问");
}
}
private LoginAccount requireAccount() {
LoginAccount account = SaTokenUtil.getLoginAccount();
if (account == null || account.getTenantId() == null) {
throw new BusinessException("当前登录状态失效,请重新登录后再试");
}
return account;
}
private Set<BigInteger> normalizeIds(Collection<BigInteger> rawIds) {
Set<BigInteger> ids = new LinkedHashSet<>();
if (rawIds != null) {
rawIds.stream().filter(Objects::nonNull).forEach(ids::add);
}
return ids;
}
}

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;
}
}