feat: 增加工作流和知识库三级权限

- 抽取统一资源访问骨架与部门可见范围判断

- 接入工作流和知识库的 READ/MANAGE 权限校验

- 增加可见范围配置与只读态前端交互
This commit is contained in:
2026-03-29 17:25:55 +08:00
parent f49d94e2fe
commit 22ceabff96
58 changed files with 3053 additions and 85 deletions

View File

@@ -20,6 +20,7 @@ import tech.easyflow.ai.entity.base.DocumentCollectionBase;
import tech.easyflow.common.util.PropertiesUtil;
import tech.easyflow.common.util.StringUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.permission.resource.VisibilityResource;
import java.math.BigDecimal;
import java.util.Map;
@@ -32,7 +33,7 @@ import java.util.Map;
*/
@Table("tb_document_collection")
public class DocumentCollection extends DocumentCollectionBase {
public class DocumentCollection extends DocumentCollectionBase implements VisibilityResource {
public static final String TYPE_DOCUMENT = "DOCUMENT";
public static final String TYPE_FAQ = "FAQ";
@@ -71,6 +72,10 @@ public class DocumentCollection extends DocumentCollectionBase {
* 是否启用重排模型
*/
public static final String KEY_RERANK_ENABLE = "rerankEnable";
public static final String KEY_SPLITTER_DEFAULT_STRATEGY = "splitter.defaultStrategyCode";
public static final String KEY_SPLITTER_AUTO_RECOMMEND_ENABLED = "splitter.autoRecommendEnabled";
public static final String KEY_SPLITTER_FALLBACK_STRATEGY = "splitter.fallbackStrategyCode";
public static final String KEY_SPLITTER_STRATEGY_PROFILES = "splitter.strategyProfiles";
public DocumentStore toDocumentStore() {
String storeType = this.getVectorStoreType();

View File

@@ -4,6 +4,7 @@ import com.easyagents.core.model.chat.tool.Tool;
import com.mybatisflex.annotation.Table;
import tech.easyflow.ai.easyagents.tool.WorkflowTool;
import tech.easyflow.ai.entity.base.WorkflowBase;
import tech.easyflow.system.permission.resource.VisibilityResource;
/**
* 实体类。
@@ -13,7 +14,7 @@ import tech.easyflow.ai.entity.base.WorkflowBase;
*/
@Table("tb_workflow")
public class Workflow extends WorkflowBase {
public class Workflow extends WorkflowBase implements VisibilityResource {
public Tool toFunction(boolean needEnglishName) {
return new WorkflowTool(this, needEnglishName);

View File

@@ -3,8 +3,10 @@ package tech.easyflow.ai.entity.base;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.KeyType;
import com.mybatisflex.core.handler.FastjsonTypeHandler;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Map;
public class DocumentChunkBase implements Serializable {
@@ -38,6 +40,12 @@ public class DocumentChunkBase implements Serializable {
@Column(comment = "分割顺序")
private Integer sorting;
/**
* 扩展元信息
*/
@Column(typeHandler = FastjsonTypeHandler.class, comment = "扩展元信息")
private Map<String, Object> options;
public BigInteger getId() {
return id;
}
@@ -78,4 +86,12 @@ public class DocumentChunkBase implements Serializable {
this.sorting = sorting;
}
public Map<String, Object> getOptions() {
return options;
}
public void setOptions(Map<String, Object> options) {
this.options = options;
}
}

View File

@@ -160,6 +160,12 @@ public class DocumentCollectionBase extends DateEntity implements Serializable {
@Column(comment = "分类ID")
private BigInteger categoryId;
/**
* 可见范围
*/
@Column(comment = "可见范围")
private String visibilityScope;
public BigInteger getId() {
return id;
}
@@ -352,4 +358,12 @@ public class DocumentCollectionBase extends DateEntity implements Serializable {
this.categoryId = categoryId;
}
public String getVisibilityScope() {
return visibilityScope;
}
public void setVisibilityScope(String visibilityScope) {
this.visibilityScope = visibilityScope;
}
}

View File

@@ -103,6 +103,12 @@ public class WorkflowBase extends DateEntity implements Serializable {
@Column(comment = "分类ID")
private BigInteger categoryId;
/**
* 可见范围
*/
@Column(comment = "可见范围")
private String visibilityScope;
public BigInteger getId() {
return id;
}
@@ -223,4 +229,12 @@ public class WorkflowBase extends DateEntity implements Serializable {
this.categoryId = categoryId;
}
public String getVisibilityScope() {
return visibilityScope;
}
public void setVisibilityScope(String visibilityScope) {
this.visibilityScope = visibilityScope;
}
}

View File

@@ -0,0 +1,45 @@
package tech.easyflow.ai.permission;
import org.springframework.stereotype.Component;
import tech.easyflow.ai.entity.DocumentChunk;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.service.DocumentChunkService;
import tech.easyflow.ai.service.DocumentCollectionService;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceLookup;
import tech.easyflow.system.permission.resource.ResolvedResourceAccess;
import tech.easyflow.system.permission.resource.ResourceAccessResolver;
import javax.annotation.Resource;
@Component
public class DocumentChunkIdKnowledgeResourceAccessResolver implements ResourceAccessResolver {
@Resource
private DocumentChunkService documentChunkService;
@Resource
private DocumentCollectionService documentCollectionService;
@Override
public boolean supports(CategoryResourceType resourceType, ResourceLookup lookup) {
return CategoryResourceType.KNOWLEDGE == resourceType && ResourceLookup.DOCUMENT_CHUNK_ID == lookup;
}
@Override
public ResolvedResourceAccess resolve(Object identifier) {
if (identifier == null) {
throw new BusinessException("文档分段不存在");
}
DocumentChunk documentChunk = documentChunkService.getById(String.valueOf(identifier));
if (documentChunk == null) {
throw new BusinessException("文档分段不存在");
}
DocumentCollection collection = documentCollectionService.getById(documentChunk.getDocumentCollectionId());
if (collection == null) {
throw new BusinessException("知识库不存在");
}
return new ResolvedResourceAccess(collection, null);
}
}

View File

@@ -0,0 +1,45 @@
package tech.easyflow.ai.permission;
import org.springframework.stereotype.Component;
import tech.easyflow.ai.entity.Document;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.service.DocumentCollectionService;
import tech.easyflow.ai.service.DocumentService;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceLookup;
import tech.easyflow.system.permission.resource.ResolvedResourceAccess;
import tech.easyflow.system.permission.resource.ResourceAccessResolver;
import javax.annotation.Resource;
@Component
public class DocumentIdKnowledgeResourceAccessResolver implements ResourceAccessResolver {
@Resource
private DocumentService documentService;
@Resource
private DocumentCollectionService documentCollectionService;
@Override
public boolean supports(CategoryResourceType resourceType, ResourceLookup lookup) {
return CategoryResourceType.KNOWLEDGE == resourceType && ResourceLookup.DOCUMENT_ID == lookup;
}
@Override
public ResolvedResourceAccess resolve(Object identifier) {
if (identifier == null) {
throw new BusinessException("文档不存在");
}
Document document = documentService.getById(String.valueOf(identifier));
if (document == null) {
throw new BusinessException("文档不存在");
}
DocumentCollection collection = documentCollectionService.getById(document.getCollectionId());
if (collection == null) {
throw new BusinessException("知识库不存在");
}
return new ResolvedResourceAccess(collection, null);
}
}

View File

@@ -0,0 +1,45 @@
package tech.easyflow.ai.permission;
import org.springframework.stereotype.Component;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.entity.FaqCategory;
import tech.easyflow.ai.service.DocumentCollectionService;
import tech.easyflow.ai.service.FaqCategoryService;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceLookup;
import tech.easyflow.system.permission.resource.ResolvedResourceAccess;
import tech.easyflow.system.permission.resource.ResourceAccessResolver;
import javax.annotation.Resource;
@Component
public class FaqCategoryIdKnowledgeResourceAccessResolver implements ResourceAccessResolver {
@Resource
private FaqCategoryService faqCategoryService;
@Resource
private DocumentCollectionService documentCollectionService;
@Override
public boolean supports(CategoryResourceType resourceType, ResourceLookup lookup) {
return CategoryResourceType.KNOWLEDGE == resourceType && ResourceLookup.FAQ_CATEGORY_ID == lookup;
}
@Override
public ResolvedResourceAccess resolve(Object identifier) {
if (identifier == null) {
throw new BusinessException("FAQ分类不存在");
}
FaqCategory faqCategory = faqCategoryService.getById(String.valueOf(identifier));
if (faqCategory == null) {
throw new BusinessException("FAQ分类不存在");
}
DocumentCollection collection = documentCollectionService.getById(faqCategory.getCollectionId());
if (collection == null) {
throw new BusinessException("知识库不存在");
}
return new ResolvedResourceAccess(collection, null);
}
}

View File

@@ -0,0 +1,45 @@
package tech.easyflow.ai.permission;
import org.springframework.stereotype.Component;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.entity.FaqItem;
import tech.easyflow.ai.service.DocumentCollectionService;
import tech.easyflow.ai.service.FaqItemService;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceLookup;
import tech.easyflow.system.permission.resource.ResolvedResourceAccess;
import tech.easyflow.system.permission.resource.ResourceAccessResolver;
import javax.annotation.Resource;
@Component
public class FaqItemIdKnowledgeResourceAccessResolver implements ResourceAccessResolver {
@Resource
private FaqItemService faqItemService;
@Resource
private DocumentCollectionService documentCollectionService;
@Override
public boolean supports(CategoryResourceType resourceType, ResourceLookup lookup) {
return CategoryResourceType.KNOWLEDGE == resourceType && ResourceLookup.FAQ_ITEM_ID == lookup;
}
@Override
public ResolvedResourceAccess resolve(Object identifier) {
if (identifier == null) {
throw new BusinessException("FAQ不存在");
}
FaqItem faqItem = faqItemService.getById(String.valueOf(identifier));
if (faqItem == null) {
throw new BusinessException("FAQ不存在");
}
DocumentCollection collection = documentCollectionService.getById(faqItem.getCollectionId());
if (collection == null) {
throw new BusinessException("知识库不存在");
}
return new ResolvedResourceAccess(collection, null);
}
}

View File

@@ -0,0 +1,36 @@
package tech.easyflow.ai.permission;
import org.springframework.stereotype.Component;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.service.DocumentCollectionService;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceLookup;
import tech.easyflow.system.permission.resource.ResolvedResourceAccess;
import tech.easyflow.system.permission.resource.ResourceAccessResolver;
import javax.annotation.Resource;
@Component
public class KnowledgeIdOrSlugResourceAccessResolver implements ResourceAccessResolver {
@Resource
private DocumentCollectionService documentCollectionService;
@Override
public boolean supports(CategoryResourceType resourceType, ResourceLookup lookup) {
return CategoryResourceType.KNOWLEDGE == resourceType && ResourceLookup.KNOWLEDGE_ID_OR_SLUG == lookup;
}
@Override
public ResolvedResourceAccess resolve(Object identifier) {
if (identifier == null) {
throw new BusinessException("知识库不存在");
}
DocumentCollection collection = documentCollectionService.getDetail(String.valueOf(identifier));
if (collection == null) {
throw new BusinessException("知识库不存在");
}
return new ResolvedResourceAccess(collection, null);
}
}

View File

@@ -0,0 +1,36 @@
package tech.easyflow.ai.permission;
import org.springframework.stereotype.Component;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.service.DocumentCollectionService;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceLookup;
import tech.easyflow.system.permission.resource.ResolvedResourceAccess;
import tech.easyflow.system.permission.resource.ResourceAccessResolver;
import javax.annotation.Resource;
@Component
public class KnowledgeIdResourceAccessResolver implements ResourceAccessResolver {
@Resource
private DocumentCollectionService documentCollectionService;
@Override
public boolean supports(CategoryResourceType resourceType, ResourceLookup lookup) {
return CategoryResourceType.KNOWLEDGE == resourceType && ResourceLookup.KNOWLEDGE_ID == lookup;
}
@Override
public ResolvedResourceAccess resolve(Object identifier) {
if (identifier == null) {
throw new BusinessException("知识库不存在");
}
DocumentCollection collection = documentCollectionService.getById(String.valueOf(identifier));
if (collection == null) {
throw new BusinessException("知识库不存在");
}
return new ResolvedResourceAccess(collection, null);
}
}

View File

@@ -0,0 +1,41 @@
package tech.easyflow.ai.permission;
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
import java.math.BigInteger;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
public class KnowledgeReadAccessSnapshot {
private final RoleCategoryAccessSnapshot categoryAccess;
private final Set<BigInteger> readableDeptIds;
public KnowledgeReadAccessSnapshot(RoleCategoryAccessSnapshot categoryAccess, Set<BigInteger> readableDeptIds) {
this.categoryAccess = categoryAccess;
this.readableDeptIds = readableDeptIds == null
? Collections.emptySet()
: Collections.unmodifiableSet(new LinkedHashSet<>(readableDeptIds));
}
public BigInteger getAccountId() {
return categoryAccess == null ? null : categoryAccess.getAccountId();
}
public boolean isSuperAdmin() {
return categoryAccess != null && categoryAccess.isSuperAdmin();
}
public boolean isRestricted() {
return categoryAccess == null || categoryAccess.isRestricted();
}
public Set<BigInteger> getCategoryIds() {
return categoryAccess == null ? Collections.emptySet() : categoryAccess.getCategoryIds();
}
public Set<BigInteger> getReadableDeptIds() {
return readableDeptIds;
}
}

View File

@@ -0,0 +1,101 @@
package tech.easyflow.ai.permission;
import com.mybatisflex.core.query.QueryCondition;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.stereotype.Component;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.VisibilityScope;
import tech.easyflow.system.service.CategoryPermissionService;
import tech.easyflow.system.service.SysDeptService;
import javax.annotation.Resource;
import java.math.BigInteger;
import java.util.Collections;
import java.util.Set;
import static tech.easyflow.ai.entity.table.DocumentCollectionTableDef.DOCUMENT_COLLECTION;
@Component
public class KnowledgeVisibilityQueryHelper {
@Resource
private CategoryPermissionService categoryPermissionService;
@Resource
private SysDeptService sysDeptService;
public KnowledgeReadAccessSnapshot getCurrentReadSnapshot() {
RoleCategoryAccessSnapshot categoryAccess = categoryPermissionService.getCurrentAccess(CategoryResourceType.KNOWLEDGE.getCode());
if (categoryAccess.isSuperAdmin()) {
return new KnowledgeReadAccessSnapshot(categoryAccess, Collections.emptySet());
}
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
Set<BigInteger> deptIds = loginAccount == null
? Collections.emptySet()
: sysDeptService.getSelfAndAncestorDeptIds(loginAccount.getDeptId());
return new KnowledgeReadAccessSnapshot(categoryAccess, deptIds);
}
public void applyReadableAccess(QueryWrapper queryWrapper) {
applyReadableAccess(queryWrapper, getCurrentReadSnapshot());
}
public void applyReadableAccess(QueryWrapper queryWrapper, KnowledgeReadAccessSnapshot snapshot) {
if (snapshot.isSuperAdmin()) {
return;
}
BigInteger accountId = snapshot.getAccountId();
if (accountId == null) {
queryWrapper.eq("id", BigInteger.valueOf(-1));
return;
}
QueryCondition ownerCondition = DOCUMENT_COLLECTION.CREATED_BY.eq(accountId);
if (snapshot.isRestricted() && snapshot.getCategoryIds().isEmpty()) {
queryWrapper.and(ownerCondition);
return;
}
QueryCondition visibilityCondition = DOCUMENT_COLLECTION.VISIBILITY_SCOPE.eq(VisibilityScope.PUBLIC.name());
if (!snapshot.getReadableDeptIds().isEmpty()) {
visibilityCondition = visibilityCondition.or(
DOCUMENT_COLLECTION.VISIBILITY_SCOPE.eq(VisibilityScope.DEPT.name())
.and(DOCUMENT_COLLECTION.DEPT_ID.in(snapshot.getReadableDeptIds()))
);
}
QueryCondition readableCondition = visibilityCondition;
if (snapshot.isRestricted()) {
readableCondition = DOCUMENT_COLLECTION.CATEGORY_ID.in(snapshot.getCategoryIds()).and(visibilityCondition);
}
queryWrapper.and(ownerCondition.or(readableCondition));
}
public boolean canRead(DocumentCollection collection) {
return canRead(collection, getCurrentReadSnapshot());
}
public boolean canRead(DocumentCollection collection, KnowledgeReadAccessSnapshot snapshot) {
if (collection == null) {
return false;
}
if (snapshot.isSuperAdmin()) {
return true;
}
BigInteger accountId = snapshot.getAccountId();
if (accountId != null && accountId.equals(collection.getCreatedBy())) {
return true;
}
if (snapshot.isRestricted() && (collection.getCategoryId() == null || !snapshot.getCategoryIds().contains(collection.getCategoryId()))) {
return false;
}
VisibilityScope scope = VisibilityScope.fromOrDefault(collection.getVisibilityScope(), VisibilityScope.PRIVATE);
if (VisibilityScope.PUBLIC == scope) {
return true;
}
return VisibilityScope.DEPT == scope
&& collection.getDeptId() != null
&& snapshot.getReadableDeptIds().contains(collection.getDeptId());
}
}

View File

@@ -0,0 +1,45 @@
package tech.easyflow.ai.permission;
import org.springframework.stereotype.Component;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.entity.WorkflowExecResult;
import tech.easyflow.ai.service.WorkflowExecResultService;
import tech.easyflow.ai.service.WorkflowService;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceLookup;
import tech.easyflow.system.permission.resource.ResolvedResourceAccess;
import tech.easyflow.system.permission.resource.ResourceAccessResolver;
import javax.annotation.Resource;
@Component
public class WorkflowExecKeyResourceAccessResolver implements ResourceAccessResolver {
@Resource
private WorkflowExecResultService workflowExecResultService;
@Resource
private WorkflowService workflowService;
@Override
public boolean supports(CategoryResourceType resourceType, ResourceLookup lookup) {
return CategoryResourceType.WORKFLOW == resourceType && ResourceLookup.EXEC_KEY == lookup;
}
@Override
public ResolvedResourceAccess resolve(Object identifier) {
if (identifier == null) {
throw new BusinessException("工作流执行记录不存在");
}
WorkflowExecResult execResult = workflowExecResultService.getByExecKey(String.valueOf(identifier));
if (execResult == null) {
throw new BusinessException("工作流执行记录不存在");
}
Workflow workflow = workflowService.getDetail(String.valueOf(execResult.getWorkflowId()));
if (workflow == null) {
throw new BusinessException("工作流不存在");
}
return new ResolvedResourceAccess(workflow, execResult.getCreatedBy());
}
}

View File

@@ -0,0 +1,36 @@
package tech.easyflow.ai.permission;
import org.springframework.stereotype.Component;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.service.WorkflowService;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceLookup;
import tech.easyflow.system.permission.resource.ResolvedResourceAccess;
import tech.easyflow.system.permission.resource.ResourceAccessResolver;
import javax.annotation.Resource;
@Component
public class WorkflowIdResourceAccessResolver implements ResourceAccessResolver {
@Resource
private WorkflowService workflowService;
@Override
public boolean supports(CategoryResourceType resourceType, ResourceLookup lookup) {
return CategoryResourceType.WORKFLOW == resourceType && ResourceLookup.WORKFLOW_ID == lookup;
}
@Override
public ResolvedResourceAccess resolve(Object identifier) {
if (identifier == null) {
throw new BusinessException("工作流不存在");
}
Workflow workflow = workflowService.getDetail(String.valueOf(identifier));
if (workflow == null) {
throw new BusinessException("工作流不存在");
}
return new ResolvedResourceAccess(workflow, null);
}
}

View File

@@ -0,0 +1,41 @@
package tech.easyflow.ai.permission;
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
import java.math.BigInteger;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
public class WorkflowReadAccessSnapshot {
private final RoleCategoryAccessSnapshot categoryAccess;
private final Set<BigInteger> readableDeptIds;
public WorkflowReadAccessSnapshot(RoleCategoryAccessSnapshot categoryAccess, Set<BigInteger> readableDeptIds) {
this.categoryAccess = categoryAccess;
this.readableDeptIds = readableDeptIds == null
? Collections.emptySet()
: Collections.unmodifiableSet(new LinkedHashSet<>(readableDeptIds));
}
public BigInteger getAccountId() {
return categoryAccess == null ? null : categoryAccess.getAccountId();
}
public boolean isSuperAdmin() {
return categoryAccess != null && categoryAccess.isSuperAdmin();
}
public boolean isRestricted() {
return categoryAccess == null || categoryAccess.isRestricted();
}
public Set<BigInteger> getCategoryIds() {
return categoryAccess == null ? Collections.emptySet() : categoryAccess.getCategoryIds();
}
public Set<BigInteger> getReadableDeptIds() {
return readableDeptIds;
}
}

View File

@@ -0,0 +1,101 @@
package tech.easyflow.ai.permission;
import com.mybatisflex.core.query.QueryCondition;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.stereotype.Component;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.VisibilityScope;
import tech.easyflow.system.service.CategoryPermissionService;
import tech.easyflow.system.service.SysDeptService;
import javax.annotation.Resource;
import java.math.BigInteger;
import java.util.Collections;
import java.util.Set;
import static tech.easyflow.ai.entity.table.WorkflowTableDef.WORKFLOW;
@Component
public class WorkflowVisibilityQueryHelper {
@Resource
private CategoryPermissionService categoryPermissionService;
@Resource
private SysDeptService sysDeptService;
public WorkflowReadAccessSnapshot getCurrentReadSnapshot() {
RoleCategoryAccessSnapshot categoryAccess = categoryPermissionService.getCurrentAccess(CategoryResourceType.WORKFLOW.getCode());
if (categoryAccess.isSuperAdmin()) {
return new WorkflowReadAccessSnapshot(categoryAccess, Collections.emptySet());
}
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
Set<BigInteger> deptIds = loginAccount == null
? Collections.emptySet()
: sysDeptService.getSelfAndAncestorDeptIds(loginAccount.getDeptId());
return new WorkflowReadAccessSnapshot(categoryAccess, deptIds);
}
public void applyReadableAccess(QueryWrapper queryWrapper) {
applyReadableAccess(queryWrapper, getCurrentReadSnapshot());
}
public void applyReadableAccess(QueryWrapper queryWrapper, WorkflowReadAccessSnapshot snapshot) {
if (snapshot.isSuperAdmin()) {
return;
}
BigInteger accountId = snapshot.getAccountId();
if (accountId == null) {
queryWrapper.eq("id", BigInteger.valueOf(-1));
return;
}
QueryCondition ownerCondition = WORKFLOW.CREATED_BY.eq(accountId);
if (snapshot.isRestricted() && snapshot.getCategoryIds().isEmpty()) {
queryWrapper.and(ownerCondition);
return;
}
QueryCondition visibilityCondition = WORKFLOW.VISIBILITY_SCOPE.eq(VisibilityScope.PUBLIC.name());
if (!snapshot.getReadableDeptIds().isEmpty()) {
visibilityCondition = visibilityCondition.or(
WORKFLOW.VISIBILITY_SCOPE.eq(VisibilityScope.DEPT.name())
.and(WORKFLOW.DEPT_ID.in(snapshot.getReadableDeptIds()))
);
}
QueryCondition readableCondition = visibilityCondition;
if (snapshot.isRestricted()) {
readableCondition = WORKFLOW.CATEGORY_ID.in(snapshot.getCategoryIds()).and(visibilityCondition);
}
queryWrapper.and(ownerCondition.or(readableCondition));
}
public boolean canRead(Workflow workflow) {
return canRead(workflow, getCurrentReadSnapshot());
}
public boolean canRead(Workflow workflow, WorkflowReadAccessSnapshot snapshot) {
if (workflow == null) {
return false;
}
if (snapshot.isSuperAdmin()) {
return true;
}
BigInteger accountId = snapshot.getAccountId();
if (accountId != null && accountId.equals(workflow.getCreatedBy())) {
return true;
}
if (snapshot.isRestricted() && (workflow.getCategoryId() == null || !snapshot.getCategoryIds().contains(workflow.getCategoryId()))) {
return false;
}
VisibilityScope scope = VisibilityScope.fromOrDefault(workflow.getVisibilityScope(), VisibilityScope.PRIVATE);
if (VisibilityScope.PUBLIC == scope) {
return true;
}
return VisibilityScope.DEPT == scope
&& workflow.getDeptId() != null
&& snapshot.getReadableDeptIds().contains(workflow.getDeptId());
}
}

View File

@@ -31,6 +31,8 @@ public interface ModelService extends IService<Model> {
List<Model> listInvokeModels();
List<Model> listSelectableModels(Model entity, Boolean asTree, String sortKey, String sortType);
Model updateInvokeConfig(BigInteger id, String invokeCode, Boolean publishEnabled);
List<Model> batchUpdateInvokePublishStatus(List<BigInteger> ids, Boolean publishEnabled);

View File

@@ -10,6 +10,7 @@ import com.easyagents.core.model.embedding.EmbeddingModel;
import com.easyagents.core.model.rerank.RerankModel;
import com.easyagents.core.store.VectorData;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.core.util.StringUtil;
import com.mybatisflex.spring.service.impl.ServiceImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -22,6 +23,9 @@ import tech.easyflow.ai.entity.ModelProvider;
import tech.easyflow.ai.mapper.ModelMapper;
import tech.easyflow.ai.service.ModelProviderService;
import tech.easyflow.ai.service.ModelService;
import tech.easyflow.common.tree.Tree;
import tech.easyflow.common.util.SqlOperatorsUtil;
import tech.easyflow.common.util.SqlUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
import javax.annotation.Resource;
@@ -249,6 +253,18 @@ public class ModelServiceImpl extends ServiceImpl<ModelMapper, Model> implements
.collect(Collectors.toList());
}
@Override
public List<Model> listSelectableModels(Model entity, Boolean asTree, String sortKey, String sortType) {
QueryWrapper queryWrapper = QueryWrapper.create(
entity,
entity == null ? com.mybatisflex.core.query.SqlOperators.empty() : SqlOperatorsUtil.build(entity.getClass())
);
queryWrapper.orderBy(buildOrderBy(sortKey, sortType));
List<Model> list = Tree.tryToTree(modelMapper.selectListWithRelationsByQuery(queryWrapper), asTree);
list.forEach(this::decorateModelTitle);
return list;
}
@Override
public Model updateInvokeConfig(BigInteger id, String invokeCode, Boolean publishEnabled) {
Model existing = getModelInstance(id);
@@ -281,6 +297,21 @@ public class ModelServiceImpl extends ServiceImpl<ModelMapper, Model> implements
return updatedModels;
}
private void decorateModelTitle(Model model) {
if (model == null) {
return;
}
String providerName = Optional.ofNullable(model.getModelProvider())
.map(ModelProvider::getProviderName)
.orElse("-");
model.setTitle(providerName + "/" + model.getTitle());
}
private String buildOrderBy(String sortKey, String sortType) {
sortKey = StringUtil.camelToUnderline(sortKey);
return SqlUtil.buildOrderBy(sortKey, sortType, "id desc");
}
private String buildDefaultInvokeCode(String modelName) {
if (StrUtil.isBlank(modelName)) {
return null;

View File

@@ -0,0 +1,117 @@
package tech.easyflow.ai.permission;
import org.junit.Assert;
import org.junit.Test;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
import java.math.BigInteger;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
public class KnowledgeVisibilityQueryHelperTest {
private final KnowledgeVisibilityQueryHelper helper = new KnowledgeVisibilityQueryHelper();
@Test
public void canRead_shouldAllowCreatorForPrivateKnowledge() {
DocumentCollection collection = buildCollection(BigInteger.valueOf(11), BigInteger.valueOf(21), "PRIVATE");
KnowledgeReadAccessSnapshot snapshot = buildSnapshot(
BigInteger.valueOf(11),
false,
false,
Collections.emptySet(),
Collections.emptySet()
);
Assert.assertTrue(helper.canRead(collection, snapshot));
}
@Test
public void canRead_shouldRejectWhenCategoryNotMatched() {
DocumentCollection collection = buildCollection(BigInteger.valueOf(11), BigInteger.valueOf(21), "PUBLIC");
KnowledgeReadAccessSnapshot snapshot = buildSnapshot(
BigInteger.valueOf(12),
false,
false,
setOf(BigInteger.valueOf(99)),
Collections.emptySet()
);
Assert.assertFalse(helper.canRead(collection, snapshot));
}
@Test
public void canRead_shouldAllowDeptScopedKnowledgeForDescendantUser() {
DocumentCollection collection = buildCollection(BigInteger.valueOf(11), BigInteger.valueOf(21), "DEPT");
collection.setDeptId(BigInteger.valueOf(3));
KnowledgeReadAccessSnapshot snapshot = buildSnapshot(
BigInteger.valueOf(12),
false,
false,
setOf(BigInteger.valueOf(21)),
setOf(BigInteger.valueOf(1), BigInteger.valueOf(3), BigInteger.valueOf(9))
);
Assert.assertTrue(helper.canRead(collection, snapshot));
}
@Test
public void canRead_shouldRejectDeptScopedKnowledgeWithoutDeptMatch() {
DocumentCollection collection = buildCollection(BigInteger.valueOf(11), BigInteger.valueOf(21), "DEPT");
collection.setDeptId(BigInteger.valueOf(7));
KnowledgeReadAccessSnapshot snapshot = buildSnapshot(
BigInteger.valueOf(12),
false,
false,
setOf(BigInteger.valueOf(21)),
setOf(BigInteger.valueOf(1), BigInteger.valueOf(3), BigInteger.valueOf(9))
);
Assert.assertFalse(helper.canRead(collection, snapshot));
}
@Test
public void canRead_shouldAllowPublicKnowledgeWhenCategoryMatched() {
DocumentCollection collection = buildCollection(BigInteger.valueOf(11), BigInteger.valueOf(21), "PUBLIC");
KnowledgeReadAccessSnapshot snapshot = buildSnapshot(
BigInteger.valueOf(12),
false,
false,
setOf(BigInteger.valueOf(21)),
Collections.emptySet()
);
Assert.assertTrue(helper.canRead(collection, snapshot));
}
private DocumentCollection buildCollection(BigInteger createdBy, BigInteger categoryId, String visibilityScope) {
DocumentCollection collection = new DocumentCollection();
collection.setCreatedBy(createdBy);
collection.setCategoryId(categoryId);
collection.setVisibilityScope(visibilityScope);
return collection;
}
private KnowledgeReadAccessSnapshot buildSnapshot(BigInteger accountId,
boolean superAdmin,
boolean allAccess,
Set<BigInteger> categoryIds,
Set<BigInteger> deptIds) {
RoleCategoryAccessSnapshot accessSnapshot = new RoleCategoryAccessSnapshot(
"KNOWLEDGE",
accountId,
superAdmin,
allAccess,
categoryIds
);
return new KnowledgeReadAccessSnapshot(accessSnapshot, deptIds);
}
private Set<BigInteger> setOf(BigInteger... values) {
Set<BigInteger> result = new LinkedHashSet<>();
Collections.addAll(result, values);
return result;
}
}