#!/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.py(15 个 M3a 契约)+ pbl_compiler/rules_export_api.py(5 个 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_.dspy —— 等于绕过 gd_rules_writer / pbl_capability_register / pbl_compiler_version_register 唯一写入口,rules_hash(29.6 确定性指纹)可被手工改写。 故为 3 张表 × 3 个写操作各建一个显式返回 Error 的 .dspy(共 9 个), 它们不是 ServerEnv 契约(不进 CONTRACTS/__init__.py),只需 RBAC 登记。 关于 CRUD 生成目录(/pbl_compiler/ 及 index.ui/get_/add_/update_/delete_ 五条): 本轮 json/ 三个定义均为只读列表(无新增/编辑表单入口),wwwroot/ 下当前无 CRUD 生成子目录。 build.sh 重跑 xls2ui 后若生成 wwwroot// 目录,其 index.ui 与 get_.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'), # —— M3a:Game 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)