import uuid from functools import wraps from fastapi import Depends, HTTPException, status, Request from fastapi.security import OAuth2PasswordBearer from sqlalchemy.orm import Session from jose import jwt, JWTError from app.db import get_db, SessionLocal from app.models import App from app.schemas import token_schema from app.core.config import settings from app.core.security import get_token_id from app.repositories import user_repository, tenant_repository from app.repositories import workspace_repository from app.models.user_model import User from app.models.tenant_model import Tenants from app.models.workspace_model import Workspace from app.services.session_service import SessionService from app.core.logging_config import get_auth_logger, get_security_logger from app.core.uow import SqlAlchemyUnitOfWork, IUnitOfWork from app.core.exceptions import PermissionDeniedException # 获取专用日志器 auth_logger = get_auth_logger() security_logger = get_security_logger() oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") class APIKeyExtractor: """ Custom dependency to extract API Key from request headers Supports two formats: 1. Authorization: Bearer 2. X-API-Key: """ async def __call__(self, request: Request) -> str: """Extract API Key from request headers Args: request: FastAPI Request object Returns: API Key string Raises: HTTPException: If API Key is not found """ # Try Authorization header first auth_header = request.headers.get("Authorization") if auth_header and " " in auth_header: auth_scheme, auth_token = auth_header.split(" ", 1) if auth_scheme.lower() == "bearer": return auth_token # Try X-API-Key header api_key = request.headers.get("X-API-Key") if api_key: return api_key # No API Key found raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="API Key not found in request headers", headers={"WWW-Authenticate": "Bearer"}, ) api_key_extractor = APIKeyExtractor() async def get_current_user( token: str = Depends(oauth2_scheme), db: Session = Depends(get_db) ) -> User: """ 获取当前认证用户 """ credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) try: auth_logger.debug("开始解析JWT token") payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) user_id: str = payload.get("sub") if user_id is None: auth_logger.warning("JWT token中缺少用户ID") raise credentials_exception token_data = token_schema.TokenData(userId=user_id) auth_logger.debug(f"JWT解析成功,用户ID: {user_id}") except JWTError as e: auth_logger.warning(f"JWT解析失败: {str(e)}") raise credentials_exception # 检查单点登录黑名单和用户token失效 try: auth_logger.debug("检查单点登录黑名单") token_id = get_token_id(token) session_service = SessionService() if await session_service.is_token_blacklisted(token_id): auth_logger.warning(f"Token已被列入黑名单: {token_id}") raise credentials_exception # 检查用户是否重置了密码(所有旧token失效) invalidation_time_str = await session_service.get_user_token_invalidation_time(user_id) if invalidation_time_str: from datetime import datetime, timezone invalidation_time = datetime.fromisoformat(invalidation_time_str) token_issued_at = datetime.fromtimestamp(payload.get("iat", 0), tz=timezone.utc) if payload.get( "iat") else None if token_issued_at and token_issued_at < invalidation_time: auth_logger.warning(f"Token在密码重置前签发,已失效: user_id={user_id}") raise credentials_exception auth_logger.debug("单点登录检查通过") except HTTPException: raise except Exception as e: auth_logger.error(f"检查token有效性时发生错误: {str(e)}") raise credentials_exception try: auth_logger.debug(f"查询用户信息: {token_data.userId}") user = user_repository.get_user_by_id(db, user_id=token_data.userId) if user is None: auth_logger.warning(f"用户不存在: {token_data.userId}") raise credentials_exception if not user.is_active: auth_logger.warning(f"用户已被停用: {user.username} (ID: {user.id})") raise credentials_exception auth_logger.info(f"用户认证成功: {user.username} (ID: {user.id})") return user except Exception as e: auth_logger.error(f"查询用户信息时发生错误: {str(e)}") raise credentials_exception async def get_current_tenant( current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ) -> Tenants: """ 获取当前用户的租户 由于每个用户只属于一个租户,直接返回用户的租户 """ auth_logger.debug(f"获取用户 {current_user.username} 的租户信息") try: # 直接从用户模型获取租户 if current_user.tenant: auth_logger.info(f"用户 {current_user.username} 的租户: {current_user.tenant.name}") return current_user.tenant else: auth_logger.warning(f"用户 {current_user.username} 没有关联的租户") raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="用户没有关联的租户" ) except HTTPException: raise except Exception as e: auth_logger.error(f"获取租户信息时发生错误: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="获取租户信息失败" ) async def get_user_tenants( current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ) -> list[Tenants]: """ 获取当前用户所属的所有租户 由于每个用户只属于一个租户,返回包含该租户的列表 """ auth_logger.debug(f"获取用户 {current_user.username} 的所有租户") try: if current_user.tenant: tenants = [current_user.tenant] auth_logger.info(f"用户 {current_user.username} 属于 1 个租户") return tenants else: auth_logger.info(f"用户 {current_user.username} 没有关联的租户") return [] except Exception as e: auth_logger.error(f"获取用户租户列表时发生错误: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="获取租户列表失败" ) async def get_current_superuser( current_user: User = Depends(get_current_user) ) -> User: """ 检查当前用户是否为超级管理员 """ auth_logger.debug(f"检查用户 {current_user.username} 是否为超级管理员") if not current_user.is_superuser: auth_logger.warning(f"用户 {current_user.username} 尝试访问超管功能但不是超级管理员") raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="只有超级管理员才能执行此操作" ) auth_logger.info(f"超级管理员 {current_user.username} 访问超管功能") return current_user # ---------------------- # Workspace Access Guard # ---------------------- # async def require_workspace_access( # workspace_id: uuid.UUID, # db: Session = Depends(get_db), # current_user: User = Depends(get_current_user), # ) -> Workspace: # """ # 校验当前用户对指定工作空间的访问权限: # - 工作空间必须存在 # - 超级管理员且与工作空间同租户可访问 # - 普通用户必须是该工作空间成员 # 返回工作空间对象以便后续使用;无权限时抛出 HTTPException。 # """ # auth_logger.debug(f"校验工作空间访问权限: workspace_id={workspace_id}, user={current_user.id}") # # 1) 工作空间存在性 # workspace = workspace_repository.get_workspace_by_id(db=db, workspace_id=workspace_id) # if not workspace: # auth_logger.warning(f"工作空间不存在: {workspace_id}") # raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Workspace not found") # # 2) 超级管理员(同租户)直接放行 # if current_user.is_superuser: # if workspace.tenant_id == current_user.tenant_id: # auth_logger.debug(f"超管同租户访问放行: user={current_user.id}, workspace={workspace_id}") # return workspace # # 超管跨租户访问不允许 # auth_logger.warning( # f"超管跨租户访问被拒: user_tenant={current_user.tenant_id}, workspace_tenant={workspace.tenant_id}" # ) # raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden") # # 3) 普通用户需要是成员 # member = workspace_repository.get_member_in_workspace( # db=db, user_id=current_user.id, workspace_id=workspace_id # ) # if not member: # auth_logger.warning(f"非成员访问被拒: user={current_user.id}, workspace={workspace_id}") # raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden") # auth_logger.debug(f"成员访问通过: user={current_user.id}, workspace={workspace_id}") # return workspace # # 针对创建应用的请求体(包含 workspace_id)提供便捷校验 # from app.schemas.app_schema import AppCreate # async def require_workspace_access_for_app_create( # payload: AppCreate, # db: Session = Depends(get_db), # current_user: User = Depends(get_current_user), # ) -> Workspace: # return await require_workspace_access(payload.workspace_id, db, current_user) # ---------------------- # Decorator (@) version # ---------------------- def _check_workspace_access_sync(db: Session, user: User, workspace_id: uuid.UUID) -> Workspace: """同步校验版本,供装饰器在同步端点中调用 - 使用权限服务""" auth_logger.debug(f"同步校验工作空间访问权限: workspace_id={workspace_id}, user={user.id}") # 1) 工作空间存在性 workspace = workspace_repository.get_workspace_by_id(db=db, workspace_id=workspace_id) if not workspace: auth_logger.warning(f"工作空间不存在: {workspace_id}") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Workspace not found") # 2) 超级用户跳过成员检查,直接验证租户 if user.is_superuser: if user.tenant_id == workspace.tenant_id: auth_logger.debug(f"超级用户访问同租户工作空间: workspace_id={workspace_id}, user={user.id}") return workspace else: auth_logger.warning(f"超级用户尝试访问其他租户工作空间: workspace_id={workspace_id}, user={user.id}") raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden") # 3) 普通用户使用权限服务检查访问权限 from app.core.permissions import permission_service, Subject, Resource, Action from app.core.permissions.policies import WorkspaceMemberPolicy, SameTenantSuperuserPolicy # Check if user is a member member = workspace_repository.get_member_in_workspace( db=db, user_id=user.id, workspace_id=workspace_id ) workspace_memberships = {workspace_id} if member else set() subject = Subject.from_user(user, workspace_memberships=workspace_memberships) resource = Resource.from_workspace(workspace) # Add workspace member policy temp_service = permission_service if member: temp_service.add_policy(WorkspaceMemberPolicy(allowed_actions={Action.READ, Action.UPDATE, Action.MANAGE})) temp_service.add_policy(SameTenantSuperuserPolicy()) try: permission_service.require_permission( subject, Action.READ, resource, error_message="Forbidden" ) return workspace except PermissionDeniedException: auth_logger.warning(f"工作空间访问被拒绝: workspace_id={workspace_id}, user={user.id}") raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden") def workspace_access_guard(get_workspace_id_from_body: bool = False): """ @ 装饰器:在端点进入前执行工作空间访问校验。 要求端点函数签名包含: - db: Session = Depends(get_db) - user 或 current_user: User = Depends(get_current_user) - workspace_id: uuid.UUID (query/path 参数)或 payload: AppCreate(body,含 workspace_id) 支持同步和异步函数。 """ import asyncio def _decorator(func): # 检查函数是否是异步的 if asyncio.iscoroutinefunction(func): @wraps(func) async def _async_wrapper(*args, **kwargs): db: Session = kwargs.get("db") user: User = kwargs.get("user") or kwargs.get("current_user") if get_workspace_id_from_body: payload = kwargs.get("payload") if not payload or not hasattr(payload, "workspace_id"): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="workspace_id missing in body") workspace_id = payload.workspace_id else: workspace_id = kwargs.get("workspace_id") if workspace_id is None: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="workspace_id is required") _check_workspace_access_sync(db, user, workspace_id) return await func(*args, **kwargs) return _async_wrapper else: @wraps(func) def _sync_wrapper(*args, **kwargs): db: Session = kwargs.get("db") user: User = kwargs.get("user") or kwargs.get("current_user") if get_workspace_id_from_body: payload = kwargs.get("payload") if not payload or not hasattr(payload, "workspace_id"): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="workspace_id missing in body") workspace_id = payload.workspace_id else: workspace_id = kwargs.get("workspace_id") if workspace_id is None: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="workspace_id is required") _check_workspace_access_sync(db, user, workspace_id) return func(*args, **kwargs) return _sync_wrapper return _decorator def get_uow() -> IUnitOfWork: """ 获取工作单元实例 Returns: IUnitOfWork: 工作单元实例 """ return SqlAlchemyUnitOfWork(SessionLocal) def cur_workspace_access_guard(): """ @ 装饰器:在端点进入前执行工作空间访问校验。 要求端点函数签名包含: - db: Session = Depends(get_db) - current_user: User = Depends(get_current_user) 支持同步和异步函数。 """ import asyncio import inspect def _decorator(func): # 检查函数是否是异步的 if asyncio.iscoroutinefunction(func): @wraps(func) async def _async_wrapper(*args, **kwargs): db: Session = kwargs.get("db") user: User = kwargs.get("current_user") workspace_id = user.current_workspace_id if workspace_id is None: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="workspace_id is required") _check_workspace_access_sync(db, user, workspace_id) return await func(*args, **kwargs) return _async_wrapper else: @wraps(func) def _sync_wrapper(*args, **kwargs): db: Session = kwargs.get("db") user: User = kwargs.get("current_user") workspace_id = user.current_workspace_id if workspace_id is None: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="workspace_id is required") _check_workspace_access_sync(db, user, workspace_id) return func(*args, **kwargs) return _sync_wrapper return _decorator class ShareTokenData: """分享 token 数据""" def __init__(self, user_id: str, share_token: str): self.user_id = user_id self.share_token = share_token async def get_share_user_id( token: str = Depends(oauth2_scheme), db: Session = Depends(get_db) ) -> ShareTokenData: """ 从分享访问 token 中获取用户 ID 和 share_token 这个函数用于公开分享的接口,验证访问 token 并返回用户信息 不需要验证用户是否存在或激活,只需要验证 token 的有效性和 share_token 是否有效 Returns: ShareTokenData: 包含 user_id 和 share_token """ from app.services.auth_service import decode_access_token from app.services.release_share_service import ReleaseShareService from app.core.exceptions import BusinessException credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) try: auth_logger.debug("开始解析分享访问 token") # 解码 token 获取 user_id 和 share_token payload = decode_access_token(token) user_id = payload["user_id"] share_token = payload["share_token"] auth_logger.debug(f"Token 解析成功,用户ID: {user_id}, share_token: {share_token}") # 验证 share_token 是否有效 service = ReleaseShareService(db) share_info = service.get_shared_release_info(share_token=share_token) if not share_info: auth_logger.warning(f"分享 token 无效: {share_token}") raise credentials_exception auth_logger.info(f"分享访问验证成功: user_id={user_id}, share_token={share_token}") return ShareTokenData(user_id=user_id, share_token=share_token) except BusinessException as e: auth_logger.warning(f"分享访问验证失败: {str(e)}") raise credentials_exception except Exception as e: auth_logger.error(f"验证分享访问 token 时发生错误: {str(e)}") raise credentials_exception async def get_app_or_workspace( api_key: str = Depends(api_key_extractor), db: Session = Depends(get_db) ) -> App | Workspace: """ Get App or Workspace from API Key Supports two API Key formats: 1. Authorization: Bearer 2. X-API-Key: Args: api_key: API Key extracted from request headers db: Database session Returns: App or Workspace object based on API Key Raises: HTTPException: If API Key is invalid or not found """ from app.services.api_key_service import ApiKeyAuthService from app.repositories.app_repository import get_apps_by_id from app.repositories.workspace_repository import get_workspace_by_id credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate API Key", headers={"WWW-Authenticate": "Bearer"}, ) try: auth_logger.debug(f"Validating API Key: {api_key[:10]}...") # Validate API Key api_key_obj = ApiKeyAuthService.validate_api_key(db, api_key) if not api_key_obj: auth_logger.warning(f"Invalid or expired API Key: {api_key[:10]}...") raise credentials_exception auth_logger.debug(f"API Key validated successfully, type: {api_key_obj.type}") # Return App or Workspace based on API Key type if (api_key_obj.type == "agent" or api_key.type == "multi_agent") and api_key_obj.resource_id: # App API Key app = get_apps_by_id(db, api_key_obj.resource_id) if not app: auth_logger.warning(f"App not found for API Key: {api_key_obj.resource_id}") raise credentials_exception ApiKeyAuthService.check_app_published(db, api_key_obj) auth_logger.info(f"App access granted: {app.id}") return app elif api_key_obj.type == "service": # Workspace API Key workspace = get_workspace_by_id(db, api_key_obj.workspace_id) if not workspace: auth_logger.warning(f"Workspace not found for API Key: {api_key_obj.workspace_id}") raise credentials_exception auth_logger.info(f"Workspace access granted: {workspace.id}") return workspace else: auth_logger.warning(f"Unsupported API Key type: {api_key_obj.type}") raise credentials_exception except HTTPException: raise except Exception as e: auth_logger.error(f"Error validating API Key: {str(e)}", exc_info=True) raise credentials_exception