approve: [M9-收口] Bug 修复状态流转 + 工作日志归档 + 交付一致性核验
This commit is contained in:
parent
d8664874cf
commit
dbd7169f5e
100
init/rbac_seed.json
Normal file
100
init/rbac_seed.json
Normal file
@ -0,0 +1,100 @@
|
||||
{
|
||||
"module": "pbl_runtime_ext",
|
||||
"milestone": "M11b-3 / M11b-4",
|
||||
"note": "PBL 运行时广播/轮询端点 RBAC 权限种子清单(逐条显式登记,禁止 % / * 通配符)。由 scripts/m11b34_rbac_seed.py 导入 permission/rolepermission,并把 scripts/load_path.py --check 收敛到 rc=0。",
|
||||
"default_role": "logined",
|
||||
"permissions": [
|
||||
{
|
||||
"name": "pbl_runtime_event_append",
|
||||
"path": "/pbl_runtime_ext/api/pbl_runtime_event_append.dspy",
|
||||
"method": "POST",
|
||||
"role": "logined",
|
||||
"endpoint": "wwwroot/api/pbl_runtime_event_append.dspy",
|
||||
"milestone": "M11b-2",
|
||||
"title": "运行时事件追加(单事务事件+状态写入)",
|
||||
"traceability": "契约 pbl_runtime_event_append → pbl_runtime_ext/api.py::api_pbl_runtime_event_append → scripts/load_path.py PATHS[0]"
|
||||
},
|
||||
{
|
||||
"name": "pbl_runtime_event_poll",
|
||||
"path": "/pbl_runtime_ext/api/pbl_runtime_event_poll.dspy",
|
||||
"method": "POST",
|
||||
"role": "logined",
|
||||
"endpoint": "wwwroot/api/pbl_runtime_event_poll.dspy",
|
||||
"milestone": "M11b-4",
|
||||
"title": "运行时事件 3s 轮询兜底拉取",
|
||||
"traceability": "契约 pbl_runtime_event_poll → pbl_runtime_ext/api.py::api_pbl_runtime_event_poll → scripts/load_path.py PATHS[1]"
|
||||
},
|
||||
{
|
||||
"name": "pbl_entity_state_get",
|
||||
"path": "/pbl_runtime_ext/api/pbl_entity_state_get.dspy",
|
||||
"method": "POST",
|
||||
"role": "logined",
|
||||
"endpoint": "wwwroot/api/pbl_entity_state_get.dspy",
|
||||
"milestone": "M11b-4",
|
||||
"title": "实体状态读取(服务端权威 state_version)",
|
||||
"traceability": "契约 pbl_entity_state_get → pbl_runtime_ext/api.py::api_pbl_entity_state_get → scripts/load_path.py PATHS[2]"
|
||||
},
|
||||
{
|
||||
"name": "pbl_entity_state_apply",
|
||||
"path": "/pbl_runtime_ext/api/pbl_entity_state_apply.dspy",
|
||||
"method": "POST",
|
||||
"role": "logined",
|
||||
"endpoint": "wwwroot/api/pbl_entity_state_apply.dspy",
|
||||
"milestone": "M11b-2",
|
||||
"title": "实体状态变更申请(客户端禁写 state_version)",
|
||||
"traceability": "契约 pbl_entity_state_apply → pbl_runtime_ext/api.py::api_pbl_entity_state_apply → scripts/load_path.py PATHS[3]"
|
||||
},
|
||||
{
|
||||
"name": "pbl_world_broadcast",
|
||||
"path": "/pbl_runtime_ext/api/pbl_world_broadcast.dspy",
|
||||
"method": "POST",
|
||||
"role": "logined",
|
||||
"endpoint": "wwwroot/api/pbl_world_broadcast.dspy",
|
||||
"milestone": "M11b-3",
|
||||
"title": "服务端世界广播(提交后广播 ≤300ms)",
|
||||
"traceability": "契约 pbl_world_broadcast → pbl_runtime_ext/api.py::api_pbl_world_broadcast → scripts/load_path.py PATHS[4]"
|
||||
},
|
||||
{
|
||||
"name": "pbl_session_member_add",
|
||||
"path": "/pbl_runtime_ext/api/pbl_session_member_add.dspy",
|
||||
"method": "POST",
|
||||
"role": "logined",
|
||||
"endpoint": "wwwroot/api/pbl_session_member_add.dspy",
|
||||
"milestone": "M11b-3",
|
||||
"title": "会话成员加入(广播订阅名单维护)",
|
||||
"traceability": "契约 pbl_session_member_add → pbl_runtime_ext/api.py::api_pbl_session_member_add → scripts/load_path.py PATHS[5]"
|
||||
},
|
||||
{
|
||||
"name": "pbl_runtime_broadcast_pull",
|
||||
"path": "/pbl_runtime_ext/api/pbl_runtime_broadcast_pull.dspy",
|
||||
"method": "POST",
|
||||
"role": "logined",
|
||||
"endpoint": "wwwroot/api/pbl_runtime_broadcast_pull.dspy",
|
||||
"milestone": "M11b-4",
|
||||
"title": "广播增量拉取(3s 轮询兜底)",
|
||||
"traceability": "契约 pbl_runtime_broadcast_pull → pbl_runtime_ext/api.py::api_pbl_runtime_broadcast_pull → scripts/load_path.py PATHS[6]"
|
||||
},
|
||||
{
|
||||
"name": "pbl_runtime_broadcast_stats",
|
||||
"path": "/pbl_runtime_ext/api/pbl_runtime_broadcast_stats.dspy",
|
||||
"method": "POST",
|
||||
"role": "logined",
|
||||
"endpoint": "wwwroot/api/pbl_runtime_broadcast_stats.dspy",
|
||||
"milestone": "M11b-4",
|
||||
"title": "广播时延统计(裁决/广播/端到端指标)",
|
||||
"traceability": "契约 pbl_runtime_broadcast_stats → pbl_runtime_ext/api.py::api_pbl_runtime_broadcast_stats → scripts/load_path.py PATHS[7]"
|
||||
}
|
||||
],
|
||||
"role_grants": {
|
||||
"logined": [
|
||||
"pbl_runtime_event_append",
|
||||
"pbl_runtime_event_poll",
|
||||
"pbl_entity_state_get",
|
||||
"pbl_entity_state_apply",
|
||||
"pbl_world_broadcast",
|
||||
"pbl_session_member_add",
|
||||
"pbl_runtime_broadcast_pull",
|
||||
"pbl_runtime_broadcast_stats"
|
||||
]
|
||||
}
|
||||
}
|
||||
379
scripts/m11b34_rbac_seed.py
Normal file
379
scripts/m11b34_rbac_seed.py
Normal file
@ -0,0 +1,379 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""M11b-3 / M11b-4 权限种子导入器(pbl_runtime_ext 广播/轮询端点,8 条)。
|
||||
|
||||
背景:anchor3 自检 `scripts/load_path.py` rc=1,8 条 PENDING 全部属 M11b-3
|
||||
服务端广播与 M11b-4 3s 轮询端点。本脚本把这 8 条 permission / rolepermission
|
||||
种子**幂等**导入应用库,使 load_path.py 的自检收敛到 rc=0。
|
||||
|
||||
硬规定(module-development-spec 6.6 / crud-definition-spec
|
||||
references/endpoint-registration.md / sqlor-database-module):
|
||||
1. **逐条显式登记,禁 `%` / `*` / `?` 通配符**——导入前静态校验,命中即 FAIL 不写半截。
|
||||
2. **load_path.py 单独不够**:新 api/*.dspy 必须同时落 DB `permission` +
|
||||
`rolepermission`,否则线上 403。角色用 `logined`(登录即可访问)。
|
||||
3. **不猜列名**:permission/rolepermission 的实际列集合运行时从
|
||||
information_schema 读取,只写交集列;缺关键列直接 FAIL 而非静默跳过。
|
||||
4. **幂等**:permission 按 path 查重(存在则 UPDATE 非键列),rolepermission 按
|
||||
(roleid, permid) 查重;id 用 `md5` 派生的稳定 32 位串,重复执行零差异。
|
||||
5. **库名不硬编码**:dbname 依次 --dbname > $PBL_RUNTIME_EXT_DBNAME >
|
||||
$PBL_DBNAME > 兜底 'pbl'(真实值由应用 get_module_dbname 决定)。
|
||||
|
||||
真实表结构(rbac,见 modules/scense/scripts/ddl.sql 实测 DDL):
|
||||
permission(id, name, description, ptype, parentid, path, title, icon)
|
||||
rolepermission(id, roleid, permid)
|
||||
|
||||
用法:
|
||||
python3 scripts/m11b34_rbac_seed.py --check # 离线校验,不连库
|
||||
python3 scripts/m11b34_rbac_seed.py --emit-sql x.sql # 出 SQL 交 build.sh 第 9 步
|
||||
python3 scripts/m11b34_rbac_seed.py --apply # 连库幂等导入
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
MODULE_DIR = os.path.dirname(HERE)
|
||||
MODULE = 'pbl_runtime_ext'
|
||||
SEED_JSON = os.path.join(MODULE_DIR, 'init', 'rbac_seed.json')
|
||||
WWWROOT_API = os.path.join(MODULE_DIR, 'wwwroot', 'api')
|
||||
LOAD_PATH_PY = os.path.join(HERE, 'load_path.py')
|
||||
|
||||
WILDCARD_RE = re.compile(r'[%*?]')
|
||||
# rbac 真实列(information_schema 取交集后才使用,缺列 = FAIL,不静默降级)
|
||||
PERM_REQUIRED = ['id', 'path']
|
||||
PERM_OPTIONAL = ['name', 'description', 'ptype', 'parentid', 'title', 'icon']
|
||||
RP_REQUIRED = ['id', 'roleid', 'permid']
|
||||
# permission.ptype:api 类权限(与 rbac 菜单类权限区分,见 appbase 菜单 ptype='menu')
|
||||
PTYPE_API = 'api'
|
||||
|
||||
|
||||
def load_seed():
|
||||
with open(SEED_JSON, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def stable_id(prefix, *parts):
|
||||
"""32 位稳定 id(重复执行幂等,不用 UUID——UUID 每次不同会插重复行)。"""
|
||||
h = hashlib.md5(('|'.join([MODULE] + [str(p) for p in parts])).encode('utf-8'))
|
||||
return ('%s%s' % (prefix, h.hexdigest()))[:32]
|
||||
|
||||
|
||||
def load_path_registered():
|
||||
"""从 scripts/load_path.py 以 AST 解析 PATHS 的 (path, role) 集合(不 exec)。"""
|
||||
import ast
|
||||
with open(LOAD_PATH_PY, 'r', encoding='utf-8') as f:
|
||||
tree = ast.parse(f.read(), filename=LOAD_PATH_PY)
|
||||
pairs = set()
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Assign):
|
||||
for tgt in node.targets:
|
||||
if isinstance(tgt, ast.Name) and tgt.id == 'PATHS':
|
||||
for item in ast.literal_eval(node.value):
|
||||
if isinstance(item, (list, tuple)) and len(item) == 2:
|
||||
pairs.add((item[0], item[1]))
|
||||
return pairs
|
||||
|
||||
|
||||
def static_validate(seed):
|
||||
"""离线三方一致性(种子 ↔ 磁盘 .dspy ↔ load_path.PATHS)+ 通配符门禁。"""
|
||||
errors = []
|
||||
perms = seed.get('permissions') or []
|
||||
if not perms:
|
||||
return False, ['init/rbac_seed.json permissions 为空']
|
||||
|
||||
disk = set(f for f in os.listdir(WWWROOT_API) if f.endswith('.dspy')) \
|
||||
if os.path.isdir(WWWROOT_API) else set()
|
||||
registered = load_path_registered()
|
||||
names, paths = set(), set()
|
||||
|
||||
for p in perms:
|
||||
name, path, role = p.get('name') or '', p.get('path') or '', p.get('role') or ''
|
||||
if name in names:
|
||||
errors.append('重复权限名: %s' % name)
|
||||
names.add(name)
|
||||
paths.add(path)
|
||||
if WILDCARD_RE.search(path):
|
||||
errors.append('禁止通配符(硬政策): %s' % path)
|
||||
if not path.startswith('/%s/api/' % MODULE):
|
||||
errors.append('路径前缀不符合模块自动路由 /%s/api/: %s' % (MODULE, path))
|
||||
if os.path.basename(path) not in disk:
|
||||
errors.append('磁盘缺 .dspy 端点文件: %s' % os.path.basename(path))
|
||||
if (path, role) not in registered:
|
||||
errors.append('scripts/load_path.py PATHS 未登记: %s (%s)' % (path, role))
|
||||
if not p.get('traceability'):
|
||||
errors.append('缺 traceability(无法追溯到端点与角色): %s' % name)
|
||||
if not p.get('milestone'):
|
||||
errors.append('缺 milestone 归属: %s' % name)
|
||||
|
||||
for path, role in sorted(registered):
|
||||
if path not in paths:
|
||||
errors.append('load_path.py 有登记但种子清单缺失: %s (%s)' % (path, role))
|
||||
|
||||
for role, lst in (seed.get('role_grants') or {}).items():
|
||||
for gname in lst:
|
||||
if gname not in names:
|
||||
errors.append('role_grants[%s] 引用不存在的权限名: %s' % (role, gname))
|
||||
return (not errors), errors
|
||||
|
||||
|
||||
def app_conf_candidates():
|
||||
"""应用 conf/config.json 候选路径(库名/连接从应用配置读,禁止在本模块硬编码 DBNAME)。"""
|
||||
ws = os.path.dirname(os.path.dirname(MODULE_DIR)) # .../sdlc_general
|
||||
return [os.path.join(ws, 'apps', 'pbls', 'conf', 'config.json'),
|
||||
os.environ.get('PBL_APP_CONF', '')]
|
||||
|
||||
|
||||
def read_app_conf():
|
||||
for path in app_conf_candidates():
|
||||
if path and os.path.isfile(path):
|
||||
try:
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f), path
|
||||
except Exception: # noqa: BLE001
|
||||
continue
|
||||
return None, None
|
||||
|
||||
|
||||
def resolve_dbname(args):
|
||||
"""dbname 优先级:--dbname > $PBL_RUNTIME_EXT_DBNAME > 应用 module_dbname 映射。"""
|
||||
if args.dbname:
|
||||
return args.dbname, '--dbname'
|
||||
env_name = os.environ.get('PBL_RUNTIME_EXT_DBNAME')
|
||||
if env_name:
|
||||
return env_name, '$PBL_RUNTIME_EXT_DBNAME'
|
||||
conf, path = read_app_conf()
|
||||
if conf:
|
||||
m = (conf.get('module_dbname') or {})
|
||||
name = m.get(MODULE) or m.get('default')
|
||||
if name:
|
||||
return name, 'conf:%s' % path
|
||||
return None, 'unresolved'
|
||||
|
||||
|
||||
def db_kwargs(dbname):
|
||||
"""从应用 conf 的 databases[dbname].kwargs 取连接参数(不硬编码凭据)。"""
|
||||
conf, _path = read_app_conf()
|
||||
kw = {}
|
||||
if conf:
|
||||
entry = (conf.get('databases') or {}).get(dbname) or {}
|
||||
kw = dict(entry.get('kwargs') or {})
|
||||
kw.setdefault('host', os.environ.get('PBL_DB_HOST', '127.0.0.1'))
|
||||
kw.setdefault('port', int(os.environ.get('PBL_DB_PORT', '3306')))
|
||||
return kw
|
||||
|
||||
|
||||
def table_columns(cur, dbname, table):
|
||||
cur.execute("SELECT COLUMN_NAME FROM information_schema.COLUMNS "
|
||||
"WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s", (dbname, table))
|
||||
return [r[0] for r in cur.fetchall()]
|
||||
|
||||
|
||||
def perm_values(p, cols, role_hint='logined'):
|
||||
"""按真实列集合构造 permission 行(只写交集列)。"""
|
||||
pid = stable_id('rp_', p['path'])
|
||||
row = {}
|
||||
vals = {
|
||||
'id': pid,
|
||||
'path': p['path'],
|
||||
'name': p['name'],
|
||||
'title': p.get('title') or p['name'],
|
||||
'ptype': PTYPE_API,
|
||||
'parentid': 'pbl_runtime_ext',
|
||||
'description': '%s | %s | role=%s | %s' % (
|
||||
p.get('title') or p['name'], p.get('milestone') or '',
|
||||
p.get('role') or role_hint, p.get('traceability') or ''),
|
||||
'icon': 'api',
|
||||
}
|
||||
for c in cols:
|
||||
row[c] = vals.get(c)
|
||||
return pid, row
|
||||
|
||||
|
||||
def build_rows(seed, perm_cols, rp_cols):
|
||||
"""生成 (permission 行, rolepermission 行) 列表,全部 id 稳定 → 幂等。"""
|
||||
perm_rows, rp_rows, name_to_pid = [], [], {}
|
||||
for p in seed['permissions']:
|
||||
pid, row = perm_values(p, perm_cols)
|
||||
name_to_pid[p['name']] = pid
|
||||
perm_rows.append((pid, row))
|
||||
for role, names in (seed.get('role_grants') or {}).items():
|
||||
for n in names:
|
||||
pid = name_to_pid.get(n)
|
||||
if not pid:
|
||||
continue
|
||||
rrow = {'id': stable_id('rr_', role, pid)}
|
||||
if 'roleid' in rp_cols:
|
||||
rrow['roleid'] = role
|
||||
if 'permid' in rp_cols:
|
||||
rrow['permid'] = pid
|
||||
rp_rows.append(rrow)
|
||||
return perm_rows, rp_rows
|
||||
|
||||
|
||||
def upsert(cur, table, cols, rows, key_col, stats):
|
||||
"""按 key_col 查重:存在 UPDATE,不存在 INSERT。rows 为 dict 列表。"""
|
||||
for r in rows:
|
||||
cur.execute('SELECT COUNT(1) FROM `%s` WHERE `%s` = %%s' % (table, key_col),
|
||||
(r[key_col],))
|
||||
exists = bool(cur.fetchone()[0])
|
||||
present = [c for c in cols if c in r]
|
||||
if exists:
|
||||
sets = ', '.join('`%s` = %%s' % c for c in present if c != key_col)
|
||||
if sets:
|
||||
cur.execute('UPDATE `%s` SET %s WHERE `%s` = %%s' % (table, sets, key_col),
|
||||
[r[c] for c in present if c != key_col] + [r[key_col]])
|
||||
stats['%s_updated' % table] = stats.get('%s_updated' % table, 0) + 1
|
||||
else:
|
||||
ph = ', '.join(['%s'] * len(present))
|
||||
cur.execute('INSERT INTO `%s` (%s) VALUES (%s)'
|
||||
% (table, ', '.join('`%s`' % c for c in present), ph),
|
||||
[r[c] for c in present])
|
||||
stats['%s_inserted' % table] = stats.get('%s_inserted' % table, 0) + 1
|
||||
return stats
|
||||
|
||||
|
||||
def emit_sql(seed, perm_cols, rp_cols, path):
|
||||
perm_rows, rp_rows = build_rows(seed, perm_cols, rp_cols)
|
||||
lines = ['-- M11b-3/M11b-4 pbl_runtime_ext 广播/轮询端点权限种子(8 条,逐条显式,无通配符)',
|
||||
'-- 由 scripts/m11b34_rbac_seed.py 生成,幂等(id 为 path/role 的 md5 派生)',
|
||||
'']
|
||||
for pid, r in perm_rows:
|
||||
present = [c for c in perm_cols if c in r]
|
||||
vals = ', '.join("'%s'" % (str(r[c]).replace("'", "''")) for c in present)
|
||||
lines.append('INSERT INTO `permission` (%s) VALUES (%s)'
|
||||
% (', '.join('`%s`' % c for c in present), vals)
|
||||
+ ' ON DUPLICATE KEY UPDATE '
|
||||
+ ', '.join('`%s`=VALUES(`%s`)' % (c, c)
|
||||
for c in present if c != 'id') + ';')
|
||||
lines.append('')
|
||||
for r in rp_rows:
|
||||
present = [c for c in rp_cols if c in r]
|
||||
vals = ', '.join("'%s'" % r[c] for c in present)
|
||||
lines.append('INSERT INTO `rolepermission` (%s) VALUES (%s)'
|
||||
% (', '.join('`%s`' % c for c in present), vals)
|
||||
+ ' ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);')
|
||||
lines.append('')
|
||||
lines.append('-- 校验:应为 8 条 permission + 8 条 rolepermission')
|
||||
lines.append("SELECT COUNT(1) AS perm_cnt FROM `permission` "
|
||||
"WHERE `path` LIKE '/pbl\\_runtime\\_ext/api/%';")
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(lines) + '\n')
|
||||
return len(perm_rows), len(rp_rows)
|
||||
|
||||
|
||||
def get_conn(dbname):
|
||||
try:
|
||||
import pymysql
|
||||
except ImportError:
|
||||
return None
|
||||
kw = db_kwargs(dbname)
|
||||
kw['charset'] = 'utf8mb4'
|
||||
kw['autocommit'] = False
|
||||
kw['connect_timeout'] = 6
|
||||
return pymysql.connect(**kw)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('--check', action='store_true', help='只做离线校验,不连库')
|
||||
ap.add_argument('--apply', action='store_true', help='连库幂等导入')
|
||||
ap.add_argument('--emit-sql', default=None, help='输出幂等 SQL 文件(不连库)')
|
||||
ap.add_argument('--dbname', default=None)
|
||||
args = ap.parse_args()
|
||||
|
||||
seed = load_seed()
|
||||
ok, errors = static_validate(seed)
|
||||
print('[%s] rbac_seed 静态校验: permissions=%d errors=%d'
|
||||
% (MODULE, len(seed['permissions']), len(errors)))
|
||||
for e in errors:
|
||||
print(' FAIL ' + e)
|
||||
if not ok:
|
||||
return 1
|
||||
print(' OK 全部路径逐条显式登记(无 %% / * 通配符);'
|
||||
'种子 ↔ 磁盘 .dspy ↔ load_path.PATHS 三方一致')
|
||||
|
||||
if args.check:
|
||||
print('[%s] --check 通过(离线,未连库)' % MODULE)
|
||||
return 0
|
||||
|
||||
dbname, src = resolve_dbname(args)
|
||||
if not dbname:
|
||||
print(' FAIL 无法解析 %s 的库名(--dbname / $PBL_RUNTIME_EXT_DBNAME / '
|
||||
'应用 conf module_dbname 均缺)' % MODULE)
|
||||
return 1
|
||||
print(' dbname=%s (来源 %s)' % (dbname, src))
|
||||
if args.emit_sql:
|
||||
np_, nr = emit_sql(seed, PERM_REQUIRED + PERM_OPTIONAL,
|
||||
RP_REQUIRED, args.emit_sql)
|
||||
print(' SQL 已落盘: %s (permission=%d rolepermission=%d,列名取 rbac 标准列)'
|
||||
% (args.emit_sql, np_, nr))
|
||||
return 0
|
||||
try:
|
||||
import pymysql # noqa: F401
|
||||
except ImportError:
|
||||
print(' SKIP 无 pymysql:改用 --emit-sql 出 SQL 交 build.sh 第 9 步导入')
|
||||
return 2
|
||||
try:
|
||||
conn = get_conn(dbname)
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(' SKIP 连库失败(%s):DB 不可达时用 --emit-sql 出 SQL,'
|
||||
'交 build.sh 第 9 步(import_rp)导入' % str(e)[:100])
|
||||
return 2
|
||||
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
perm_cols = table_columns(cur, dbname, 'permission')
|
||||
rp_cols = table_columns(cur, dbname, 'rolepermission')
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(' SKIP 无法读取 %s 库的 rbac 表结构(%s):'
|
||||
'先执行 rbac 建表或用 --emit-sql 出 SQL' % (dbname, str(e)[:80]))
|
||||
conn.close()
|
||||
return 2
|
||||
if not perm_cols or not rp_cols:
|
||||
print(' SKIP 库 %s 缺 permission/rolepermission 表(rbac 未建表),'
|
||||
'本任务不冒充成功' % dbname)
|
||||
conn.close()
|
||||
return 2
|
||||
missing = [c for c in PERM_REQUIRED if c not in perm_cols] \
|
||||
+ [c for c in RP_REQUIRED if c not in rp_cols]
|
||||
if missing:
|
||||
print(' FAIL rbac 表缺关键列: %s(实际列 %s / %s)'
|
||||
% (missing, perm_cols, rp_cols))
|
||||
conn.close()
|
||||
return 1
|
||||
|
||||
use_perm = [c for c in PERM_REQUIRED + PERM_OPTIONAL if c in perm_cols]
|
||||
use_rp = [c for c in RP_REQUIRED if c in rp_cols]
|
||||
perm_rows, rp_rows = build_rows(seed, use_perm, use_rp)
|
||||
|
||||
if not args.apply:
|
||||
print(' DRY:加 --apply 才写库,或 --emit-sql 出 SQL')
|
||||
conn.close()
|
||||
return 0
|
||||
|
||||
stats = {}
|
||||
stats = upsert(cur, 'permission', use_perm, [r for _, r in perm_rows], 'path', stats)
|
||||
stats = upsert(cur, 'rolepermission', use_rp, rp_rows, 'id', stats)
|
||||
conn.commit()
|
||||
cur.execute("SELECT COUNT(1) FROM `permission` WHERE `path` LIKE ${p}$".replace(
|
||||
'${p}$', '%s'), ('/%s/api/%%' % MODULE,))
|
||||
stats['verify_permission_in_db'] = cur.fetchone()[0]
|
||||
cur.execute("SELECT COUNT(1) FROM `rolepermission` rp JOIN `permission` p "
|
||||
"ON rp.permid = p.id WHERE p.path LIKE %s", ('/%s/api/%%' % MODULE,))
|
||||
stats['verify_rolepermission_in_db'] = cur.fetchone()[0]
|
||||
conn.close()
|
||||
print('[%s] 导入结果: %s' % (MODULE, json.dumps(stats, ensure_ascii=False)))
|
||||
good = (stats.get('verify_permission_in_db') == len(seed['permissions'])
|
||||
and stats.get('verify_rolepermission_in_db') == len(seed['permissions']))
|
||||
print(' %s 库内回查:permission=%s rolepermission=%s 期望各 %d'
|
||||
% ('OK' if good else 'FAIL', stats.get('verify_permission_in_db'),
|
||||
stats.get('verify_rolepermission_in_db'), len(seed['permissions'])))
|
||||
return 0 if good else 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
23
sql/m11b34_rbac_seed.sql
Normal file
23
sql/m11b34_rbac_seed.sql
Normal file
@ -0,0 +1,23 @@
|
||||
-- M11b-3/M11b-4 pbl_runtime_ext 广播/轮询端点权限种子(8 条,逐条显式,无通配符)
|
||||
-- 由 scripts/m11b34_rbac_seed.py 生成,幂等(id 为 path/role 的 md5 派生)
|
||||
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_450b1b77dc9b1beb7a06a9b0f0fca', '/pbl_runtime_ext/api/pbl_runtime_event_append.dspy', 'pbl_runtime_event_append', '运行时事件追加(单事务事件+状态写入) | M11b-2 | role=logined | 契约 pbl_runtime_event_append → pbl_runtime_ext/api.py::api_pbl_runtime_event_append → scripts/load_path.py PATHS[0]', 'api', 'pbl_runtime_ext', '运行时事件追加(单事务事件+状态写入)', 'api') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_58d7b9596b19d722759a7973ad50d', '/pbl_runtime_ext/api/pbl_runtime_event_poll.dspy', 'pbl_runtime_event_poll', '运行时事件 3s 轮询兜底拉取 | M11b-4 | role=logined | 契约 pbl_runtime_event_poll → pbl_runtime_ext/api.py::api_pbl_runtime_event_poll → scripts/load_path.py PATHS[1]', 'api', 'pbl_runtime_ext', '运行时事件 3s 轮询兜底拉取', 'api') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_0d3922b6801b0ed20b946590f1ba9', '/pbl_runtime_ext/api/pbl_entity_state_get.dspy', 'pbl_entity_state_get', '实体状态读取(服务端权威 state_version) | M11b-4 | role=logined | 契约 pbl_entity_state_get → pbl_runtime_ext/api.py::api_pbl_entity_state_get → scripts/load_path.py PATHS[2]', 'api', 'pbl_runtime_ext', '实体状态读取(服务端权威 state_version)', 'api') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_56a266cb31f203815239c7b25c5f1', '/pbl_runtime_ext/api/pbl_entity_state_apply.dspy', 'pbl_entity_state_apply', '实体状态变更申请(客户端禁写 state_version) | M11b-2 | role=logined | 契约 pbl_entity_state_apply → pbl_runtime_ext/api.py::api_pbl_entity_state_apply → scripts/load_path.py PATHS[3]', 'api', 'pbl_runtime_ext', '实体状态变更申请(客户端禁写 state_version)', 'api') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_fbe9adbe519a2c38a7a23c48f9cc3', '/pbl_runtime_ext/api/pbl_world_broadcast.dspy', 'pbl_world_broadcast', '服务端世界广播(提交后广播 ≤300ms) | M11b-3 | role=logined | 契约 pbl_world_broadcast → pbl_runtime_ext/api.py::api_pbl_world_broadcast → scripts/load_path.py PATHS[4]', 'api', 'pbl_runtime_ext', '服务端世界广播(提交后广播 ≤300ms)', 'api') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_5ca8286318bbbbd7cd270080a7753', '/pbl_runtime_ext/api/pbl_session_member_add.dspy', 'pbl_session_member_add', '会话成员加入(广播订阅名单维护) | M11b-3 | role=logined | 契约 pbl_session_member_add → pbl_runtime_ext/api.py::api_pbl_session_member_add → scripts/load_path.py PATHS[5]', 'api', 'pbl_runtime_ext', '会话成员加入(广播订阅名单维护)', 'api') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_ad04e35c34433e171ac69800fe736', '/pbl_runtime_ext/api/pbl_runtime_broadcast_pull.dspy', 'pbl_runtime_broadcast_pull', '广播增量拉取(3s 轮询兜底) | M11b-4 | role=logined | 契约 pbl_runtime_broadcast_pull → pbl_runtime_ext/api.py::api_pbl_runtime_broadcast_pull → scripts/load_path.py PATHS[6]', 'api', 'pbl_runtime_ext', '广播增量拉取(3s 轮询兜底)', 'api') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_abddb912040e109aef26528c6bb61', '/pbl_runtime_ext/api/pbl_runtime_broadcast_stats.dspy', 'pbl_runtime_broadcast_stats', '广播时延统计(裁决/广播/端到端指标) | M11b-4 | role=logined | 契约 pbl_runtime_broadcast_stats → pbl_runtime_ext/api.py::api_pbl_runtime_broadcast_stats → scripts/load_path.py PATHS[7]', 'api', 'pbl_runtime_ext', '广播时延统计(裁决/广播/端到端指标)', 'api') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_0365a6a1679328328a337aaae4ce4', 'logined', 'rp_450b1b77dc9b1beb7a06a9b0f0fca') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_e601324f76b0703166ae853124b62', 'logined', 'rp_58d7b9596b19d722759a7973ad50d') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_54e96ce003e48250e153e0cea47f5', 'logined', 'rp_0d3922b6801b0ed20b946590f1ba9') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_119eac70d6411625f72a05b94a1f0', 'logined', 'rp_56a266cb31f203815239c7b25c5f1') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_9cd4cf0eec4378e78a751d0b900c6', 'logined', 'rp_fbe9adbe519a2c38a7a23c48f9cc3') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_824b636853f523ee9c5c3af35d8a9', 'logined', 'rp_5ca8286318bbbbd7cd270080a7753') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_0047385267d96c7d6b519b68c68be', 'logined', 'rp_ad04e35c34433e171ac69800fe736') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_85058b3d449dc86c47380293a2f45', 'logined', 'rp_abddb912040e109aef26528c6bb61') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
|
||||
-- 校验:应为 8 条 permission + 8 条 rolepermission
|
||||
SELECT COUNT(1) AS perm_cnt FROM `permission` WHERE `path` LIKE '/pbl\_runtime\_ext/api/%';
|
||||
Loading…
x
Reference in New Issue
Block a user