refactor(auth): replace legacy middleware sentinel
This commit is contained in:
+27
-133
@@ -1,166 +1,60 @@
|
||||
"""
|
||||
Legacy auth middleware.
|
||||
Authentication dependency compatibility layer.
|
||||
|
||||
Disabled because it depends on the removed DI container. Rebuild it around the
|
||||
canonical JWT settings and SQLAlchemy-backed user repository before reuse.
|
||||
Canonical bearer-token parsing lives in app.auth. This module remains only so
|
||||
legacy imports have a safe target while workspace dependencies are rebuilt.
|
||||
"""
|
||||
|
||||
raise RuntimeError("apps.api.app.middleware.auth is disabled: rebuild auth dependency wiring before importing it")
|
||||
from __future__ import annotations
|
||||
|
||||
from app.auth import AuthenticatedUser
|
||||
from app.auth import get_current_user as get_authenticated_user
|
||||
from app.dependencies import get_user_repository
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
|
||||
from apps.api.app.dependencies import get_container
|
||||
from packages.domain.auth import jwt_service
|
||||
from packages.domain.entities import User
|
||||
from packages.ports.user_repository import UserRepository
|
||||
|
||||
security = HTTPBearer()
|
||||
optional_bearer_scheme = HTTPBearer(auto_error=False)
|
||||
|
||||
|
||||
async def get_current_user(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
authenticated_user: AuthenticatedUser = Depends(get_authenticated_user),
|
||||
) -> User:
|
||||
"""
|
||||
获取当前登录用户
|
||||
|
||||
从 Authorization header 中提取 JWT token 并验证
|
||||
|
||||
Raises:
|
||||
HTTPException: Token 无效或过期
|
||||
|
||||
Returns:
|
||||
当前用户对象
|
||||
"""
|
||||
token = credentials.credentials
|
||||
|
||||
try:
|
||||
# 验证 token
|
||||
payload = jwt_service.verify_token(token)
|
||||
user_id = payload.get("sub")
|
||||
|
||||
if not user_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token: missing user_id",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# 从数据库获取用户
|
||||
container = get_container()
|
||||
user = container.user_repository.find_by_id(user_id)
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="User not found",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=f"Invalid token: {str(e)}",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
return authenticated_user.user
|
||||
|
||||
|
||||
async def get_current_user_optional(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(HTTPBearer(auto_error=False)),
|
||||
credentials: HTTPAuthorizationCredentials | None = Depends(optional_bearer_scheme),
|
||||
user_repository: UserRepository = Depends(get_user_repository),
|
||||
) -> User | None:
|
||||
"""
|
||||
获取当前登录用户(可选)
|
||||
|
||||
如果没有提供 token,返回 None 而不是抛出异常
|
||||
|
||||
Returns:
|
||||
当前用户对象或 None
|
||||
"""
|
||||
if not credentials:
|
||||
if credentials is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
return await get_current_user(credentials)
|
||||
authenticated_user = await get_authenticated_user(credentials, user_repository)
|
||||
except HTTPException:
|
||||
return None
|
||||
return authenticated_user.user
|
||||
|
||||
|
||||
def require_workspace_access(workspace_id: str, user: User = Depends(get_current_user)) -> tuple[str, str]:
|
||||
"""
|
||||
要求用户可以访问指定工作空间
|
||||
|
||||
Args:
|
||||
workspace_id: 工作空间 ID
|
||||
user: 当前用户
|
||||
|
||||
Raises:
|
||||
HTTPException: 用户没有访问权限
|
||||
|
||||
Returns:
|
||||
(workspace_id, user_role)
|
||||
"""
|
||||
container = get_container()
|
||||
permission_checker = container.permission_checker
|
||||
|
||||
has_access, role = permission_checker.check_workspace_access(workspace_id, user.id)
|
||||
|
||||
if not has_access:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="You don't have access to this workspace",
|
||||
)
|
||||
|
||||
return workspace_id, role
|
||||
raise _workspace_dependency_not_ready(workspace_id, user.id)
|
||||
|
||||
|
||||
def require_workspace_admin(workspace_id: str, user: User = Depends(get_current_user)) -> str:
|
||||
"""
|
||||
要求用户是工作空间的 Admin 或 Owner
|
||||
|
||||
Args:
|
||||
workspace_id: 工作空间 ID
|
||||
user: 当前用户
|
||||
|
||||
Raises:
|
||||
HTTPException: 用户没有管理权限
|
||||
|
||||
Returns:
|
||||
workspace_id
|
||||
"""
|
||||
container = get_container()
|
||||
permission_checker = container.permission_checker
|
||||
|
||||
if not permission_checker.check_is_admin_or_owner(workspace_id, user.id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only workspace owner or admin can perform this action",
|
||||
)
|
||||
|
||||
return workspace_id
|
||||
raise _workspace_dependency_not_ready(workspace_id, user.id)
|
||||
|
||||
|
||||
def require_workspace_owner(workspace_id: str, user: User = Depends(get_current_user)) -> str:
|
||||
"""
|
||||
要求用户是工作空间的 Owner
|
||||
raise _workspace_dependency_not_ready(workspace_id, user.id)
|
||||
|
||||
Args:
|
||||
workspace_id: 工作空间 ID
|
||||
user: 当前用户
|
||||
|
||||
Raises:
|
||||
HTTPException: 用户不是 Owner
|
||||
|
||||
Returns:
|
||||
workspace_id
|
||||
"""
|
||||
container = get_container()
|
||||
permission_checker = container.permission_checker
|
||||
|
||||
if not permission_checker.check_is_owner(workspace_id, user.id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only workspace owner can perform this action",
|
||||
)
|
||||
|
||||
return workspace_id
|
||||
def _workspace_dependency_not_ready(workspace_id: str, user_id: str) -> HTTPException:
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
detail=(
|
||||
"Workspace authorization dependencies require SQLAlchemy workspace-member "
|
||||
f"repository wiring before use: workspace_id={workspace_id}, user_id={user_id}"
|
||||
),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import jwt
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
from fastapi.security import HTTPAuthorizationCredentials
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
API_ROOT = ROOT / "apps" / "api"
|
||||
if str(API_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(API_ROOT))
|
||||
|
||||
from app.config import settings
|
||||
from app.middleware.auth import get_current_user, get_current_user_optional, require_workspace_access
|
||||
|
||||
from packages.domain.auth import password_hasher
|
||||
from packages.domain.entities import User
|
||||
|
||||
|
||||
class InMemoryUserRepository:
|
||||
def __init__(self):
|
||||
self.users = {}
|
||||
|
||||
def save(self, user):
|
||||
self.users[user.id] = user
|
||||
|
||||
def find_by_id(self, user_id):
|
||||
return self.users.get(user_id)
|
||||
|
||||
def find_by_email(self, email):
|
||||
return next((user for user in self.users.values() if user.email == email), None)
|
||||
|
||||
def find_by_username(self, username):
|
||||
return next((user for user in self.users.values() if user.username == username), None)
|
||||
|
||||
def find_by_verification_token(self, token):
|
||||
return None
|
||||
|
||||
def find_by_password_reset_token(self, token):
|
||||
return None
|
||||
|
||||
def delete(self, user_id):
|
||||
return self.users.pop(user_id, None) is not None
|
||||
|
||||
|
||||
def _repo_with_user():
|
||||
repo = InMemoryUserRepository()
|
||||
user = User(
|
||||
id="user-1",
|
||||
email="user@example.com",
|
||||
username="user",
|
||||
display_name="User",
|
||||
password_hash=password_hasher.hash_password("Password1"),
|
||||
)
|
||||
repo.save(user)
|
||||
return repo
|
||||
|
||||
|
||||
def _credentials(token_type="user_auth"):
|
||||
token = jwt.encode({"sub": "user-1", "sid": "session-1", "type": token_type}, settings.JWT_SECRET_KEY, "HS256")
|
||||
return HTTPAuthorizationCredentials(scheme="Bearer", credentials=token)
|
||||
|
||||
|
||||
def test_legacy_middleware_get_current_user_delegates_to_canonical_auth():
|
||||
user = asyncio.run(get_current_user(asyncio.run(_authenticated_user())))
|
||||
|
||||
assert user.id == "user-1"
|
||||
|
||||
|
||||
def test_legacy_middleware_optional_user_returns_none_without_credentials():
|
||||
assert asyncio.run(get_current_user_optional(None, _repo_with_user())) is None
|
||||
|
||||
|
||||
def test_legacy_middleware_optional_user_returns_user_with_valid_credentials():
|
||||
user = asyncio.run(get_current_user_optional(_credentials(), _repo_with_user()))
|
||||
|
||||
assert user is not None
|
||||
assert user.id == "user-1"
|
||||
|
||||
|
||||
def test_workspace_dependency_fails_closed_until_repository_is_wired():
|
||||
with pytest.raises(HTTPException) as error:
|
||||
require_workspace_access("workspace-1", _repo_with_user().find_by_id("user-1"))
|
||||
|
||||
assert error.value.status_code == 501
|
||||
assert "SQLAlchemy workspace-member repository" in error.value.detail
|
||||
|
||||
|
||||
async def _authenticated_user():
|
||||
from app.auth import get_current_user as get_authenticated_user
|
||||
|
||||
return await get_authenticated_user(_credentials(), _repo_with_user())
|
||||
Reference in New Issue
Block a user