# -*- coding: utf-8 -*- """pbl_blueprint RBAC 路径注册清单(显式枚举,无 %/* 通配符)。 本文件是 RBAC 路径的唯一声明源:init.py 的 RBAC_PATHS 必须与此处 PATHS 完全一致, selfcheck.py 会逐条比对(不一致即退出码非 0)。 用法: python3 modules/pbl_blueprint/scripts/load_path.py # 打印清单 python3 modules/pbl_blueprint/scripts/load_path.py --check # 与 init.py 比对 """ import os import sys MODULE_NAME = "pbl_blueprint" # 蓝图主表 PATHS_BLUEPRINT = ( "/api/pbl_blueprint/list.dspy", "/api/pbl_blueprint/create.dspy", "/api/pbl_blueprint/get.dspy", "/api/pbl_blueprint/update.dspy", "/api/pbl_blueprint/delete.dspy", "/api/pbl_blueprint/tree.dspy", "/api/pbl_blueprint/fork.dspy", "/api/pbl_blueprint/forks.dspy", "/api/pbl_blueprint/stats.dspy", ) # 子对象节点 PATHS_NODE = ( "/api/pbl_blueprint_node/list.dspy", "/api/pbl_blueprint_node/create.dspy", "/api/pbl_blueprint_node/get.dspy", "/api/pbl_blueprint_node/update.dspy", "/api/pbl_blueprint_node/delete.dspy", ) # 关系边 PATHS_EDGE = ( "/api/pbl_blueprint_edge/list.dspy", "/api/pbl_blueprint_edge/create.dspy", "/api/pbl_blueprint_edge/get.dspy", "/api/pbl_blueprint_edge/update.dspy", "/api/pbl_blueprint_edge/delete.dspy", ) # 版本与差异 PATHS_VERSION = ( "/api/pbl_blueprint_version/list.dspy", "/api/pbl_blueprint_version/get.dspy", "/api/pbl_blueprint_version/save.dspy", "/api/pbl_blueprint_version/rollback.dspy", "/api/pbl_blueprint_version_delta/list.dspy", "/api/pbl_blueprint_version_delta/get.dspy", ) # 模板 PATHS_TEMPLATE = ( "/api/pbl_blueprint_template/list.dspy", "/api/pbl_blueprint_template/create.dspy", "/api/pbl_blueprint_template/get.dspy", "/api/pbl_blueprint_template/update.dspy", "/api/pbl_blueprint_template/delete.dspy", "/api/pbl_blueprint_template/instantiate.dspy", ) # 发布 PATHS_PUBLISH = ( "/api/pbl_blueprint_publish/list.dspy", "/api/pbl_blueprint_publish/create.dspy", "/api/pbl_blueprint_publish/get.dspy", "/api/pbl_blueprint_publish/revoke.dspy", ) # 离线包 PATHS_OFFLINE = ( "/api/pbl_blueprint_offline/list.dspy", "/api/pbl_blueprint_offline/export.dspy", "/api/pbl_blueprint_offline/import.dspy", "/api/pbl_blueprint_offline/get.dspy", ) # 血缘 PATHS_FORK = ( "/api/pbl_blueprint_fork/list.dspy", "/api/pbl_blueprint_fork/get.dspy", ) # 编辑锁 PATHS_LOCK = ( "/api/pbl_blueprint_lock/lock.dspy", "/api/pbl_blueprint_lock/unlock.dspy", "/api/pbl_blueprint_lock/list.dspy", "/api/pbl_blueprint_lock/clean_expired.dspy", ) # 审计(只读) PATHS_AUDIT = ( "/api/pbl_blueprint_audit/list.dspy", "/api/pbl_blueprint_audit/get.dspy", ) PATHS = ( PATHS_BLUEPRINT + PATHS_NODE + PATHS_EDGE + PATHS_VERSION + PATHS_TEMPLATE + PATHS_PUBLISH + PATHS_OFFLINE + PATHS_FORK + PATHS_LOCK + PATHS_AUDIT ) # 只读路径(RBAC 上归为 read 权限组) READONLY_PATHS = tuple(p for p in PATHS if p.rsplit("/", 1)[-1].split(".")[0] in ("list", "get", "tree", "forks", "stats")) WRITE_PATHS = tuple(p for p in PATHS if p not in READONLY_PATHS) def validate(paths=None): """校验路径合法性:非空、以 / 开头、.dspy 结尾、无通配符、无重复。返回错误列表。""" ps = list(paths if paths is not None else PATHS) errs = [] seen = set() for p in ps: if not p or not isinstance(p, str): errs.append("空路径或非字符串: %r" % (p,)) continue if not p.startswith("/api/"): errs.append("路径必须以 /api/ 开头: %s" % p) if not p.endswith(".dspy"): errs.append("路径必须以 .dspy 结尾: %s" % p) if "%" in p or "*" in p or "?" in p: errs.append("路径含通配符(禁止): %s" % p) if " " in p: errs.append("路径含空格: %s" % p) if p in seen: errs.append("路径重复: %s" % p) seen.add(p) return errs def check_against_init(): """与 pbl_blueprint/init.py 的 RBAC_PATHS 逐条比对,返回 (ok, diff_msg)。""" root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) if root not in sys.path: sys.path.insert(0, root) try: from pbl_blueprint import init as _init # noqa except Exception as e: return False, "无法导入 pbl_blueprint.init: %s" % e a = list(PATHS) b = list(getattr(_init, "RBAC_PATHS", ())) if a == b: return True, "RBAC 路径一致(%d 条,顺序相同)" % len(a) only_a = [x for x in a if x not in b] only_b = [x for x in b if x not in a] return False, "RBAC 路径不一致: 仅 load_path=%s / 仅 init=%s / 顺序差异=%s" % ( only_a, only_b, a != b and not only_a and not only_b) def main(argv=None): argv = list(sys.argv[1:] if argv is None else argv) if "--check" in argv: errs = validate() good, msg = check_against_init() for e in errs: print("[PATH-ERR] %s" % e) print("[CHECK] %s" % msg) print("[STATS] total=%d readonly=%d write=%d" % (len(PATHS), len(READONLY_PATHS), len(WRITE_PATHS))) return 0 if (good and not errs) else 1 print("# %s RBAC 路径清单(显式枚举,共 %d 条,无通配符)" % (MODULE_NAME, len(PATHS))) for p in PATHS: print(p) errs = validate() if errs: for e in errs: print("[PATH-ERR] %s" % e, file=sys.stderr) return 1 return 0 if __name__ == "__main__": sys.exit(main())