pbl_compiler/scripts/load_path.py
agent.develop cb1132e6a9 fix(pbl_compiler): M3b RBAC 29条显式登记+CRUD只读拒绝端点+json根键统一+audit BinOp解析修复
- scripts/load_path.py: PATHS_CONTRACTS 20 条(含 M3b 5 个导出契约) + PATHS_READONLY_DENY 9 条
  = 29 条全显式登记,无通配符;与 wwwroot/api/*.dspy(29) 一一对应
- wwwroot/api/: 新增 9 个 CRUD 只读拒绝端点(create/update/delete × 3 表),
  封死 xls2ui 回退默认写入口,保 rules_hash 29.6 确定性指纹唯一写入路径
- json/*.json: 根级 browserfields 合并进 params,三文件统一 tblname/alias/title/params 根键集,
  editable 齐备 new_/update_/delete_data_url 且指向真实 .dspy
- scripts/audit_rbac_parity.py: 修复 PATHS = A + B (BinOp) 解析取空的自身缺陷(假 FAIL 根因),
  新增 json/ 根键白名单 + editable 三 URL + 幽灵/漏登记 检查
- pbl_compiler/init.py: 文件头注释登记条数与代码事实对齐(29=20+9)
- 实跑: audit_rbac_parity.py rc=0 PASS;test_m3b_mapping.py PASS=113/FAIL=0
2026-09-19 11:04:55 +08:00

141 lines
7.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""pbl_compiler RBAC 路径注册(硬门禁 6.6 / QC 第四章 RBAC
约定:
- 路径 = 模块自动路由 `/pbl_compiler/api/<契约>.dspy`,不带端口、不带 /wss 前缀;
- 角色 `logined` = 登录即可访问;
- 由 apps/pbls/build.sh 第 8 步调用 `register()`rbac CLI 不在位时打印清单(不静默跳过);
- **禁止通配符**(规范硬规定),每条 .dspy 显式登记。
两个登记集合(合计 29 条,全部显式):
PATHS_CONTRACTS —— 20 条Web 契约端点,与四处事实源一一对应
① 实现pbl_compiler/api.py15 个 M3a 契约)+ pbl_compiler/rules_export_api.py5 个 M3b 契约)
② 导出pbl_compiler/__init__.py import + __all__
③ 注册pbl_compiler/init.py CONTRACTS共 20 条)
④ 端点wwwroot/api/<契约名>.dspy共 20 个)
PATHS_READONLY_DENY —— 9 条CRUD 只读拒绝端点QC #2 方案 A
json/*.json 三张表pbl_game_definition / pbl_capability_registry /
pbl_compiler_version都是编译产物或需经专用契约写入的注册表
按 crud-definition-spec 要求 editable 必须齐备 new_/update_/delete_data_url
否则 xls2ui 回退生成默认 add_/update_/delete_<table>.dspy —— 等于绕过
gd_rules_writer / pbl_capability_register / pbl_compiler_version_register
唯一写入口rules_hash29.6 确定性指纹)可被手工改写。
故为 3 张表 × 3 个写操作各建一个显式返回 Error 的 .dspy共 9 个),
它们不是 ServerEnv 契约(不进 CONTRACTS/__init__.py只需 RBAC 登记。
关于 CRUD 生成目录(/pbl_compiler/<alias> 及 index.ui/get_/add_/update_/delete_ 五条):
本轮 json/ 三个定义均为只读列表(无新增/编辑表单入口wwwroot/ 下当前无 CRUD 生成子目录。
build.sh 重跑 xls2ui 后若生成 wwwroot/<alias>/ 目录,其 index.ui 与 get_<alias>.dspy
必须按 scripts/audit_rbac_parity.py 的 CRUD 登记面检查逐条补登(禁通配);
在此之前不登记不存在的路径——幽灵登记会污染 permission 表并被审计判 FAIL。
自证(交付前实跑 scripts/audit_rbac_parity.py
ls wwwroot/api/*.dspy | wc -l → 29
PATHS 中 /api/ 条目数 → 29契约 20 + 只读拒绝 9
集合差为空(无漏登记、无幽灵登记)
"""
import os
import subprocess
import sys
MODULE = 'pbl_compiler'
# ── ① Web 契约端点20 条,与 init.py CONTRACTS 一一对应,无通配符)──────────
PATHS_CONTRACTS = [
# —— M3a编译主流程 / 预览 / 对比3——
('/pbl_compiler/api/pbl_compiler_compile.dspy', 'logined'),
('/pbl_compiler/api/pbl_compiler_preview.dspy', 'logined'),
('/pbl_compiler/api/pbl_compiler_compare.dspy', 'logined'),
# —— M3a编译任务2——
('/pbl_compiler/api/pbl_compiler_task_get.dspy', 'logined'),
('/pbl_compiler/api/pbl_compiler_task_list.dspy', 'logined'),
# —— M3aGame Definition 产物2——
('/pbl_compiler/api/pbl_game_definition_get.dspy', 'logined'),
('/pbl_compiler/api/pbl_game_definition_get_by_blueprint.dspy', 'logined'),
# —— M3a确定性验证US-11 / F-CP-03 验收入口1——
('/pbl_compiler/api/pbl_compiler_verify_determinism.dspy', 'logined'),
# —— M3a编译器版本管理5——
('/pbl_compiler/api/pbl_compiler_version_register.dspy', 'logined'),
('/pbl_compiler/api/pbl_compiler_version_get.dspy', 'logined'),
('/pbl_compiler/api/pbl_compiler_version_list.dspy', 'logined'),
('/pbl_compiler/api/pbl_compiler_version_diff.dspy', 'logined'),
('/pbl_compiler/api/pbl_compiler_version_save.dspy', 'logined'),
# —— M3a能力注册表2——
('/pbl_compiler/api/pbl_capability_list.dspy', 'logined'),
('/pbl_compiler/api/pbl_capability_register.dspy', 'logined'),
# —— M3b第12章 event→condition→response 规则映射与导出5——
('/pbl_compiler/api/pbl_compiler_export_rules.dspy', 'logined'),
('/pbl_compiler/api/pbl_compiler_rules_preview.dspy', 'logined'),
('/pbl_compiler/api/pbl_compiler_rules_diff.dspy', 'logined'),
('/pbl_compiler/api/pbl_game_definition_rules_get.dspy', 'logined'),
('/pbl_compiler/api/pbl_script_rule_list.dspy', 'logined'),
]
# ── ② CRUD 只读拒绝端点9 条,非契约,仅封死 xls2ui 默认写入口QC #2 方案 A──
PATHS_READONLY_DENY = [
# pbl_game_definition编译产物表唯一写入口 gd_rules_writer.write_gd_rules()
('/pbl_compiler/api/pbl_game_definition_create.dspy', 'logined'),
('/pbl_compiler/api/pbl_game_definition_update.dspy', 'logined'),
('/pbl_compiler/api/pbl_game_definition_delete.dspy', 'logined'),
# pbl_capability_registry唯一写入口契约 pbl_capability_register
('/pbl_compiler/api/pbl_capability_registry_create.dspy', 'logined'),
('/pbl_compiler/api/pbl_capability_registry_update.dspy', 'logined'),
('/pbl_compiler/api/pbl_capability_registry_delete.dspy', 'logined'),
# pbl_compiler_version唯一写入口契约 pbl_compiler_version_register
('/pbl_compiler/api/pbl_compiler_version_create.dspy', 'logined'),
('/pbl_compiler/api/pbl_compiler_version_update.dspy', 'logined'),
('/pbl_compiler/api/pbl_compiler_version_delete.dspy', 'logined'),
]
# 完整登记面audit_rbac_parity.py 以本变量为事实源)
PATHS = PATHS_CONTRACTS + PATHS_READONLY_DENY
def registered_api_paths():
"""返回 PATHS 中登记的 /api/ 端点文件名集合(供 parity 审计复用)。"""
return {p.rsplit('/', 1)[-1] for p, _role in PATHS if '/api/' in p}
def registered_contract_paths():
"""返回 Web 契约端点文件名集合(与 init.py CONTRACTS 比对的口径)。"""
return {p.rsplit('/', 1)[-1] for p, _role in PATHS_CONTRACTS if '/api/' in p}
def registered_readonly_deny_paths():
"""返回只读拒绝端点文件名集合非契约QC #2 方案 A"""
return {p.rsplit('/', 1)[-1] for p, _role in PATHS_READONLY_DENY
if '/api/' in p}
def register():
"""逐条注册 RBAC 路径。rbac CLI 不在位时收集为 pending 并打印(不静默跳过)。
:return: True 全部注册成功False 存在 pending调用方据此决定退出码
"""
tool = os.environ.get('RBAC_SET_PERM', 'set_role_perm.py')
py = sys.executable if os.environ.get('PY') else 'python3'
done, missing = 0, []
for path, role in PATHS:
try:
rc = subprocess.call([py, tool, role, path],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
except Exception: # noqa: BLE001 - CLI 缺失不崩,记 pending
rc = 1
if rc == 0:
done += 1
else:
missing.append((path, role))
# 4 个占位符 ↔ 4 个实参QC #3 修复保持)
print('[%s] rbac paths: total=%d (contracts=%d, readonly_deny=%d) ok=%d pending=%d'
% (MODULE, len(PATHS), len(PATHS_CONTRACTS),
len(PATHS_READONLY_DENY), done, len(missing)))
for path, role in missing:
print(' PENDING %-12s %s' % (role, path))
return len(missing) == 0
if __name__ == '__main__':
sys.exit(0 if register() else 1)