Files
hms/tools/check_permissions.py
iven c82f7bda1d
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
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 兼容性
2026-05-08 08:52:16 +08:00

179 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()