From 05a19f59e95474160ef57a082fb642a7ea13563b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E6=BD=AE?= Date: Fri, 26 Jun 2026 15:13:11 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=BE=E5=AE=BD=E5=AE=A2=E6=88=B7/=E5=88=86?= =?UTF-8?q?=E4=BA=AB=E6=8E=A5=E5=8F=A3=E6=9D=83=E9=99=90=EF=BC=9Abot=20?= =?UTF-8?q?=E4=B8=BB=E4=BA=BA=E5=8D=B3=E5=8F=AF=E4=BD=BF=E7=94=A8=EF=BC=8C?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E8=A6=81=E6=B1=82=20is=5Fadmin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _require_admin_user → _require_app_user:允许所有非客户账号调用 - end-user CRUD 按 parent_id 隔离,每个主账号只看到/管理自己的客户 - 子账号自动折叠到主账号 id 上(与 bot 体系一致) - masterkey 仍可看到全部,但不能创建客户账号 --- routes/bot_share.py | 98 +++++++++++++++++++++++++++++++-------------- 1 file changed, 67 insertions(+), 31 deletions(-) diff --git a/routes/bot_share.py b/routes/bot_share.py index 5265da8..a967c69 100644 --- a/routes/bot_share.py +++ b/routes/bot_share.py @@ -23,7 +23,6 @@ from agent.db_pool_manager import get_db_pool_manager from routes.bot_manager import ( verify_user_auth, is_bot_owner, - is_admin_user, hash_password, get_parent_user_id, ) @@ -93,16 +92,29 @@ class SimpleSuccessResponse(BaseModel): # ============== 内部权限辅助 ============== -async def _require_admin_user(authorization: Optional[str]) -> str: - """要求当前请求是 admin(is_admin=True 或 masterkey),返回 user_id""" +async def _require_app_user(authorization: Optional[str]) -> str: + """ + 要求当前请求是任意应用内用户(admin / 普通用户 / 子账号 / masterkey),但**不允许**对外客户账号。 + 子账号会被折叠到主账号 id 上(effective_user_id),便于按"主账号 = 资源所有者"做隔离。 + 返回 effective_user_id(masterkey 时返回 '__masterkey__')。 + """ valid, user_id, _ = await verify_user_auth(authorization) if not valid or not user_id: raise HTTPException(status_code=401, detail="未登录或 token 无效") if user_id == "__masterkey__": return user_id - if not await is_admin_user(authorization): - raise HTTPException(status_code=403, detail="仅 admin 可执行此操作") - return user_id + pool = get_db_pool_manager().pool + async with pool.connection() as conn: + 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: @@ -131,7 +143,7 @@ async def create_share_token( body: CreateShareTokenRequest, 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): raise HTTPException(status_code=403, detail="您不是该 bot 的所有者") @@ -184,7 +196,7 @@ async def list_share_tokens( bot_id: str, 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): raise HTTPException(status_code=403, detail="您不是该 bot 的所有者") @@ -221,7 +233,7 @@ async def revoke_share_token( share_token_id: str, 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 async with pool.connection() as conn: @@ -245,14 +257,18 @@ async def revoke_share_token( return SimpleSuccessResponse(message="已吊销") -# ============== End-user CRUD(admin 用) ============== +# ============== End-user CRUD(bot 主人用) ============== +# 客户账号的归属用 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) async def create_end_user( body: CreateEndUserRequest, 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: raise HTTPException(status_code=400, detail="username 和 password 必填") @@ -268,11 +284,11 @@ async def create_end_user( await cursor.execute( """ - INSERT INTO agent_user (username, email, password_hash, is_admin, is_subaccount, is_end_user) - VALUES (%s, %s, %s, FALSE, FALSE, TRUE) + INSERT INTO agent_user (username, email, password_hash, is_admin, is_subaccount, is_end_user, parent_id) + VALUES (%s, %s, %s, FALSE, FALSE, TRUE, %s) 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() await conn.commit() @@ -288,18 +304,29 @@ async def create_end_user( @router.get("/api/v1/end-users", response_model=EndUserListResponse) 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 async with pool.connection() as conn: async with conn.cursor() as cursor: - await cursor.execute( - """ - SELECT id, username, email, is_active, created_at, last_login - FROM agent_user - WHERE is_end_user = TRUE - ORDER BY created_at DESC - """ - ) + if owner_id == "__masterkey__": + await cursor.execute( + """ + SELECT id, username, email, is_active, created_at, last_login + FROM agent_user + 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() items = [ EndUserItem( @@ -315,24 +342,32 @@ async def list_end_users(authorization: Optional[str] = Header(None)): return EndUserListResponse(items=items) +async def _ensure_owns_end_user(cursor, end_user_id: str, owner_id: str): + """校验 end_user_id 存在且归属当前 owner_id(masterkey 通过)""" + 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) async def reset_end_user_password( end_user_id: str, body: ResetEndUserPasswordRequest, authorization: Optional[str] = Header(None), ): - await _require_admin_user(authorization) + owner_id = await _require_app_user(authorization) if not body.password: raise HTTPException(status_code=400, detail="password 必填") pool = get_db_pool_manager().pool async with pool.connection() as conn: async with conn.cursor() as cursor: - await cursor.execute( - "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 _ensure_owns_end_user(cursor, end_user_id, owner_id) await cursor.execute( "UPDATE agent_user SET password_hash = %s WHERE id = %s", @@ -352,10 +387,11 @@ async def delete_end_user( end_user_id: str, authorization: Optional[str] = Header(None), ): - await _require_admin_user(authorization) + owner_id = await _require_app_user(authorization) pool = get_db_pool_manager().pool async with pool.connection() as conn: async with conn.cursor() as cursor: + await _ensure_owns_end_user(cursor, end_user_id, owner_id) await cursor.execute( "DELETE FROM agent_user WHERE id = %s AND is_end_user = TRUE", (end_user_id,),