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 兼容性
179 lines
6.5 KiB
Python
179 lines
6.5 KiB
Python
#!/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()
|