190 lines
8.1 KiB
Python
190 lines
8.1 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""pbl_appcodes 模块 RBAC / 路径声明脚本(per-module,中央层 apps/pbls 聚合)。
|
||
|
||
用法:
|
||
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_appcodes_inject.dspy + api/pbl_appcodes_list.dspy),
|
||
即「文件在、路由在、RBAC 不在」= 点进去一律 403 的同类缺口。
|
||
本次补齐**逐条显式**登记,并把「声明与磁盘一致」做进 --check 门禁。
|
||
铁律 = 禁通配符(% / *),所有路径逐条显式;新增路径须在本台账追加一行,
|
||
并确认模块名在中央 apps/pbls/scripts/load_path.py 的 MODULES 中(聚合式双写)。
|
||
═══════════════════════════════════════════════════════════════════════════════
|
||
"""
|
||
import os
|
||
import sys
|
||
|
||
MODULE = 'pbl_appcodes'
|
||
MODULE_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
WWWROOT = os.path.join(MODULE_ROOT, 'wwwroot')
|
||
|
||
# ── any 层:无需登录即可访问的公共资源(本模块无)────────────────────────────
|
||
PATHS_ANY = []
|
||
|
||
# ── logined 层:模块首页 + api/*.dspy(与磁盘 wwwroot 一一对应,逐条显式)────
|
||
PATHS_LOGINED = [
|
||
'/{m}'.format(m=MODULE),
|
||
'/{m}/index.ui'.format(m=MODULE),
|
||
'/{m}/api/pbl_appcodes_inject.dspy'.format(m=MODULE),
|
||
'/{m}/api/pbl_appcodes_list.dspy'.format(m=MODULE),
|
||
]
|
||
|
||
# ── owner.* 层:当前无受限路径,显式声明为空(避免「漏登记」被误判为「待补」)─
|
||
PATHS_OWNER_SUPERUSER = []
|
||
PATHS_OWNER_OPERATOR = []
|
||
|
||
# ── 兼容扁平写法(中央聚合器两种写法都支持)────────────────────────────────
|
||
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 _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)
|
||
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()
|
||
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
|
||
|
||
|
||
if __name__ == '__main__':
|
||
sys.exit(main())
|