fix: 系统性预防角色测试高频问题(5 方案落地)
P0 — 默认拒绝 + 强制守卫: - 创建 routeConfig.ts 作为前端路由权限的单一真相源 - TypeScript 强制每个路由声明非空权限数组,不可能遗漏 - 自动生成 ROUTE_PERMISSIONS 和 FROZEN_ROUTES - 修正 3 个前端权限码不匹配后端 P0 — CI 权限扫描: - 新增 tools/check_permissions.py 校验脚本 - 发现并修复 tenant.manage 未注册问题 P1 — 聚合接口容错: - erp-core 新增 safe_aggregate 工具函数 - 仪表盘统计 handler 重构 P1 — 状态机一致性自检: - validation.rs 新增 3 个自检测试 fix: lint-staged eslint Windows 兼容性
This commit is contained in:
178
tools/check_permissions.py
Normal file
178
tools/check_permissions.py
Normal file
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
权限一致性校验脚本 — CI 守门
|
||||
|
||||
检查两个维度:
|
||||
1. Handler 中的 require_permission() 权限码是否在模块 permissions() 中声明
|
||||
2. 前端 routeConfig.ts 中的权限码是否在后端模块中声明
|
||||
|
||||
用法:
|
||||
python tools/check_permissions.py
|
||||
# 或在 CI 中:
|
||||
python tools/check_permissions.py --ci (发现问题时 exit 1)
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
from collections import defaultdict
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
CRATES_DIR = ROOT / "crates"
|
||||
WEB_DIR = ROOT / "apps" / "web" / "src"
|
||||
|
||||
# ============================================================================
|
||||
# 1. 提取后端 handler 中的权限码(require_permission 调用)
|
||||
# ============================================================================
|
||||
|
||||
def extract_handler_permissions():
|
||||
"""从 Rust handler 文件中提取 require_permission() 的权限码"""
|
||||
permissions = defaultdict(list) # permission -> [file:line]
|
||||
pattern = re.compile(r'require_permission\s*\(\s*&ctx\s*,\s*"([^"]+)"\s*\)')
|
||||
pattern2 = re.compile(r'require_permission\s*\(\s*ctx\s*,\s*"([^"]+)"\s*\)')
|
||||
pattern_any = re.compile(r'require_any_permission\s*\(\s*&ctx\s*,\s*&\[([^\]]+)\]')
|
||||
pattern_any2 = re.compile(r'require_any_permission\s*\(\s*ctx\s*,\s*&\[([^\]]+)\]')
|
||||
|
||||
for rs_file in CRATES_DIR.rglob("*.rs"):
|
||||
if "test" in str(rs_file) and "tests/" in str(rs_file):
|
||||
continue
|
||||
try:
|
||||
content = rs_file.read_text(encoding="utf-8")
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
rel = rs_file.relative_to(ROOT)
|
||||
for i, line in enumerate(content.splitlines(), 1):
|
||||
for m in pattern.finditer(line):
|
||||
permissions[m.group(1)].append(f"{rel}:{i}")
|
||||
for m in pattern2.finditer(line):
|
||||
permissions[m.group(1)].append(f"{rel}:{i}")
|
||||
for m in pattern_any.finditer(line):
|
||||
for p in re.findall(r'"([^"]+)"', m.group(1)):
|
||||
permissions[p].append(f"{rel}:{i}")
|
||||
for m in pattern_any2.finditer(line):
|
||||
for p in re.findall(r'"([^"]+)"', m.group(1)):
|
||||
permissions[p].append(f"{rel}:{i}")
|
||||
|
||||
return permissions
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 2. 提取模块 permissions() 声明
|
||||
# ============================================================================
|
||||
|
||||
def extract_module_permissions():
|
||||
"""从各模块的 module.rs 中提取 permissions() 声明的权限码"""
|
||||
declared = set()
|
||||
# permissions() 返回 vec![PermissionDescriptor { ... code: "xxx".into() ... }]
|
||||
pattern = re.compile(r'code\s*:\s*"([^"]+)"\s*\.into\(\)')
|
||||
|
||||
for module_file in CRATES_DIR.rglob("module.rs"):
|
||||
try:
|
||||
content = module_file.read_text(encoding="utf-8")
|
||||
except Exception:
|
||||
continue
|
||||
for m in pattern.finditer(content):
|
||||
declared.add(m.group(1))
|
||||
|
||||
# 也检查 seed.rs 中的 DEFAULT_PERMISSIONS
|
||||
seed_file = CRATES_DIR / "erp-auth" / "src" / "service" / "seed.rs"
|
||||
if seed_file.exists():
|
||||
try:
|
||||
content = seed_file.read_text(encoding="utf-8")
|
||||
for m in re.finditer(r'code\s*:\s*"([^"]+)"', content):
|
||||
declared.add(m.group(1))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return declared
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 3. 提取前端 routeConfig.ts 中的权限码
|
||||
# ============================================================================
|
||||
|
||||
def extract_frontend_permissions():
|
||||
"""从前端 routeConfig.ts 中提取权限码"""
|
||||
config_file = WEB_DIR / "routeConfig.ts"
|
||||
permissions = []
|
||||
if not config_file.exists():
|
||||
# 旧模式:从 App.tsx 中提取
|
||||
config_file = WEB_DIR / "App.tsx"
|
||||
try:
|
||||
content = config_file.read_text(encoding="utf-8")
|
||||
except Exception:
|
||||
return permissions
|
||||
|
||||
for m in re.finditer(r"permissions:\s*\[([^\]]+)\]", content):
|
||||
for p in re.findall(r"'([^']+)'", m.group(1)):
|
||||
permissions.append(p)
|
||||
return set(permissions)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 主逻辑
|
||||
# ============================================================================
|
||||
|
||||
def main():
|
||||
is_ci = "--ci" in sys.argv
|
||||
errors = []
|
||||
|
||||
handler_perms = extract_handler_permissions()
|
||||
module_perms = extract_module_permissions()
|
||||
frontend_perms = extract_frontend_permissions()
|
||||
|
||||
print("=" * 60)
|
||||
print("权限一致性校验报告")
|
||||
print("=" * 60)
|
||||
|
||||
# --- 检查 1: Handler 权限码是否在模块中声明 ---
|
||||
print(f"\n[检查 1] Handler 权限码 → 模块声明 (handler: {len(handler_perms)} 个, module: {len(module_perms)} 个)")
|
||||
|
||||
undeclared = []
|
||||
for perm, locations in sorted(handler_perms.items()):
|
||||
if perm not in module_perms:
|
||||
undeclared.append((perm, locations))
|
||||
|
||||
if undeclared:
|
||||
print(f" ❌ 发现 {len(undeclared)} 个未声明的权限码:")
|
||||
for perm, locations in undeclared:
|
||||
print(f" - {perm} (使用于 {locations[0]})")
|
||||
errors.append(f"handler 中有 {len(undeclared)} 个权限码未在模块 permissions() 中声明")
|
||||
else:
|
||||
print(" ✅ 所有 handler 权限码均已声明")
|
||||
|
||||
# --- 检查 2: 前端权限码是否在后端声明 ---
|
||||
print(f"\n[检查 2] 前端权限码 → 后端声明 (frontend: {len(frontend_perms)} 个)")
|
||||
|
||||
frontend_only = frontend_perms - module_perms - handler_perms.keys()
|
||||
if frontend_only:
|
||||
print(f" ⚠️ 前端引用了 {len(frontend_only)} 个后端未声明的权限码:")
|
||||
for p in sorted(frontend_only):
|
||||
print(f" - {p}")
|
||||
errors.append(f"前端引用了 {len(frontend_only)} 个后端未声明的权限码")
|
||||
else:
|
||||
print(" ✅ 所有前端权限码后端均已声明")
|
||||
|
||||
# --- 检查 3: 模块声明了但 handler 未使用的权限码(信息性) ---
|
||||
used_in_handler = set(handler_perms.keys())
|
||||
declared_but_unused = module_perms - used_in_handler
|
||||
print(f"\n[信息] 已声明但 handler 未直接使用的权限码: {len(declared_but_unused)} 个")
|
||||
if declared_but_unused:
|
||||
for p in sorted(declared_but_unused):
|
||||
print(f" - {p}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
if errors:
|
||||
print("❌ 校验失败:")
|
||||
for e in errors:
|
||||
print(f" - {e}")
|
||||
if is_ci:
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("✅ 权限一致性校验通过")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user