feat: align v12 operator role permissions

This commit is contained in:
WorkBuddy
2026-10-25 09:15:12 +08:00
parent dd291868fb
commit ec9252bd3d
5 changed files with 164 additions and 2 deletions
@@ -0,0 +1,5 @@
-- V072: 为操作员组织范围查询补充幂等索引。
-- 仅优化已有 nullable 外键列的查询;不改变存量组织归属,也不把 NULL 组织强行归组。
CREATE INDEX IF NOT EXISTS idx_operators_organization_id
ON operators (organization_id)
WHERE organization_id IS NOT NULL;
@@ -0,0 +1,41 @@
-- V073: 对齐 V1.2 其他运营角色的最小权限集合。
-- 仅按角色代码授予明确权限;不授予平台范围 analytics/data.export/permission.manage。
-- 幂等:重复执行不会产生重复 role_permissions。
-- reviewer:资料与本地资格审核,不进入红娘、咨询、活动或风险域。
INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM roles r CROSS JOIN permissions p
WHERE r.code = 'reviewer'
AND p.code IN ('review.view', 'review.process', 'local_eligibility.view', 'local_eligibility.review')
ON CONFLICT (role_id, permission_id) DO NOTHING;
-- activity_manager:活动流程及活动所需的非敏感用户基础信息。
INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM roles r CROSS JOIN permissions p
WHERE r.code = 'activity_manager'
AND p.code IN ('user.view', 'activity.view', 'activity.create', 'activity.review_registration', 'activity.checkin')
ON CONFLICT (role_id, permission_id) DO NOTHING;
-- risk_operator:举报处置及案件所需用户信息;敏感查看是风控专门授权。
INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM roles r CROSS JOIN permissions p
WHERE r.code = 'risk_operator'
AND p.code IN ('user.view', 'user.sensitive.view', 'risk.report.view', 'risk.report.process', 'risk.user.ban')
ON CONFLICT (role_id, permission_id) DO NOTHING;
-- organization_admin:仅本组织的运营管理权限;组织范围由服务端数据范围门禁执行。
-- 刻意不授 analytics.view:当前统计表缺少统一 organization_id,避免暴露全平台数据。
INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM roles r CROSS JOIN permissions p
WHERE r.code = 'organization_admin'
AND p.code IN (
'user.view', 'review.view', 'review.process',
'matchmaker.view', 'matchmaker.service',
'activity.view', 'activity.create', 'activity.review_registration',
'organization.manage'
)
ON CONFLICT (role_id, permission_id) DO NOTHING;
@@ -41,7 +41,12 @@ class MigrationValidationTest {
// 用户展示编号:V058 users.display_number 及历史数据回填。 // 用户展示编号:V058 users.display_number 及历史数据回填。
// 公共身份展示编号:V059 operators.display_id 及历史数据回填。 // 公共身份展示编号:V059 operators.display_id 及历史数据回填。
// 红娘协助对象与授权字段:V061 matchmaker_services 扩展。 // 红娘协助对象与授权字段:V061 matchmaker_services 扩展。
private static final int LATEST_MIGRATION = 61; // V1.2:V062-V066 账号生命周期、导入批次、服务授权、自主注册与建档权限;
// V067 工作台聚合权限与红娘角色边界;V068/V069 红娘工作台权限;
// V070 收回红娘的咨询、活动、预约、统计权限;
// V071 收回红娘的资料审核、本地资格审核权限。
// V073 对齐 reviewer、activity_manager、risk_operator、organization_admin 最小权限。
private static final int LATEST_MIGRATION = 73;
private static final String MIGRATION_DIR = private static final String MIGRATION_DIR =
"backend/src/main/resources/db/migration/"; "backend/src/main/resources/db/migration/";
@@ -77,7 +82,10 @@ class MigrationValidationTest {
assertTrue(version >= 1 && version <= LATEST_MIGRATION, assertTrue(version >= 1 && version <= LATEST_MIGRATION,
"版本号超出当前基线: " + name); "版本号超出当前基线: " + name);
} }
assertEquals(LATEST_MIGRATION, count, "迁移文件数量不符合当前基线"); // V072 为 Task 2 的可选范围约束迁移;未生成时允许 V073 合法跳号。
int expectedCount = Files.exists(Paths.get(MIGRATION_DIR, "V072__validate_matchmaker_organization_scope.sql"))
? LATEST_MIGRATION : LATEST_MIGRATION - 1;
assertEquals(expectedCount, count, "迁移文件数量不符合当前基线");
} }
} }
@@ -94,6 +102,7 @@ class MigrationValidationTest {
content.contains("CREATE INDEX") || content.contains("CREATE INDEX") ||
content.contains("CREATE UNIQUE INDEX") || content.contains("CREATE UNIQUE INDEX") ||
content.contains("INSERT INTO") || content.contains("INSERT INTO") ||
content.contains("DELETE FROM") ||
content.contains("CREATE TYPE"); content.contains("CREATE TYPE");
assertTrue(hasDDL || content.contains("占位"), assertTrue(hasDDL || content.contains("占位"),
"迁移文件缺少DDL语句且未声明为占位: " + file.getFileName()); "迁移文件缺少DDL语句且未声明为占位: " + file.getFileName());
@@ -0,0 +1,70 @@
package com.danyang.match.platform.deployment;
import org.junit.jupiter.api.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.junit.jupiter.api.Assertions.*;
/** Locks the minimum V1.2 permissions for non-admin operator roles. */
class OperatorRolePermissionContractTest {
private static final Path MIGRATION = Path.of(
"backend/src/main/resources/db/migration/V073__align_operator_role_permissions.sql");
@Test
void migrationIsIdempotentAndKeepsEachRoleScoped() throws Exception {
String sql = Files.readString(MIGRATION);
assertEquals(4, count(sql, "WHERE r.code = '"), "V073 应只处理四个正式运营角色");
assertEquals(4, count(sql, "ON CONFLICT (role_id, permission_id) DO NOTHING"),
"每个角色授权块必须幂等");
assertFalse(sql.contains("SELECT r.id, p.id FROM roles r CROSS JOIN permissions p\nWHERE p.code"),
"禁止无角色条件的批量授权");
assertFalse(sql.contains("p.code IN (SELECT"), "禁止通过全量权限子查询扩权");
}
@Test
void minimumPermissionsAndForbiddenBoundariesAreExplicit() throws Exception {
String sql = Files.readString(MIGRATION);
Map<String, String[]> required = Map.of(
"reviewer", new String[]{"review.view", "review.process", "local_eligibility.view", "local_eligibility.review"},
"activity_manager", new String[]{"user.view", "activity.view", "activity.create", "activity.review_registration", "activity.checkin"},
"risk_operator", new String[]{"user.view", "user.sensitive.view", "risk.report.view", "risk.report.process", "risk.user.ban"},
"organization_admin", new String[]{"user.view", "review.view", "review.process", "matchmaker.view", "matchmaker.service", "activity.view", "activity.create", "activity.review_registration", "organization.manage"}
);
for (Map.Entry<String, String[]> entry : required.entrySet()) {
String block = roleBlock(sql, entry.getKey());
for (String permission : entry.getValue()) {
assertTrue(block.contains("'" + permission + "'"),
entry.getKey() + " 缺少最小权限 " + permission);
}
assertFalse(block.contains("analytics.view"), entry.getKey() + " 不得获得平台统计权限");
assertFalse(block.contains("data.export"), entry.getKey() + " 不得获得数据导出权限");
assertFalse(block.contains("permission.manage"), entry.getKey() + " 不得获得权限管理权限");
}
assertFalse(sql.contains("r.code = 'super_admin'") || sql.contains("r.code = 'platform_admin'"),
"V073 不得修改管理员角色授权");
}
private static String roleBlock(String sql, String role) {
Pattern pattern = Pattern.compile("WHERE r\\.code = '" + Pattern.quote(role)
+ "'(.*?)ON CONFLICT", Pattern.DOTALL);
Matcher matcher = pattern.matcher(sql);
assertTrue(matcher.find(), "缺少角色授权块: " + role);
return matcher.group(1);
}
private static int count(String value, String needle) {
int count = 0;
int from = 0;
while ((from = value.indexOf(needle, from)) >= 0) {
count++;
from += needle.length();
}
return count;
}
}
+37
View File
@@ -0,0 +1,37 @@
# V1.2 Task 5 Completion Report
## Status
PARTIAL
## Implemented
- 新增 `V073__align_operator_role_permissions.sql`,以角色代码交叉查询幂等授予四类运营角色的最小权限:
- `reviewer`:资料/本地资格审核;
- `activity_manager`:活动流程及必要的基础用户信息;
- `risk_operator`:举报处置及风控案件所需信息;
- `organization_admin`:组织内运营管理,不授平台统计权限。
- 未授予 `analytics.view`、`data.export`、`permission.manage`,也未修改管理员角色或历史迁移。
- 新增 `OperatorRolePermissionContractTest`,锁定角色授权块、幂等性和禁止扩权边界。
- 更新迁移验证守卫以支持可选 V072 后的合法 V073 跳号。
- 补充 V072 的幂等组织索引,支持 Task 2 的组织范围查询且不改变存量数据。
- 管理端菜单/认证文件未修改;现有入口已按服务端权限码显隐,Task 4 文件保持不变。
## Automated Tests
```text
mvn -B -f backend/pom.xml -s backend/maven-settings.xml "-Dtest=com.danyang.match.platform.deployment.OperatorRolePermissionContractTest" test
2 tests passed, 0 failures, 0 errors
```
迁移命名/结构守卫已覆盖 V001-V073;当前未连接干净 PostgreSQL,因此未声称数据库迁移验收通过。
## Not Verified
- 未在干净 PostgreSQL/Redis 栈执行 Flyway V001-V073 和真实角色登录验证。
- 未执行完整后端测试、管理端构建或浏览器验收。
- `organization_admin` 的组织范围仍依赖后端数据范围门禁,迁移本身不提供跨组织隔离。
## Next Phase
Task 6:使用干净运行栈验证角色登录、菜单、工作台/详情范围和跨组织拒绝。