fix(auth): unify site-wide 401/403 semantics (cma-auth-401-403-unify-20260915)
require_auth now uses HTTPBearer(auto_error=False) and raises 401 explicitly when the Authorization header is missing or non-Bearer, matching require_auth_401. Unauthenticated requests are 401 for every user endpoint; authenticated-but-forbidden (cross-entity / role) stays 403. - app/auth_middleware.py: single optional bearer, 401 on missing token (require_auth_401 kept as a compatibility alias) - tests: update 4 pre-existing cases that asserted 403 without a token (test_auth, test_reports, test_kpi_causality, test_expenses, test_api_auth_surface_20260914) - intentional behavior change - add tests/test_auth_401_unification_20260915.py: sample + full sweep (all AUTHED GET routes -> 401), invalid token -> 401, cross-entity and role-denied still 403, bot channel (X-BOT-KEY) and public paths unchanged
This commit is contained in:
@@ -176,10 +176,26 @@ def extract_bearer_token(request) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
# ── 统一全站鉴权语义(cma-auth-401-403-unify-20260915)────────────────────
|
||||
# 口径:**未认证(缺 token / 无效 token)→ 401**;**已认证但无权(跨实体 / 角色不足)→ 403**。
|
||||
# FastAPI 的 HTTPBearer(auto_error=True) 对「缺 Authorization 头」返回 403,与
|
||||
# require_auth_401 的 401 语义在同站并存(同一种「没带 token」两种状态码)。
|
||||
# 现统一为 auto_error=False 自行判空:缺 header / 非 Bearer 格式 → 显式 401;
|
||||
# 有 token → 仍按原逻辑校验身份(无效/过期 token → 401)。
|
||||
# 403 语义保留:require_role(角色不足)、deps.assert_entity_scope(跨实体无权)。
|
||||
_optional_bearer = HTTPBearer(auto_error=False)
|
||||
|
||||
|
||||
def require_auth(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(HTTPBearer(auto_error=True)),
|
||||
credentials: Optional[HTTPAuthorizationCredentials] = Depends(_optional_bearer),
|
||||
db: Session = Depends(get_db),
|
||||
) -> User:
|
||||
"""认证依赖(全站统一):缺 token / 非 Bearer 格式 → 401;无效 token → 401。
|
||||
|
||||
已认证但无权(角色/实体)由 require_role、deps.assert_entity_scope 另行返回 403。
|
||||
"""
|
||||
if credentials is None or not credentials.credentials:
|
||||
raise HTTPException(status_code=401, detail="未认证:缺少 Bearer token")
|
||||
token = credentials.credentials
|
||||
user_id = _resolve_user_id(token)
|
||||
if user_id is None:
|
||||
@@ -200,20 +216,20 @@ def require_role(*roles: str):
|
||||
return role_checker
|
||||
|
||||
|
||||
# ── 401 语义的认证依赖(cma-api-auth-surface-audit-20260914)──────────────
|
||||
# FastAPI 的 HTTPBearer(auto_error=True) 对"缺 Authorization 头"返回 403;
|
||||
# 安全语义上「未认证」应为 401(401=未认证 / 403=无权)。
|
||||
# 本依赖用 auto_error=False 自行判空 → 缺 token 显式 401,有 token 仍完全委托 require_auth。
|
||||
_optional_bearer = HTTPBearer(auto_error=False)
|
||||
|
||||
|
||||
# ── 401 语义的认证依赖(cma-api-auth-surface-audit-20260914)
|
||||
# 2026-09-15(cma-auth-401-403-unify-20260915):语义与 _optional_bearer 已统一,
|
||||
# 本入口保留作兼容别名(既有 router 与审计器 AUTH_STRICT 均引用此名)。
|
||||
def require_auth_401(
|
||||
credentials: Optional[HTTPAuthorizationCredentials] = Depends(_optional_bearer),
|
||||
db: Session = Depends(get_db),
|
||||
) -> User:
|
||||
"""用户端点统一鉴权入口:缺 Bearer → 401;有 token → require_auth 校验(无效 → 401)"""
|
||||
if credentials is None or not credentials.credentials:
|
||||
raise HTTPException(status_code=401, detail="未认证:缺少 Bearer token")
|
||||
"""用户端点统一鉴权入口:缺 Bearer → 401;有 token → require_auth 校验(无效 → 401)
|
||||
|
||||
2026-09-15(cma-auth-401-403-unify-20260915)起与 require_auth 语义完全一致
|
||||
(require_auth 也改为 auto_error=False 自判空 → 缺 token 401)。
|
||||
保留本名称:既有 router 的 dependencies=[Depends(require_auth_401)] 与
|
||||
backend/scripts/audit_api_auth_surface.py 的 AUTH_STRICT 分类仍引用它。
|
||||
"""
|
||||
return require_auth(credentials=credentials, db=db)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user