#!/usr/bin/env python """ 遍历 wwwroot 下所有通过 ln -s 链接进来的目录, 找出没有任何角色拥有权限的文件。 用法: ./py3/bin/python find_unauth_files.py [wwwroot_path] 示例: # 默认 sage/wwwroot ./py3/bin/python find_unauth_files.py # 指定路径 ./py3/bin/python find_unauth_files.py /home/hermesai/repos/sage/wwwroot 说明: - 只扫描 wwwroot 下直接子目录中是符号链接的目录(ln -s 指向的) - 跟随符号链接遍历(followlinks=True),防环 - 检查每个文件是否在 permission 表中有任意角色关联 - 输出未授权文件清单 注意:此脚本只读数据库,不做任何修改。 """ import os import sys import asyncio sage_root = os.environ.get('SAGE_ROOT') if sage_root and sage_root not in sys.path: sys.path.insert(0, sage_root) from sqlor.dbpools import DBPools from appPublic.jsonConfig import getConfig def find_symlink_dirs(wwwroot): """找出 wwwroot 下直接子目录中是符号链接的目录。""" symlinks = [] for entry in sorted(os.listdir(wwwroot)): full = os.path.join(wwwroot, entry) if os.path.islink(full) and os.path.isdir(full): target = os.readlink(full) symlinks.append((entry, full, target)) return symlinks def walk_symlink_dirs(wwwroot, symlinks): """ 遍历所有符号链接目录下的文件,返回 (relative_path, absolute_path) 列表。 relative_path 是相对于 wwwroot 的路径,格式如 /harnessed_agent/index.ui 防环:跟踪已访问的真实路径。 """ real_wwwroot = os.path.realpath(wwwroot) visited_real = set() files = [] for name, link_path, target in symlinks: for root, dirs, filenames in os.walk(link_path, followlinks=True): real_root = os.path.realpath(root) # 防环:回到 wwwroot 自身或其他已访问路径 if real_root in visited_real: dirs.clear() continue visited_real.add(real_root) # 排除指向 wwwroot 自身的子目录 dirs[:] = [d for d in dirs if os.path.realpath(os.path.join(root, d)) != real_wwwroot] for fname in sorted(filenames): abs_path = os.path.join(root, fname) # 相对于 wwwroot 的路径,带前导 / rel = '/' + os.path.relpath(abs_path, wwwroot) files.append((rel, abs_path)) return files async def main(): wwwroot = sys.argv[1] if len(sys.argv) > 1 else None if not wwwroot: # 默认: sage_root/wwwroot if sage_root: wwwroot = os.path.join(sage_root, 'wwwroot') else: # 尝试从当前目录推断 wwwroot = os.path.join(os.getcwd(), 'wwwroot') wwwroot = os.path.abspath(wwwroot) if not os.path.isdir(wwwroot): print(f"错误: wwwroot 目录不存在: {wwwroot}") sys.exit(1) print(f"wwwroot: {wwwroot}") print(f"SAGE_ROOT: {sage_root or '(未设置)'}") # 1. 找符号链接目录 symlinks = find_symlink_dirs(wwwroot) if not symlinks: print("\n未发现任何符号链接目录。") sys.exit(0) print(f"\n发现 {len(symlinks)} 个符号链接目录:") for name, full, target in symlinks: print(f" {name}/ -> {target}") # 2. 遍历所有文件 all_files = walk_symlink_dirs(wwwroot, symlinks) print(f"\n共 {len(all_files)} 个文件,开始检查权限...\n") # 3. 批量加载所有 permission + rolepermission 记录到内存 config = getConfig('.') db = DBPools(config.databases) async with db.sqlorContext('sage') as sor: # 加载所有 permission 记录 perm_recs = await sor.sqlExe("SELECT id, path FROM permission") # 构建 path -> perm_id 映射 path_to_permid = {} for r in perm_recs: path_to_permid[r.path] = r.id # 加载所有 rolepermission 记录 rp_recs = await sor.sqlExe("SELECT permid FROM rolepermission") # 构建 perm_id -> 有权限 的标记 perms_with_roles = set() for r in rp_recs: perms_with_roles.add(r.permid) print(f" permission 表共 {len(path_to_permid)} 条记录") print(f" 有角色关联的 permission 共 {len(perms_with_roles)} 条") # 4. 检查每个文件 unauth_files = [] authed_files = [] for rel_path, abs_path in all_files: permid = path_to_permid.get(rel_path) if permid is None: # 路径完全未注册 unauth_files.append((rel_path, abs_path, "路径未注册")) elif permid not in perms_with_roles: # 路径有 permission 记录但没有任何角色关联 unauth_files.append((rel_path, abs_path, "有记录但无角色")) else: authed_files.append(rel_path) # 5. 输出结果 print(f"\n{'='*80}") print(f"结果汇总:") print(f" 已授权文件: {len(authed_files)}") print(f" 未授权文件: {len(unauth_files)}") print(f"{'='*80}") if not unauth_files: print("\n所有文件均有权限覆盖,无需处理。") return print(f"\n未授权文件清单 ({len(unauth_files)} 个):\n") # 按模块目录分组 from collections import defaultdict by_module = defaultdict(list) for rel_path, abs_path, reason in unauth_files: # 提取模块名: /harnessed_agent/xxx -> harnessed_agent parts = rel_path.strip('/').split('/') module = parts[0] if parts else 'root' by_module[module].append((rel_path, abs_path, reason)) for module in sorted(by_module.keys()): items = by_module[module] print(f"\n--- {module}/ ({len(items)} 个文件) ---") for rel_path, abs_path, reason in items: print(f" [{reason}] {rel_path}") # 也输出纯路径列表(方便管道处理) print(f"\n{'='*80}") print("纯路径列表(方便复制):") for rel_path, abs_path, reason in unauth_files: print(f" {rel_path}") if __name__ == '__main__': asyncio.get_event_loop().run_until_complete(main())