refactor(auth): 去多身份/多端口——账号单一角色,删 user_identities/select-identity/多端口路由

- 删 UserIdentity 模型/IdentityRepository/db.identities,账号统一单一角色(users.role)
- auth.py 去 select-identity/_identity_summaries,登录恒返回 identities=[];_login_response_for_user 按单角色签发
- 删多端口路由 rbac_government/investor/developer 及其 main 挂载
- seed 去多身份回填(_migrate_identities/_ensure_port_agents/多端身份/端口标签映射)
- dependencies: require_port 去端口隔离(单角色放宽)、optional_current_user 修 identity_id 多余参数
- user_admin_service create_user 不再建端口身份;schemas 去 SelectIdentityRequest
- 新增迁移 0014_drop_user_identities(drop user_identities 表)
- 测试改写为单角色契约(登录空 identities、无 select-identity、账号级智能体)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Pine
2026-08-26 19:15:51 +08:00
parent 97f0a0698c
commit ac2df72b21
14 changed files with 91 additions and 843 deletions
+15 -43
View File
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
"""账号↔端口多身份:登录返回身份列表、选择身份签发新令牌、按身份访问。"""
"""账号单一角色(去多身份:登录返回空 identities、role=账号类型、按角色访问。"""
from __future__ import annotations
from fastapi.testclient import TestClient
@@ -15,59 +15,31 @@ def auth(token: str) -> dict:
return {"Authorization": f"Bearer {token}"}
def test_single_identity_auto_login(client):
"""单身份账号(opc01)登录即绑定该身份,可直接访问对应端口。"""
def test_single_role_login(client):
"""opc01(单角色 OPC)登录:identities 恒空、role=opc_member、可按角色访问 OPC 端口。"""
body = login(client, "opc01")
assert len(body["identities"]) == 1
assert body["identities"][0]["port"] == "opc"
assert body["identities"] == []
assert body["role"] == "opc_member"
assert body.get("identity_id") == body["identities"][0]["id"]
assert bool(body["token"])
res = client.get("/opc/dashboard", headers=auth(body["token"]))
assert res.status_code == 200, res.text
def test_multi_identity_login_returns_all_and_neutral(client):
"""多身份账号(ent01:企业 + OPC)登录返回全部身份,令牌为中性(回退单角色)"""
body = login(client, "ent01")
ports = [i["port"] for i in body["identities"]]
assert "enterprise" in ports and "opc" in ports
# 中性令牌按 users.role 回退为 enterprise:访问 OPC 端口应 403
res = client.get("/opc/dashboard", headers=auth(body["token"]))
assert res.status_code == 403
def test_operator_login_has_operator_role(client):
"""pine(平台运营方超管)登录:role=operator,具备运营端权限"""
body = login(client, "pine")
assert body["role"] == "operator"
assert "permissions" in body and len(body["permissions"]) > 0
assert any(p.startswith("menu:") for p in body["permissions"])
def test_select_identity_grants_opc_access(client):
"""选择 OPC 身份后,新令牌可访问 OPC 端口,且 /me 反映该身份"""
body = login(client, "ent01")
opc_identity = next(i for i in body["identities"] if i["port"] == "opc")
res = client.post(
"/auth/select-identity",
headers=auth(body["token"]),
json={"identity_id": opc_identity["id"]},
)
assert res.status_code == 200, res.text
selected = res.json()
assert selected["role"] == "opc_member"
assert selected["identity_id"] == opc_identity["id"]
dash = client.get("/opc/dashboard", headers=auth(selected["token"]))
assert dash.status_code == 200, dash.text
me = client.get("/auth/me", headers=auth(selected["token"])).json()
assert me["role"] == "opc_member"
assert me.get("port") == "opc"
def test_select_identity_rejects_other_users_identity(client):
"""不能选择不属于当前账号的身份。"""
def test_select_identity_endpoint_removed(client):
"""多身份选择端点已移除:/auth/select-identity 不再存在(405"""
body = login(client, "opc01")
other = "ident_ent01_opc" # 属于 ent01 的身份 id
res = client.post(
"/auth/select-identity",
headers=auth(body["token"]),
json={"identity_id": other},
json={"identity_id": "whatever"},
)
assert res.status_code == 404
assert res.status_code == 405 or res.status_code == 404
+15 -45
View File
@@ -152,51 +152,21 @@ def test_ecosystem_crosscut(client):
assert join.status_code == 200 and join.json()["joined"] is True
def test_agent_per_port_isolation(client):
# 同一账号在不同端口拥有各自独立的智能体(多端口彻底隔离)
body = client.post("/auth/login", json={"username": "pine", "password": "123456"}).json()
token = body["token"]
opc_ident = next(i for i in body["identities"] if i["port"] == "opc")
op_id = next(i for i in body["identities"] if i["port"] == "operator")
# 运营端:创建专属智能体
sel_op = client.post("/auth/select-identity", headers=auth(token), json={"identity_id": op_id["id"]}).json()
created = client.post("/agents", headers=auth(sel_op["token"]), json={"name": "运营专属助手"}).json()
assert created["port"] == "operator"
op_agents = client.get("/agents", headers=auth(sel_op["token"])).json()
assert any(a["id"] == created["id"] for a in op_agents)
# OPC 端:看不到运营端的专属智能体(彻底隔离)
sel_opc = client.post("/auth/select-identity", headers=auth(token), json={"identity_id": opc_ident["id"]}).json()
opc_agents = client.get("/agents", headers=auth(sel_opc["token"])).json()
assert all(a["id"] != created["id"] for a in opc_agents)
assert all(a["port"] == "opc" for a in opc_agents)
def test_agent_neutral_token_fully_isolated(client):
# 多身份账号登录后未 select-identity 的中性令牌:不得跨端口访问任何智能体
body = client.post("/auth/login", json={"username": "pine", "password": "123456"}).json()
neutral = body["token"]
# 中性令牌(未解析端口身份)访问智能体一律 403
assert client.get("/agents", headers=auth(neutral)).status_code == 403
# 先选身份创建 opc 智能体
opc_ident = next(i for i in body["identities"] if i["port"] == "opc")["id"]
ot = client.post(
"/auth/select-identity", headers=auth(neutral), json={"identity_id": opc_ident}
).json()["token"]
created = client.post("/agents", headers=auth(ot), json={"name": "OPC专属"}).json()
assert created["port"] == "opc"
# 中性令牌改/删该智能体:403(不能跨端口或绕过身份)
assert (
client.put(
f"/agents/{created['id']}", headers=auth(neutral), json={"name": "x"}
).status_code
== 403
)
assert client.delete(f"/agents/{created['id']}", headers=auth(neutral)).status_code == 403
# 带身份令牌仍可正常管理(回归)
upd = client.put(
f"/agents/{created['id']}", headers=auth(ot), json={"name": "OPC专属v2"}
)
assert upd.status_code == 200 and upd.json()["name"] == "OPC专属v2"
def test_agents_single_role(client):
# 单角色:智能体归账号所有(不再按端口隔离),可增改删;他人不可见/不可改
pine = login(client, "pine")
created = client.post("/agents", headers=auth(pine), json={"name": "我的助手"}).json()
assert created["name"] == "我的助手"
mine = client.get("/agents", headers=auth(pine)).json()
assert any(a["id"] == created["id"] for a in mine)
# 他人(OPC)不可见
opc = login(client, "opc01")
other = client.get("/agents", headers=auth(opc)).json()
assert all(a["id"] != created["id"] for a in other)
# 本人可更新/删除
upd = client.put(f"/agents/{created['id']}", headers=auth(pine), json={"name": "助手v2"})
assert upd.status_code == 200 and upd.json()["name"] == "助手v2"
assert client.delete(f"/agents/{created['id']}", headers=auth(pine)).status_code == 200
def test_org_members(client):