权限回收级联清理:管理员被回收权限后自动清理其直建员工的越权授权
- 保存管理员权限时,若其权限被回收,级联删除其直接创建的员工中超出 该管理员当前有效范围的授权(创建时快照遗留的越权项) - 只清理越权项,保留员工合法持有的其他授权;超级管理员不受此限制
This commit is contained in:
+39
@@ -15,6 +15,7 @@ import com.nanri.aiimage.modules.permission.model.vo.ImageVideoDataPermissionUse
|
|||||||
import com.nanri.aiimage.modules.permission.model.vo.PermissionMenuItemVo;
|
import com.nanri.aiimage.modules.permission.model.vo.PermissionMenuItemVo;
|
||||||
import com.nanri.aiimage.modules.permission.model.vo.UserColumnIdsVo;
|
import com.nanri.aiimage.modules.permission.model.vo.UserColumnIdsVo;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -41,6 +42,7 @@ import java.util.stream.Collectors;
|
|||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
public class PermissionMenuService {
|
public class PermissionMenuService {
|
||||||
|
|
||||||
public static final String MENU_TYPE_APP = "app";
|
public static final String MENU_TYPE_APP = "app";
|
||||||
@@ -401,6 +403,43 @@ public class PermissionMenuService {
|
|||||||
entity.setColumnId(columnId);
|
entity.setColumnId(columnId);
|
||||||
userColumnPermissionMapper.insert(entity);
|
userColumnPermissionMapper.insert(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 目标管理员的权限被回收后,其直接创建的员工若仍持有超出范围的
|
||||||
|
// 授权(创建时快照遗留,员工权限独立于管理员后续变更),必须级联
|
||||||
|
// 删除,否则回收了管理员的菜单权限,员工菜单权限却还在。
|
||||||
|
cascadeCleanupSubordinateOverreach(target, finalGrantIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除普通管理员直接创建的员工中,超出该管理员当前有效权限范围的授权。
|
||||||
|
* 只清理越权项,保留员工合法持有的其他授权。超级管理员无此限制。
|
||||||
|
*/
|
||||||
|
private void cascadeCleanupSubordinateOverreach(AdminUserEntity target, Set<Long> targetDirectIds) {
|
||||||
|
if (target == null || target.getId() == null || isSuperAdmin(target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Set<Long> targetEffectiveIds = expandDescendantIds(new LinkedHashSet<>(targetDirectIds), loadMenus(null));
|
||||||
|
List<AdminUserEntity> subordinates = adminUserMapper.selectList(new LambdaQueryWrapper<AdminUserEntity>()
|
||||||
|
.eq(AdminUserEntity::getCreatedById, target.getId()));
|
||||||
|
for (AdminUserEntity subordinate : subordinates) {
|
||||||
|
Long subordinateId = subordinate.getId();
|
||||||
|
if (subordinateId == null || subordinateId.equals(target.getId())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<Long> subordinateGrantIds = loadDirectColumnIds(subordinateId);
|
||||||
|
if (subordinateGrantIds.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<Long> overreachIds = subordinateGrantIds.stream()
|
||||||
|
.filter(id -> !targetEffectiveIds.contains(id))
|
||||||
|
.toList();
|
||||||
|
if (overreachIds.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
userColumnPermissionMapper.deleteByUserIdAndColumnIds(subordinateId, overreachIds);
|
||||||
|
log.info("removed overreach permissions target={} subordinate={} droppedColumnIds={}",
|
||||||
|
target.getId(), subordinateId, overreachIds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<PermissionMenuEntity> loadMenus(String menuType) {
|
private List<PermissionMenuEntity> loadMenus(String menuType) {
|
||||||
|
|||||||
+37
@@ -413,6 +413,43 @@ class PermissionMenuServiceTest {
|
|||||||
.containsExactly(1L);
|
.containsExactly(1L);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void adminPermissionRevocationCascadesToCreatedSubordinates() {
|
||||||
|
// 回收管理员的菜单权限后,其直接创建员工超出范围的授权必须级联删除。
|
||||||
|
// 生产场景:管理员 1000 的"去重数据汇总"被回收后,员工 1002 仍持有它。
|
||||||
|
PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class);
|
||||||
|
UserColumnPermissionMapper permissionMapper = mock(UserColumnPermissionMapper.class);
|
||||||
|
AdminUserMapper userMapper = mock(AdminUserMapper.class);
|
||||||
|
PermissionMenuService service = new PermissionMenuService(menuMapper, permissionMapper, userMapper);
|
||||||
|
|
||||||
|
// 超管操作员编辑管理员 10:管理员仅保留 id=1 的授权(回收 2)
|
||||||
|
AdminUserEntity operator = user(1L, "super_admin", 1);
|
||||||
|
AdminUserEntity target = user(10L, "admin", 1);
|
||||||
|
target.setCreatedById(1L);
|
||||||
|
AdminUserEntity subordinate = user(20L, "normal", 0);
|
||||||
|
subordinate.setCreatedById(10L);
|
||||||
|
when(userMapper.selectById(10L)).thenReturn(target);
|
||||||
|
when(menuMapper.selectCount(any())).thenReturn(1L);
|
||||||
|
when(menuMapper.selectOne(any())).thenReturn(null);
|
||||||
|
when(menuMapper.selectList(any())).thenReturn(List.of(
|
||||||
|
menu(1L, null, "admin", 1),
|
||||||
|
menu(2L, null, "admin", 2)));
|
||||||
|
// 管理员当前授权:1(新)+ 2(被回收前存在)
|
||||||
|
when(permissionMapper.selectByUserId(10L))
|
||||||
|
.thenReturn(List.of(grant(10L, 1L), grant(10L, 2L)));
|
||||||
|
// 员工 20 的授权:1(合法)+ 2(越权,应被级联删除)
|
||||||
|
when(permissionMapper.selectByUserId(20L))
|
||||||
|
.thenReturn(List.of(grant(20L, 1L), grant(20L, 2L)));
|
||||||
|
when(userMapper.selectList(any())).thenReturn(List.of(subordinate));
|
||||||
|
UserColumnPermissionUpdateRequest request = new UserColumnPermissionUpdateRequest();
|
||||||
|
request.setColumnIds(List.of(1L));
|
||||||
|
|
||||||
|
service.updateUserColumnPermissions(operator, 10L, request);
|
||||||
|
|
||||||
|
// 员工越权授权 2 必须被清除,合法授权 1 保留
|
||||||
|
verify(permissionMapper).deleteByUserIdAndColumnIds(20L, List.of(2L));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void replacementStoresOnlyDirectRequestedParentId() {
|
void replacementStoresOnlyDirectRequestedParentId() {
|
||||||
PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class);
|
PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class);
|
||||||
|
|||||||
Reference in New Issue
Block a user