- 勘误③真落:docs/T3-report.md §2 V32「两个不同教师」补注(实为两个 roleId=7 操作者,教师(1) 按 P21 无代填权) - P47 收紧:FormModuleKeys.anchorCustomIds 只认 path 空节点的 customId,DataScopeService.hasScopeAllAnchor 切过去;补「带 path 的导航行误配 fc:scope:all 不升级范围」用例(mvn -o clean package 178 例全绿) - docs/T5-report.md:T5 做了什么 / 逐条自验证据 / 问题单 P48~P53 / 两仓推送与 CI
This commit is contained in:
@@ -30,13 +30,23 @@ public final class FormModuleKeys {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 授权 customId 集合(path 空的权限锚点主要靠它表达) */
|
||||
public static Set<String> customIds(List<BoaModule> tree) {
|
||||
/**
|
||||
* 权限锚点的 customId 集合:<b>只收 path 为空的节点</b>(契约 §8「锚点行形态」注记 / P47)。
|
||||
*
|
||||
* <p>数据范围升级位({@code fc:scope:all})按契约只应挂在<b>不进导航的 path 空行</b>上,
|
||||
* 扫描必须按这个形态收:带 path 的导航行若误配了该值,不该把看数据的人悄悄升级成"全校全部"
|
||||
* ——宽方向的误配同样是不受控的授权膨胀。</p>
|
||||
*/
|
||||
public static Set<String> anchorCustomIds(List<BoaModule> tree) {
|
||||
Set<String> result = new LinkedHashSet<>();
|
||||
collect(tree, false, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归展平(wantPath=true 取 path;false 取「path 空节点」的 customId)。
|
||||
* 取不出值的节点自然被跳过,子节点照常下钻(锚点可以挂在分组层下面)。
|
||||
*/
|
||||
private static void collect(List<BoaModule> nodes, boolean wantPath, Set<String> out) {
|
||||
if (nodes == null) {
|
||||
return;
|
||||
@@ -45,11 +55,16 @@ public final class FormModuleKeys {
|
||||
if (node == null) {
|
||||
continue;
|
||||
}
|
||||
String value = wantPath ? node.getPath() : node.getCustomId();
|
||||
if (value != null && !value.trim().isEmpty()) {
|
||||
out.add(value.trim());
|
||||
String path = trimToNull(node.getPath());
|
||||
String value = wantPath ? path : (path == null ? trimToNull(node.getCustomId()) : null);
|
||||
if (value != null) {
|
||||
out.add(value);
|
||||
}
|
||||
collect(node.getChildren(), wantPath, out);
|
||||
}
|
||||
}
|
||||
|
||||
private static String trimToNull(String value) {
|
||||
return value == null || value.trim().isEmpty() ? null : value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,9 +119,9 @@ public class DataScopeService {
|
||||
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "数据范围缺少实现器:" + scope);
|
||||
}
|
||||
|
||||
/** 是否命中 fc:scope:all 权限锚点(path 空的模块行,只由后端扫描授权集合消费,不进导航) */
|
||||
/** 是否命中 fc:scope:all 权限锚点(只认 path 空节点,P47;见 FormModuleKeys#anchorCustomIds) */
|
||||
private boolean hasScopeAllAnchor(BoaUser user) {
|
||||
return FormModuleKeys.customIds(moduleTreeSource.authorizedModules(user)).contains(SCOPE_ALL_CUSTOM_ID);
|
||||
return FormModuleKeys.anchorCustomIds(moduleTreeSource.authorizedModules(user)).contains(SCOPE_ALL_CUSTOM_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -130,6 +130,25 @@ class DataScopeServiceTest {
|
||||
.containsExactly(DataScope.SELF_SUBMITTED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("P47:锚点扫描只认 path 空行——带 path 的导航行误配 fc:scope:all 不升级范围")
|
||||
void customIdOnNavRowDoesNotUpgradeScope() {
|
||||
BoaUser teacher = user(1);
|
||||
|
||||
// 误配形态:一行既有页面 path(会被渲染成菜单),又挂了锚点 customId
|
||||
InMemoryModuleTreeSource misconfigured = new InMemoryModuleTreeSource();
|
||||
misconfigured.grantPathWithCustomId(teacher.getId(), "/admin/data",
|
||||
DataScopeService.SCOPE_ALL_CUSTOM_ID);
|
||||
assertThat(serviceWith(misconfigured).resolve(teacher, form(STUDENT_FACING)))
|
||||
.as("契约 §8 的锚点行形态是「path 空 + customId」,导航行上的误配值不参与范围升级")
|
||||
.containsExactly(DataScope.SELF_SUBMITTED);
|
||||
|
||||
// 对照:同一用户同一值,改挂到 path 空行即升级——证明失败面只在"行形态",不是扫描整体失效
|
||||
misconfigured.grantCustomId(teacher.getId(), DataScopeService.SCOPE_ALL_CUSTOM_ID);
|
||||
assertThat(serviceWith(misconfigured).resolve(teacher, form(STUDENT_FACING)))
|
||||
.containsExactly(DataScope.ALL);
|
||||
}
|
||||
|
||||
// ==================== V28:任教班级在枚举里但一期不可选 ====================
|
||||
|
||||
@Test
|
||||
|
||||
@@ -34,6 +34,14 @@ public class InMemoryModuleTreeSource implements ModuleTreeSource {
|
||||
rows(userId).add(row(null, customId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 误配形态(P47 取证用):一条<em>会进导航</em>的页面路由行同时挂了 customId。
|
||||
* 契约 §8 的锚点行形态是「path 空 + customId」,这种行不该被当成锚点消费。
|
||||
*/
|
||||
public void grantPathWithCustomId(Integer userId, String path, String customId) {
|
||||
rows(userId).add(row(path, customId));
|
||||
}
|
||||
|
||||
/** 清空某人的授权(构造"角色是管理员但没被授权模块行"的反例) */
|
||||
public void clear(Integer userId) {
|
||||
byUser.remove(userId);
|
||||
|
||||
Reference in New Issue
Block a user