增加投顾(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:
@@ -25,7 +25,8 @@
|
|||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| 9001 | **`cust_t`** | `123456` | `customer` | 客户界面:自己的会话、适当性、知识问答 |
|
| 9001 | **`cust_t`** | `123456` | `customer` | 客户界面:自己的会话、适当性、知识问答 |
|
||||||
| 9002 | **`risk_t`** | `666666` | `risk_operator` | 风控界面:预警队列、证据、日报 |
|
| 9002 | **`risk_t`** | `666666` | `risk_operator` | 风控界面:预警队列、证据、日报 |
|
||||||
| 9003 | **`admin_t`** | `88888888` | `admin` | 管理界面:配置发布、模型端点、审计 |
|
| 9003 | **`admin_t`** | `88888888` | `admin` | 管理界面:配置发布、模型端点、审计、RBAC 清单 |
|
||||||
|
| 9020 | **`advisor_t`** | `abc12345` | `advisor` | 投顾界面:组合分析、产品对比、投资目标、方案发布 |
|
||||||
|
|
||||||
> 密码由 `tools/set_user_password.py` 设置。**这三种弱口令仅用于演示**,
|
> 密码由 `tools/set_user_password.py` 设置。**这三种弱口令仅用于演示**,
|
||||||
> 上线前必须全部更换。
|
> 上线前必须全部更换。
|
||||||
@@ -135,9 +136,9 @@ python tools/create_test_user.py --id 9010 --username test_cust --role customer
|
|||||||
python tools/create_test_user.py --id 9011 --username test_risk --role risk_operator --password abc12345
|
python tools/create_test_user.py --id 9011 --username test_risk --role risk_operator --password abc12345
|
||||||
```
|
```
|
||||||
|
|
||||||
**角色只有三个可选值**:`customer` / `risk_operator` / `admin`(`sys_role` 里现成的三个)。
|
**角色可选值**:`customer` / `risk_operator` / `admin` / `advisor`(投顾)。
|
||||||
要引入**新角色**得同时定义它的权限集合(`sys_role_permission`),那超出这个脚本的范围,
|
要引入**新角色**得同时定义它的权限集合(`sys_role_permission`)——
|
||||||
找平台侧。
|
`advisor` 就是由 `tools/grant_advisor_role.py` 建立的,可参照它。
|
||||||
|
|
||||||
### 脚本会做三件事,并**验证第四件**
|
### 脚本会做三件事,并**验证第四件**
|
||||||
|
|
||||||
|
|||||||
@@ -126,22 +126,56 @@ async def test_each_role_can_login_with_its_own_role(
|
|||||||
async def test_issued_token_actually_works_on_a_real_endpoint() -> None:
|
async def test_issued_token_actually_works_on_a_real_endpoint() -> None:
|
||||||
"""签出来的令牌必须能真的用 —— 这是本文件不用替身的理由。
|
"""签出来的令牌必须能真的用 —— 这是本文件不用替身的理由。
|
||||||
|
|
||||||
`GET /api/v1/users/me/memory-profile` 需要 `memory:read:self`(客户角色有),
|
用管理员令牌调 `/api/v1/admin/roles`(需要 `audit:read`),走的是
|
||||||
走的是 `build_request_context` → `JwtAuthenticator` → `IdentityService.resolve`
|
`build_request_context` → `JwtAuthenticator` → `IdentityService.resolve` 全链路,
|
||||||
这条真实链路:令牌只带 `sub`,角色与权限全部查库解析。
|
与真实请求完全一致。再补一条**反向对照**:不带令牌必须 401 ——
|
||||||
|
否则"返回 200"也可能只是这个端点根本没鉴权。
|
||||||
|
|
||||||
|
⚠️ 这里**刻意不用** `/users/me/memory-profile`:它对客户有"必须先完成开户风险测评
|
||||||
|
问卷"的业务前置,而演示客户 9001 没有测评记录,于是返回 403 ——
|
||||||
|
那看起来像令牌坏了,实际与令牌无关(见下面那条用例)。
|
||||||
|
"""
|
||||||
|
async with client() as http:
|
||||||
|
response = await login(http, "admin_t", "88888888")
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
token = response.json()["data"]["access_token"]
|
||||||
|
|
||||||
|
authorized = await http.get(
|
||||||
|
"/api/v1/admin/roles", headers={"Authorization": f"Bearer {token}"}
|
||||||
|
)
|
||||||
|
assert authorized.status_code == 200, authorized.text
|
||||||
|
assert isinstance(authorized.json()["data"], list)
|
||||||
|
|
||||||
|
anonymous = await http.get("/api/v1/admin/roles")
|
||||||
|
assert anonymous.status_code == 401, "不带令牌必须 401,否则上面的 200 说明不了令牌有效"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_customer_profile_requires_completed_assessment() -> None:
|
||||||
|
"""画像接口对客户有业务前置:必须先完成开户风险测评问卷。
|
||||||
|
|
||||||
|
演示客户 9001 目前**没有测评记录**(`fin_risk_assessment` 没有它的行),所以拿它的
|
||||||
|
令牌调自己的画像会得到 `403 请先完成开户风险测评问卷`。这不是令牌或 RBAC 的问题
|
||||||
|
—— 同一个接口用管理员令牌是 200。
|
||||||
|
|
||||||
|
把这个条件写成用例,是为了让后来者一眼看到它,而不是像这次的集成测试那样
|
||||||
|
反复怀疑"是不是登录/令牌坏了"。要演示客户画像,先给 9001 补一条测评记录。
|
||||||
"""
|
"""
|
||||||
async with client() as http:
|
async with client() as http:
|
||||||
response = await login(http, "cust_t", "123456")
|
response = await login(http, "cust_t", "123456")
|
||||||
assert response.status_code == 200, response.text
|
assert response.status_code == 200, response.text
|
||||||
token = response.json()["data"]["access_token"]
|
token = response.json()["data"]["access_token"]
|
||||||
|
|
||||||
authorized = await http.get(
|
profile = await http.get(
|
||||||
"/api/v1/users/me/memory-profile",
|
"/api/v1/users/me/memory-profile",
|
||||||
headers={"Authorization": f"Bearer {token}"},
|
headers={"Authorization": f"Bearer {token}"},
|
||||||
)
|
)
|
||||||
# 200=有画像,404=该客户还没有画像行;两者都说明**令牌被接受并通过了 RBAC**。
|
|
||||||
# 401/403 则说明令牌或身份解析链有问题。
|
# 令牌是被接受的(不是 401);被拒的原因是业务前置而非鉴权。
|
||||||
assert authorized.status_code in (200, 404), authorized.text
|
assert profile.status_code in (200, 403), profile.text
|
||||||
|
if profile.status_code == 403:
|
||||||
|
assert profile.json()["error"]["code"] == "AGENT_PERMISSION_DENIED"
|
||||||
|
assert "测评" in profile.json()["error"]["message"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -40,12 +40,14 @@ from app.service.identity_service import IdentityService
|
|||||||
if hasattr(sys.stdout, "reconfigure"):
|
if hasattr(sys.stdout, "reconfigure"):
|
||||||
sys.stdout.reconfigure(errors="replace") # type: ignore[union-attr]
|
sys.stdout.reconfigure(errors="replace") # type: ignore[union-attr]
|
||||||
|
|
||||||
#: 库里现成的三个角色(`tools/seed_test_rbac.py` 建的)。新用户复用它们。
|
#: 库里现成的角色。新用户复用它们。
|
||||||
#: 要引入**新角色**得同时定义它的权限集合(`sys_role_permission`),超出本脚本范围。
|
#: 要引入**新角色**得同时定义它的权限集合(`sys_role_permission`)——
|
||||||
|
#: `advisor` 就是由 `tools/grant_advisor_role.py` 建立的。
|
||||||
ROLE_IDS: dict[str, int] = {
|
ROLE_IDS: dict[str, int] = {
|
||||||
"customer": 9001,
|
"customer": 9001,
|
||||||
"risk_operator": 9002,
|
"risk_operator": 9002,
|
||||||
"admin": 9003,
|
"admin": 9003,
|
||||||
|
"advisor": 9004,
|
||||||
}
|
}
|
||||||
|
|
||||||
#: 角色 → `sys_user.user_type`。注意这是 `user_type`,与 `employee_role` 不是一回事。
|
#: 角色 → `sys_user.user_type`。注意这是 `user_type`,与 `employee_role` 不是一回事。
|
||||||
@@ -53,6 +55,7 @@ ROLE_USER_TYPE: dict[str, str] = {
|
|||||||
"customer": "customer",
|
"customer": "customer",
|
||||||
"risk_operator": "employee",
|
"risk_operator": "employee",
|
||||||
"admin": "employee",
|
"admin": "employee",
|
||||||
|
"advisor": "employee",
|
||||||
}
|
}
|
||||||
|
|
||||||
#: 客户的开户状态。风控扫描等链路会读它,写成 `closed` 会让部分规则不成立。
|
#: 客户的开户状态。风控扫描等链路会读它,写成 `closed` 会让部分规则不成立。
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
"""建立投顾(`advisor`)角色并授权。
|
||||||
|
|
||||||
|
## 为什么需要它
|
||||||
|
|
||||||
|
投顾这条线合并进来后,`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 个权限码
|
||||||
|
(`seed_test_rbac.py` 只建到 9019)。结果是:投顾登录后拿不到任何投顾权限,
|
||||||
|
所有投顾接口一律 403,而报错看起来像"权限配错了",实际是**角色根本不存在**。
|
||||||
|
|
||||||
|
## 权限怎么分
|
||||||
|
|
||||||
|
| 类别 | 权限码 | 给谁 |
|
||||||
|
|---|---|---|
|
||||||
|
| 投顾工作流(10) | `asset-allocation:generate:self`、`investment-goal:read:self` / `:review` / `:publish`、`portfolio-analysis:read:self`、`product-comparison:read:self`、`product-recommendation:read:self` / `:generate:self` / `:review` / `:publish` | `advisor` + `admin` |
|
||||||
|
| 治理类(6) | `asset-allocation:backtest`、`product-governance:read` / `:review` / `:sync`、`profile-governance:read` / `:review` | **只给 `admin`** |
|
||||||
|
|
||||||
|
`review` / `publish` 也给投顾,与项目既有决策一致 —— 此前已裁定**不做双人复核**
|
||||||
|
(`admin` 发布配置时也是"创建人自审")。治理类不给投顾:那是平台侧的活。
|
||||||
|
|
||||||
|
## ⚠️ 与 `seed_test_rbac.py` 的冲突
|
||||||
|
|
||||||
|
那个脚本是 **DELETE 重建**语义,它的
|
||||||
|
`DELETE FROM sys_permission WHERE id BETWEEN 9001 AND 9099` 会**清掉本脚本建的权限**
|
||||||
|
(本脚本用 9020-9035)。将来若要在种子里固化投顾权限,请把它并进
|
||||||
|
`seed_test_rbac.py` 的 `PERMISSIONS` 常量,而不是只跑本脚本。
|
||||||
|
|
||||||
|
本脚本自身**只增不删**:重复执行只补齐缺失项,不动任何已有绑定。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
|
||||||
|
python tools/grant_advisor_role.py --dry-run # 只打印将写入什么
|
||||||
|
python tools/grant_advisor_role.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
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 用 9004:`seed_test_rbac.py` 只重建 9001-9003,不会碰它。
|
||||||
|
ADVISOR_ROLE_ID = 9004
|
||||||
|
ADVISOR_ROLE_CODE = "advisor"
|
||||||
|
ADVISOR_ROLE_NAME = "投资顾问"
|
||||||
|
|
||||||
|
#: 权限 id 从 9020 起,避开种子已用的 9001-9019。
|
||||||
|
#: (id, permission_code, resource, action, data_scope)
|
||||||
|
ADVISOR_PERMISSIONS: tuple[tuple[int, str, str, str, str], ...] = (
|
||||||
|
(9020, "asset-allocation:generate:self", "asset-allocation", "generate", "self"),
|
||||||
|
(9021, "asset-allocation:backtest", "asset-allocation", "backtest", "all"),
|
||||||
|
(9022, "investment-goal:read:self", "investment-goal", "read", "self"),
|
||||||
|
(9023, "investment-goal:review", "investment-goal", "review", "all"),
|
||||||
|
(9024, "investment-goal:publish", "investment-goal", "publish", "all"),
|
||||||
|
(9025, "portfolio-analysis:read:self", "portfolio-analysis", "read", "self"),
|
||||||
|
(9026, "product-comparison:read:self", "product-comparison", "read", "self"),
|
||||||
|
(9027, "product-recommendation:read:self", "product-recommendation", "read", "self"),
|
||||||
|
(9028, "product-recommendation:generate:self", "product-recommendation", "generate", "self"),
|
||||||
|
(9029, "product-recommendation:review", "product-recommendation", "review", "all"),
|
||||||
|
(9030, "product-recommendation:publish", "product-recommendation", "publish", "all"),
|
||||||
|
(9031, "product-governance:read", "product-governance", "read", "all"),
|
||||||
|
(9032, "product-governance:review", "product-governance", "review", "all"),
|
||||||
|
(9033, "product-governance:sync", "product-governance", "sync", "all"),
|
||||||
|
(9034, "profile-governance:read", "profile-governance", "read", "all"),
|
||||||
|
(9035, "profile-governance:review", "profile-governance", "review", "all"),
|
||||||
|
)
|
||||||
|
|
||||||
|
#: 投顾拿哪些 —— 工作流那 10 个;治理类 6 个只给 admin。
|
||||||
|
ADVISOR_GRANTED_CODES: tuple[str, ...] = (
|
||||||
|
"asset-allocation:generate:self",
|
||||||
|
"investment-goal:read:self",
|
||||||
|
"investment-goal:review",
|
||||||
|
"investment-goal:publish",
|
||||||
|
"portfolio-analysis:read:self",
|
||||||
|
"product-comparison:read:self",
|
||||||
|
"product-recommendation:read:self",
|
||||||
|
"product-recommendation:generate:self",
|
||||||
|
"product-recommendation:review",
|
||||||
|
"product-recommendation:publish",
|
||||||
|
)
|
||||||
|
|
||||||
|
#: admin 角色 id(`seed_test_rbac.py` 建的)。
|
||||||
|
ADMIN_ROLE_ID = 9003
|
||||||
|
|
||||||
|
|
||||||
|
async def apply(*, dry_run: bool) -> int:
|
||||||
|
now = datetime.now(UTC).replace(tzinfo=None)
|
||||||
|
async with SessionFactory() as session, session.begin():
|
||||||
|
existing_codes = set(
|
||||||
|
(await session.scalars(
|
||||||
|
text("SELECT permission_code FROM sys_permission")
|
||||||
|
)).all()
|
||||||
|
)
|
||||||
|
to_create = [p for p in ADVISOR_PERMISSIONS if p[1] not in existing_codes]
|
||||||
|
print(f"权限:已存在 {len(existing_codes)} 个,本次新增 {len(to_create)} 个")
|
||||||
|
for _, code, resource, action, scope in to_create:
|
||||||
|
print(f" + {code:<44} {resource}:{action} scope={scope}")
|
||||||
|
|
||||||
|
role_exists = await session.scalar(
|
||||||
|
text("SELECT id FROM sys_role WHERE role_code = :code"),
|
||||||
|
{"code": ADVISOR_ROLE_CODE},
|
||||||
|
)
|
||||||
|
print(f"角色 {ADVISOR_ROLE_CODE}:{'已存在' if role_exists else '将新建(id=9004)'}")
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
print("\n[dry-run] 未写入任何数据。")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
for perm_id, code, resource, action, scope in to_create:
|
||||||
|
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},
|
||||||
|
)
|
||||||
|
|
||||||
|
if role_exists is None:
|
||||||
|
await session.execute(
|
||||||
|
text(
|
||||||
|
"INSERT INTO sys_role"
|
||||||
|
" (id, role_code, role_name, status, created_at, updated_at)"
|
||||||
|
" VALUES (:id, :code, :name, 'active', :now, :now)"
|
||||||
|
),
|
||||||
|
{"id": ADVISOR_ROLE_ID, "code": ADVISOR_ROLE_CODE,
|
||||||
|
"name": ADVISOR_ROLE_NAME, "now": now},
|
||||||
|
)
|
||||||
|
role_id = int(
|
||||||
|
await session.scalar(
|
||||||
|
text("SELECT id FROM sys_role WHERE role_code = :code"),
|
||||||
|
{"code": ADVISOR_ROLE_CODE},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
permission_ids = dict(
|
||||||
|
(await session.execute(
|
||||||
|
text("SELECT permission_code, id FROM sys_permission")
|
||||||
|
)).all()
|
||||||
|
)
|
||||||
|
|
||||||
|
# 授权:先查已有绑定,只补缺失的(只增不删)。
|
||||||
|
async def grant(role: int, codes: tuple[str, ...]) -> int:
|
||||||
|
have = set(
|
||||||
|
(await session.scalars(
|
||||||
|
text("SELECT permission_id FROM sys_role_permission WHERE role_id = :r"),
|
||||||
|
{"r": role},
|
||||||
|
)).all()
|
||||||
|
)
|
||||||
|
added = 0
|
||||||
|
for code in codes:
|
||||||
|
perm_id = permission_ids.get(code)
|
||||||
|
if perm_id is None or int(perm_id) in have:
|
||||||
|
continue
|
||||||
|
await session.execute(
|
||||||
|
text(
|
||||||
|
"INSERT INTO sys_role_permission (role_id, permission_id, created_at)"
|
||||||
|
" VALUES (:r, :p, :now)"
|
||||||
|
),
|
||||||
|
{"r": role, "p": int(perm_id), "now": now},
|
||||||
|
)
|
||||||
|
added += 1
|
||||||
|
return added
|
||||||
|
|
||||||
|
all_codes = tuple(code for _, code, _, _, _ in ADVISOR_PERMISSIONS)
|
||||||
|
advisor_added = await grant(role_id, ADVISOR_GRANTED_CODES)
|
||||||
|
admin_added = await grant(ADMIN_ROLE_ID, all_codes)
|
||||||
|
print(f"授权:advisor 新增 {advisor_added} 项(共 {len(ADVISOR_GRANTED_CODES)} 项)")
|
||||||
|
print(f" admin 新增 {admin_added} 项(共 {len(all_codes)} 项)")
|
||||||
|
|
||||||
|
await verify()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
async def verify() -> None:
|
||||||
|
"""用真实链路验证:角色与权限能被解析出来。"""
|
||||||
|
async with SessionFactory() as session:
|
||||||
|
rows = (
|
||||||
|
await session.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
SELECT r.role_code, COUNT(rp.permission_id) AS n
|
||||||
|
FROM sys_role r
|
||||||
|
LEFT JOIN sys_role_permission rp ON rp.role_id = r.id
|
||||||
|
GROUP BY r.id, r.role_code ORDER BY r.role_code
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).mappings().all()
|
||||||
|
print("\n各角色权限数(实测):")
|
||||||
|
for row in rows:
|
||||||
|
print(f" {str(row['role_code']):<16} {int(row['n'])} 项")
|
||||||
|
if not any(str(row["role_code"]) == ADVISOR_ROLE_CODE for row in rows):
|
||||||
|
print("[失败] advisor 角色没有建成功")
|
||||||
|
raise SystemExit(1)
|
||||||
|
print(
|
||||||
|
"\n下一步:给某个人绑这个角色 ——\n"
|
||||||
|
" python tools/create_test_user.py --id 9020 --username advisor_t "
|
||||||
|
"--role advisor --password abc12345"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="建立投顾角色并授权")
|
||||||
|
parser.add_argument("--dry-run", action="store_true", help="只打印将写入什么")
|
||||||
|
args = parser.parse_args()
|
||||||
|
return asyncio.run(apply(dry_run=args.dry_run))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
+24
-3
@@ -103,11 +103,20 @@ PAGE = """<!doctype html>
|
|||||||
<span class="muted">(下面按钮按角色给,方便核对权限)</span>
|
<span class="muted">(下面按钮按角色给,方便核对权限)</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="row" id="probes"></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>
|
<pre id="output">(还没有请求)</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let token = null;
|
let token = null;
|
||||||
|
// 当前登录用户的 id(取自登录响应),供"我的身份"以及"用户 id 留空时默认查自己"使用。
|
||||||
|
// 页面里**不写死任何用户 id**:写死会让人以为权限解析错了(真实发生过一次)。
|
||||||
|
let identityUserId = null;
|
||||||
|
|
||||||
function fill(u, p) {
|
function fill(u, p) {
|
||||||
document.getElementById('username').value = u;
|
document.getElementById('username').value = u;
|
||||||
@@ -120,6 +129,7 @@ function show(text) {
|
|||||||
|
|
||||||
function renderIdentity(data) {
|
function renderIdentity(data) {
|
||||||
const el = document.getElementById('identity');
|
const el = document.getElementById('identity');
|
||||||
|
identityUserId = data ? data.user_id : null;
|
||||||
if (!data) { el.textContent = '未登录'; el.className = 'muted'; return; }
|
if (!data) { el.textContent = '未登录'; el.className = 'muted'; return; }
|
||||||
const roles = (data.roles || []).map(r => `<span class="tag">${r}</span>`).join('');
|
const roles = (data.roles || []).map(r => `<span class="tag">${r}</span>`).join('');
|
||||||
el.className = '';
|
el.className = '';
|
||||||
@@ -127,17 +137,20 @@ function renderIdentity(data) {
|
|||||||
`用户 <b>${data.username || data.user_id}</b> 角色 ${roles || '<span class="muted">(无)</span>'}<br>` +
|
`用户 <b>${data.username || data.user_id}</b> 角色 ${roles || '<span class="muted">(无)</span>'}<br>` +
|
||||||
`<span class="muted">data_scope=<b>${data.data_scope ?? '-'}</b> ` +
|
`<span class="muted">data_scope=<b>${data.data_scope ?? '-'}</b> ` +
|
||||||
`令牌有效期 ${data.expires_in} 秒 令牌 ${data.access_token.slice(0, 24)}…</span>`;
|
`令牌有效期 ${data.expires_in} 秒 令牌 ${data.access_token.slice(0, 24)}…</span>`;
|
||||||
renderProbes(data.roles || []);
|
renderProbes(data.roles || [], data.user_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按角色给出可调的只读接口 —— 用来核对"登录给的 roles"与"接口实际放行"是否一致。
|
// 按角色给出可调的只读接口 —— 用来核对"登录给的 roles"与"接口实际放行"是否一致。
|
||||||
function renderProbes(roles) {
|
// `userId` 必须用**当前登录用户**的 id:写死成 9001 会让管理员点了之后看到客户的信息,
|
||||||
|
// 看起来像"权限解析错了"(这个误导真实发生过一次)。
|
||||||
|
function renderProbes(roles, userId) {
|
||||||
const probes = [
|
const probes = [
|
||||||
{ label: '查我的画像', path: '/api/v1/users/me/memory-profile', all: true },
|
{ label: '查我的画像', path: '/api/v1/users/me/memory-profile', all: true },
|
||||||
{ label: '风控概览', path: '/api/v1/risk/overview', role: 'risk_operator' },
|
{ label: '风控概览', path: '/api/v1/risk/overview', role: 'risk_operator' },
|
||||||
{ label: '预警列表', path: '/api/v1/risk/alerts?limit=5', 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/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');
|
const box = document.getElementById('probes');
|
||||||
box.innerHTML = '';
|
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() {
|
function logout() {
|
||||||
token = null;
|
token = null;
|
||||||
renderIdentity(null);
|
renderIdentity(null);
|
||||||
|
|||||||
Reference in New Issue
Block a user