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

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

View File

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

View File

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

View File

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

View File

@@ -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",