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:
+15
-43
@@ -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
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user