143 lines
5.6 KiB
Python
143 lines
5.6 KiB
Python
"""幂等补齐 `handover:write`(客服转人工工单处置)权限并授权给 admin。
|
||||
|
|
|
|||
|
|
## 为什么需要它
|
|||
|
|
|
|||
|
|
`docs/02-数据库建表设计.md` §7.2 定义了工单状态机
|
|||
|
|
(`pending -> assigned -> processing -> resolved -> closed`,未解决可 `cancelled`),
|
|||
|
|
但平台此前**只有** `handover:read`(9046,只读队列)—— 于是工单只能看、不能推进,
|
|||
|
|
库里 40 张单子全部停在 `pending`。处置端点与状态机见
|
|||
|
|
`app/service/customer_service_handover_action_service.py` 与
|
|||
|
|
`app/api/controllers/admin.py` 的五个 action 端点。
|
|||
|
|
|
|||
|
|
权限码 **9069 已并进种子** `tools/seed_test_rbac.py`(那是定义源),
|
|||
|
|
本脚本只做"幂等补齐 + 授权",不重建任何东西;id 必须与种子逐条一致
|
|||
|
|
(一致性由 `tools/check_rbac_seed_consistency.py` 守着)。
|
|||
|
|
|
|||
|
|
用法::
|
|||
|
|
|
|||
|
|
python tools/grant_handover_write_permission.py # 只打印要做什么
|
|||
|
|
python tools/grant_handover_write_permission.py --apply # 真写
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import argparse
|
|||
|
|
import asyncio
|
|||
|
|
import sys
|
|||
|
|
from datetime import UTC, datetime
|
|||
|
|
|
|||
|
|
from sqlalchemy import text
|
|||
|
|
|
|||
|
|
from app.infrastructure.db import SessionFactory
|
|||
|
|
|
|||
|
|
if hasattr(sys.stdout, "reconfigure"):
|
|||
|
|
sys.stdout.reconfigure(errors="replace") # type: ignore[union-attr]
|
|||
|
|
|
|||
|
|
#: (id, permission_code, resource, action, data_scope) —— 与种子里那一行逐字一致。
|
|||
|
|
PERMISSION: tuple[int, str, str, str, str] = (
|
|||
|
|
9069, "handover:write", "handover", "write", "all",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
#: 授权给哪些角色:工单队列是**管理面**(读侧也是 admin),所以只给 admin。
|
|||
|
|
#: `ADMIN_PERMISSIONS` 在种子里是全量元组,重跑种子也会自动带上这一条。
|
|||
|
|
GRANTED_ROLES: tuple[str, ...] = ("admin",)
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def apply(*, dry_run: bool) -> int:
|
|||
|
|
now = datetime.now(UTC).replace(tzinfo=None)
|
|||
|
|
perm_id, code, resource, action, scope = PERMISSION
|
|||
|
|
async with SessionFactory() as session, session.begin():
|
|||
|
|
found = await session.scalar(
|
|||
|
|
text("SELECT id FROM sys_permission WHERE permission_code = :code"), {"code": code}
|
|||
|
|
)
|
|||
|
|
print(f"权限 {code}: {'已存在(跳过插入)' if found else '缺失,将新增 id=' + str(perm_id)}")
|
|||
|
|
if dry_run:
|
|||
|
|
role = await session.scalar(
|
|||
|
|
text("SELECT id FROM sys_role WHERE role_code = 'admin'")
|
|||
|
|
)
|
|||
|
|
print(f"角色 admin: {'存在' if role else '缺失(请先跑 seed_test_rbac.py)'}")
|
|||
|
|
print("\n[dry-run] 未写入任何数据。加 --apply 真写。")
|
|||
|
|
return 0
|
|||
|
|
|
|||
|
|
if found is None:
|
|||
|
|
await session.execute(
|
|||
|
|
text(
|
|||
|
|
"""
|
|||
|
|
INSERT INTO sys_permission
|
|||
|
|
(id, permission_code, resource, action, data_scope, created_at, updated_at)
|
|||
|
|
VALUES (:id, :code, :resource, :action, :scope, :now, :now)
|
|||
|
|
"""
|
|||
|
|
),
|
|||
|
|
{"id": perm_id, "code": code, "resource": resource, "action": action,
|
|||
|
|
"scope": scope, "now": now},
|
|||
|
|
)
|
|||
|
|
resolved_id = perm_id
|
|||
|
|
else:
|
|||
|
|
resolved_id = int(found)
|
|||
|
|
|
|||
|
|
for role_code in GRANTED_ROLES:
|
|||
|
|
role_id = await session.scalar(
|
|||
|
|
text("SELECT id FROM sys_role WHERE role_code = :code"), {"code": role_code}
|
|||
|
|
)
|
|||
|
|
if role_id is None:
|
|||
|
|
print(f"跳过角色 {role_code}:不存在")
|
|||
|
|
continue
|
|||
|
|
have = await session.scalar(
|
|||
|
|
text(
|
|||
|
|
"SELECT 1 FROM sys_role_permission WHERE role_id = :r AND permission_id = :p"
|
|||
|
|
),
|
|||
|
|
{"r": int(role_id), "p": resolved_id},
|
|||
|
|
)
|
|||
|
|
if have:
|
|||
|
|
print(f"授权:{role_code} 已拥有 {code}(跳过)")
|
|||
|
|
continue
|
|||
|
|
await session.execute(
|
|||
|
|
text(
|
|||
|
|
"INSERT INTO sys_role_permission (role_id, permission_id, created_at)"
|
|||
|
|
" VALUES (:r, :p, :now)"
|
|||
|
|
),
|
|||
|
|
{"r": int(role_id), "p": resolved_id, "now": now},
|
|||
|
|
)
|
|||
|
|
print(f"授权:{role_code} += {code}")
|
|||
|
|
|
|||
|
|
return await verify()
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def verify() -> int:
|
|||
|
|
"""按权限码实测一遍,而不是只看插了几行。"""
|
|||
|
|
async with SessionFactory() as session:
|
|||
|
|
rows = (
|
|||
|
|
await session.execute(
|
|||
|
|
text(
|
|||
|
|
"""
|
|||
|
|
SELECT p.permission_code, p.data_scope, r.role_code
|
|||
|
|
FROM sys_permission p
|
|||
|
|
LEFT JOIN sys_role_permission rp ON rp.permission_id = p.id
|
|||
|
|
LEFT JOIN sys_role r ON r.id = rp.role_id
|
|||
|
|
WHERE p.permission_code = 'handover:write'
|
|||
|
|
ORDER BY r.role_code
|
|||
|
|
"""
|
|||
|
|
)
|
|||
|
|
)
|
|||
|
|
).all()
|
|||
|
|
if not rows:
|
|||
|
|
print("校验失败:库里查不到 handover:write")
|
|||
|
|
return 1
|
|||
|
|
for code, scope, role_code in rows:
|
|||
|
|
print(f"校验:{code} scope={scope} → 角色 {role_code or '(未授权任何角色)'}")
|
|||
|
|
if not any(role_code for _code, _scope, role_code in rows):
|
|||
|
|
print("校验失败:handover:write 存在但没有授权给任何角色(处置端点会一律 403)")
|
|||
|
|
return 1
|
|||
|
|
return 0
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main() -> int:
|
|||
|
|
parser = argparse.ArgumentParser(description="补齐并授权 handover:write")
|
|||
|
|
parser.add_argument("--apply", action="store_true", help="真正写入(默认只打印)")
|
|||
|
|
args = parser.parse_args()
|
|||
|
|
return asyncio.run(apply(dry_run=not args.apply))
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
raise SystemExit(main())
|