feat: 完善 Skill 管理与发布治理
- 实现标准资源存储、能力绑定及双格式导入导出 - 接入分类、可见范围、审批发布与资源权限校验 - 补充并发、租户隔离、安全边界和迁移契约测试
This commit is contained in:
@@ -2,13 +2,43 @@ package tech.easyflow.system.permission.resource;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* 可由统一资源权限服务判定可见性与动作权限的资源契约。
|
||||
*/
|
||||
public interface VisibilityResource {
|
||||
|
||||
/**
|
||||
* 获取资源所属租户。
|
||||
*
|
||||
* @return 租户 ID
|
||||
*/
|
||||
BigInteger getTenantId();
|
||||
|
||||
/**
|
||||
* 获取资源创建者。
|
||||
*
|
||||
* @return 创建者账号 ID
|
||||
*/
|
||||
BigInteger getCreatedBy();
|
||||
|
||||
/**
|
||||
* 获取资源所属部门。
|
||||
*
|
||||
* @return 部门 ID
|
||||
*/
|
||||
BigInteger getDeptId();
|
||||
|
||||
/**
|
||||
* 获取资源所属分类。
|
||||
*
|
||||
* @return 分类 ID,未分类时可为空
|
||||
*/
|
||||
BigInteger getCategoryId();
|
||||
|
||||
/**
|
||||
* 获取资源可见范围。
|
||||
*
|
||||
* @return 可见范围编码
|
||||
*/
|
||||
String getVisibilityScope();
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ public class CategoryPermissionServiceImpl implements CategoryPermissionService
|
||||
@Override
|
||||
public void assertCategoryResourceVisible(String resourceType, BigInteger createdBy, BigInteger categoryId, String message) {
|
||||
if (!canAccessCategory(resourceType, createdBy, categoryId)) {
|
||||
throw new BusinessException(message == null ? "无权限访问该资源" : message);
|
||||
throw new BusinessException(403, 403, message == null ? "无权限访问该资源" : message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,10 @@ public class ResourceAccessServiceImpl implements ResourceAccessService {
|
||||
if (loginAccount == null || loginAccount.getId() == null) {
|
||||
return false;
|
||||
}
|
||||
if (loginAccount.getTenantId() == null || resource.getTenantId() == null
|
||||
|| !loginAccount.getTenantId().equals(resource.getTenantId())) {
|
||||
return false;
|
||||
}
|
||||
BigInteger accountId = loginAccount.getId();
|
||||
// 分享访问需要先完成密钥校验与审计,即使当前账号同时也是资源创建者或超管。
|
||||
if (hasExtendedGrant(loginAccount, resourceType, resource, action)) {
|
||||
@@ -67,6 +71,10 @@ public class ResourceAccessServiceImpl implements ResourceAccessService {
|
||||
if (ResourceAction.MANAGE == action) {
|
||||
return false;
|
||||
}
|
||||
if (CategoryResourceType.SKILL == resourceType && resource.getCategoryId() == null
|
||||
&& categoryPermissionService.getAccess(resourceType.getCode(), loginAccount).isAllAccess()) {
|
||||
return true;
|
||||
}
|
||||
if (!categoryPermissionService.canAccessCategory(loginAccount, resourceType.getCode(), resource.getCreatedBy(), resource.getCategoryId())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
package tech.easyflow.system.service.impl;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.enums.VisibilityScope;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
import tech.easyflow.system.permission.resource.VisibilityResource;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.system.service.SysDeptService;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* {@link ResourceAccessServiceImpl} 资源动作与可见范围回归测试。
|
||||
*/
|
||||
public class ResourceAccessServiceImplTest {
|
||||
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
private SysDeptService sysDeptService;
|
||||
private ResourceAccessServiceImpl service;
|
||||
|
||||
/**
|
||||
* 初始化被测服务及权限依赖。
|
||||
*
|
||||
* @throws Exception 反射注入失败时抛出
|
||||
*/
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
categoryPermissionService = Mockito.mock(CategoryPermissionService.class);
|
||||
sysDeptService = Mockito.mock(SysDeptService.class);
|
||||
service = new ResourceAccessServiceImpl();
|
||||
inject(service, "categoryPermissionService", categoryPermissionService);
|
||||
inject(service, "sysDeptService", sysDeptService);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证创建者始终可以管理自己的 Skill。
|
||||
*/
|
||||
@Test
|
||||
public void ownerShouldManageOwnResource() {
|
||||
LoginAccount account = account(7, 70);
|
||||
VisibilityResource resource = resource(7, 99, 700, VisibilityScope.PRIVATE);
|
||||
|
||||
assertTrue(service.canAccess(account, CategoryResourceType.SKILL, resource, ResourceAction.MANAGE));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证非创建者即使能查看分类,也不能获得 MANAGE。
|
||||
*/
|
||||
@Test
|
||||
public void nonOwnerShouldNotManageResource() {
|
||||
LoginAccount account = account(8, 80);
|
||||
VisibilityResource resource = resource(7, 80, 700, VisibilityScope.PUBLIC);
|
||||
Mockito.when(categoryPermissionService.canAccessCategory(
|
||||
account, CategoryResourceType.SKILL.getCode(), BigInteger.valueOf(7), BigInteger.valueOf(700)))
|
||||
.thenReturn(true);
|
||||
|
||||
assertFalse(service.canAccess(account, CategoryResourceType.SKILL, resource, ResourceAction.MANAGE));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 PUBLIC 仍必须先通过分类范围,避免公开标记绕过分类授权。
|
||||
*/
|
||||
@Test
|
||||
public void publicResourceShouldStillRequireCategoryAccess() {
|
||||
LoginAccount account = account(8, 80);
|
||||
VisibilityResource resource = resource(7, 90, 700, VisibilityScope.PUBLIC);
|
||||
|
||||
Mockito.when(categoryPermissionService.canAccessCategory(
|
||||
account, CategoryResourceType.SKILL.getCode(), BigInteger.valueOf(7), BigInteger.valueOf(700)))
|
||||
.thenReturn(false);
|
||||
|
||||
assertFalse(service.canAccess(account, CategoryResourceType.SKILL, resource, ResourceAction.READ));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证部门可见资源使用现有部门树访问判定。
|
||||
*/
|
||||
@Test
|
||||
public void departmentResourceShouldUseDepartmentAccess() {
|
||||
LoginAccount account = account(8, 80);
|
||||
VisibilityResource resource = resource(7, 90, 700, VisibilityScope.DEPT);
|
||||
Mockito.when(categoryPermissionService.canAccessCategory(
|
||||
account, CategoryResourceType.SKILL.getCode(), BigInteger.valueOf(7), BigInteger.valueOf(700)))
|
||||
.thenReturn(true);
|
||||
Mockito.when(sysDeptService.canUserAccessDeptScopedResource(BigInteger.valueOf(80), BigInteger.valueOf(90)))
|
||||
.thenReturn(true);
|
||||
|
||||
assertTrue(service.canAccess(account, CategoryResourceType.SKILL, resource, ResourceAction.READ));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 PRIVATE 对分类内其他用户仍不可见。
|
||||
*/
|
||||
@Test
|
||||
public void privateResourceShouldStayPrivateWithinCategory() {
|
||||
LoginAccount account = account(8, 80);
|
||||
VisibilityResource resource = resource(7, 80, 700, VisibilityScope.PRIVATE);
|
||||
Mockito.when(categoryPermissionService.canAccessCategory(
|
||||
account, CategoryResourceType.SKILL.getCode(), BigInteger.valueOf(7), BigInteger.valueOf(700)))
|
||||
.thenReturn(true);
|
||||
|
||||
assertFalse(service.canAccess(account, CategoryResourceType.SKILL, resource, ResourceAction.READ));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 Skill 分类 ALL 范围可以读取其他创建者的未分类私有草稿。
|
||||
*/
|
||||
@Test
|
||||
public void allCategoryScopeShouldReadUnclassifiedPrivateSkill() {
|
||||
LoginAccount account = account(8, 80);
|
||||
VisibilityResource resource = new TestVisibilityResource(
|
||||
BigInteger.ONE, BigInteger.valueOf(7), BigInteger.valueOf(90), null,
|
||||
VisibilityScope.PRIVATE.name());
|
||||
Mockito.when(categoryPermissionService.getAccess(CategoryResourceType.SKILL.getCode(), account))
|
||||
.thenReturn(new RoleCategoryAccessSnapshot(
|
||||
CategoryResourceType.SKILL.getCode(), account.getId(), false, true, Set.of()));
|
||||
|
||||
assertTrue(service.canAccess(account, CategoryResourceType.SKILL, resource, ResourceAction.READ));
|
||||
assertFalse(service.canAccess(account, CategoryResourceType.SKILL, resource, ResourceAction.MANAGE));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证资源动作不能跨越租户边界,即使资源是公开状态。
|
||||
*/
|
||||
@Test
|
||||
public void resourceShouldNeverCrossTenantBoundary() {
|
||||
LoginAccount account = account(8, 80);
|
||||
VisibilityResource resource = resource(7, 80, 700, VisibilityScope.PUBLIC, BigInteger.TWO);
|
||||
|
||||
assertFalse(service.canAccess(account, CategoryResourceType.SKILL, resource, ResourceAction.READ));
|
||||
Mockito.verifyNoInteractions(categoryPermissionService, sysDeptService);
|
||||
}
|
||||
|
||||
private LoginAccount account(long id, long deptId) {
|
||||
LoginAccount account = new LoginAccount();
|
||||
account.setId(BigInteger.valueOf(id));
|
||||
account.setDeptId(BigInteger.valueOf(deptId));
|
||||
account.setTenantId(BigInteger.ONE);
|
||||
return account;
|
||||
}
|
||||
|
||||
private VisibilityResource resource(long createdBy, long deptId, long categoryId, VisibilityScope scope) {
|
||||
return resource(createdBy, deptId, categoryId, scope, BigInteger.ONE);
|
||||
}
|
||||
|
||||
private VisibilityResource resource(long createdBy, long deptId, long categoryId,
|
||||
VisibilityScope scope, BigInteger tenantId) {
|
||||
return new TestVisibilityResource(
|
||||
tenantId,
|
||||
BigInteger.valueOf(createdBy),
|
||||
BigInteger.valueOf(deptId),
|
||||
BigInteger.valueOf(categoryId),
|
||||
scope.name());
|
||||
}
|
||||
|
||||
private void inject(Object target, String fieldName, Object value) throws Exception {
|
||||
Field field = target.getClass().getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
}
|
||||
|
||||
private record TestVisibilityResource(BigInteger tenantId,
|
||||
BigInteger createdBy,
|
||||
BigInteger deptId,
|
||||
BigInteger categoryId,
|
||||
String visibilityScope) implements VisibilityResource {
|
||||
@Override
|
||||
public BigInteger getTenantId() {
|
||||
return tenantId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigInteger getCreatedBy() {
|
||||
return createdBy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigInteger getDeptId() {
|
||||
return deptId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigInteger getCategoryId() {
|
||||
return categoryId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVisibilityScope() {
|
||||
return visibilityScope;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user