Files
aiagent/backend/app/api/deps.py
renjianbo 876789fac1 feat: multi-tenant workspace isolation, RBAC, sidebar nav, billing, and Android enhancements
- Backend: workspace_id isolation for 14 model tables + safe migration/backfill
- Backend: RBAC system with 4 roles and 23 permissions, seeded on startup
- Backend: workspace admin endpoints (list/manage all workspaces)
- Backend: admin user management API (CRUD, reset password)
- Backend: billing API with subscription plans, usage tracking, rate limiting
- Backend: fix system_logs.py UNION query and wrong column references
- Backend: WebSocket JWT auth and workspace enforcement
- Frontend: sidebar navigation replacing top dropdown menu
- Frontend: user management page (Users.vue) for admins
- Frontend: enhanced Workspaces.vue with admin table view
- Frontend: workspace RBAC computed properties in user store
- Android: agent marketplace, billing/subscription UI, onboarding wizard
- Android: phone login, analytics tracker, crash handler, network diagnostics
- Android: splash screen, encrypted token storage, app update enhancements
- Docs: multi-tenant RBAC guide with 8 sections and role-permission matrix

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-04 01:00:22 +08:00

198 lines
6.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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