添加权限
This commit is contained in:
parent
1be155c478
commit
fcc39984a9
10
.env.example
10
.env.example
|
|
@ -33,6 +33,16 @@ PMS_WEB_PORT=38100
|
||||||
# openssl rand -base64 48
|
# openssl rand -base64 48
|
||||||
PMS_SESSION_SECRET=
|
PMS_SESSION_SECRET=
|
||||||
|
|
||||||
|
# ---- 登录角色标记 (2026-08 加权限) ----
|
||||||
|
# 角色从 bshop 登录返回的权限清单里认。两项都可逗号分隔多条; 联测时直接指到测试账号真有的权限即可。
|
||||||
|
# 系统管理员 = 权限里含 PMS_ADMIN_PERMS 任一条 (system_key:menu_key)。默认 pms:admin;
|
||||||
|
# 想先拿现成权限联测, 就临时指到管理员账号有的某条, 比如 band:yhyqx (用户与权限)。
|
||||||
|
PMS_ADMIN_PERMS=band:yhyqx # 含这条权限的账号 = 系统管理员(示例用“用户与权限”)
|
||||||
|
# 交易员准入 = 进入 PMS 至少要占 PMS_TRADER_PERMS 任一条; 留空 = 任何能登录的人都是交易员。
|
||||||
|
# 想把交易员也限成某条权限 (只给持仓管理权限的人才是交易员), 就指到那条, 比如 band:ccgl。
|
||||||
|
# 机器:部署服务器 tlai4090(.env 里)
|
||||||
|
PMS_TRADER_PERMS=band:ccgl # 进 PMS 至少要占这条 = 交易员;留空则任何能登录的人都是交易员
|
||||||
|
|
||||||
# ---- QMT WebSocket 直连通道的密钥 (协议 QMT_WS_PROTOCOL.md §10.1.1) ----
|
# ---- QMT WebSocket 直连通道的密钥 (协议 QMT_WS_PROTOCOL.md §10.1.1) ----
|
||||||
# 这两项是**密钥**, 只在这里维护: 不入库、不进 ParamStore、不上管理页面、不进代码。
|
# 这两项是**密钥**, 只在这里维护: 不入库、不进 ParamStore、不上管理页面、不进代码。
|
||||||
# 生成一对新密钥 (公钥带外交给 QMT 侧, 私钥不出机器):
|
# 生成一对新密钥 (公钥带外交给 QMT 侧, 私钥不出机器):
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,14 @@
|
||||||
"""
|
"""
|
||||||
PMS 登录与权限 —— 认证解析 / 角色判定 / 会话票签验 / 接口鉴权判定(纯函数)
|
PMS 登录与权限 —— 认证解析 / 角色判定 / 会话票签验 / 接口鉴权判定(纯函数)
|
||||||
=======================================================================
|
=======================================================================
|
||||||
方案见「PMS 加登录与两个角色(交易系统管理员 / 交易员)」。本模块刻意只放纯函数:
|
方案见「PMS 加登录与两个角色(系统管理员 / 交易员)」。本模块刻意只放纯函数:
|
||||||
不连库、几乎不联网,喂进去字典或字符串就能算,便于单测
|
不连库、几乎不联网,喂进去字典或字符串就能算,便于单测
|
||||||
(scripts/test_batch18_units.py)。唯一碰网络的是 call_bshop_login 一个函数,
|
(scripts/test_batch18_units.py)。唯一碰网络的是 call_bshop_login 一个函数,
|
||||||
它把真正的 HTTP 调用隔离出来,其余全部可离线测。
|
它把真正的 HTTP 调用隔离出来,其余全部可离线测。
|
||||||
|
|
||||||
三块职责:
|
三块职责:
|
||||||
一、认证:解析第三方登录系统 bshop 的登录返回(parse_bshop_login)。
|
一、认证:解析第三方登录系统 bshop 的登录返回(parse_bshop_login)。
|
||||||
二、角色:从 bshop 返回的权限清单里认出交易系统管理员 / 交易员(role_from_permissions)。
|
二、角色:从 bshop 返回的权限清单里认出系统管理员 / 交易员(role_from_permissions)。
|
||||||
三、会话与鉴权:自签会话票(make_session / verify_session),以及按「方法 + 路径」
|
三、会话与鉴权:自签会话票(make_session / verify_session),以及按「方法 + 路径」
|
||||||
判这个请求要什么角色(authz_decision)。
|
判这个请求要什么角色(authz_decision)。
|
||||||
|
|
||||||
|
|
@ -26,7 +26,7 @@ import time
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
# 角色常量:既是写进会话票的字面值,也是回给前端、供前端判断的字面值。
|
# 角色常量:既是写进会话票的字面值,也是回给前端、供前端判断的字面值。
|
||||||
ROLE_ADMIN = "交易系统管理员"
|
ROLE_ADMIN = "系统管理员"
|
||||||
ROLE_TRADER = "交易员"
|
ROLE_TRADER = "交易员"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -92,7 +92,7 @@ def _markers(csv: str) -> set:
|
||||||
def role_from_permissions(permissions, admin_perms_csv: str,
|
def role_from_permissions(permissions, admin_perms_csv: str,
|
||||||
trader_perms_csv: str = "") -> Optional[str]:
|
trader_perms_csv: str = "") -> Optional[str]:
|
||||||
"""按权限清单判角色。
|
"""按权限清单判角色。
|
||||||
- 命中任一「管理员标记」权限 -> 交易系统管理员
|
- 命中任一「管理员标记」权限 -> 系统管理员
|
||||||
- 否则若配置了「交易员准入」且一条都不占 -> None(无权进入)
|
- 否则若配置了「交易员准入」且一条都不占 -> None(无权进入)
|
||||||
- 否则 -> 交易员
|
- 否则 -> 交易员
|
||||||
permissions 是 "system_key:menu_key" 字符串列表(parse_bshop_login 已归一)。
|
permissions 是 "system_key:menu_key" 字符串列表(parse_bshop_login 已归一)。
|
||||||
|
|
@ -159,20 +159,19 @@ def verify_session(cookie: str, secret: str, now: Optional[float] = None) -> Opt
|
||||||
|
|
||||||
|
|
||||||
# ============================================================ 四、接口鉴权判定
|
# ============================================================ 四、接口鉴权判定
|
||||||
# 交易员放行的写接口(白名单)。其余任何写接口一律只给管理员 —— 「写操作默认拒绝」。
|
# 分工按前端两块视图划分(用户 2026-08 拍板):交易员工作台(前端 mode=trader)上能点到
|
||||||
# 路径按段匹配,"*" 吃掉一段路径参数(如 {command_id})。
|
# 的都归交易员;只有运维视图(mode=ops)里的系统维护操作归系统管理员。所以这里反过来只
|
||||||
_TRADER_WRITE = [
|
# 登记「只给管理员」的那一小撮运维接口,其余已登录的人(交易员)默认放行 —— 交易员的能力
|
||||||
("POST", ("api", "commands")), # 下命令
|
# 跟着他那一页走:日常下单、撤单、拍板、策略、参数微调、大盘冷热采纳,都算交易员的活。
|
||||||
("POST", ("api", "commands", "*", "cancel")), # 撤命令
|
# 只给管理员的接口族(前缀匹配,任何方法都算):
|
||||||
("POST", ("api", "commands", "*", "replan")), # 改期重排
|
_ADMIN_PREFIX = [
|
||||||
("POST", ("api", "proposals", "*", "decide")), # 拍板提议
|
("api", "ops"), # 回放 / 对账 / 日终 / 日报 / 择时扫描 / plan-refresh 等全部运维操作
|
||||||
|
("api", "ws-channel"), # ws 直连通道管理
|
||||||
|
("api", "industry"), # 行业划分数据导入
|
||||||
]
|
]
|
||||||
|
# 只给管理员的单个接口(精确匹配方法与整条路径):
|
||||||
# 只给管理员的读接口(其余 GET 所有登录人可看)。
|
_ADMIN_EXACT = [
|
||||||
_ADMIN_GET = [
|
("POST", ("api", "proposals")), # 人工补录提议:影子期造数调试口,没有交易员入口
|
||||||
("GET", ("api", "ops", "rebuild-preflight")),
|
|
||||||
("GET", ("api", "ops", "rebuild-accept")),
|
|
||||||
("GET", ("api", "ops", "downstream-schema")),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -187,22 +186,19 @@ def _match(pattern: tuple, segs: tuple) -> bool:
|
||||||
|
|
||||||
|
|
||||||
def authz_decision(method: str, path: str) -> str:
|
def authz_decision(method: str, path: str) -> str:
|
||||||
"""判这个请求所需的最低角色,返回 "admin"(要交易系统管理员)或 "any"(任何登录人可)。
|
"""判这个请求所需的最低角色,返回 "admin"(要系统管理员)或 "any"(任何登录人可)。
|
||||||
规则:写方法(POST/PUT/PATCH/DELETE)不在交易员白名单里就要管理员;
|
规则:命中「只给管理员」的运维接口族,或那条人工补录提议的调试口,就要管理员;其余
|
||||||
GET/HEAD 默认放行,仅少数运维读接口要管理员。
|
已登录的人都放行。只对「已登录、且不在公开白名单」的请求调用(白名单在 is_public_path)。
|
||||||
只对「已登录、且不在公开白名单」的请求调用(公开白名单在 is_public_path)。
|
|
||||||
"""
|
"""
|
||||||
method = (method or "").upper()
|
method = (method or "").upper()
|
||||||
segs = _seg(path)
|
segs = _seg(path)
|
||||||
if method in ("GET", "HEAD"):
|
for m, pat in _ADMIN_EXACT:
|
||||||
for m, pat in _ADMIN_GET:
|
|
||||||
if m == method and _match(pat, segs):
|
if m == method and _match(pat, segs):
|
||||||
return "admin"
|
return "admin"
|
||||||
return "any"
|
for pref in _ADMIN_PREFIX:
|
||||||
for m, pat in _TRADER_WRITE:
|
if len(segs) >= len(pref) and tuple(segs[:len(pref)]) == pref:
|
||||||
if m == method and _match(pat, segs):
|
|
||||||
return "any"
|
|
||||||
return "admin"
|
return "admin"
|
||||||
|
return "any"
|
||||||
|
|
||||||
|
|
||||||
# 公开路径:不需要登录也放行。首页 / 必须放行,否则登录框自己都加载不出来。
|
# 公开路径:不需要登录也放行。首页 / 必须放行,否则登录框自己都加载不出来。
|
||||||
|
|
|
||||||
|
|
@ -405,7 +405,7 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
||||||
<div v-if="macroGateOn" style="color:var(--warn)">大盘偏热,系统已经停掉它自己发起的买入,以及策略里的买入动作。卖出不受影响,你自己下的命令也不受影响。</div>
|
<div v-if="macroGateOn" style="color:var(--warn)">大盘偏热,系统已经停掉它自己发起的买入,以及策略里的买入动作。卖出不受影响,你自己下的命令也不受影响。</div>
|
||||||
<div v-if="mzSig.advice && !mzSig.advice.adopted" style="margin-top:4px">
|
<div v-if="mzSig.advice && !mzSig.advice.adopted" style="margin-top:4px">
|
||||||
<b>今日建议:{{ mzSig.advice.direction==='REDUCE' ? '降仓' : '升仓' }} {{ (mzSig.advice.pct*100).toFixed(1) }}%</b>
|
<b>今日建议:{{ mzSig.advice.direction==='REDUCE' ? '降仓' : '升仓' }} {{ (mzSig.advice.pct*100).toFixed(1) }}%</b>
|
||||||
<button v-if="isAdmin" class="tbtn pri" style="margin-left:8px" @click="macroAdopt(mzSig.key)">采纳并下命令</button>
|
<button class="tbtn pri" style="margin-left:8px" @click="macroAdopt(mzSig.key)">采纳并下命令</button>
|
||||||
<div class="muted" style="font-size:11px">{{ mzSig.advice.why }} · 当日有效,隔日作废</div>
|
<div class="muted" style="font-size:11px">{{ mzSig.advice.why }} · 当日有效,隔日作废</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="muted" v-if="mzSig.last_command" style="font-size:11px">最近一条大盘冷热命令 {{ mzSig.last_command.command_id }} · {{ mzSig.last_command.cmd_type==='REDUCE_EXPOSURE' ? '降仓' : '升仓' }} · {{ mzSig.last_command.status }}</div>
|
<div class="muted" v-if="mzSig.last_command" style="font-size:11px">最近一条大盘冷热命令 {{ mzSig.last_command.command_id }} · {{ mzSig.last_command.cmd_type==='REDUCE_EXPOSURE' ? '降仓' : '升仓' }} · {{ mzSig.last_command.status }}</div>
|
||||||
|
|
@ -872,7 +872,7 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
||||||
<el-table-column label="下单量 / 已成交" width="130" align="right"><template #default="s"><span class="mono">{{ s.row.qty }} / {{ s.row.exec_qty||0 }}</span></template></el-table-column>
|
<el-table-column label="下单量 / 已成交" width="130" align="right"><template #default="s"><span class="mono">{{ s.row.qty }} / {{ s.row.exec_qty||0 }}</span></template></el-table-column>
|
||||||
<el-table-column label="限价" width="82" align="right"><template #default="s"><span class="mono">{{ s.row.limit_price||'—' }}</span></template></el-table-column>
|
<el-table-column label="限价" width="82" align="right"><template #default="s"><span class="mono">{{ s.row.limit_price||'—' }}</span></template></el-table-column>
|
||||||
<el-table-column label="进展" min-width="130"><template #default="s"><span class="chip idle"><span class="dot"></span>{{ tx('insStatus', s.row.status) }}</span></template></el-table-column>
|
<el-table-column label="进展" min-width="130"><template #default="s"><span class="chip idle"><span class="dot"></span>{{ tx('insStatus', s.row.status) }}</span></template></el-table-column>
|
||||||
<el-table-column label="操作" width="76"><template #default="s"><button v-if="isAdmin" class="tbtn sm" @click="cancelIns(s.row.instruction_id)">撤单</button></template></el-table-column>
|
<el-table-column label="操作" width="76"><template #default="s"><button class="tbtn sm" @click="cancelIns(s.row.instruction_id)">撤单</button></template></el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane name="done">
|
<el-tab-pane name="done">
|
||||||
|
|
@ -1043,7 +1043,7 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
||||||
<div class="st" style="color:var(--warn)" v-if="s.buy_paused">
|
<div class="st" style="color:var(--warn)" v-if="s.buy_paused">
|
||||||
这只票现在只卖不买。决策系统提示{{ s.buy_paused.reason || '资金异动' }},已经停掉买入;
|
这只票现在只卖不买。决策系统提示{{ s.buy_paused.reason || '资金异动' }},已经停掉买入;
|
||||||
卖出、以及把当天做T买的那部分卖回去,都不受影响。</div>
|
卖出、以及把当天做T买的那部分卖回去,都不受影响。</div>
|
||||||
<div style="margin-top:6px" v-if="isAdmin">
|
<div style="margin-top:6px">
|
||||||
<el-button v-if="s.status==='ACTIVE'" size="small" @click="setStrategyStatus(s.strategy_id,'PAUSED')">暂停</el-button>
|
<el-button v-if="s.status==='ACTIVE'" size="small" @click="setStrategyStatus(s.strategy_id,'PAUSED')">暂停</el-button>
|
||||||
<el-button v-else-if="s.status==='PAUSED'" size="small" type="success" @click="setStrategyStatus(s.strategy_id,'ACTIVE')">恢复</el-button>
|
<el-button v-else-if="s.status==='PAUSED'" size="small" type="success" @click="setStrategyStatus(s.strategy_id,'ACTIVE')">恢复</el-button>
|
||||||
<el-button v-if="s.buy_paused" size="small" type="warning" @click="resumeBuy(s)">恢复买入</el-button>
|
<el-button v-if="s.buy_paused" size="small" type="warning" @click="resumeBuy(s)">恢复买入</el-button>
|
||||||
|
|
@ -1259,8 +1259,8 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
||||||
</el-alert>
|
</el-alert>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button v-if="isAdmin" size="small" @click="validateStrategy" :loading="stratDlg.busy">先校验</el-button>
|
<el-button size="small" @click="validateStrategy" :loading="stratDlg.busy">先校验</el-button>
|
||||||
<el-button v-if="isAdmin" size="small" type="primary" @click="attachStrategy" :loading="stratDlg.busy">挂上并生效</el-button>
|
<el-button size="small" type="primary" @click="attachStrategy" :loading="stratDlg.busy">挂上并生效</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
|
@ -1286,7 +1286,7 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
||||||
<el-tabs v-model="tab" type="border-card">
|
<el-tabs v-model="tab" type="border-card">
|
||||||
|
|
||||||
<!-- ==================== ① 参数设置 ==================== -->
|
<!-- ==================== ① 参数设置 ==================== -->
|
||||||
<el-tab-pane label="参数设置" name="params" v-if="isAdmin">
|
<el-tab-pane label="参数设置" name="params">
|
||||||
<div class="row" style="margin-bottom:10px">
|
<div class="row" style="margin-bottom:10px">
|
||||||
<span class="muted">页面改动持久化到 pms_runtime_param, 优先于 settings.py 初值; 基础设施连接串只在 .env 维护。</span>
|
<span class="muted">页面改动持久化到 pms_runtime_param, 优先于 settings.py 初值; 基础设施连接串只在 .env 维护。</span>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
|
|
@ -1421,9 +1421,9 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
||||||
@click="cancelCmd(s.row.command_id)">撤销</el-button>
|
@click="cancelCmd(s.row.command_id)">撤销</el-button>
|
||||||
<el-button size="small" :disabled="!canCancel(s.row) || s.row.cmd_class!=='task'"
|
<el-button size="small" :disabled="!canCancel(s.row) || s.row.cmd_class!=='task'"
|
||||||
@click="replan(s.row.command_id)">重规划</el-button>
|
@click="replan(s.row.command_id)">重规划</el-button>
|
||||||
<el-button v-if="isAdmin && cmdArchivable(s.row) && !s.row.archived_at" size="small"
|
<el-button v-if="cmdArchivable(s.row) && !s.row.archived_at" size="small"
|
||||||
@click="archiveRow('commands', s.row.command_id, true)">移除</el-button>
|
@click="archiveRow('commands', s.row.command_id, true)">移除</el-button>
|
||||||
<el-button v-if="isAdmin && s.row.archived_at" size="small" type="info"
|
<el-button v-if="s.row.archived_at" size="small" type="info"
|
||||||
@click="archiveRow('commands', s.row.command_id, false)">恢复显示</el-button>
|
@click="archiveRow('commands', s.row.command_id, false)">恢复显示</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
@ -1547,7 +1547,7 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
||||||
<el-table-column prop="origin_type" label="来源" width="80"></el-table-column>
|
<el-table-column prop="origin_type" label="来源" width="80"></el-table-column>
|
||||||
<el-table-column label="操作" width="90">
|
<el-table-column label="操作" width="90">
|
||||||
<template #default="s">
|
<template #default="s">
|
||||||
<el-button v-if="isAdmin" size="small" @click="cancelIns(s.row.instruction_id)">撤销</el-button>
|
<el-button size="small" @click="cancelIns(s.row.instruction_id)">撤销</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
@ -1617,7 +1617,7 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
||||||
scripts/ws_smoke.py place --yes —— 那条路径有显式确认、有参数校验、
|
scripts/ws_smoke.py place --yes —— 那条路径有显式确认、有参数校验、
|
||||||
有"这张单会真成交"的警告, 且不受 PMS_DISPATCH_MODE 影响。页面上加个
|
有"这张单会真成交"的警告, 且不受 PMS_DISPATCH_MODE 影响。页面上加个
|
||||||
发单按钮等于在生产界面开了第二条绕过开关的下单入口, 不值当。 -->
|
发单按钮等于在生产界面开了第二条绕过开关的下单入口, 不值当。 -->
|
||||||
<el-tab-pane label="ws 通道" name="ws" v-if="isAdmin">
|
<el-tab-pane label="ws 通道" name="ws">
|
||||||
<div class="row" style="margin-bottom:10px">
|
<div class="row" style="margin-bottom:10px">
|
||||||
<el-tag :type="wsc.process_alive ? 'success':'danger'" effect="dark" size="small">
|
<el-tag :type="wsc.process_alive ? 'success':'danger'" effect="dark" size="small">
|
||||||
进程 {{ wsc.process_alive ? '在线':'不在线' }}</el-tag>
|
进程 {{ wsc.process_alive ? '在线':'不在线' }}</el-tag>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
第十八批: 登录与权限 (交易系统管理员 / 交易员) 的纯逻辑 —— 不联网、不连库
|
第十八批: 登录与权限 (系统管理员 / 交易员) 的纯逻辑 —— 不联网、不连库
|
||||||
=====================================================================
|
=====================================================================
|
||||||
本批只测 app/web/auth.py 里的纯函数, 不导入 app.web.main (那会拉起 fastapi 与一堆
|
本批只测 app/web/auth.py 里的纯函数, 不导入 app.web.main (那会拉起 fastapi 与一堆
|
||||||
基础设施依赖)。覆盖四件事:
|
基础设施依赖)。覆盖四件事:
|
||||||
1. 角色判定 role_from_permissions: 管理员标记命中/未中、交易员准入闸放行与拦截;
|
1. 角色判定 role_from_permissions: 管理员标记命中/未中、交易员准入闸放行与拦截;
|
||||||
2. 会话票 make_session / verify_session: 签发-校验往返, 篡改/过期/换密钥/空密钥一律拒;
|
2. 会话票 make_session / verify_session: 签发-校验往返, 篡改/过期/换密钥/空密钥一律拒;
|
||||||
3. bshop 返回解析 parse_bshop_login: 喂真实样例抽出手机号/用户名/权限/组织, code!=200 判失败;
|
3. bshop 返回解析 parse_bshop_login: 喂真实样例抽出手机号/用户名/权限/组织, code!=200 判失败;
|
||||||
4. 接口鉴权 authz_decision + is_public_path: 「写操作默认拒绝」——交易员白名单外的写接口
|
4. 接口鉴权 authz_decision + is_public_path: 分工按前端两块视图划分 —— 交易员工作台上能点到
|
||||||
一律要管理员 (专门钉死 /api/macro/adopt、POST /api/proposals、.../archive 与 .../cancel
|
的都归交易员 (下单/撤单/拍板/策略/参数微调/大盘冷热采纳), 只有运维视图里的系统维护
|
||||||
结尾段区分), GET 默认放行仅少数运维读接口要管理员; 公开白名单放行首页与登录接口。
|
操作 (ops/* 、ws-channel、industry 导入、人工补录提议) 归管理员; 公开白名单放行首页与登录接口。
|
||||||
运行: python scripts/test_batch18_units.py
|
运行: python scripts/test_batch18_units.py
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
|
@ -50,25 +50,25 @@ SAMPLE = {
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- 一、角色判定
|
# ---------------------------------------------------------------- 一、角色判定
|
||||||
@case("角色: 命中管理员标记 -> 交易系统管理员")
|
@case("角色: 命中管理员标记 => 管理员")
|
||||||
def _role_admin():
|
def _role_admin():
|
||||||
r = A.role_from_permissions(["band:bkld", "pms:admin"], "pms:admin", "")
|
r = A.role_from_permissions(["band:bkld", "pms:admin"], "pms:admin", "")
|
||||||
assert r == A.ROLE_ADMIN, r
|
assert r == A.ROLE_ADMIN, r
|
||||||
|
|
||||||
|
|
||||||
@case("角色: 只有普通权限 -> 交易员")
|
@case("角色: 只有普通权限 => 交易员")
|
||||||
def _role_trader():
|
def _role_trader():
|
||||||
r = A.role_from_permissions(["band:bkld", "band:yhyqx"], "pms:admin", "")
|
r = A.role_from_permissions(["band:bkld", "band:yhyqx"], "pms:admin", "")
|
||||||
assert r == A.ROLE_TRADER, r
|
assert r == A.ROLE_TRADER, r
|
||||||
|
|
||||||
|
|
||||||
@case("角色: 配了交易员准入且占其一 -> 交易员")
|
@case("角色: 配了交易员准入且占其一 => 交易员")
|
||||||
def _role_trader_gate_pass():
|
def _role_trader_gate_pass():
|
||||||
r = A.role_from_permissions(["band:ccgl"], "pms:admin", "band:ccgl,pms:-")
|
r = A.role_from_permissions(["band:ccgl"], "pms:admin", "band:ccgl,pms:-")
|
||||||
assert r == A.ROLE_TRADER, r
|
assert r == A.ROLE_TRADER, r
|
||||||
|
|
||||||
|
|
||||||
@case("角色: 配了交易员准入却一条不占 -> None (无权进入)")
|
@case("角色: 配了交易员准入却一条不占 => None (无权进入)")
|
||||||
def _role_denied():
|
def _role_denied():
|
||||||
r = A.role_from_permissions(["band:bkld"], "pms:admin", "band:ccgl,pms:-")
|
r = A.role_from_permissions(["band:bkld"], "pms:admin", "band:ccgl,pms:-")
|
||||||
assert r is None, r
|
assert r is None, r
|
||||||
|
|
@ -89,7 +89,7 @@ def _sess_roundtrip():
|
||||||
assert p["role"] == A.ROLE_ADMIN and p["exp"] == 1000 + 12 * 3600, p
|
assert p["role"] == A.ROLE_ADMIN and p["exp"] == 1000 + 12 * 3600, p
|
||||||
|
|
||||||
|
|
||||||
@case("会话: 篡改正文或签名 -> None")
|
@case("会话: 篡改正文或签名 => None")
|
||||||
def _sess_tampered():
|
def _sess_tampered():
|
||||||
tok = A.make_session("186", "u", A.ROLE_TRADER, "s3cr3t", 12, now=1000.0)
|
tok = A.make_session("186", "u", A.ROLE_TRADER, "s3cr3t", 12, now=1000.0)
|
||||||
b, sig = tok.split(".", 1)
|
b, sig = tok.split(".", 1)
|
||||||
|
|
@ -107,7 +107,7 @@ def _sess_reject():
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- 三、bshop 解析
|
# ---------------------------------------------------------------- 三、bshop 解析
|
||||||
@case("解析: 真实样例 -> 手机号/用户名/权限/组织")
|
@case("解析: 真实样例 => 手机号/用户名/权限/组织")
|
||||||
def _parse_ok():
|
def _parse_ok():
|
||||||
r = A.parse_bshop_login(SAMPLE)
|
r = A.parse_bshop_login(SAMPLE)
|
||||||
assert r["ok"] is True, r
|
assert r["ok"] is True, r
|
||||||
|
|
@ -124,7 +124,7 @@ def _parse_then_role():
|
||||||
assert A.role_from_permissions(r["permissions"], "band:yhyqx", "") == A.ROLE_ADMIN
|
assert A.role_from_permissions(r["permissions"], "band:yhyqx", "") == A.ROLE_ADMIN
|
||||||
|
|
||||||
|
|
||||||
@case("解析: code!=200 / 缺手机号 / 非字典 -> 失败")
|
@case("解析: code!=200 / 缺手机号 / 非字典 => 失败")
|
||||||
def _parse_fail():
|
def _parse_fail():
|
||||||
assert A.parse_bshop_login({"code": 401, "msg": "密码错误"})["ok"] is False
|
assert A.parse_bshop_login({"code": 401, "msg": "密码错误"})["ok"] is False
|
||||||
assert A.parse_bshop_login({"code": 200, "data": {"user": {}}})["ok"] is False
|
assert A.parse_bshop_login({"code": 200, "data": {"user": {}}})["ok"] is False
|
||||||
|
|
@ -132,35 +132,44 @@ def _parse_fail():
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- 四、接口鉴权
|
# ---------------------------------------------------------------- 四、接口鉴权
|
||||||
@case("鉴权: 交易员白名单 4 条写接口放行 (any)")
|
@case("鉴权: 交易员工作台上的写操作都放行 (any)")
|
||||||
def _authz_trader_ok():
|
def _authz_trader_ok():
|
||||||
for m, path in [("POST", "/api/commands"),
|
trader_paths = [
|
||||||
|
("POST", "/api/commands"),
|
||||||
("POST", "/api/commands/CMD-1/cancel"),
|
("POST", "/api/commands/CMD-1/cancel"),
|
||||||
("POST", "/api/commands/CMD-1/replan"),
|
("POST", "/api/commands/CMD-1/replan"),
|
||||||
("POST", "/api/proposals/PRP-1/decide")]:
|
("POST", "/api/commands/CMD-1/archive"),
|
||||||
|
("POST", "/api/proposals/PRP-1/decide"),
|
||||||
|
("POST", "/api/proposals/PRP-1/archive"),
|
||||||
|
("POST", "/api/instructions/INS-1/cancel"), # 用户点名: 撤单归交易员
|
||||||
|
("POST", "/api/macro/adopt"), # 用户点名: 大盘冷热采纳归交易员
|
||||||
|
("POST", "/api/params"), # 工作台上有参数微调面板
|
||||||
|
("POST", "/api/strategies"),
|
||||||
|
("POST", "/api/strategies/validate"),
|
||||||
|
("POST", "/api/strategies/S1/status"),
|
||||||
|
("POST", "/api/strategies/S1/resume-buy"),
|
||||||
|
("POST", "/api/strategies/S1/archive"),
|
||||||
|
]
|
||||||
|
for m, path in trader_paths:
|
||||||
assert A.authz_decision(m, path) == "any", (m, path, A.authz_decision(m, path))
|
assert A.authz_decision(m, path) == "any", (m, path, A.authz_decision(m, path))
|
||||||
|
|
||||||
|
|
||||||
@case("鉴权: 写操作默认拒绝 —— 白名单外一律要管理员")
|
@case("鉴权: 只有系统维护类接口归管理员")
|
||||||
def _authz_admin_default():
|
def _authz_admin_only():
|
||||||
admin_paths = [
|
admin_paths = [
|
||||||
("POST", "/api/params"),
|
|
||||||
("POST", "/api/ops/reconcile"),
|
("POST", "/api/ops/reconcile"),
|
||||||
("POST", "/api/strategies"),
|
("POST", "/api/ops/daily-settle"),
|
||||||
("POST", "/api/strategies/S1/status"),
|
("POST", "/api/ops/macro-scan"),
|
||||||
("POST", "/api/industry/import"),
|
("POST", "/api/ops/plan-refresh"),
|
||||||
("POST", "/api/ws-channel/clear-resync"),
|
("POST", "/api/ws-channel/clear-resync"),
|
||||||
("POST", "/api/instructions/INS-1/cancel"),
|
("POST", "/api/industry/import"),
|
||||||
("POST", "/api/macro/adopt"), # 评审点出的漏网写接口, 兜住
|
("POST", "/api/proposals"), # 人工补录 (无 id 段) = 造数调试口
|
||||||
("POST", "/api/proposals"), # 人工补录=造数调试口, 归管理员
|
|
||||||
("POST", "/api/commands/CMD-1/archive"), # 结尾段 archive != cancel
|
|
||||||
("POST", "/api/proposals/PRP-1/archive"),
|
|
||||||
]
|
]
|
||||||
for m, path in admin_paths:
|
for m, path in admin_paths:
|
||||||
assert A.authz_decision(m, path) == "admin", (m, path, A.authz_decision(m, path))
|
assert A.authz_decision(m, path) == "admin", (m, path, A.authz_decision(m, path))
|
||||||
|
|
||||||
|
|
||||||
@case("鉴权: GET 默认放行, 仅三条运维读接口要管理员")
|
@case("鉴权: 运维读接口要管理员, 普通读接口谁都能看")
|
||||||
def _authz_gets():
|
def _authz_gets():
|
||||||
for path in ["/api/ops/rebuild-preflight", "/api/ops/rebuild-accept",
|
for path in ["/api/ops/rebuild-preflight", "/api/ops/rebuild-accept",
|
||||||
"/api/ops/downstream-schema"]:
|
"/api/ops/downstream-schema"]:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue