feat: 完成工作流 Public API 授权闭环
- 新增访问令牌工作流 API 全局授权与 Public Workflow API 权限断言 - 补齐 API Key 执行记录归属、状态查询与下线后不可恢复边界 - 增加管理端接口调用说明与访问令牌授权开关
This commit is contained in:
@@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.ai.service.KnowledgeSharePermissionService;
|
||||
import tech.easyflow.ai.service.WorkflowApiPermissionService;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
@@ -46,6 +47,8 @@ public class SysApiKeyController extends BaseCurdController<SysApiKeyService, Sy
|
||||
private SysApiKeyResourceMappingService sysApiKeyResourceMappingService;
|
||||
@Resource
|
||||
private KnowledgeSharePermissionService knowledgeSharePermissionService;
|
||||
@Resource
|
||||
private WorkflowApiPermissionService workflowApiPermissionService;
|
||||
/**
|
||||
* 添加(保存)数据
|
||||
*
|
||||
@@ -88,6 +91,9 @@ public class SysApiKeyController extends BaseCurdController<SysApiKeyService, Sy
|
||||
if (entity.getKnowledgeShareEnabled() != null) {
|
||||
knowledgeSharePermissionService.replaceApiShareEnabled(entity.getId(), entity.getKnowledgeShareEnabled());
|
||||
}
|
||||
if (entity.getWorkflowApiEnabled() != null) {
|
||||
workflowApiPermissionService.replaceWorkflowApiEnabled(entity.getId(), entity.getWorkflowApiEnabled());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -129,5 +135,11 @@ public class SysApiKeyController extends BaseCurdController<SysApiKeyService, Sy
|
||||
.eq(SysApiKeyResourceMapping::getApiKeyId, entity.getId())
|
||||
.eq(SysApiKeyResourceMapping::getResourceType, "KNOWLEDGE");
|
||||
entity.setKnowledgeShareEnabled(sysApiKeyResourceMappingService.count(knowledgeWrapper) > 0);
|
||||
|
||||
QueryWrapper workflowWrapper = QueryWrapper.create()
|
||||
.select(SysApiKeyResourceMapping::getId)
|
||||
.eq(SysApiKeyResourceMapping::getApiKeyId, entity.getId())
|
||||
.eq(SysApiKeyResourceMapping::getResourceType, WorkflowApiPermissionService.RESOURCE_TYPE_WORKFLOW);
|
||||
entity.setWorkflowApiEnabled(sysApiKeyResourceMappingService.count(workflowWrapper) > 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
package tech.easyflow.admin.controller.system;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import tech.easyflow.common.annotation.UsePermission;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.system.entity.SysApiKeyResource;
|
||||
import tech.easyflow.system.service.SysApiKeyResourceService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 请求接口表 控制层。
|
||||
*
|
||||
@@ -20,4 +25,26 @@ public class SysApiKeyResourceController extends BaseCurdController<SysApiKeyRes
|
||||
public SysApiKeyResourceController(SysApiKeyResourceService service) {
|
||||
super(service);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询普通 API Key 接口授权资源。
|
||||
*
|
||||
* <p>工作流 Public API 使用独立的全局授权开关,不进入普通接口授权列表,避免用户误以为勾选
|
||||
* 具体接口资源即可完成工作流调用授权。</p>
|
||||
*
|
||||
* @param entity 查询条件
|
||||
* @param asTree 是否树形返回
|
||||
* @param sortKey 排序字段
|
||||
* @param sortType 排序方向
|
||||
* @return 普通接口授权资源
|
||||
*/
|
||||
@Override
|
||||
@GetMapping("list")
|
||||
public Result<List<SysApiKeyResource>> list(SysApiKeyResource entity, Boolean asTree, String sortKey, String sortType) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||
queryWrapper.notLike(SysApiKeyResource::getRequestInterface, "/public-api/workflow/");
|
||||
queryWrapper.notLike(SysApiKeyResource::getRequestInterface, "/public-api/knowledge-share/");
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
return Result.ok(service.list(queryWrapper));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,22 +4,31 @@ import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import tech.easyflow.approval.annotation.RequirePublishedAccess;
|
||||
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
|
||||
import tech.easyflow.ai.easyagentsflow.service.TinyFlowService;
|
||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
|
||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowRunningParameterResolver;
|
||||
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.entity.WorkflowExecResult;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.service.WorkflowExecResultService;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.ai.service.WorkflowApiPermissionService;
|
||||
import tech.easyflow.ai.utils.WorkFlowUtil;
|
||||
import tech.easyflow.common.constant.Constants;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.system.entity.SysApiKey;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.HashMap;
|
||||
@@ -43,6 +52,10 @@ public class PublicWorkflowController {
|
||||
private WorkflowCheckService workflowCheckService;
|
||||
@Resource
|
||||
private WorkflowRunningParameterResolver workflowRunningParameterResolver;
|
||||
@Resource
|
||||
private WorkflowApiPermissionService workflowApiPermissionService;
|
||||
@Resource
|
||||
private WorkflowExecResultService workflowExecResultService;
|
||||
|
||||
/**
|
||||
* 通过id或别名获取工作流详情
|
||||
@@ -54,8 +67,11 @@ public class PublicWorkflowController {
|
||||
@RequirePublishedAccess(resourceType = "WORKFLOW", idExpr = "#key", denyMessage = "工作流尚未发布")
|
||||
public Result<Workflow> getByIdOrAlias(
|
||||
@RequestParam
|
||||
@NotBlank(message = "key不能为空") String key) {
|
||||
@NotBlank(message = "key不能为空") String key,
|
||||
HttpServletRequest request) {
|
||||
workflowApiPermissionService.assertWorkflowApi(request.getHeader("ApiKey"), request.getRequestURI());
|
||||
Workflow workflow = workflowService.getPublishedDetail(key);
|
||||
assertStrictPublishedWorkflow(workflow);
|
||||
return Result.ok(workflow);
|
||||
}
|
||||
|
||||
@@ -88,19 +104,20 @@ public class PublicWorkflowController {
|
||||
* 运行工作流 - v2
|
||||
*/
|
||||
@PostMapping("/runAsync")
|
||||
@SaCheckPermission("/api/v1/workflow/save")
|
||||
@RequirePublishedAccess(resourceType = "WORKFLOW", idExpr = "#id", denyMessage = "工作流尚未发布")
|
||||
public Result<String> runAsync(@JsonBody(value = "id", required = true) BigInteger id,
|
||||
@JsonBody("variables") Map<String, Object> variables) {
|
||||
@JsonBody("variables") Map<String, Object> variables,
|
||||
HttpServletRequest request) {
|
||||
SysApiKey apiKey = workflowApiPermissionService.assertWorkflowApi(request.getHeader("ApiKey"), request.getRequestURI());
|
||||
if (variables == null) {
|
||||
variables = new HashMap<>();
|
||||
}
|
||||
Workflow workflow = workflowService.getPublishedById(id);
|
||||
if (workflow == null) {
|
||||
throw new RuntimeException("工作流不存在");
|
||||
}
|
||||
assertStrictPublishedWorkflow(workflow);
|
||||
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
||||
variables = workflowRunningParameterResolver.normalizeRuntimeVariables(workflow.getContent(), variables);
|
||||
variables.put(Constants.LOGIN_USER_KEY, buildApiKeyLoginAccount(apiKey));
|
||||
variables.put(WorkFlowUtil.CREATED_KEY_MEMORY_KEY, WorkFlowUtil.API_KEY);
|
||||
String executeId = chainExecutor.executeAsync(PublishedWorkflowDefinitionIds.published(id.toString()), variables);
|
||||
return Result.ok(executeId);
|
||||
}
|
||||
@@ -110,7 +127,10 @@ public class PublicWorkflowController {
|
||||
*/
|
||||
@PostMapping("/getChainStatus")
|
||||
public Result<ChainInfo> getChainStatus(@JsonBody(value = "executeId") String executeId,
|
||||
@JsonBody("nodes") List<NodeInfo> nodes) {
|
||||
@JsonBody("nodes") List<NodeInfo> nodes,
|
||||
HttpServletRequest request) {
|
||||
SysApiKey apiKey = workflowApiPermissionService.assertWorkflowApi(request.getHeader("ApiKey"), request.getRequestURI());
|
||||
assertApiKeyExecutionOwnership(apiKey, executeId);
|
||||
ChainInfo res = tinyFlowService.getChainStatus(executeId, nodes);
|
||||
return Result.ok(res);
|
||||
}
|
||||
@@ -119,22 +139,23 @@ public class PublicWorkflowController {
|
||||
* 恢复工作流运行 - v2
|
||||
*/
|
||||
@PostMapping("/resume")
|
||||
@SaCheckPermission("/api/v1/workflow/save")
|
||||
public Result<Void> resume(@JsonBody(value = "executeId", required = true) String executeId,
|
||||
@JsonBody("confirmParams") Map<String, Object> confirmParams) {
|
||||
@JsonBody("confirmParams") Map<String, Object> confirmParams,
|
||||
HttpServletRequest request) {
|
||||
SysApiKey apiKey = workflowApiPermissionService.assertWorkflowApi(request.getHeader("ApiKey"), request.getRequestURI());
|
||||
WorkflowExecResult execResult = assertApiKeyExecutionOwnership(apiKey, executeId);
|
||||
assertWorkflowExecutionResumable(execResult);
|
||||
chainExecutor.resumeAsync(executeId, confirmParams);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@GetMapping("getRunningParameters")
|
||||
@SaCheckPermission("/api/v1/workflow/query")
|
||||
@RequirePublishedAccess(resourceType = "WORKFLOW", idExpr = "#id", denyMessage = "工作流尚未发布")
|
||||
public Result<?> getRunningParameters(@RequestParam BigInteger id) {
|
||||
public Result<?> getRunningParameters(@RequestParam BigInteger id, HttpServletRequest request) {
|
||||
workflowApiPermissionService.assertWorkflowApi(request.getHeader("ApiKey"), request.getRequestURI());
|
||||
Workflow workflow = workflowService.getPublishedById(id);
|
||||
|
||||
if (workflow == null) {
|
||||
return Result.fail(1, "can not find the workflow by id: " + id);
|
||||
}
|
||||
assertStrictPublishedWorkflow(workflow);
|
||||
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
||||
Map<String, Object> res = workflowRunningParameterResolver.buildRunningParametersView(workflow);
|
||||
if (res == null) {
|
||||
@@ -142,4 +163,72 @@ public class PublicWorkflowController {
|
||||
}
|
||||
return Result.ok(res);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 API Key 调用方的运行身份。
|
||||
*
|
||||
* @param apiKey 访问令牌
|
||||
* @return 工作流运行身份
|
||||
*/
|
||||
private LoginAccount buildApiKeyLoginAccount(SysApiKey apiKey) {
|
||||
LoginAccount account = new LoginAccount();
|
||||
account.setId(apiKey.getId());
|
||||
account.setDeptId(apiKey.getDeptId() == null ? BigInteger.ZERO : apiKey.getDeptId());
|
||||
account.setTenantId(apiKey.getTenantId() == null ? BigInteger.ZERO : apiKey.getTenantId());
|
||||
account.setLoginName("apikey:" + apiKey.getId());
|
||||
account.setNickname("API 调用方");
|
||||
return account;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验工作流 Public API 只能访问严格已发布且存在发布快照的工作流。
|
||||
*
|
||||
* @param workflow 工作流发布视图
|
||||
*/
|
||||
private void assertStrictPublishedWorkflow(Workflow workflow) {
|
||||
if (workflow == null || !PublishStatus.PUBLISHED.getCode().equals(workflow.getPublishStatus())
|
||||
|| workflow.getPublishedSnapshotJson() == null || workflow.getPublishedSnapshotJson().isEmpty()) {
|
||||
throw new BusinessException("工作流尚未发布");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 Public Workflow API 后续操作只能作用于当前 API Key 发起的执行实例。
|
||||
*
|
||||
* @param apiKey 当前 API Key
|
||||
* @param executeId 执行 ID
|
||||
* @return 已通过归属校验的执行记录
|
||||
*/
|
||||
private WorkflowExecResult assertApiKeyExecutionOwnership(SysApiKey apiKey, String executeId) {
|
||||
if (executeId == null || executeId.isBlank()) {
|
||||
throw new BusinessException("执行ID不能为空");
|
||||
}
|
||||
WorkflowExecResult execResult = workflowExecResultService.getByExecKey(executeId);
|
||||
if (execResult == null) {
|
||||
throw new BusinessException("工作流执行记录不存在,请稍后重试");
|
||||
}
|
||||
if (!WorkFlowUtil.API_KEY.equals(execResult.getCreatedKey())
|
||||
|| apiKey == null
|
||||
|| apiKey.getId() == null
|
||||
|| !String.valueOf(apiKey.getId()).equals(execResult.getCreatedBy())) {
|
||||
throw new BusinessException("无权限访问当前工作流执行记录");
|
||||
}
|
||||
return execResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验当前执行实例是否仍允许恢复。
|
||||
*
|
||||
* @param execResult 执行记录
|
||||
*/
|
||||
private void assertWorkflowExecutionResumable(WorkflowExecResult execResult) {
|
||||
if (execResult == null || execResult.getWorkflowId() == null) {
|
||||
throw new BusinessException("工作流执行记录不存在,请稍后重试");
|
||||
}
|
||||
Workflow workflow = workflowService.getById(execResult.getWorkflowId());
|
||||
if (workflow == null || !PublishStatus.PUBLISHED.getCode().equals(workflow.getPublishStatus())
|
||||
|| workflow.getPublishedSnapshotJson() == null || workflow.getPublishedSnapshotJson().isEmpty()) {
|
||||
throw new BusinessException("工作流已下线或不可恢复执行");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user