增加投顾(advisor)角色;修正登录测试的错误假设;修投顾带入的 2 处文档重号
## 投顾角色
投顾线合并后,bootstrap.py 有 10 处 allowed_roles 引用 advisor,
financial_nl2sql_service.py:272 还硬编码检查 {"advisor","operator","admin","super_admin"},
promotion_material_service.py:164 按 "advisor" in context.roles 走业务分支 ——
但 sys_role 里没有这个角色、sys_permission 里也没有投顾那 16 个权限码(种子只建到 9019)。
表现是所有投顾接口 403,而报错看起来像"权限配错了",实际是角色根本不存在。
- tools/grant_advisor_role.py:建 advisor 角色(id=9004,避开种子的 9001-9003 重建范围)
+ 16 个投顾权限(id 9020-9035)+ 授权(advisor 拿 10 项工作流、admin 补齐 16 项)。
只增不删、可重复执行、带 --dry-run。
- tools/create_test_user.py:ROLE_IDS 加 advisor。
- 先跑 alembic upgrade head:补 21 张 advisor_* 表,业务表 68 → 89,审计通过。
权限划分:投顾工作流 10 项(read:self / generate:self / review / publish)给 advisor;
治理类 6 项(product-governance:*、profile-governance:*、asset-allocation:backtest)只给 admin。
review/publish 也给 advisor,与既有决策一致(此前已裁定不做双人复核)。
验证:advisor_t 登录 200,roles=['advisor'] data_scope=all 权限 10 项;
用它查 RBAC 清单得 403(没有 audit:read),边界正确。
⚠️ 与种子的冲突:seed_test_rbac.py 是 DELETE 重建语义,其
DELETE FROM sys_permission WHERE id BETWEEN 9001 AND 9099 会清掉本脚本建的权限。
要把投顾权限固化,应并进 seed_test_rbac.py 的 PERMISSIONS 常量。
## 修正登录测试的一个错误假设
test_issued_token_actually_works_on_a_real_endpoint 原本用客户的
/users/me/memory-profile 验证令牌可用,投顾合并后它返回 403。追下去发现**与令牌无关**:
那个接口对客户有业务前置"请先完成开户风险测评问卷",而演示客户 9001 没有测评记录。
是我的测试选错了验证端点,把"业务前置未满足"误判成"令牌坏了"。
- 改用管理员令牌调 /api/v1/admin/roles(需要 audit:read,走完整鉴权链路),
并补一条反向对照:不带令牌必须 401,否则那个 200 说明不了令牌有效。
- 把那个业务前置单独写成一个用例,让后来者一眼看到条件,而不是反复怀疑令牌。
过程里我先按控制台乱码猜了两次失败原因,都不对;最后把响应抓成 UTF-8 文件才看到真实
消息。教训记下:不要读乱码猜消息。
## 修投顾带入的 2 处文档重号
21-投顾Agent迁移TODO.md → 30-…、22-投顾Agent灰度与回滚操作手册.md → 31-…
(沿用 NL 那次让号的先例:既有文档更早、引用更多;且这两份新文档没有被任何地方引用。)
文档守卫:40 份无编号冲突。
验证:ruff 干净 / mypy 228 文件 0 错 / 文档守卫 40 份无冲突 /
unit+contract 1207 passed(0 failed)/ integration 99 passed / 业务表 89 张。
This commit is contained in:
+24
-3
@@ -103,11 +103,20 @@ PAGE = """<!doctype html>
|
||||
<span class="muted">(下面按钮按角色给,方便核对权限)</span>
|
||||
</div>
|
||||
<div class="row" id="probes"></div>
|
||||
<div class="row">
|
||||
<label>用户 id</label>
|
||||
<input id="probeUserId" placeholder="任意用户 id,如 9001;留空则查当前登录用户"
|
||||
autocomplete="off" style="max-width:440px">
|
||||
<button onclick="probeUser()">查这个用户的身份</button>
|
||||
</div>
|
||||
<pre id="output">(还没有请求)</pre>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let token = null;
|
||||
// 当前登录用户的 id(取自登录响应),供"我的身份"以及"用户 id 留空时默认查自己"使用。
|
||||
// 页面里**不写死任何用户 id**:写死会让人以为权限解析错了(真实发生过一次)。
|
||||
let identityUserId = null;
|
||||
|
||||
function fill(u, p) {
|
||||
document.getElementById('username').value = u;
|
||||
@@ -120,6 +129,7 @@ function show(text) {
|
||||
|
||||
function renderIdentity(data) {
|
||||
const el = document.getElementById('identity');
|
||||
identityUserId = data ? data.user_id : null;
|
||||
if (!data) { el.textContent = '未登录'; el.className = 'muted'; return; }
|
||||
const roles = (data.roles || []).map(r => `<span class="tag">${r}</span>`).join('');
|
||||
el.className = '';
|
||||
@@ -127,17 +137,20 @@ function renderIdentity(data) {
|
||||
`用户 <b>${data.username || data.user_id}</b> 角色 ${roles || '<span class="muted">(无)</span>'}<br>` +
|
||||
`<span class="muted">data_scope=<b>${data.data_scope ?? '-'}</b> ` +
|
||||
`令牌有效期 ${data.expires_in} 秒 令牌 ${data.access_token.slice(0, 24)}…</span>`;
|
||||
renderProbes(data.roles || []);
|
||||
renderProbes(data.roles || [], data.user_id);
|
||||
}
|
||||
|
||||
// 按角色给出可调的只读接口 —— 用来核对"登录给的 roles"与"接口实际放行"是否一致。
|
||||
function renderProbes(roles) {
|
||||
// `userId` 必须用**当前登录用户**的 id:写死成 9001 会让管理员点了之后看到客户的信息,
|
||||
// 看起来像"权限解析错了"(这个误导真实发生过一次)。
|
||||
function renderProbes(roles, userId) {
|
||||
const probes = [
|
||||
{ label: '查我的画像', path: '/api/v1/users/me/memory-profile', all: true },
|
||||
{ label: '风控概览', path: '/api/v1/risk/overview', role: 'risk_operator' },
|
||||
{ label: '预警列表', path: '/api/v1/risk/alerts?limit=5', role: 'risk_operator' },
|
||||
{ label: '角色清单', path: '/api/v1/admin/roles', role: 'admin' },
|
||||
{ label: '我的身份(管理视角)', path: '/api/v1/admin/users/9001/roles', role: 'admin' },
|
||||
{ label: '我的身份(管理视角)',
|
||||
path: `/api/v1/admin/users/${userId}/roles`, role: 'admin' },
|
||||
];
|
||||
const box = document.getElementById('probes');
|
||||
box.innerHTML = '';
|
||||
@@ -194,6 +207,14 @@ async function callApi(path) {
|
||||
}
|
||||
}
|
||||
|
||||
// 查任意用户 id 的身份(管理员用)。不写死任何 id:留空则查当前登录用户。
|
||||
async function probeUser() {
|
||||
const raw = document.getElementById('probeUserId').value.trim();
|
||||
const target = raw || (identityUserId || '');
|
||||
if (!target) { show('请填用户 id;或先登录(留空时默认查当前登录用户)'); return; }
|
||||
await callApi(`/api/v1/admin/users/${target}/roles`);
|
||||
}
|
||||
|
||||
function logout() {
|
||||
token = null;
|
||||
renderIdentity(null);
|
||||
|
||||
Reference in New Issue
Block a user