fix: 系统性预防角色测试高频问题(5 方案落地)
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

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:
iven
2026-05-08 08:52:16 +08:00
parent 645ec39e8b
commit c82f7bda1d
11 changed files with 594 additions and 90 deletions

178
tools/check_permissions.py Normal file
View 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()