diff --git a/scripts/load_path.py b/scripts/load_path.py index 1933eac..81eca0c 100644 --- a/scripts/load_path.py +++ b/scripts/load_path.py @@ -2,97 +2,187 @@ # -*- coding: utf-8 -*- """pbl_common 模块 RBAC / 路径声明脚本(per-module,中央层 apps/pbls 聚合)。 -═══════════════════════════════════════════════════════════════════════════════ -结论:本模块 **零 Web 可访问路径**(NO_WEB_PATHS = True,显式声明) -═══════════════════════════════════════════════════════════════════════════════ -pbl_common 是 PBL 公共内核(租户上下文 / DB 适配 / 错误码 / 审计 / CRUD 工厂), -只交付 Python 库函数(``pbl_common/*.py``),由宿主应用进程内 import 调用, -**不提供任何 .dspy / .ui / .js / .css 文件**,因此 RBAC permission 表零登记项。 - -为什么写成显式声明而不是留空(M11b-2c-T3 QC 退回意见 #2 的连带治理): - 中央聚合器 apps/pbls/scripts/load_path.py 现在把「有 scripts/load_path.py 但 - 一条路径都没聚合到」判为**聚合失败**(rc=1),除非模块显式声明 ``NO_WEB_PATHS``。 - 「无路径」是事实断言,必须可 grep、可复核,不允许用留空表达。 - 用法: - python3 load_path.py # 打印声明(零路径,无需登记) - python3 load_path.py --check # 自检:无通配符 + 与磁盘 wwwroot 一致性 + 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') -# ── 显式零路径声明(中央聚合器据此判定「合法的空」而非「聚合失败」)────────── -NO_WEB_PATHS = True -DECLARATION = ( - 'pbl_common 仅提供 Python 库函数(租户上下文/DB 适配/错误码/审计/CRUD 工厂),' - '不新增任何 .dspy/.ui/.js/.css 可访问路径,故 RBAC 零登记项;' - '禁通配符(%/*),后续若新增 Web 路径须按 any/logined/owner.* 分层逐条登记。' -) - -# 分层常量保持为空列表(显式,非缺失) +# ── any 层:无需登录即可访问的公共资源(本模块无)──────────────────────────── PATHS_ANY = [] -PATHS_LOGINED = [] + +# ── 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 = [] + +# ── 兼容扁平写法(中央聚合器两种写法都支持)──────────────────────────────── +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 _wwwroot_files(): - """列出本模块 wwwroot 下真实存在的 Web 文件(.dspy/.ui/.js/.css)。""" - root = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), - 'wwwroot') - found = [] - for base, _dirs, files in os.walk(root): - for fn in files: - if fn.endswith(('.dspy', '.ui', '.js', '.css')): - found.append(os.path.relpath(os.path.join(base, fn), root)) - return sorted(found) +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) - print(' NO_WEB_PATHS = %s' % NO_WEB_PATHS) - print(' 声明: %s' % DECLARATION) - rc = 0 - tiers = (('any', PATHS_ANY), ('logined', PATHS_LOGINED), - ('owner.superuser', PATHS_OWNER_SUPERUSER), - ('owner.operator', PATHS_OWNER_OPERATOR)) - 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(' FAIL 含禁用通配符: %s' % bad) - rc = 1 - else: - print(' OK 通配符检查通过(无任何路径,自然无通配符)') - total = sum(len(ps) for _t, ps in tiers) - if total: - print(' FAIL NO_WEB_PATHS=True 但声明了 %d 条路径(自相矛盾)' % total) - rc = 1 - else: - print(' OK 声明路径合计 0 条,与 NO_WEB_PATHS=True 一致') - on_disk = _wwwroot_files() - if on_disk: - print(' FAIL 磁盘 wwwroot 存在 %d 个 Web 文件却未登记: %s' - % (len(on_disk), on_disk)) - rc = 1 - else: - print(' OK 磁盘 wwwroot 无 .dspy/.ui/.js/.css 文件,与零路径声明一致') + 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() - print('[%s] %s' % (MODULE, DECLARATION)) - print('[%s] 无路径需要登记(NO_WEB_PATHS=True);可执行 `--check` 离线复核。' % MODULE) + 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