#!/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()