feat: 增加分类权限控制

- 新增角色分类授权模型与超级管理员配置接口

- 接入助手、插件、工作流、知识库、素材的分类可见性过滤

- 增加角色页分类权限树与插件多分类可见性支持
This commit is contained in:
2026-03-29 17:16:37 +08:00
parent aaf4c61ff8
commit f49d94e2fe
46 changed files with 1963 additions and 128 deletions

View File

@@ -1,11 +1,20 @@
package tech.easyflow.usercenter.controller.ai;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.GetMapping;
import tech.easyflow.ai.entity.BotCategory;
import tech.easyflow.ai.service.BotCategoryService;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
import tech.easyflow.system.service.CategoryPermissionService;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.List;
/**
* bot分类 控制层。
@@ -17,7 +26,24 @@ import tech.easyflow.common.web.controller.BaseCurdController;
@RequestMapping("/userCenter/botCategory")
@UsePermission(moduleName = "/api/v1/bot")
public class UcBotCategoryController extends BaseCurdController<BotCategoryService, BotCategory> {
@Resource
private CategoryPermissionService categoryPermissionService;
public UcBotCategoryController(BotCategoryService service) {
super(service);
}
}
@GetMapping("visibleList")
public Result<List<BotCategory>> visibleList(BotCategory entity, Boolean asTree, String sortKey, String sortType) {
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("BOT");
if (access.isRestricted()) {
if (access.getCategoryIds().isEmpty()) {
return Result.ok(Collections.emptyList());
}
queryWrapper.in(BotCategory::getId, access.getCategoryIds());
}
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
return Result.ok(service.list(queryWrapper));
}
}

View File

@@ -3,7 +3,9 @@ package tech.easyflow.usercenter.controller.ai;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaIgnore;
import cn.dev33.satoken.stp.StpUtil;
import com.alicp.jetcache.Cache;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.keygen.impl.SnowFlakeIDKeyGenerator;
import com.mybatisflex.core.query.QueryWrapper;
import org.slf4j.Logger;
@@ -25,6 +27,8 @@ 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.system.entity.vo.RoleCategoryAccessSnapshot;
import tech.easyflow.system.service.CategoryPermissionService;
import javax.annotation.Resource;
import java.io.Serializable;
@@ -34,6 +38,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static tech.easyflow.ai.entity.table.BotTableDef.BOT;
/**
* 控制层。
*
@@ -70,6 +76,8 @@ public class UcBotController extends BaseCurdController<BotService, Bot> {
private BotPluginService botPluginService;
@Resource
private BotConversationService conversationMessageService;
@Resource
private CategoryPermissionService categoryPermissionService;
@GetMapping("/generateConversationId")
public Result<Long> generateConversationId() {
@@ -188,7 +196,11 @@ public class UcBotController extends BaseCurdController<BotService, Bot> {
@GetMapping("getDetail")
@SaIgnore
public Result<Bot> getDetail(String id) {
return Result.ok(botService.getDetail(id));
Bot bot = botService.getDetail(id);
if (bot != null && StpUtil.isLogin()) {
categoryPermissionService.assertCategoryResourceVisible("BOT", bot.getCreatedBy(), bot.getCategoryId(), "无权限访问聊天助手");
}
return Result.ok(bot);
}
@Override
@@ -198,6 +210,9 @@ public class UcBotController extends BaseCurdController<BotService, Bot> {
if (data == null) {
return Result.ok(data);
}
if (StpUtil.isLogin()) {
categoryPermissionService.assertCategoryResourceVisible("BOT", data.getCreatedBy(), data.getCategoryId(), "无权限访问聊天助手");
}
Map<String, Object> llmOptions = data.getModelOptions();
if (llmOptions == null) {
@@ -229,6 +244,32 @@ public class UcBotController extends BaseCurdController<BotService, Bot> {
return Result.ok(data);
}
@Override
public Result<List<Bot>> list(Bot entity, Boolean asTree, String sortKey, String sortType) {
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
applyCategoryPermission(queryWrapper);
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
return Result.ok(service.list(queryWrapper));
}
@Override
protected Page<Bot> queryPage(Page<Bot> page, QueryWrapper queryWrapper) {
applyCategoryPermission(queryWrapper);
return super.queryPage(page, queryWrapper);
}
private void applyCategoryPermission(QueryWrapper queryWrapper) {
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("BOT");
if (!access.isRestricted()) {
return;
}
if (access.getCategoryIds().isEmpty()) {
queryWrapper.eq(Bot::getCreatedBy, access.getAccountId());
return;
}
queryWrapper.and(BOT.CREATED_BY.eq(access.getAccountId()).or(BOT.CATEGORY_ID.in(access.getCategoryIds())));
}
@Override
protected Result<?> onSaveOrUpdateBefore(Bot entity, boolean isSave) {

View File

@@ -13,10 +13,15 @@ import tech.easyflow.common.domain.Result;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
import tech.easyflow.system.service.CategoryPermissionService;
import java.io.ByteArrayInputStream;
import java.math.BigInteger;
import java.util.Date;
import java.util.List;
import static tech.easyflow.ai.entity.table.ResourceTableDef.RESOURCE;
/**
* 素材库
@@ -28,6 +33,9 @@ import java.util.Date;
@RequestMapping("/userCenter/resource")
@UsePermission(moduleName = "/api/v1/resource")
public class UcResourceController extends BaseCurdController<ResourceService, Resource> {
@javax.annotation.Resource
private CategoryPermissionService categoryPermissionService;
public UcResourceController(ResourceService service) {
super(service);
}
@@ -52,7 +60,36 @@ public class UcResourceController extends BaseCurdController<ResourceService, Re
@Override
protected Page<Resource> queryPage(Page<Resource> page, QueryWrapper queryWrapper) {
queryWrapper.eq(Resource::getCreatedBy, SaTokenUtil.getLoginAccount().getId().toString());
applyCategoryPermission(queryWrapper);
return super.queryPage(page, queryWrapper);
}
}
@Override
public Result<List<Resource>> list(Resource entity, Boolean asTree, String sortKey, String sortType) {
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
applyCategoryPermission(queryWrapper);
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
return Result.ok(service.list(queryWrapper));
}
@Override
public Result<Resource> detail(String id) {
Resource resource = service.getById(id);
if (resource != null) {
categoryPermissionService.assertCategoryResourceVisible("RESOURCE", resource.getCreatedBy(), resource.getCategoryId(), "无权限访问素材");
}
return Result.ok(resource);
}
private void applyCategoryPermission(QueryWrapper queryWrapper) {
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("RESOURCE");
if (!access.isRestricted()) {
return;
}
if (access.getCategoryIds().isEmpty()) {
queryWrapper.eq(Resource::getCreatedBy, access.getAccountId());
return;
}
queryWrapper.and(RESOURCE.CREATED_BY.eq(access.getAccountId()).or(RESOURCE.CATEGORY_ID.in(access.getCategoryIds())));
}
}

View File

@@ -1,15 +1,22 @@
package tech.easyflow.usercenter.controller.ai;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.GetMapping;
import tech.easyflow.ai.entity.WorkflowCategory;
import tech.easyflow.ai.service.WorkflowCategoryService;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
import tech.easyflow.system.service.CategoryPermissionService;
import javax.annotation.Resource;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* 工作流分类
@@ -21,6 +28,8 @@ import java.util.Collection;
@RequestMapping("/userCenter/workflowCategory")
@UsePermission(moduleName = "/api/v1/workflow")
public class UcWorkflowCategoryController extends BaseCurdController<WorkflowCategoryService, WorkflowCategory> {
@Resource
private CategoryPermissionService categoryPermissionService;
public UcWorkflowCategoryController(WorkflowCategoryService service) {
super(service);
@@ -35,4 +44,18 @@ public class UcWorkflowCategoryController extends BaseCurdController<WorkflowCat
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
return Result.fail("-");
}
}
@GetMapping("visibleList")
public Result<List<WorkflowCategory>> visibleList(WorkflowCategory entity, Boolean asTree, String sortKey, String sortType) {
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("WORKFLOW");
if (access.isRestricted()) {
if (access.getCategoryIds().isEmpty()) {
return Result.ok(Collections.emptyList());
}
queryWrapper.in(WorkflowCategory::getId, access.getCategoryIds());
}
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
return Result.ok(service.list(queryWrapper));
}
}

View File

@@ -2,11 +2,14 @@ package tech.easyflow.usercenter.controller.ai;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.stp.StpUtil;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import com.easyagents.flow.core.chain.ChainDefinition;
import com.easyagents.flow.core.chain.Parameter;
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
import com.easyagents.flow.core.parser.ChainParser;
import org.springframework.web.bind.annotation.*;
import tech.easyflow.ai.permission.WorkflowVisibilityQueryHelper;
import tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
@@ -20,6 +23,10 @@ import tech.easyflow.common.domain.Result;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.jsonbody.JsonBody;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceAction;
import tech.easyflow.system.enums.ResourceLookup;
import tech.easyflow.system.permission.resource.RequireResourceAccess;
import javax.annotation.Resource;
import java.io.Serializable;
@@ -45,6 +52,8 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
private TinyFlowService tinyFlowService;
@Resource
private WorkflowCheckService workflowCheckService;
@Resource
private WorkflowVisibilityQueryHelper workflowVisibilityQueryHelper;
public UcWorkflowController(WorkflowService service) {
super(service);
@@ -55,6 +64,13 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
*/
@PostMapping("/singleRun")
@SaCheckPermission("/api/v1/workflow/save")
@RequireResourceAccess(
resource = CategoryResourceType.WORKFLOW,
action = ResourceAction.USE,
lookup = ResourceLookup.WORKFLOW_ID,
idExpr = "#workflowId",
denyMessage = "无权限运行工作流"
)
public Result<?> singleRun(
@JsonBody(value = "workflowId", required = true) BigInteger workflowId,
@JsonBody(value = "nodeId", required = true) String nodeId,
@@ -73,6 +89,13 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
*/
@PostMapping("/runAsync")
@SaCheckPermission("/api/v1/workflow/save")
@RequireResourceAccess(
resource = CategoryResourceType.WORKFLOW,
action = ResourceAction.USE,
lookup = ResourceLookup.WORKFLOW_ID,
idExpr = "#id",
denyMessage = "无权限运行工作流"
)
public Result<String> runAsync(@JsonBody(value = "id", required = true) BigInteger id,
@JsonBody("variables") Map<String, Object> variables) {
if (variables == null) {
@@ -94,6 +117,13 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
* 获取工作流运行状态 - v2
*/
@PostMapping("/getChainStatus")
@RequireResourceAccess(
resource = CategoryResourceType.WORKFLOW,
action = ResourceAction.USE,
lookup = ResourceLookup.EXEC_KEY,
idExpr = "#executeId",
denyMessage = "无权限访问该执行记录"
)
public Result<ChainInfo> getChainStatus(@JsonBody(value = "executeId") String executeId,
@JsonBody("nodes") List<NodeInfo> nodes) {
ChainInfo res = tinyFlowService.getChainStatus(executeId, nodes);
@@ -105,6 +135,13 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
*/
@PostMapping("/resume")
@SaCheckPermission("/api/v1/workflow/save")
@RequireResourceAccess(
resource = CategoryResourceType.WORKFLOW,
action = ResourceAction.USE,
lookup = ResourceLookup.EXEC_KEY,
idExpr = "#executeId",
denyMessage = "无权限恢复工作流执行"
)
public Result<Void> resume(@JsonBody(value = "executeId", required = true) String executeId,
@JsonBody("confirmParams") Map<String, Object> confirmParams) {
chainExecutor.resumeAsync(executeId, confirmParams);
@@ -116,6 +153,13 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
*/
@GetMapping("getRunningParameters")
@SaCheckPermission("/api/v1/workflow/query")
@RequireResourceAccess(
resource = CategoryResourceType.WORKFLOW,
action = ResourceAction.READ,
lookup = ResourceLookup.WORKFLOW_ID,
idExpr = "#id",
denyMessage = "无权限访问工作流"
)
public Result<?> getRunningParameters(@RequestParam BigInteger id) {
Workflow workflow = service.getById(id);
@@ -146,4 +190,32 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
return Result.fail("-");
}
@Override
public Result<List<Workflow>> list(Workflow entity, Boolean asTree, String sortKey, String sortType) {
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper);
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
return Result.ok(service.list(queryWrapper));
}
@Override
protected Page<Workflow> queryPage(Page<Workflow> page, QueryWrapper queryWrapper) {
workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper);
return super.queryPage(page, queryWrapper);
}
@Override
@GetMapping("detail")
@RequireResourceAccess(
resource = CategoryResourceType.WORKFLOW,
action = ResourceAction.READ,
lookup = ResourceLookup.WORKFLOW_ID,
idExpr = "#id",
denyMessage = "无权限访问工作流"
)
public Result<Workflow> detail(String id) {
Workflow workflow = service.getDetail(id);
return Result.ok(workflow);
}
}