#!/usr/bin/env python3 # -*- coding: utf-8 -*- """pbl_common 模块 RBAC / 路径声明脚本(per-module,中央层 apps/pbls 聚合)。 用法: python3 load_path.py # 找到 Sage 根时逐条 set_role_perm 登记;找不到则打印跳过说明 python3 load_path.py --check # 离线自检:无通配符 + 声明与磁盘 wwwroot 一致 ═══════════════════════════════════════════════════════════════════════════════ 里程碑 RBAC 声明台账(每个里程碑必须显式登记一行,禁止留空/待补) ═══════════════════════════════════════════════════════════════════════════════ M11b-2(T3,2026-09-22): 结论 = 本里程碑新增路径 0 条(M11b-2 只交付库函数与表定义,见 world_sync 侧台账)。 顺带闭环(QC 退回意见 #2「同类缺陷未闭环」): 本模块此前 scripts/load_path.py 只有一行注释、**没有任何 PATHS 常量**,中央聚合器 把它当「empty (skipped)」静默跳过;而磁盘 wwwroot 下真实存在 3 个 Web 文件 (index.ui + api/pbl_common_audit.dspy + api/pbl_common_ping.dspy), 即「文件在、路由在、RBAC 不在」= 点进去一律 403 的同类缺口。 本次补齐**逐条显式**登记,并把「声明与磁盘一致」做进 --check 门禁: 磁盘有 Web 文件而未登记 → rc=1;登记了而磁盘没有 → rc=1。 铁律 = 禁通配符(% / *),所有路径逐条显式;新增路径须在本台账追加一行, 并确认模块名在中央 apps/pbls/scripts/load_path.py 的 MODULES 中(聚合式双写)。 ═══════════════════════════════════════════════════════════════════════════════ """ import os import sys MODULE = 'pbl_common' MODULE_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) WWWROOT = os.path.join(MODULE_ROOT, 'wwwroot') # ── any 层:无需登录即可访问的公共资源(本模块无)──────────────────────────── PATHS_ANY = [] # ── logined 层:模块首页 + api/*.dspy(与磁盘 wwwroot 一一对应,逐条显式)──── PATHS_LOGINED = [ '/{m}'.format(m=MODULE), '/{m}/index.ui'.format(m=MODULE), '/{m}/api/pbl_common_audit.dspy'.format(m=MODULE), '/{m}/api/pbl_common_ping.dspy'.format(m=MODULE), ] # ── owner.* 层:当前无受限路径,显式声明为空(避免「漏登记」被误判为「待补」)─ PATHS_OWNER_SUPERUSER = [] PATHS_OWNER_OPERATOR = [] # ── 兼容扁平写法(中央聚合器两种写法都支持)──────────────────────────────── PATHS = [(p, 'logined') for p in PATHS_LOGINED] # 本模块**有** Web 路径,故不声明 NO_WEB_PATHS(该常量仅用于纯库函数模块) NO_WEB_PATHS = False # ── 里程碑声明台账(供 --check 回显,也是 QC 可 grep 的硬证据)──────────────── MILESTONE_DECLARATIONS = [ { 'milestone': 'M11b-2', 'new_paths': 0, 'declaration': ( 'M11b-2 本里程碑不新增 .dspy/.ui/.js/.css 路径,仅提供库函数' '(world_sync / pbl_runtime_ext 的 *.py 与 models/*.json),' '故无 RBAC 登记项;禁通配符(%/*),' '如有路径须按 any/logined/owner.* 分层逐条登记。' '(本模块存量 4 条路径于 T3 补齐登记,闭环「有文件无 RBAC」的 403 缺口。)' ), }, ] FORBIDDEN_WILDCARDS = ('%', '*') WEB_SUFFIXES = ('.dspy', '.ui', '.js', '.css') def _tiers(): return (('any', PATHS_ANY), ('logined', PATHS_LOGINED), ('owner.superuser', PATHS_OWNER_SUPERUSER), ('owner.operator', PATHS_OWNER_OPERATOR)) def _assert_no_wildcard(): """通配符硬门禁:任何一层路径出现 % 或 * 直接判失败(非静默)。""" bad = [t + ':' + p for t, ps in _tiers() for p in ps if any(w in p for w in FORBIDDEN_WILDCARDS)] if bad: print('[%s] FAIL 路径声明含禁用通配符: %s' % (MODULE, bad)) return 1 print('[%s] OK 通配符检查通过(PATHS_ANY/LOGINED/OWNER.* 均为逐条显式路径,无 %% / *)' % MODULE) return 0 def _declared_paths(): out = set() for _t, ps in _tiers(): out.update(ps) return out def _disk_web_paths(): """磁盘 wwwroot 下真实 Web 文件对应的 URL 路径集合(含模块根目录本身)。""" found = set() if os.path.isdir(WWWROOT): for base, _dirs, files in os.walk(WWWROOT): for fn in files: if fn.endswith(WEB_SUFFIXES): rel = os.path.relpath(os.path.join(base, fn), WWWROOT) found.add('/' + MODULE + '/' + rel.replace(os.sep, '/')) if found: found.add('/' + MODULE) # 目录访问(走 index.ui) return found def _consistency(): """声明 ↔ 磁盘 一致性门禁:缺登记或多登记都判 FAIL。""" declared, on_disk = _declared_paths(), _disk_web_paths() rc = 0 missing = sorted(on_disk - declared) extra = sorted(declared - on_disk) if missing: print('[%s] FAIL 磁盘存在 Web 文件但未登记 RBAC(点击即 403): %s' % (MODULE, missing)) rc = 1 else: print('[%s] OK 磁盘 %d 个 Web 路径已全部登记,无遗漏' % (MODULE, len(on_disk))) if extra: print('[%s] FAIL 登记了磁盘上不存在的多余路径: %s' % (MODULE, extra)) rc = 1 return rc def check(): print('=' * 78) print('[%s] RBAC 路径声明自检 (--check) module=%s' % (MODULE, MODULE)) print('=' * 78) rc = _assert_no_wildcard() print('\n[声明台账]') for d in MILESTONE_DECLARATIONS: print(' - %s: 新增路径 %d 条 -> %s' % (d['milestone'], d['new_paths'], d['declaration'])) print('\n[路径登记(逐条显式)]') for tier, paths in _tiers(): print(' tier=%s count=%d' % (tier, len(paths))) for p in paths: print(' %s' % p) rc |= _consistency() total = len(_declared_paths()) print('\n[汇总] 登记路径合计 %d 条;M11b-2 新增 0 条(本次为存量补齐登记)' % total) print('[%s] --check 结论: %s' % (MODULE, 'PASS' if rc == 0 else 'FAIL')) return rc def _find_sage_root(): for c in (os.path.expanduser('~/repos/sage'), os.path.expanduser('~/sage'), '/d/ymq/repos/sage'): if os.path.isdir(os.path.join(c, 'wwwroot')) and \ os.path.isdir(os.path.join(c, 'py3', 'bin')): return c return None def main(): if '--check' in sys.argv[1:]: return check() root = _find_sage_root() if not root: print('[%s] 未找到 Sage 根目录,跳过(中央 apps/pbls/scripts/load_path.py 会聚合登记)' % MODULE) print('[%s] 本里程碑 M11b-2 无新路径登记;本次补齐的是存量 %d 条路径。' % (MODULE, len(_declared_paths()))) print('[%s] 可执行 `python3 load_path.py --check` 离线校验声明合规性与磁盘一致性。' % MODULE) return 0 sys.path.insert(0, root) try: from set_role_perm import set_role_perm except ImportError: print('[%s] set_role_perm 不可用,请改用中央 apps/pbls/scripts/load_path.py' % MODULE) return 0 rc = _assert_no_wildcard() if rc != 0: return rc for tier, paths in _tiers(): for p in paths: set_role_perm(p, tier) print('[%s] 已注册 %d 条 RBAC 路径(M11b-2 新增 0 条,全部为存量补齐)' % (MODULE, len(_declared_paths()))) return 0 if __name__ == '__main__': sys.exit(main())