feat: 完善 Skill 管理与发布治理
- 实现标准资源存储、能力绑定及双格式导入导出 - 接入分类、可见范围、审批发布与资源权限校验 - 补充并发、租户隔离、安全边界和迁移契约测试
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
package tech.easyflow.ai.permission;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
/**
|
||||
* MCP 查询与使用权限检查器。
|
||||
*
|
||||
* <p>MCP 当前没有独立的资源级 {@code USE} 权限,平台沿用 MCP 管理模块已有的
|
||||
* {@code /api/v1/mcp/query} 权限作为查看、选择和使用 MCP 的授权边界。</p>
|
||||
*/
|
||||
@Component
|
||||
public class McpAccessPermissionChecker {
|
||||
|
||||
/** MCP 模块现有查询权限码。 */
|
||||
public static final String MCP_QUERY_PERMISSION = "/api/v1/mcp/query";
|
||||
|
||||
/**
|
||||
* 判断当前登录用户是否可以查询和使用 MCP。
|
||||
*
|
||||
* @return 已登录且拥有 MCP 查询权限时返回 {@code true}
|
||||
*/
|
||||
public boolean canUseMcp() {
|
||||
return StpUtil.isLogin() && StpUtil.hasPermission(MCP_QUERY_PERMISSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验当前登录用户是否可以查询和使用 MCP。
|
||||
*
|
||||
* @throws BusinessException 未登录或缺少 MCP 查询权限时抛出
|
||||
*/
|
||||
public void assertCanUseMcp() {
|
||||
if (!StpUtil.isLogin()) {
|
||||
throw new BusinessException(401, 401, "未登录或登录态无效");
|
||||
}
|
||||
if (!StpUtil.hasPermission(MCP_QUERY_PERMISSION)) {
|
||||
throw new BusinessException(403, 403, "无权限查询或使用 MCP");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,6 +166,42 @@ public abstract class AbstractAiResourceLifecycleHandler<T> implements ApprovalS
|
||||
protected void validateDelete(T resource, PublishStatus currentStatus) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建删除审批使用的治理快照。
|
||||
*
|
||||
* <p>默认沿用资源快照;包含敏感配置或需要发布级校验的资源可覆盖此方法,
|
||||
* 返回不依赖发布可用性的最小治理信息。</p>
|
||||
*
|
||||
* @param resource 资源
|
||||
* @return 删除审批治理快照
|
||||
*/
|
||||
protected Map<String, Object> buildDeleteResourceSnapshot(T resource) {
|
||||
return buildResourceSnapshot(resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean canAccessApprovalDetail(Object identifier) {
|
||||
if (identifier == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
T resource = requireResource(new BigInteger(String.valueOf(identifier)));
|
||||
assertManagePermission(resource);
|
||||
return true;
|
||||
} catch (NumberFormatException exception) {
|
||||
return false;
|
||||
} catch (BusinessException exception) {
|
||||
// 资源不存在或无权管理都按不可见处理;服务端异常仍向上抛出,避免静默掩盖故障。
|
||||
if (exception.getHttpStatus() >= 400 && exception.getHttpStatus() < 500) {
|
||||
return false;
|
||||
}
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下线成功后的额外副作用。
|
||||
*
|
||||
@@ -286,7 +322,7 @@ public abstract class AbstractAiResourceLifecycleHandler<T> implements ApprovalS
|
||||
throw new BusinessException("当前" + resourceLabel() + "存在进行中的审批,请先处理完成");
|
||||
}
|
||||
validateDelete(resource, currentStatus);
|
||||
return buildResourceSnapshot(resource);
|
||||
return buildDeleteResourceSnapshot(resource);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package tech.easyflow.ai.permission;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
|
||||
/**
|
||||
* {@link McpAccessPermissionChecker} 的现有 MCP RBAC 语义回归测试。
|
||||
*/
|
||||
public class McpAccessPermissionCheckerTest {
|
||||
|
||||
/**
|
||||
* 未登录调用方必须收到 401。
|
||||
*/
|
||||
@Test
|
||||
public void unauthenticatedCallerIsRejected() {
|
||||
try (MockedStatic<StpUtil> stpUtil = mockStatic(StpUtil.class)) {
|
||||
stpUtil.when(StpUtil::isLogin).thenReturn(false);
|
||||
|
||||
BusinessException exception = assertThrows(BusinessException.class,
|
||||
() -> new McpAccessPermissionChecker().assertCanUseMcp());
|
||||
|
||||
assertEquals(401, exception.getHttpStatus());
|
||||
assertFalse(new McpAccessPermissionChecker().canUseMcp());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 已登录但缺少 MCP 查询权限的调用方必须收到 403。
|
||||
*/
|
||||
@Test
|
||||
public void callerWithoutMcpQueryPermissionIsRejected() {
|
||||
try (MockedStatic<StpUtil> stpUtil = mockStatic(StpUtil.class)) {
|
||||
stpUtil.when(StpUtil::isLogin).thenReturn(true);
|
||||
stpUtil.when(() -> StpUtil.hasPermission(McpAccessPermissionChecker.MCP_QUERY_PERMISSION))
|
||||
.thenReturn(false);
|
||||
|
||||
BusinessException exception = assertThrows(BusinessException.class,
|
||||
() -> new McpAccessPermissionChecker().assertCanUseMcp());
|
||||
|
||||
assertEquals(403, exception.getHttpStatus());
|
||||
assertFalse(new McpAccessPermissionChecker().canUseMcp());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MCP 查询权限同时授予 MCP 候选查看和绑定使用能力。
|
||||
*/
|
||||
@Test
|
||||
public void mcpQueryPermissionAllowsUse() {
|
||||
try (MockedStatic<StpUtil> stpUtil = mockStatic(StpUtil.class)) {
|
||||
stpUtil.when(StpUtil::isLogin).thenReturn(true);
|
||||
stpUtil.when(() -> StpUtil.hasPermission(McpAccessPermissionChecker.MCP_QUERY_PERMISSION))
|
||||
.thenReturn(true);
|
||||
McpAccessPermissionChecker checker = new McpAccessPermissionChecker();
|
||||
|
||||
checker.assertCanUseMcp();
|
||||
|
||||
assertTrue(checker.canUseMcp());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user