feat: align v12 operator role permissions
This commit is contained in:
+5
@@ -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;
|
||||||
+11
-2
@@ -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());
|
||||||
|
|||||||
+70
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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:使用干净运行栈验证角色登录、菜单、工作台/详情范围和跨组织拒绝。
|
||||||
Reference in New Issue
Block a user