- 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>
198 lines
6.0 KiB
Python
198 lines
6.0 KiB
Python
"""
|
||
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
|