放宽客户/分享接口权限:bot 主人即可使用,不再要求 is_admin

- _require_admin_user → _require_app_user:允许所有非客户账号调用
- end-user CRUD 按 parent_id 隔离,每个主账号只看到/管理自己的客户
- 子账号自动折叠到主账号 id 上(与 bot 体系一致)
- masterkey 仍可看到全部,但不能创建客户账号
This commit is contained in:
朱潮 2026-06-26 15:13:11 +08:00
parent 680dd02595
commit 05a19f59e9

View File

@ -23,7 +23,6 @@ from agent.db_pool_manager import get_db_pool_manager
from routes.bot_manager import ( from routes.bot_manager import (
verify_user_auth, verify_user_auth,
is_bot_owner, is_bot_owner,
is_admin_user,
hash_password, hash_password,
get_parent_user_id, get_parent_user_id,
) )
@ -93,16 +92,29 @@ class SimpleSuccessResponse(BaseModel):
# ============== 内部权限辅助 ============== # ============== 内部权限辅助 ==============
async def _require_admin_user(authorization: Optional[str]) -> str: async def _require_app_user(authorization: Optional[str]) -> str:
"""要求当前请求是 adminis_admin=True 或 masterkey返回 user_id""" """
要求当前请求是任意应用内用户admin / 普通用户 / 子账号 / masterkey**不允许**对外客户账号
子账号会被折叠到主账号 id effective_user_id便于按"主账号 = 资源所有者"做隔离
返回 effective_user_idmasterkey 时返回 '__masterkey__'
"""
valid, user_id, _ = await verify_user_auth(authorization) valid, user_id, _ = await verify_user_auth(authorization)
if not valid or not user_id: if not valid or not user_id:
raise HTTPException(status_code=401, detail="未登录或 token 无效") raise HTTPException(status_code=401, detail="未登录或 token 无效")
if user_id == "__masterkey__": if user_id == "__masterkey__":
return user_id return user_id
if not await is_admin_user(authorization): pool = get_db_pool_manager().pool
raise HTTPException(status_code=403, detail="仅 admin 可执行此操作") async with pool.connection() as conn:
return user_id async with conn.cursor() as cursor:
await cursor.execute(
"SELECT is_end_user FROM agent_user WHERE id = %s",
(user_id,)
)
row = await cursor.fetchone()
if row and row[0]:
raise HTTPException(status_code=403, detail="客户账号不能访问该接口")
# 子账号折叠到主账号
return await get_parent_user_id(user_id)
async def _require_end_user(authorization: Optional[str]) -> str: async def _require_end_user(authorization: Optional[str]) -> str:
@ -131,7 +143,7 @@ async def create_share_token(
body: CreateShareTokenRequest, body: CreateShareTokenRequest,
authorization: Optional[str] = Header(None), authorization: Optional[str] = Header(None),
): ):
user_id = await _require_admin_user(authorization) user_id = await _require_app_user(authorization)
if user_id != "__masterkey__" and not await is_bot_owner(bot_id, user_id): if user_id != "__masterkey__" and not await is_bot_owner(bot_id, user_id):
raise HTTPException(status_code=403, detail="您不是该 bot 的所有者") raise HTTPException(status_code=403, detail="您不是该 bot 的所有者")
@ -184,7 +196,7 @@ async def list_share_tokens(
bot_id: str, bot_id: str,
authorization: Optional[str] = Header(None), authorization: Optional[str] = Header(None),
): ):
user_id = await _require_admin_user(authorization) user_id = await _require_app_user(authorization)
if user_id != "__masterkey__" and not await is_bot_owner(bot_id, user_id): if user_id != "__masterkey__" and not await is_bot_owner(bot_id, user_id):
raise HTTPException(status_code=403, detail="您不是该 bot 的所有者") raise HTTPException(status_code=403, detail="您不是该 bot 的所有者")
@ -221,7 +233,7 @@ async def revoke_share_token(
share_token_id: str, share_token_id: str,
authorization: Optional[str] = Header(None), authorization: Optional[str] = Header(None),
): ):
user_id = await _require_admin_user(authorization) user_id = await _require_app_user(authorization)
pool = get_db_pool_manager().pool pool = get_db_pool_manager().pool
async with pool.connection() as conn: async with pool.connection() as conn:
@ -245,14 +257,18 @@ async def revoke_share_token(
return SimpleSuccessResponse(message="已吊销") return SimpleSuccessResponse(message="已吊销")
# ============== End-user CRUDadmin 用) ============== # ============== End-user CRUDbot 主人用) ==============
# 客户账号的归属用 agent_user.parent_id 表示end-user 的 parent_id 指向其主人 user_id
# 注:子账号也用 parent_id 表示主账号;区分由 is_subaccount / is_end_user 标志位负责。
@router.post("/api/v1/end-users", response_model=EndUserItem) @router.post("/api/v1/end-users", response_model=EndUserItem)
async def create_end_user( async def create_end_user(
body: CreateEndUserRequest, body: CreateEndUserRequest,
authorization: Optional[str] = Header(None), authorization: Optional[str] = Header(None),
): ):
await _require_admin_user(authorization) owner_id = await _require_app_user(authorization)
if owner_id == "__masterkey__":
raise HTTPException(status_code=400, detail="masterkey 不能拥有客户账号,请用普通账号登录")
if not body.username or not body.password: if not body.username or not body.password:
raise HTTPException(status_code=400, detail="username 和 password 必填") raise HTTPException(status_code=400, detail="username 和 password 必填")
@ -268,11 +284,11 @@ async def create_end_user(
await cursor.execute( await cursor.execute(
""" """
INSERT INTO agent_user (username, email, password_hash, is_admin, is_subaccount, is_end_user) INSERT INTO agent_user (username, email, password_hash, is_admin, is_subaccount, is_end_user, parent_id)
VALUES (%s, %s, %s, FALSE, FALSE, TRUE) VALUES (%s, %s, %s, FALSE, FALSE, TRUE, %s)
RETURNING id, created_at, is_active RETURNING id, created_at, is_active
""", """,
(body.username, body.email, hash_password(body.password)), (body.username, body.email, hash_password(body.password), owner_id),
) )
row = await cursor.fetchone() row = await cursor.fetchone()
await conn.commit() await conn.commit()
@ -288,18 +304,29 @@ async def create_end_user(
@router.get("/api/v1/end-users", response_model=EndUserListResponse) @router.get("/api/v1/end-users", response_model=EndUserListResponse)
async def list_end_users(authorization: Optional[str] = Header(None)): async def list_end_users(authorization: Optional[str] = Header(None)):
await _require_admin_user(authorization) owner_id = await _require_app_user(authorization)
pool = get_db_pool_manager().pool pool = get_db_pool_manager().pool
async with pool.connection() as conn: async with pool.connection() as conn:
async with conn.cursor() as cursor: async with conn.cursor() as cursor:
await cursor.execute( if owner_id == "__masterkey__":
""" await cursor.execute(
SELECT id, username, email, is_active, created_at, last_login """
FROM agent_user SELECT id, username, email, is_active, created_at, last_login
WHERE is_end_user = TRUE FROM agent_user
ORDER BY created_at DESC WHERE is_end_user = TRUE
""" ORDER BY created_at DESC
) """
)
else:
await cursor.execute(
"""
SELECT id, username, email, is_active, created_at, last_login
FROM agent_user
WHERE is_end_user = TRUE AND parent_id = %s
ORDER BY created_at DESC
""",
(owner_id,),
)
rows = await cursor.fetchall() rows = await cursor.fetchall()
items = [ items = [
EndUserItem( EndUserItem(
@ -315,24 +342,32 @@ async def list_end_users(authorization: Optional[str] = Header(None)):
return EndUserListResponse(items=items) return EndUserListResponse(items=items)
async def _ensure_owns_end_user(cursor, end_user_id: str, owner_id: str):
"""校验 end_user_id 存在且归属当前 owner_idmasterkey 通过)"""
await cursor.execute(
"SELECT parent_id FROM agent_user WHERE id = %s AND is_end_user = TRUE",
(end_user_id,),
)
row = await cursor.fetchone()
if not row:
raise HTTPException(status_code=404, detail="客户账号不存在")
if owner_id != "__masterkey__" and str(row[0]) != owner_id:
raise HTTPException(status_code=403, detail="您不是该客户账号的所有者")
@router.post("/api/v1/end-users/{end_user_id}/reset-password", response_model=SimpleSuccessResponse) @router.post("/api/v1/end-users/{end_user_id}/reset-password", response_model=SimpleSuccessResponse)
async def reset_end_user_password( async def reset_end_user_password(
end_user_id: str, end_user_id: str,
body: ResetEndUserPasswordRequest, body: ResetEndUserPasswordRequest,
authorization: Optional[str] = Header(None), authorization: Optional[str] = Header(None),
): ):
await _require_admin_user(authorization) owner_id = await _require_app_user(authorization)
if not body.password: if not body.password:
raise HTTPException(status_code=400, detail="password 必填") raise HTTPException(status_code=400, detail="password 必填")
pool = get_db_pool_manager().pool pool = get_db_pool_manager().pool
async with pool.connection() as conn: async with pool.connection() as conn:
async with conn.cursor() as cursor: async with conn.cursor() as cursor:
await cursor.execute( await _ensure_owns_end_user(cursor, end_user_id, owner_id)
"SELECT id FROM agent_user WHERE id = %s AND is_end_user = TRUE",
(end_user_id,),
)
if not await cursor.fetchone():
raise HTTPException(status_code=404, detail="客户账号不存在")
await cursor.execute( await cursor.execute(
"UPDATE agent_user SET password_hash = %s WHERE id = %s", "UPDATE agent_user SET password_hash = %s WHERE id = %s",
@ -352,10 +387,11 @@ async def delete_end_user(
end_user_id: str, end_user_id: str,
authorization: Optional[str] = Header(None), authorization: Optional[str] = Header(None),
): ):
await _require_admin_user(authorization) owner_id = await _require_app_user(authorization)
pool = get_db_pool_manager().pool pool = get_db_pool_manager().pool
async with pool.connection() as conn: async with pool.connection() as conn:
async with conn.cursor() as cursor: async with conn.cursor() as cursor:
await _ensure_owns_end_user(cursor, end_user_id, owner_id)
await cursor.execute( await cursor.execute(
"DELETE FROM agent_user WHERE id = %s AND is_end_user = TRUE", "DELETE FROM agent_user WHERE id = %s AND is_end_user = TRUE",
(end_user_id,), (end_user_id,),