feat: 增加工作流和知识库三级权限
- 抽取统一资源访问骨架与部门可见范围判断 - 接入工作流和知识库的 READ/MANAGE 权限校验 - 增加可见范围配置与只读态前端交互
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package tech.easyflow.system.enums;
|
||||
|
||||
public enum ResourceAction {
|
||||
READ,
|
||||
USE,
|
||||
MANAGE
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package tech.easyflow.system.enums;
|
||||
|
||||
public enum ResourceLookup {
|
||||
WORKFLOW_ID,
|
||||
EXEC_KEY,
|
||||
KNOWLEDGE_ID,
|
||||
KNOWLEDGE_ID_OR_SLUG,
|
||||
DOCUMENT_ID,
|
||||
DOCUMENT_CHUNK_ID,
|
||||
FAQ_ITEM_ID,
|
||||
FAQ_CATEGORY_ID
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package tech.easyflow.system.enums;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
public enum VisibilityScope {
|
||||
|
||||
PRIVATE,
|
||||
DEPT,
|
||||
PUBLIC;
|
||||
|
||||
public static VisibilityScope from(String code) {
|
||||
if (code == null || code.isBlank()) {
|
||||
throw new IllegalArgumentException("visibilityScope不能为空");
|
||||
}
|
||||
String normalized = code.trim().toUpperCase(Locale.ROOT);
|
||||
return Arrays.stream(values())
|
||||
.filter(item -> item.name().equals(normalized))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("不支持的visibilityScope: " + code));
|
||||
}
|
||||
|
||||
public static VisibilityScope fromOrDefault(String code, VisibilityScope defaultValue) {
|
||||
if (code == null || code.isBlank()) {
|
||||
return defaultValue;
|
||||
}
|
||||
return from(code);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package tech.easyflow.system.permission.resource;
|
||||
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.enums.ResourceLookup;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface RequireResourceAccess {
|
||||
|
||||
CategoryResourceType resource();
|
||||
|
||||
ResourceAction action();
|
||||
|
||||
ResourceLookup lookup();
|
||||
|
||||
String idExpr();
|
||||
|
||||
String denyMessage() default "无权限访问该资源";
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package tech.easyflow.system.permission.resource;
|
||||
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.context.expression.MethodBasedEvaluationContext;
|
||||
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.expression.ExpressionParser;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceLookup;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
@Aspect
|
||||
@Component
|
||||
public class RequireResourceAccessAspect {
|
||||
|
||||
private final List<ResourceAccessResolver> resolvers;
|
||||
private final ResourceAccessService resourceAccessService;
|
||||
private final CategoryPermissionService categoryPermissionService;
|
||||
private final ExpressionParser expressionParser = new SpelExpressionParser();
|
||||
private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
|
||||
|
||||
public RequireResourceAccessAspect(List<ResourceAccessResolver> resolvers,
|
||||
ResourceAccessService resourceAccessService,
|
||||
CategoryPermissionService categoryPermissionService) {
|
||||
this.resolvers = resolvers;
|
||||
this.resourceAccessService = resourceAccessService;
|
||||
this.categoryPermissionService = categoryPermissionService;
|
||||
}
|
||||
|
||||
@Around("@annotation(requireResourceAccess)")
|
||||
public Object doAround(ProceedingJoinPoint joinPoint, RequireResourceAccess requireResourceAccess) throws Throwable {
|
||||
Object identifier = resolveIdentifier(joinPoint, requireResourceAccess);
|
||||
ResourceAccessResolver resolver = findResolver(requireResourceAccess.resource(), requireResourceAccess.lookup());
|
||||
ResolvedResourceAccess resolved = resolver.resolve(identifier);
|
||||
assertExecutionOwner(resolved);
|
||||
resourceAccessService.assertAccess(
|
||||
requireResourceAccess.resource(),
|
||||
resolved.getResource(),
|
||||
requireResourceAccess.action(),
|
||||
requireResourceAccess.denyMessage()
|
||||
);
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
|
||||
private Object resolveIdentifier(ProceedingJoinPoint joinPoint, RequireResourceAccess requireResourceAccess) {
|
||||
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
|
||||
MethodBasedEvaluationContext context = new MethodBasedEvaluationContext(
|
||||
joinPoint.getTarget(),
|
||||
method,
|
||||
joinPoint.getArgs(),
|
||||
parameterNameDiscoverer
|
||||
);
|
||||
return expressionParser.parseExpression(requireResourceAccess.idExpr()).getValue(context);
|
||||
}
|
||||
|
||||
private ResourceAccessResolver findResolver(CategoryResourceType resourceType, ResourceLookup lookup) {
|
||||
return resolvers.stream()
|
||||
.filter(item -> item.supports(resourceType, lookup))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("未找到资源访问解析器: " + resourceType + "/" + lookup));
|
||||
}
|
||||
|
||||
private void assertExecutionOwner(ResolvedResourceAccess resolved) {
|
||||
String executionOwnerKey = resolved.getExecutionOwnerKey();
|
||||
if (executionOwnerKey == null || executionOwnerKey.isBlank() || categoryPermissionService.isCurrentSuperAdmin()) {
|
||||
return;
|
||||
}
|
||||
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
|
||||
String accountId = loginAccount == null || loginAccount.getId() == null ? null : loginAccount.getId().toString();
|
||||
if (!executionOwnerKey.equals(accountId)) {
|
||||
throw new BusinessException("无权限访问该执行记录");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package tech.easyflow.system.permission.resource;
|
||||
|
||||
public class ResolvedResourceAccess {
|
||||
|
||||
private final VisibilityResource resource;
|
||||
private final String executionOwnerKey;
|
||||
|
||||
public ResolvedResourceAccess(VisibilityResource resource, String executionOwnerKey) {
|
||||
this.resource = resource;
|
||||
this.executionOwnerKey = executionOwnerKey;
|
||||
}
|
||||
|
||||
public VisibilityResource getResource() {
|
||||
return resource;
|
||||
}
|
||||
|
||||
public String getExecutionOwnerKey() {
|
||||
return executionOwnerKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package tech.easyflow.system.permission.resource;
|
||||
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceLookup;
|
||||
|
||||
public interface ResourceAccessResolver {
|
||||
|
||||
boolean supports(CategoryResourceType resourceType, ResourceLookup lookup);
|
||||
|
||||
ResolvedResourceAccess resolve(Object identifier);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package tech.easyflow.system.permission.resource;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
public interface VisibilityResource {
|
||||
|
||||
BigInteger getCreatedBy();
|
||||
|
||||
BigInteger getDeptId();
|
||||
|
||||
BigInteger getCategoryId();
|
||||
|
||||
String getVisibilityScope();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package tech.easyflow.system.service;
|
||||
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.permission.resource.VisibilityResource;
|
||||
|
||||
public interface ResourceAccessService {
|
||||
|
||||
boolean canAccess(CategoryResourceType resourceType, VisibilityResource resource, ResourceAction action);
|
||||
|
||||
void assertAccess(CategoryResourceType resourceType, VisibilityResource resource, ResourceAction action, String message);
|
||||
}
|
||||
@@ -3,6 +3,9 @@ package tech.easyflow.system.service;
|
||||
import tech.easyflow.system.entity.SysDept;
|
||||
import com.mybatisflex.core.service.IService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 部门表 服务层。
|
||||
*
|
||||
@@ -11,4 +14,7 @@ import com.mybatisflex.core.service.IService;
|
||||
*/
|
||||
public interface SysDeptService extends IService<SysDept> {
|
||||
|
||||
Set<BigInteger> getSelfAndAncestorDeptIds(BigInteger currentDeptId);
|
||||
|
||||
boolean canUserAccessDeptScopedResource(BigInteger currentDeptId, BigInteger resourceDeptId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package tech.easyflow.system.service.impl;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.enums.VisibilityScope;
|
||||
import tech.easyflow.system.permission.resource.VisibilityResource;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
import tech.easyflow.system.service.SysDeptService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
|
||||
@Service
|
||||
public class ResourceAccessServiceImpl implements ResourceAccessService {
|
||||
|
||||
@Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
|
||||
@Resource
|
||||
private SysDeptService sysDeptService;
|
||||
|
||||
@Override
|
||||
public boolean canAccess(CategoryResourceType resourceType, VisibilityResource resource, ResourceAction action) {
|
||||
if (resource == null) {
|
||||
return false;
|
||||
}
|
||||
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
|
||||
if (loginAccount == null || loginAccount.getId() == null) {
|
||||
return false;
|
||||
}
|
||||
BigInteger accountId = loginAccount.getId();
|
||||
if (categoryPermissionService.isCurrentSuperAdmin()) {
|
||||
return true;
|
||||
}
|
||||
if (accountId.equals(resource.getCreatedBy())) {
|
||||
return true;
|
||||
}
|
||||
if (ResourceAction.MANAGE == action) {
|
||||
return false;
|
||||
}
|
||||
if (!categoryPermissionService.canAccessCategory(resourceType.getCode(), resource.getCreatedBy(), resource.getCategoryId())) {
|
||||
return false;
|
||||
}
|
||||
VisibilityScope scope = VisibilityScope.fromOrDefault(resource.getVisibilityScope(), VisibilityScope.PRIVATE);
|
||||
if (VisibilityScope.PUBLIC == scope) {
|
||||
return true;
|
||||
}
|
||||
if (VisibilityScope.DEPT == scope) {
|
||||
return sysDeptService.canUserAccessDeptScopedResource(loginAccount.getDeptId(), resource.getDeptId());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assertAccess(CategoryResourceType resourceType, VisibilityResource resource, ResourceAction action, String message) {
|
||||
if (!canAccess(resourceType, resource, action)) {
|
||||
throw new BusinessException(message == null ? "无权限访问该资源" : message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,11 @@ import tech.easyflow.system.service.SysDeptService;
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 部门表 服务层实现。
|
||||
*
|
||||
@@ -15,4 +20,39 @@ import org.springframework.stereotype.Service;
|
||||
@Service
|
||||
public class SysDeptServiceImpl extends ServiceImpl<SysDeptMapper, SysDept> implements SysDeptService {
|
||||
|
||||
@Override
|
||||
public Set<BigInteger> getSelfAndAncestorDeptIds(BigInteger currentDeptId) {
|
||||
if (currentDeptId == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
SysDept currentDept = getById(currentDeptId);
|
||||
if (currentDept == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
Set<BigInteger> deptIds = new LinkedHashSet<>();
|
||||
String ancestors = currentDept.getAncestors();
|
||||
if (ancestors != null && !ancestors.isBlank()) {
|
||||
String[] items = ancestors.split(",");
|
||||
for (String item : items) {
|
||||
if (item == null || item.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
BigInteger deptId = new BigInteger(item.trim());
|
||||
if (BigInteger.ZERO.equals(deptId)) {
|
||||
continue;
|
||||
}
|
||||
deptIds.add(deptId);
|
||||
}
|
||||
}
|
||||
deptIds.add(currentDeptId);
|
||||
return deptIds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canUserAccessDeptScopedResource(BigInteger currentDeptId, BigInteger resourceDeptId) {
|
||||
if (currentDeptId == null || resourceDeptId == null) {
|
||||
return false;
|
||||
}
|
||||
return getSelfAndAncestorDeptIds(currentDeptId).contains(resourceDeptId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package tech.easyflow.system.service.impl;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.system.entity.SysDept;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Set;
|
||||
|
||||
public class SysDeptServiceImplTest {
|
||||
|
||||
@Test
|
||||
public void shouldReturnSelfAndAncestors() {
|
||||
SysDept dept = new SysDept();
|
||||
dept.setId(BigInteger.valueOf(5));
|
||||
dept.setAncestors("0,1,3");
|
||||
|
||||
SysDeptServiceImpl service = new SysDeptServiceImpl() {
|
||||
@Override
|
||||
public SysDept getById(Serializable id) {
|
||||
return dept;
|
||||
}
|
||||
};
|
||||
|
||||
Set<BigInteger> deptIds = service.getSelfAndAncestorDeptIds(BigInteger.valueOf(5));
|
||||
Assert.assertTrue(deptIds.contains(BigInteger.valueOf(1)));
|
||||
Assert.assertTrue(deptIds.contains(BigInteger.valueOf(3)));
|
||||
Assert.assertTrue(deptIds.contains(BigInteger.valueOf(5)));
|
||||
Assert.assertFalse(deptIds.contains(BigInteger.ZERO));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldMatchDeptScopedResourceByAncestorChain() {
|
||||
SysDept dept = new SysDept();
|
||||
dept.setId(BigInteger.valueOf(9));
|
||||
dept.setAncestors("1,5");
|
||||
|
||||
SysDeptServiceImpl service = new SysDeptServiceImpl() {
|
||||
@Override
|
||||
public SysDept getById(Serializable id) {
|
||||
return dept;
|
||||
}
|
||||
};
|
||||
|
||||
Assert.assertTrue(service.canUserAccessDeptScopedResource(BigInteger.valueOf(9), BigInteger.valueOf(1)));
|
||||
Assert.assertTrue(service.canUserAccessDeptScopedResource(BigInteger.valueOf(9), BigInteger.valueOf(5)));
|
||||
Assert.assertTrue(service.canUserAccessDeptScopedResource(BigInteger.valueOf(9), BigInteger.valueOf(9)));
|
||||
Assert.assertFalse(service.canUserAccessDeptScopedResource(BigInteger.valueOf(9), BigInteger.valueOf(7)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user