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 及历史数据回填。
|
||||
// 公共身份展示编号:V059 operators.display_id 及历史数据回填。
|
||||
// 红娘协助对象与授权字段: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 =
|
||||
"backend/src/main/resources/db/migration/";
|
||||
@@ -77,7 +82,10 @@ class MigrationValidationTest {
|
||||
assertTrue(version >= 1 && version <= LATEST_MIGRATION,
|
||||
"版本号超出当前基线: " + 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 UNIQUE INDEX") ||
|
||||
content.contains("INSERT INTO") ||
|
||||
content.contains("DELETE FROM") ||
|
||||
content.contains("CREATE TYPE");
|
||||
assertTrue(hasDDL || content.contains("占位"),
|
||||
"迁移文件缺少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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user