fix: 收口管理端页面权限与工作流运行授权
- 页面选项接口改用所属页面权限并返回最小数据视图 - 统一校验工作流引用、租户、状态与定时任务执行主体 - 补充聊天记录权限迁移和权限隔离回归测试
This commit is contained in:
@@ -3,36 +3,31 @@ package tech.easyflow.ai.config;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import tech.easyflow.ai.mapper.*;
|
||||
import tech.easyflow.ai.mapper.BotCategoryMapper;
|
||||
import tech.easyflow.common.util.SpringContextUtil;
|
||||
import tech.easyflow.common.dict.DictManager;
|
||||
import tech.easyflow.common.dict.loader.DbDataLoader;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* 注册仍由 Bot 兼容页面使用的数据库字典。
|
||||
*/
|
||||
@Configuration
|
||||
public class AiDictAutoConfig {
|
||||
|
||||
@Resource
|
||||
private WorkflowMapper workflowMapper;
|
||||
@Resource
|
||||
private WorkflowCategoryMapper workflowCategoryMapper;
|
||||
/** Bot 分类字典数据访问器。 */
|
||||
@Resource
|
||||
private BotCategoryMapper botCategoryMapper;
|
||||
@Resource
|
||||
private ResourceCategoryMapper resourceCategoryMapper;
|
||||
@Resource
|
||||
private DocumentCollectionCategoryMapper documentCollectionCategoryMapper;
|
||||
|
||||
/**
|
||||
* 应用启动完成后注册 Bot 兼容字典。
|
||||
*/
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void onApplicationStartup() {
|
||||
|
||||
DictManager dictManager = SpringContextUtil.getBean(DictManager.class);
|
||||
dictManager.putLoader(new DbDataLoader<>("aiWorkFlow", workflowMapper, "id", "title", null, null, false));
|
||||
dictManager.putLoader(new DbDataLoader<>("aiWorkFlowCategory", workflowCategoryMapper, "id", "category_name", null, null, false));
|
||||
dictManager.putLoader(new DbDataLoader<>("aiBotCategory", botCategoryMapper, "id", "category_name", null, null, false));
|
||||
dictManager.putLoader(new DbDataLoader<>("aiResourceCategory", resourceCategoryMapper, "id", "category_name", null, null, false));
|
||||
dictManager.putLoader(new DbDataLoader<>("aiDocumentCollectionCategory", documentCollectionCategoryMapper, "id", "category_name", null, null, false));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package tech.easyflow.ai.service;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.common.constant.enums.EnumDataStatus;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
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.Objects;
|
||||
|
||||
/**
|
||||
* 工作流使用权限校验服务。
|
||||
*
|
||||
* <p>统一封装工作流存在性、租户、启用状态和资源使用权限校验,供页面能力和后台任务复用。</p>
|
||||
*/
|
||||
@Service
|
||||
public class WorkflowUsageAuthorizationService {
|
||||
|
||||
/** 工作流服务。 */
|
||||
private final WorkflowService workflowService;
|
||||
|
||||
/** 资源访问控制服务。 */
|
||||
private final ResourceAccessService resourceAccessService;
|
||||
|
||||
/**
|
||||
* 创建工作流使用权限校验服务。
|
||||
*
|
||||
* @param workflowService 工作流服务
|
||||
* @param resourceAccessService 资源访问控制服务
|
||||
*/
|
||||
public WorkflowUsageAuthorizationService(
|
||||
WorkflowService workflowService,
|
||||
ResourceAccessService resourceAccessService) {
|
||||
this.workflowService = workflowService;
|
||||
this.resourceAccessService = resourceAccessService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前账号可使用的启用工作流。
|
||||
*
|
||||
* @param workflowId 工作流 ID
|
||||
* @param account 使用工作流的账号
|
||||
* @param denyMessage 校验失败提示
|
||||
* @return 可使用的工作流
|
||||
* @throws BusinessException 工作流不存在、未启用、跨租户或无使用权限时抛出
|
||||
*/
|
||||
public Workflow requireUsableWorkflow(
|
||||
BigInteger workflowId,
|
||||
LoginAccount account,
|
||||
String denyMessage) {
|
||||
String message = denyMessage == null || denyMessage.isBlank()
|
||||
? "工作流不存在、已禁用或无权使用"
|
||||
: denyMessage;
|
||||
if (workflowId == null || account == null || account.getId() == null
|
||||
|| account.getTenantId() == null) {
|
||||
throw new BusinessException(403, 403, message);
|
||||
}
|
||||
Workflow workflow = workflowService.getById(workflowId);
|
||||
boolean usable = workflow != null
|
||||
&& Objects.equals(workflow.getTenantId(), account.getTenantId())
|
||||
&& EnumDataStatus.AVAILABLE.getCode().equals(workflow.getStatus())
|
||||
&& resourceAccessService.canAccess(
|
||||
account,
|
||||
CategoryResourceType.WORKFLOW,
|
||||
workflow,
|
||||
ResourceAction.USE);
|
||||
if (!usable) {
|
||||
throw new BusinessException(403, 403, message);
|
||||
}
|
||||
return workflow;
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,14 @@ public final class WorkflowSharePolicy {
|
||||
private static final Set<String> ALLOWED_REQUESTS = Set.of(
|
||||
permissionKey("GET", "/api/v1/workflow/detail", ResourceAction.READ),
|
||||
permissionKey("GET", "/api/v1/workflow/getRunningParameters", ResourceAction.READ),
|
||||
permissionKey("GET", "/api/v1/workflow/designer/options", ResourceAction.READ),
|
||||
permissionKey("GET", "/api/v1/workflow/designer/plugins", ResourceAction.READ),
|
||||
permissionKey("GET", "/api/v1/workflow/designer/pluginTinyFlow", ResourceAction.READ),
|
||||
permissionKey("GET", "/api/v1/workflow/designer/childWorkflow", ResourceAction.READ),
|
||||
permissionKey("GET", "/api/v1/workflow/designer/dataSources", ResourceAction.READ),
|
||||
permissionKey("GET", "/api/v1/workflow/designer/catalogs", ResourceAction.READ),
|
||||
permissionKey("GET", "/api/v1/workflow/designer/managedTables", ResourceAction.READ),
|
||||
permissionKey("GET", "/api/v1/workflow/designer/schema", ResourceAction.READ),
|
||||
permissionKey("POST", "/api/v1/workflow/update", ResourceAction.MANAGE),
|
||||
permissionKey("POST", "/api/v1/workflow/check", ResourceAction.MANAGE),
|
||||
permissionKey("POST", "/api/v1/workflow/singleRun", ResourceAction.USE),
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package tech.easyflow.ai.service;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.common.constant.enums.EnumDataStatus;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* {@link WorkflowUsageAuthorizationService} 工作流使用权限校验测试。
|
||||
*/
|
||||
public class WorkflowUsageAuthorizationServiceTest {
|
||||
|
||||
/**
|
||||
* 验证禁用工作流即使资源权限允许也不能被使用。
|
||||
*/
|
||||
@Test
|
||||
public void shouldRejectDisabledWorkflow() {
|
||||
BigInteger workflowId = BigInteger.valueOf(101);
|
||||
WorkflowService workflowService = mock(WorkflowService.class);
|
||||
ResourceAccessService resourceAccessService = mock(ResourceAccessService.class);
|
||||
Workflow workflow = workflow(workflowId, BigInteger.TEN, EnumDataStatus.UNAVAILABLE.getCode());
|
||||
LoginAccount account = account(BigInteger.ONE, BigInteger.TEN);
|
||||
when(workflowService.getById(workflowId)).thenReturn(workflow);
|
||||
when(resourceAccessService.canAccess(
|
||||
account,
|
||||
CategoryResourceType.WORKFLOW,
|
||||
workflow,
|
||||
ResourceAction.USE)).thenReturn(true);
|
||||
WorkflowUsageAuthorizationService service =
|
||||
new WorkflowUsageAuthorizationService(workflowService, resourceAccessService);
|
||||
|
||||
BusinessException exception = Assert.assertThrows(
|
||||
BusinessException.class,
|
||||
() -> service.requireUsableWorkflow(workflowId, account, "工作流不可用")
|
||||
);
|
||||
|
||||
Assert.assertEquals(exception.getMessage(), "工作流不可用");
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证工作流与账号租户不一致时拒绝使用。
|
||||
*/
|
||||
@Test
|
||||
public void shouldRejectCrossTenantWorkflow() {
|
||||
BigInteger workflowId = BigInteger.valueOf(102);
|
||||
WorkflowService workflowService = mock(WorkflowService.class);
|
||||
ResourceAccessService resourceAccessService = mock(ResourceAccessService.class);
|
||||
Workflow workflow = workflow(
|
||||
workflowId,
|
||||
BigInteger.valueOf(20),
|
||||
EnumDataStatus.AVAILABLE.getCode());
|
||||
LoginAccount account = account(BigInteger.ONE, BigInteger.TEN);
|
||||
when(workflowService.getById(workflowId)).thenReturn(workflow);
|
||||
WorkflowUsageAuthorizationService service =
|
||||
new WorkflowUsageAuthorizationService(workflowService, resourceAccessService);
|
||||
|
||||
Assert.assertThrows(
|
||||
BusinessException.class,
|
||||
() -> service.requireUsableWorkflow(workflowId, account, "工作流不可用")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证启用、同租户且具有使用权限的工作流可以返回。
|
||||
*/
|
||||
@Test
|
||||
public void shouldReturnUsableWorkflow() {
|
||||
BigInteger workflowId = BigInteger.valueOf(103);
|
||||
WorkflowService workflowService = mock(WorkflowService.class);
|
||||
ResourceAccessService resourceAccessService = mock(ResourceAccessService.class);
|
||||
Workflow workflow = workflow(
|
||||
workflowId,
|
||||
BigInteger.TEN,
|
||||
EnumDataStatus.AVAILABLE.getCode());
|
||||
LoginAccount account = account(BigInteger.ONE, BigInteger.TEN);
|
||||
when(workflowService.getById(workflowId)).thenReturn(workflow);
|
||||
when(resourceAccessService.canAccess(
|
||||
account,
|
||||
CategoryResourceType.WORKFLOW,
|
||||
workflow,
|
||||
ResourceAction.USE)).thenReturn(true);
|
||||
WorkflowUsageAuthorizationService service =
|
||||
new WorkflowUsageAuthorizationService(workflowService, resourceAccessService);
|
||||
|
||||
Workflow result = service.requireUsableWorkflow(workflowId, account, "工作流不可用");
|
||||
|
||||
Assert.assertSame(result, workflow);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建工作流测试数据。
|
||||
*
|
||||
* @param id 工作流 ID
|
||||
* @param tenantId 租户 ID
|
||||
* @param status 工作流状态
|
||||
* @return 工作流
|
||||
*/
|
||||
private Workflow workflow(BigInteger id, BigInteger tenantId, Integer status) {
|
||||
Workflow workflow = new Workflow();
|
||||
workflow.setId(id);
|
||||
workflow.setTenantId(tenantId);
|
||||
workflow.setStatus(status);
|
||||
return workflow;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建登录账号测试数据。
|
||||
*
|
||||
* @param id 账号 ID
|
||||
* @param tenantId 租户 ID
|
||||
* @return 登录账号
|
||||
*/
|
||||
private LoginAccount account(BigInteger id, BigInteger tenantId) {
|
||||
LoginAccount account = new LoginAccount();
|
||||
account.setId(id);
|
||||
account.setTenantId(tenantId);
|
||||
return account;
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,11 @@ public class WorkflowSharePolicyTest {
|
||||
"/api/v1/workflow/detail",
|
||||
ResourceAction.READ
|
||||
));
|
||||
Assert.assertTrue(WorkflowSharePolicy.isAllowedRequest(
|
||||
"GET",
|
||||
"/api/v1/workflow/designer/childWorkflow",
|
||||
ResourceAction.READ
|
||||
));
|
||||
Assert.assertTrue(WorkflowSharePolicy.isAllowedRequest(
|
||||
"POST",
|
||||
"/api/v1/workflow/update",
|
||||
|
||||
@@ -42,5 +42,17 @@
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-module-ai</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>5.12.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
package tech.easyflow.job.service;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||
import com.mybatisflex.core.tenant.TenantManager;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.ai.service.WorkflowUsageAuthorizationService;
|
||||
import tech.easyflow.common.constant.Constants;
|
||||
import tech.easyflow.common.constant.enums.EnumDataStatus;
|
||||
import tech.easyflow.common.constant.enums.EnumJobStatus;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.job.entity.SysJob;
|
||||
import tech.easyflow.job.job.JobConstant;
|
||||
import tech.easyflow.job.support.SysJobWorkflowReferenceSupport;
|
||||
import tech.easyflow.system.entity.SysAccount;
|
||||
import tech.easyflow.system.service.SysAccountService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 工作流定时任务执行服务。
|
||||
*
|
||||
* <p>每次触发都重新加载任务、账号和工作流,并按服务端记录恢复执行主体及重新授权。</p>
|
||||
*/
|
||||
@Service
|
||||
public class WorkflowJobExecutionService {
|
||||
|
||||
/** 定时任务服务。 */
|
||||
private final SysJobService sysJobService;
|
||||
|
||||
/** 系统账号服务。 */
|
||||
private final SysAccountService sysAccountService;
|
||||
|
||||
/** 工作流使用权限校验服务。 */
|
||||
private final WorkflowUsageAuthorizationService workflowUsageAuthorizationService;
|
||||
|
||||
/** 工作流执行器。 */
|
||||
private final ChainExecutor chainExecutor;
|
||||
|
||||
/**
|
||||
* 创建工作流定时任务执行服务。
|
||||
*
|
||||
* @param sysJobService 定时任务服务
|
||||
* @param sysAccountService 系统账号服务
|
||||
* @param workflowUsageAuthorizationService 工作流使用权限校验服务
|
||||
* @param chainExecutor 工作流执行器
|
||||
*/
|
||||
public WorkflowJobExecutionService(
|
||||
SysJobService sysJobService,
|
||||
SysAccountService sysAccountService,
|
||||
WorkflowUsageAuthorizationService workflowUsageAuthorizationService,
|
||||
ChainExecutor chainExecutor) {
|
||||
this.sysJobService = sysJobService;
|
||||
this.sysAccountService = sysAccountService;
|
||||
this.workflowUsageAuthorizationService = workflowUsageAuthorizationService;
|
||||
this.chainExecutor = chainExecutor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用当前数据库状态执行工作流定时任务。
|
||||
*
|
||||
* @param scheduledJob Quartz 中保存的任务快照
|
||||
* @return 工作流执行结果
|
||||
* @throws IllegalStateException 任务、账号或租户状态非法时抛出
|
||||
*/
|
||||
public Object execute(SysJob scheduledJob) {
|
||||
if (scheduledJob == null || scheduledJob.getId() == null) {
|
||||
throw new IllegalStateException("定时任务不存在或缺少ID");
|
||||
}
|
||||
return TenantManager.withoutTenantCondition(
|
||||
() -> executeWithoutTenantCondition(
|
||||
scheduledJob.getId(),
|
||||
scheduledJob.getTenantId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 在已关闭 ORM 租户条件的作用域中执行任务,并显式完成租户边界校验。
|
||||
*
|
||||
* @param jobId 定时任务 ID
|
||||
* @param scheduledTenantId Quartz 任务快照中的租户 ID
|
||||
* @return 工作流执行结果
|
||||
* @throws IllegalStateException 任务、账号或租户状态非法时抛出
|
||||
*/
|
||||
private Object executeWithoutTenantCondition(
|
||||
BigInteger jobId,
|
||||
BigInteger scheduledTenantId) {
|
||||
SysJob job = sysJobService.getById(jobId);
|
||||
if (job == null) {
|
||||
throw new IllegalStateException("定时任务不存在或已删除,id=" + jobId);
|
||||
}
|
||||
if (scheduledTenantId == null
|
||||
|| job.getTenantId() == null
|
||||
|| !Objects.equals(scheduledTenantId, job.getTenantId())) {
|
||||
throw new IllegalStateException("定时任务租户信息不一致,id=" + jobId);
|
||||
}
|
||||
if (!Integer.valueOf(EnumJobStatus.RUNNING.getCode()).equals(job.getStatus())) {
|
||||
throw new IllegalStateException("定时任务未处于运行状态,id=" + jobId);
|
||||
}
|
||||
if (!SysJobWorkflowReferenceSupport.isWorkflowJob(job)) {
|
||||
throw new IllegalStateException("定时任务类型已变更,id=" + jobId);
|
||||
}
|
||||
|
||||
SysAccount account = requireAvailableOwner(job);
|
||||
LoginAccount loginAccount = new LoginAccount();
|
||||
BeanUtil.copyProperties(account, loginAccount);
|
||||
BigInteger workflowId = SysJobWorkflowReferenceSupport.requireWorkflowId(job);
|
||||
workflowUsageAuthorizationService.requireUsableWorkflow(
|
||||
workflowId,
|
||||
loginAccount,
|
||||
"定时任务关联的工作流不存在、已禁用或无权运行");
|
||||
|
||||
JSONObject workflowParams = resolveWorkflowParams(job.getJobParams());
|
||||
workflowParams.put(Constants.LOGIN_USER_KEY, loginAccount);
|
||||
return chainExecutor.execute(workflowId.toString(), workflowParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务创建账号并校验账号仍可用于执行任务。
|
||||
*
|
||||
* @param job 当前数据库中的定时任务
|
||||
* @return 可用的任务创建账号
|
||||
* @throws IllegalStateException 创建账号缺失、禁用或跨租户时抛出
|
||||
*/
|
||||
private SysAccount requireAvailableOwner(SysJob job) {
|
||||
BigInteger accountId = job.getCreatedBy();
|
||||
if (accountId == null) {
|
||||
throw new IllegalStateException("定时任务缺少服务端归属账号,id=" + job.getId());
|
||||
}
|
||||
SysAccount account = sysAccountService.getById(accountId);
|
||||
if (account == null) {
|
||||
throw new IllegalStateException("定时任务归属账号不存在,id=" + accountId);
|
||||
}
|
||||
if (!EnumDataStatus.AVAILABLE.getCode().equals(account.getStatus())) {
|
||||
throw new IllegalStateException("定时任务归属账号未启用,id=" + accountId);
|
||||
}
|
||||
if (!Objects.equals(job.getTenantId(), account.getTenantId())) {
|
||||
throw new IllegalStateException("定时任务与归属账号租户不一致,id=" + job.getId());
|
||||
}
|
||||
return account;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析工作流运行参数并返回可写对象。
|
||||
*
|
||||
* @param jobParams 定时任务参数
|
||||
* @return 工作流运行参数
|
||||
*/
|
||||
private JSONObject resolveWorkflowParams(Map<String, Object> jobParams) {
|
||||
if (jobParams == null) {
|
||||
return new JSONObject();
|
||||
}
|
||||
JSONObject params = new JSONObject(jobParams)
|
||||
.getJSONObject(JobConstant.WORKFLOW_PARAMS_KEY);
|
||||
return params == null ? new JSONObject() : new JSONObject(params);
|
||||
}
|
||||
}
|
||||
@@ -2,24 +2,15 @@ package tech.easyflow.job.util;
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.mybatisflex.core.tenant.TenantManager;
|
||||
import com.easyagents.flow.core.chain.ChainDefinition;
|
||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||
import org.quartz.JobKey;
|
||||
import org.quartz.TriggerKey;
|
||||
import tech.easyflow.common.constant.Constants;
|
||||
import tech.easyflow.common.constant.enums.EnumJobType;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.util.SpringContextUtil;
|
||||
import tech.easyflow.job.entity.SysJob;
|
||||
import tech.easyflow.job.job.JobConstant;
|
||||
import tech.easyflow.job.support.SysJobWorkflowReferenceSupport;
|
||||
import tech.easyflow.system.entity.SysAccount;
|
||||
import tech.easyflow.system.service.SysAccountService;
|
||||
import tech.easyflow.job.service.WorkflowJobExecutionService;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -68,34 +59,16 @@ public class JobUtil {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过任务模块的受控执行服务运行工作流。
|
||||
*
|
||||
* @param job Quartz 中保存的任务快照
|
||||
* @return 工作流执行结果
|
||||
*/
|
||||
public static Object execWorkFlow(SysJob job) {
|
||||
Map<String, Object> jobParams = job.getJobParams();
|
||||
JSONObject obj = new JSONObject(jobParams);
|
||||
BigInteger workflowId = SysJobWorkflowReferenceSupport.requireWorkflowId(job);
|
||||
JSONObject params = obj.getJSONObject(JobConstant.WORKFLOW_PARAMS_KEY);
|
||||
|
||||
ChainExecutor executor = SpringContextUtil.getBean(ChainExecutor.class);
|
||||
Object accountId = obj.get(JobConstant.ACCOUNT_ID);
|
||||
SysAccountService accountService = SpringContextUtil.getBean(SysAccountService.class);
|
||||
|
||||
try {
|
||||
TenantManager.ignoreTenantCondition();
|
||||
|
||||
ChainDefinition chain = executor.getDefinitionRepository().getChainDefinitionById(workflowId.toString());
|
||||
if (chain == null) {
|
||||
throw new IllegalStateException("定时任务关联的工作流不存在或已删除,id=" + workflowId);
|
||||
}
|
||||
if (accountId != null) {
|
||||
// 设置的归属者
|
||||
SysAccount account = accountService.getById(accountId.toString());
|
||||
if (account != null) {
|
||||
params.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
|
||||
}
|
||||
}
|
||||
return executor.execute(workflowId.toString(), params);
|
||||
} finally {
|
||||
TenantManager.restoreTenantCondition();
|
||||
}
|
||||
WorkflowJobExecutionService executionService =
|
||||
SpringContextUtil.getBean(WorkflowJobExecutionService.class);
|
||||
return executionService.execute(job);
|
||||
}
|
||||
|
||||
public static Object execute(SysJob job) {
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
package tech.easyflow.job.service;
|
||||
|
||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import tech.easyflow.ai.service.WorkflowUsageAuthorizationService;
|
||||
import tech.easyflow.common.constant.Constants;
|
||||
import tech.easyflow.common.constant.enums.EnumDataStatus;
|
||||
import tech.easyflow.common.constant.enums.EnumJobStatus;
|
||||
import tech.easyflow.common.constant.enums.EnumJobType;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.job.entity.SysJob;
|
||||
import tech.easyflow.job.job.JobConstant;
|
||||
import tech.easyflow.system.entity.SysAccount;
|
||||
import tech.easyflow.system.service.SysAccountService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyMap;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* {@link WorkflowJobExecutionService} 运行时授权测试。
|
||||
*/
|
||||
public class WorkflowJobExecutionServiceTest {
|
||||
|
||||
/**
|
||||
* 验证每次触发都会按数据库当前状态授权,并将任务创建账号注入工作流参数。
|
||||
*/
|
||||
@Test
|
||||
public void shouldReauthorizeAndRestoreServerControlledOwner() {
|
||||
BigInteger jobId = BigInteger.valueOf(101);
|
||||
BigInteger tenantId = BigInteger.valueOf(201);
|
||||
BigInteger accountId = BigInteger.valueOf(301);
|
||||
BigInteger workflowId = BigInteger.valueOf(401);
|
||||
SysJobService jobService = mock(SysJobService.class);
|
||||
SysAccountService accountService = mock(SysAccountService.class);
|
||||
WorkflowUsageAuthorizationService authorizationService =
|
||||
mock(WorkflowUsageAuthorizationService.class);
|
||||
ChainExecutor chainExecutor = mock(ChainExecutor.class);
|
||||
SysJob currentJob = workflowJob(jobId, tenantId, accountId, workflowId);
|
||||
SysAccount account = account(accountId, tenantId, EnumDataStatus.AVAILABLE.getCode());
|
||||
when(jobService.getById(jobId)).thenReturn(currentJob);
|
||||
when(accountService.getById(accountId)).thenReturn(account);
|
||||
Map<String, Object> executionResult = Map.of("status", "done");
|
||||
when(chainExecutor.execute(eq(workflowId.toString()), anyMap()))
|
||||
.thenReturn(executionResult);
|
||||
WorkflowJobExecutionService service = new WorkflowJobExecutionService(
|
||||
jobService,
|
||||
accountService,
|
||||
authorizationService,
|
||||
chainExecutor);
|
||||
|
||||
Object result = service.execute(scheduledJob(jobId, tenantId));
|
||||
|
||||
Assert.assertSame(executionResult, result);
|
||||
ArgumentCaptor<LoginAccount> accountCaptor =
|
||||
ArgumentCaptor.forClass(LoginAccount.class);
|
||||
verify(authorizationService).requireUsableWorkflow(
|
||||
eq(workflowId),
|
||||
accountCaptor.capture(),
|
||||
anyString());
|
||||
Assert.assertEquals(accountCaptor.getValue().getId(), accountId);
|
||||
Assert.assertEquals(accountCaptor.getValue().getTenantId(), tenantId);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<Map<String, Object>> paramsCaptor =
|
||||
ArgumentCaptor.forClass(Map.class);
|
||||
verify(chainExecutor).execute(eq(workflowId.toString()), paramsCaptor.capture());
|
||||
Object loginUser = paramsCaptor.getValue().get(Constants.LOGIN_USER_KEY);
|
||||
Assert.assertTrue(loginUser instanceof LoginAccount);
|
||||
Assert.assertEquals(((LoginAccount) loginUser).getId(), accountId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证权限已撤销时执行器不会启动工作流。
|
||||
*/
|
||||
@Test
|
||||
public void shouldRejectExecutionAfterPermissionRevoked() {
|
||||
BigInteger jobId = BigInteger.valueOf(102);
|
||||
BigInteger tenantId = BigInteger.valueOf(202);
|
||||
BigInteger accountId = BigInteger.valueOf(302);
|
||||
BigInteger workflowId = BigInteger.valueOf(402);
|
||||
SysJobService jobService = mock(SysJobService.class);
|
||||
SysAccountService accountService = mock(SysAccountService.class);
|
||||
WorkflowUsageAuthorizationService authorizationService =
|
||||
mock(WorkflowUsageAuthorizationService.class);
|
||||
ChainExecutor chainExecutor = mock(ChainExecutor.class);
|
||||
when(jobService.getById(jobId))
|
||||
.thenReturn(workflowJob(jobId, tenantId, accountId, workflowId));
|
||||
when(accountService.getById(accountId))
|
||||
.thenReturn(account(accountId, tenantId, EnumDataStatus.AVAILABLE.getCode()));
|
||||
when(authorizationService.requireUsableWorkflow(
|
||||
eq(workflowId),
|
||||
any(LoginAccount.class),
|
||||
anyString()))
|
||||
.thenThrow(new BusinessException("工作流权限已撤销"));
|
||||
WorkflowJobExecutionService service = new WorkflowJobExecutionService(
|
||||
jobService,
|
||||
accountService,
|
||||
authorizationService,
|
||||
chainExecutor);
|
||||
|
||||
BusinessException exception = Assert.assertThrows(
|
||||
BusinessException.class,
|
||||
() -> service.execute(scheduledJob(jobId, tenantId))
|
||||
);
|
||||
|
||||
Assert.assertTrue(exception.getMessage().contains("权限已撤销"));
|
||||
verify(chainExecutor, never()).execute(anyString(), anyMap());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证任务快照与数据库租户不一致时执行失败。
|
||||
*/
|
||||
@Test
|
||||
public void shouldRejectCrossTenantJobSnapshot() {
|
||||
BigInteger jobId = BigInteger.valueOf(103);
|
||||
BigInteger tenantId = BigInteger.valueOf(203);
|
||||
SysJobService jobService = mock(SysJobService.class);
|
||||
SysAccountService accountService = mock(SysAccountService.class);
|
||||
WorkflowUsageAuthorizationService authorizationService =
|
||||
mock(WorkflowUsageAuthorizationService.class);
|
||||
ChainExecutor chainExecutor = mock(ChainExecutor.class);
|
||||
when(jobService.getById(jobId)).thenReturn(workflowJob(
|
||||
jobId,
|
||||
tenantId,
|
||||
BigInteger.valueOf(303),
|
||||
BigInteger.valueOf(403)));
|
||||
WorkflowJobExecutionService service = new WorkflowJobExecutionService(
|
||||
jobService,
|
||||
accountService,
|
||||
authorizationService,
|
||||
chainExecutor);
|
||||
|
||||
IllegalStateException exception = Assert.assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> service.execute(scheduledJob(jobId, BigInteger.valueOf(999)))
|
||||
);
|
||||
|
||||
Assert.assertTrue(exception.getMessage().contains("租户"));
|
||||
verify(authorizationService, never()).requireUsableWorkflow(
|
||||
any(BigInteger.class),
|
||||
any(LoginAccount.class),
|
||||
anyString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Quartz 任务快照。
|
||||
*
|
||||
* @param jobId 定时任务 ID
|
||||
* @param tenantId 租户 ID
|
||||
* @return 任务快照
|
||||
*/
|
||||
private SysJob scheduledJob(BigInteger jobId, BigInteger tenantId) {
|
||||
SysJob job = new SysJob();
|
||||
job.setId(jobId);
|
||||
job.setTenantId(tenantId);
|
||||
job.setJobType(EnumJobType.TINY_FLOW.getCode());
|
||||
return job;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建数据库中的工作流定时任务。
|
||||
*
|
||||
* @param jobId 定时任务 ID
|
||||
* @param tenantId 租户 ID
|
||||
* @param accountId 任务创建账号 ID
|
||||
* @param workflowId 工作流 ID
|
||||
* @return 工作流定时任务
|
||||
*/
|
||||
private SysJob workflowJob(
|
||||
BigInteger jobId,
|
||||
BigInteger tenantId,
|
||||
BigInteger accountId,
|
||||
BigInteger workflowId) {
|
||||
SysJob job = new SysJob();
|
||||
job.setId(jobId);
|
||||
job.setTenantId(tenantId);
|
||||
job.setCreatedBy(accountId);
|
||||
job.setStatus(EnumJobStatus.RUNNING.getCode());
|
||||
job.setJobType(EnumJobType.TINY_FLOW.getCode());
|
||||
job.setJobParams(Map.of(
|
||||
JobConstant.WORKFLOW_KEY, workflowId.toString(),
|
||||
JobConstant.WORKFLOW_PARAMS_KEY, Map.of("question", "hello")
|
||||
));
|
||||
return job;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建系统账号。
|
||||
*
|
||||
* @param accountId 账号 ID
|
||||
* @param tenantId 租户 ID
|
||||
* @param status 账号状态
|
||||
* @return 系统账号
|
||||
*/
|
||||
private SysAccount account(
|
||||
BigInteger accountId,
|
||||
BigInteger tenantId,
|
||||
Integer status) {
|
||||
SysAccount account = new SysAccount();
|
||||
account.setId(accountId);
|
||||
account.setTenantId(tenantId);
|
||||
account.setStatus(status);
|
||||
return account;
|
||||
}
|
||||
}
|
||||
@@ -2,52 +2,38 @@ package tech.easyflow.system.config;
|
||||
|
||||
import tech.easyflow.common.util.SpringContextUtil;
|
||||
import tech.easyflow.common.dict.DictManager;
|
||||
import tech.easyflow.common.dict.loader.DbDataLoader;
|
||||
import tech.easyflow.system.entity.SysDict;
|
||||
import tech.easyflow.system.mapper.*;
|
||||
import tech.easyflow.system.service.SysDictService;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.event.EventListener;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 注册由字典表维护的静态业务字典。
|
||||
*/
|
||||
@Configuration
|
||||
public class SysDictAutoConfig {
|
||||
|
||||
private SysDictService service;
|
||||
|
||||
@Resource
|
||||
private SysMenuMapper sysMenuMapper;
|
||||
@Resource
|
||||
private SysDeptMapper sysDeptMapper;
|
||||
@Resource
|
||||
private SysRoleMapper sysRoleMapper;
|
||||
@Resource
|
||||
private SysPositionMapper sysPositionMapper;
|
||||
@Resource
|
||||
private SysAccountMapper sysAccountMapper;
|
||||
private final SysDictService service;
|
||||
|
||||
/**
|
||||
* 创建系统字典自动配置。
|
||||
*
|
||||
* @param service 系统字典服务
|
||||
*/
|
||||
public SysDictAutoConfig(SysDictService service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用启动完成后注册静态字典。
|
||||
*/
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void onApplicationStartup() {
|
||||
|
||||
DictManager dictManager = SpringContextUtil.getBean(DictManager.class);
|
||||
// 菜单表字典
|
||||
dictManager.putLoader(new DbDataLoader<>("sysMenu", sysMenuMapper, "id", "menu_title", "parent_id", "sort_no asc", false));
|
||||
// 部门表字典
|
||||
dictManager.putLoader(new DbDataLoader<>("sysDept", sysDeptMapper, "id", "dept_name", "parent_id", "sort_no asc", false));
|
||||
// 角色表字典
|
||||
dictManager.putLoader(new DbDataLoader<>("sysRole", sysRoleMapper, "id", "role_name", null, null, true));
|
||||
// 职位字典
|
||||
dictManager.putLoader(new DbDataLoader<>("sysPosition", sysPositionMapper, "id", "position_name", null, null, true));
|
||||
// 用户字典
|
||||
dictManager.putLoader(new DbDataLoader<>("sysAccount", sysAccountMapper, "id", "login_name", null, null, true));
|
||||
|
||||
List<SysDict> sysDicts = service.list();
|
||||
if (sysDicts != null) {
|
||||
sysDicts.forEach(sysDict -> dictManager.putLoader(sysDict.buildLoader()));
|
||||
|
||||
Reference in New Issue
Block a user