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

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

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

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

View File

@@ -0,0 +1,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);
}
}

View File

@@ -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) {

View File

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