#!/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)