accounting/scripts/load_path.py

357 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
accounting 模块 RBAC 权限管理脚本角色分层版2026-08-27 重构)
重构原因(安全修复):
旧版把整个财务后台(余额台账/账户/科目/分录/日志/信用/币种/汇率的增删改查)
全部注册给 logined 角色,任何注册用户可见/可改/可删全平台余额与账务数据。
现按角色分层:
- any 免登录静态资源(菜单片段/图标)+ 带 localhost 校验的定时任务入口
- logined 客户自服务页面(后端均已按本人/本机构过滤:我的账户、账务明细、账单、我的信用额度)
- 财务角色 财务后台全部 CRUD余额台账、账户、科目、分录配置、日志、信用管理、币种汇率
- 分销商角色 代客充值(含按用户名查询客户余额,属分销商业务需要)
行为:
默认先清理 /accounting/ 下全部旧授权rolepermission再按新矩阵重新注册。
(旧授权里 logined、customer.customer 等角色持有财务后台增删改权限,不清理则泄漏依旧)
--add-only 跳过清理,仅增量注册(新路径/新宿主接入时用)
角色解析:
权限缓存按 role.id 关联rbac/userperm.py load_roleperms同名角色在不同宿主
的 role.id 可能不同(如 'owner.superuser' 字面量 vs 随机 id因此本脚本
用 orgtypeid+name 查 role 表解析出真实 role.id 后注册,同名多条全部注册。
使用方法:
cd <app root>(含 py3/ 的目录,如 /d/pipeline/pipeline-app
./py3/bin/python pkgs/accounting/scripts/load_path.py [--add-only]
"""
import os
import sys
import asyncio
def find_app_root():
candidates = [
os.path.expanduser("~/repos/sage"),
os.path.expanduser("~/sage"),
# pkgs/accounting/scripts/load_path.py 的上三级 = 应用根目录
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
os.getcwd(),
]
for c in candidates:
if os.path.isdir(os.path.join(c, "py3")) and os.path.isdir(os.path.join(c, "wwwroot")):
return c
return None
APP_ROOT = find_app_root()
if not APP_ROOT:
print("ERROR: Cannot find app root directory")
sys.exit(1)
MOD = "accounting"
# ============================================================
# 角色定义(标签格式 <orgtypeid>.<name>,运行时解析为真实 role.id
# ============================================================
# 财务后台管理角色:平台方超管/管理员/会计 + 分销商管理员/会计/运营
FIN_ROLES = [
"owner.superuser",
"owner.admin",
"owner.account",
"reseller.admin",
"reseller.accountant",
"reseller.operator",
]
# 代客充值角色:分销商侧可代客户充值(需按用户名查询客户账户,属业务需要)
PROXY_ROLES = [
"reseller.admin",
"reseller.accountant",
"reseller.operator",
"reseller.sale",
]
# ============================================================
# 权限路径矩阵
# ============================================================
# any — 无需登录(静态资源 + 自带 localhost 校验的定时任务入口)
PATHS_ANY = [
f"/{MOD}/usermenu.ui",
f"/{MOD}/imgs/accbalance.svg",
f"/{MOD}/imgs/accdetail.svg",
f"/{MOD}/imgs/account.svg",
f"/{MOD}/imgs/bill.svg",
# 定时任务入口build.sh 注入的 cron 裸 curl 无会话,必须 any
# dspy 内部已做 client_ip=localhost 校验nginx XFF 追加模式伪造不成立)
f"/{MOD}/api/fetch_forex_rates.dspy",
]
# logined — 客户自服务(后端查询均已按当前用户/本机构过滤)
PATHS_LOGINED = [
f"/{MOD}/myaccounts.ui",
f"/{MOD}/myaccounts.dspy",
f"/{MOD}/mybalance.dspy",
f"/{MOD}/accdetail.ui",
f"/{MOD}/accdetail.dspy",
f"/{MOD}/billing.ui",
f"/{MOD}/billing.dspy",
f"/{MOD}/billing_download.dspy",
f"/{MOD}/get_user_balance.dspy",
f"/{MOD}/open_customer_accounts_with_orgid.dspy",
f"/{MOD}/error_accounting.ui",
# 我的信用额度(查询按本机构过滤)
f"/{MOD}/credit_limit/credit_overview.ui",
]
# 财务后台 — 仅财务角色
PATHS_FIN = [
# 模块入口
f"/{MOD}",
f"/{MOD}/index.ui",
# 统计卡片(按机构汇总的财务看板)
f"/{MOD}/stat_total_balance.ui",
f"/{MOD}/stat_today_consumption.ui",
f"/{MOD}/stat_month_consumption.ui",
f"/{MOD}/stat_account_count.ui",
# 开户管理(平台/分销商侧操作,可指定任意 orgid
f"/{MOD}/oca.dspy",
f"/{MOD}/open_customer_accounts.dspy",
f"/{MOD}/open_owner_accounts.dspy",
f"/{MOD}/open_provider_accounts.dspy",
f"/{MOD}/open_reseller_accounts.dspy",
f"/{MOD}/open_reseller_provider_accounts.dspy",
# acc_balance/(日终余额台账,全平台数据,无 orgid 列)
f"/{MOD}/acc_balance/index.ui",
f"/{MOD}/acc_balance/get_acc_balance.dspy",
f"/{MOD}/acc_balance/add_acc_balance.dspy",
f"/{MOD}/acc_balance/update_acc_balance.dspy",
f"/{MOD}/acc_balance/delete_acc_balance.dspy",
# acc_detail/(记账明细台账)
f"/{MOD}/acc_detail/index.ui",
f"/{MOD}/acc_detail/get_acc_detail.dspy",
f"/{MOD}/acc_detail/add_acc_detail.dspy",
f"/{MOD}/acc_detail/update_acc_detail.dspy",
f"/{MOD}/acc_detail/delete_acc_detail.dspy",
# account/(账户管理)
f"/{MOD}/account/index.ui",
f"/{MOD}/account/get_account.dspy",
f"/{MOD}/account/add_account.dspy",
f"/{MOD}/account/update_account.dspy",
f"/{MOD}/account/delete_account.dspy",
# account_config/(参与方科目配置)
f"/{MOD}/account_config/index.ui",
f"/{MOD}/account_config/get_account_config.dspy",
f"/{MOD}/account_config/add_account_config.dspy",
f"/{MOD}/account_config/update_account_config.dspy",
f"/{MOD}/account_config/delete_account_config.dspy",
# accounting_config/(会计分录配置)
f"/{MOD}/accounting_config/index.ui",
f"/{MOD}/accounting_config/get_accounting_config.dspy",
f"/{MOD}/accounting_config/add_accounting_config.dspy",
f"/{MOD}/accounting_config/update_accounting_config.dspy",
f"/{MOD}/accounting_config/delete_accounting_config.dspy",
# accounting_log/(记账日志)
f"/{MOD}/accounting_log/index.ui",
f"/{MOD}/accounting_log/get_accounting_log.dspy",
f"/{MOD}/accounting_log/add_accounting_log.dspy",
f"/{MOD}/accounting_log/update_accounting_log.dspy",
f"/{MOD}/accounting_log/delete_accounting_log.dspy",
# subject/(科目)
f"/{MOD}/subject/index.ui",
f"/{MOD}/subject/get_subject.dspy",
f"/{MOD}/subject/add_subject.dspy",
f"/{MOD}/subject/update_subject.dspy",
f"/{MOD}/subject/delete_subject.dspy",
# credit_limit/(信用额度管理侧;客户自服务的 hub/overview 在 logined 组)
f"/{MOD}/credit_limit/index.ui",
f"/{MOD}/credit_limit/get_credit_limit.dspy",
f"/{MOD}/credit_limit/add_credit_limit.dspy",
f"/{MOD}/credit_limit/update_credit_limit.dspy",
f"/{MOD}/credit_limit/delete_credit_limit.dspy",
f"/{MOD}/credit_limit/credit_manage.ui",
f"/{MOD}/credit_limit/api/credit_summary.dspy",
f"/{MOD}/credit_limit/api/set_credit_form.ui",
f"/{MOD}/credit_limit/api/set_customer_credit.dspy",
# currency/ + exchange_rate/(币种与汇率管理,目录名以 build 生成的 tblname 为准)
f"/{MOD}/currency/index.ui",
f"/{MOD}/currency/get_currency.dspy",
f"/{MOD}/currency/add_currency.dspy",
f"/{MOD}/currency/update_currency.dspy",
f"/{MOD}/currency/delete_currency.dspy",
f"/{MOD}/exchange_rate/index.ui",
f"/{MOD}/exchange_rate/get_exchange_rate.dspy",
f"/{MOD}/exchange_rate/add_exchange_rate.dspy",
f"/{MOD}/exchange_rate/update_exchange_rate.dspy",
f"/{MOD}/exchange_rate/delete_exchange_rate.dspy",
# api币种/汇率手工维护入口)
f"/{MOD}/api/currency_create.dspy",
f"/{MOD}/api/currency_update.dspy",
f"/{MOD}/api/currency_delete.dspy",
f"/{MOD}/api/exchange_rate_create.dspy",
f"/{MOD}/api/exchange_rate_update.dspy",
f"/{MOD}/api/exchange_rate_delete.dspy",
]
# 代客充值 — 仅分销商角色
PATHS_PROXY = [
f"/{MOD}/proxy_recharge.ui",
f"/{MOD}/proxy_recharge_submit.dspy",
]
# 充值错帐处理(冲正)— 财务角色FIN_ROLESowner 财务 + reseller 管理员/会计/运营。
# 敏感操作不开放给 reseller.sale代客充值可冲正不可
PATHS_FIN += [
f"/{MOD}/recharge_reverse.ui",
f"/{MOD}/recharge_reverse_list.dspy",
f"/{MOD}/recharge_reverse_submit.dspy",
]
# ============================================================
# 数据库辅助(角色解析 + 旧授权清理)
# ============================================================
def _get_db():
sys.path.insert(0, APP_ROOT)
from sqlor.dbpools import DBPools
from appPublic.jsonConfig import getConfig
config = getConfig(APP_ROOT, NS={'workdir': APP_ROOT})
return DBPools(config.databases)
def _row_get(row, key):
try:
return row[key]
except Exception:
return getattr(row, key, None)
async def _find_rbac_db(db):
"""遍历 config.databases找到含 role 表的库pipeline 应用是 pipeline 库sage 是 sage 库)。"""
names = list(db.databases.keys()) if hasattr(db, 'databases') else []
for dbname in names:
try:
async with db.sqlorContext(dbname) as sor:
await sor.sqlExe('select count(*) as c from role', {})
await sor.sqlExe('COMMIT', {})
return dbname
except Exception:
continue
return None
async def resolve_role_ids(sor, labels):
"""标签 'orgtypeid.name' → 真实 role.id 列表(同名多条全返回)。"""
mapping = {}
for label in labels:
if label in ('any', 'logined', 'anonymous'):
mapping[label] = [label]
continue
orgtypeid, _, name = label.partition('.')
recs = await sor.sqlExe(
'select id from role where orgtypeid=${o}$ and name=${n}$',
{'o': orgtypeid, 'n': name})
ids = [_row_get(r, 'id') for r in recs]
ids = [i for i in ids if i]
if not ids:
print(f'WARN: 角色不存在 {label},跳过')
mapping[label] = ids
return mapping
async def set_role_perm(sor, role_id, path):
"""注册单条权限幂等permission 缺则建rolepermission 缺则建。"""
from appPublic.uniqueID import getID
recs = await sor.R('permission', {'path': path})
if not recs:
permid = getID()
await sor.C('permission', {'id': permid, 'path': path})
else:
permid = _row_get(recs[0], 'id')
rp = await sor.R('rolepermission', {'roleid': role_id, 'permid': permid})
if rp:
return False
await sor.C('rolepermission', {'id': getID(), 'roleid': role_id, 'permid': permid})
return True
async def run_all(add_only):
db = _get_db()
dbname = await _find_rbac_db(db)
if not dbname:
print('ERROR: 无法定位 RBAC 库role 表不可达),中止')
return 1
print(f'RBAC 库: {dbname}')
total = 0
async with db.sqlorContext(dbname) as sor:
if not add_only:
print('=== 清理旧授权 ===')
perms = await sor.sqlExe(
'select id from permission where path like ${p}$ or path=${p2}$',
{'p': f'/{MOD}/%', 'p2': f'/{MOD}'})
for p in perms:
await sor.sqlExe('delete from rolepermission where permid=${pid}$',
{'pid': _row_get(p, 'id')})
await sor.sqlExe('COMMIT', {})
print(f'清理完成:/accounting/ 下 {len(perms)} 个 permission 的旧授权已删除')
print('=== 解析角色 ===')
all_labels = sorted(set(FIN_ROLES + PROXY_ROLES))
role_map = await resolve_role_ids(sor, all_labels)
for lb in all_labels:
print(f' {lb} -> {role_map.get(lb) or "(不存在)"}')
print('=== 注册新矩阵 ===')
async def register_group(group_label, role_ids, paths):
nonlocal total
if not role_ids:
return
added = 0
for rid in role_ids:
for p in paths:
if await set_role_perm(sor, rid, p):
added += 1
await sor.sqlExe('COMMIT', {})
total += added
print(f' {group_label} ({",".join(role_ids)}): +{added}'
f'(目标 {len(paths) * len(role_ids)} 条,其余为已存在)')
await register_group('any', ['any'], PATHS_ANY)
await register_group('logined', ['logined'], PATHS_LOGINED)
for label in FIN_ROLES:
await register_group(label, role_map.get(label, []), PATHS_FIN)
for label in PROXY_ROLES:
await register_group(label, role_map.get(label, []), PATHS_PROXY)
print(f'\nDone. 本次新增 {total} 条授权。')
print('NOTE: 重启应用(或调用 /rbac/refresh_userperm.dspy后权限生效。')
return 0
def main():
add_only = '--add-only' in sys.argv
print(f"App root: {APP_ROOT}")
sys.exit(asyncio.run(run_all(add_only)))
if __name__ == "__main__":
main()