Files
aiagent/backend/app/api/deps.py

198 lines
6.0 KiB
Python
Raw Normal View History

"""
FastAPI 依赖注入 Workspace 上下文RBAC 权限检查
"""
from dataclasses import dataclass
import logging
from typing import Optional
from fastapi import Depends, HTTPException, Request
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.security import decode_access_token
from app.models.user import User
from app.models.workspace import WorkspaceMembership
logger = logging.getLogger(__name__)
@dataclass
class WorkspaceContext:
"""工作区上下文 — 包装当前用户 + 当前工作区 ID"""
user: User
workspace_id: str
def get_current_workspace_id(request: Request) -> str:
"""从 JWT 的 `ws` 字段提取当前工作区 ID。"""
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
raise HTTPException(status_code=401, detail="未提供有效的认证令牌")
token = auth_header[7:]
payload = decode_access_token(token)
if payload is None:
raise HTTPException(status_code=401, detail="无效的访问令牌")
ws_id = payload.get("ws")
if not ws_id:
raise HTTPException(status_code=400, detail="令牌中未包含工作区信息,请重新登录")
return ws_id
def get_workspace_context(
request: Request,
db: Session = Depends(get_db),
) -> WorkspaceContext:
"""获取完整的 Workspace 上下文(用户 + 工作区)。
注意此函数仅认证用户并提取 workspace_id不验证 workspace 成员资格
用于跨 workspace admin 端点需要强制成员资格验证的端点请使用
get_workspace_membership()
"""
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
raise HTTPException(status_code=401, detail="未提供有效的认证令牌")
token = auth_header[7:]
payload = decode_access_token(token)
if payload is None:
raise HTTPException(status_code=401, detail="无效的访问令牌")
user_id = payload.get("sub")
ws_id = payload.get("ws")
if not user_id:
raise HTTPException(status_code=401, detail="无效的访问令牌")
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=401, detail="用户不存在")
if user.status == "deleted":
raise HTTPException(status_code=403, detail="账号已注销")
return WorkspaceContext(user=user, workspace_id=ws_id or "")
# ─── 多租户 RBAC 依赖 (v1.3) ───
def get_workspace_membership(
request: Request,
db: Session = Depends(get_db),
) -> WorkspaceContext:
"""认证用户 + 验证 workspace 成员资格。
平台管理员user.role == 'admin'绕过 workspace 成员资格检查
普通用户必须属于 JWT `ws` 指定的 workspace
"""
ctx = get_workspace_context(request, db)
# 平台管理员可访问所有 workspace
if ctx.user.role == "admin":
return ctx
if not ctx.workspace_id:
raise HTTPException(status_code=400, detail="未选择工作区")
membership = (
db.query(WorkspaceMembership)
.filter(
WorkspaceMembership.workspace_id == ctx.workspace_id,
WorkspaceMembership.user_id == ctx.user.id,
)
.first()
)
if not membership:
logger.warning("用户 %s 尝试访问未加入的工作区 %s", ctx.user.id, ctx.workspace_id)
raise HTTPException(status_code=403, detail="无权访问此工作区")
return ctx
async def require_workspace_admin(
ctx: WorkspaceContext = Depends(get_workspace_membership),
db: Session = Depends(get_db),
) -> WorkspaceContext:
"""要求 workspace 管理员或平台管理员权限。
用于删除/发布/修改工作区设置等敏感操作
"""
# 平台管理员
if ctx.user.role == "admin":
return ctx
# workspace 管理员
membership = (
db.query(WorkspaceMembership)
.filter(
WorkspaceMembership.workspace_id == ctx.workspace_id,
WorkspaceMembership.user_id == ctx.user.id,
)
.first()
)
if not membership or membership.role != "admin":
raise HTTPException(status_code=403, detail="需要工作区管理员权限")
return ctx
class WorkspacePermission:
"""细粒度权限检查依赖 — 桥接系统 RBAC 与 workspace 上下文。
用法:
@router.delete("/agents/{id}")
async def delete_agent(
...,
ctx: WorkspaceContext = Depends(WorkspacePermission("agent:delete")),
):
"""
def __init__(self, permission_code: str):
self.permission_code = permission_code
async def __call__(
self,
ctx: WorkspaceContext = Depends(get_workspace_membership),
db: Session = Depends(get_db),
) -> WorkspaceContext:
# 平台管理员总是通过
if ctx.user.role == "admin":
return ctx
# workspace 管理员拥有该 workspace 内所有权限
membership = (
db.query(WorkspaceMembership)
.filter(
WorkspaceMembership.workspace_id == ctx.workspace_id,
WorkspaceMembership.user_id == ctx.user.id,
)
.first()
)
if membership and membership.role == "admin":
return ctx
# 检查系统级 RBAC 权限
if _user_has_permission(db, ctx.user.id, self.permission_code):
return ctx
raise HTTPException(status_code=403, detail=f"缺少权限: {self.permission_code}")
def _user_has_permission(db: Session, user_id: str, permission_code: str) -> bool:
"""检查用户是否拥有指定权限(通过系统级 RBAC"""
from app.models.permission import Role, Permission
from app.models.user import User
result = (
db.query(Permission)
.join(Permission.roles)
.join(Role.users)
.filter(
Permission.code == permission_code,
User.id == user_id,
)
.first()
)
return result is not None