Compare commits
No commits in common. "1d7f2c81769146ba9cc297596df1484a4b4c60c7" and "4879fdca88daada186c9bb38a5e628d6fabb49d0" have entirely different histories.
1d7f2c8176
...
4879fdca88
@ -18,8 +18,7 @@ import uuid
|
|||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import Depends, HTTPException, Request, status
|
from fastapi import Depends, Header, HTTPException, Request, status
|
||||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
||||||
|
|
||||||
from src.api.dependencies.db import SessionDep
|
from src.api.dependencies.db import SessionDep
|
||||||
from src.common.exceptions import AuthenticationFailedError
|
from src.common.exceptions import AuthenticationFailedError
|
||||||
@ -62,27 +61,6 @@ def _login_forbidden(detail: str, email: str, user_id: uuid.UUID | None = None)
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Схема безопасности OpenAPI (issue #29): без неё Swagger UI не показывает Authorize, а заголовок выглядит как обычный
|
|
||||||
# параметр. `auto_error=False`: нет заголовка или чужая схема → `None`, прежний 401 формирует `parse_bearer`.
|
|
||||||
BEARER_SCHEME_NAME = "bearerAuth"
|
|
||||||
bearer_scheme = HTTPBearer(
|
|
||||||
scheme_name=BEARER_SCHEME_NAME,
|
|
||||||
bearerFormat="JWT",
|
|
||||||
auto_error=False,
|
|
||||||
description=(
|
|
||||||
"JWT РАИСА (прод) или dev-токен `POST /dev/token` (DEBUG). В Swagger: Authorize → токен без слова Bearer."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
BearerDep = Annotated[HTTPAuthorizationCredentials | None, Depends(bearer_scheme)]
|
|
||||||
|
|
||||||
|
|
||||||
def header_value(credentials: HTTPAuthorizationCredentials | None) -> str | None:
|
|
||||||
"""Значение заголовка `Authorization` как строка — для `parse_bearer` с прежними правилами (три части → 401)."""
|
|
||||||
if credentials is None:
|
|
||||||
return None
|
|
||||||
return f"{credentials.scheme} {credentials.credentials}"
|
|
||||||
|
|
||||||
|
|
||||||
def parse_bearer(authorization: str | None) -> str:
|
def parse_bearer(authorization: str | None) -> str:
|
||||||
"""Токен из значения заголовка `Authorization: Bearer <jwt>`; иначе 401 (схема без учёта регистра)."""
|
"""Токен из значения заголовка `Authorization: Bearer <jwt>`; иначе 401 (схема без учёта регистра)."""
|
||||||
if authorization is not None:
|
if authorization is not None:
|
||||||
@ -106,10 +84,10 @@ def get_token_verifier(request: Request) -> TokenVerifier | None:
|
|||||||
|
|
||||||
async def verified_claims(
|
async def verified_claims(
|
||||||
verifier: Annotated[TokenVerifier | None, Depends(get_token_verifier)],
|
verifier: Annotated[TokenVerifier | None, Depends(get_token_verifier)],
|
||||||
credentials: BearerDep = None,
|
authorization: Annotated[str | None, Header()] = None,
|
||||||
) -> Claims:
|
) -> Claims:
|
||||||
"""Claims проверенного токена. Не обращается к БД: все 401 решаются до неё."""
|
"""Claims проверенного токена. Не обращается к БД: все 401 решаются до неё."""
|
||||||
token = parse_bearer(header_value(credentials))
|
token = parse_bearer(authorization)
|
||||||
if verifier is None:
|
if verifier is None:
|
||||||
raise _token_rejected("аутентификация не настроена")
|
raise _token_rejected("аутентификация не настроена")
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -199,30 +199,3 @@ def test_counterparty_mapping_routes_declare_documented_responses(schema: dict[s
|
|||||||
assert mapping["200"]["content"]["application/json"]["schema"]["$ref"].endswith("/MapCounterpartyOut")
|
assert mapping["200"]["content"]["application/json"]["schema"]["$ref"].endswith("/MapCounterpartyOut")
|
||||||
row = schema["components"]["schemas"]["MapCounterpartyOut"]["properties"]["row"]
|
row = schema["components"]["schemas"]["MapCounterpartyOut"]["properties"]["row"]
|
||||||
assert {"type": "null"} in row["anyOf"]
|
assert {"type": "null"} in row["anyOf"]
|
||||||
|
|
||||||
|
|
||||||
# --- Схема безопасности (issue #29): Swagger UI показывает Authorize только при securitySchemes в OpenAPI ---
|
|
||||||
|
|
||||||
BEARER_SCHEME = "bearerAuth"
|
|
||||||
# Маршруты без аутентификации: выдача dev-токена и healthcheck
|
|
||||||
PUBLIC_PATHS = {"/api/v1/dev/token", "/healthcheck"}
|
|
||||||
|
|
||||||
|
|
||||||
def test_openapi_declares_bearer_security_scheme(schema: dict[str, Any]) -> None:
|
|
||||||
schemes = schema.get("components", {}).get("securitySchemes", {})
|
|
||||||
bearer = schemes.get(BEARER_SCHEME, {})
|
|
||||||
assert (bearer.get("type"), bearer.get("scheme"), bearer.get("bearerFormat")) == ("http", "bearer", "JWT"), schemes
|
|
||||||
assert "Authorize" in bearer.get("description", "") # подсказка для Swagger UI
|
|
||||||
|
|
||||||
|
|
||||||
def test_protected_routes_require_bearer_and_public_do_not(schema: dict[str, Any]) -> None:
|
|
||||||
missing: list[str] = []
|
|
||||||
for path, methods in schema["paths"].items():
|
|
||||||
for method, operation in methods.items():
|
|
||||||
requirements = operation.get("security", [])
|
|
||||||
has_bearer = any(BEARER_SCHEME in requirement for requirement in requirements)
|
|
||||||
if path in PUBLIC_PATHS:
|
|
||||||
assert not has_bearer, f"{method.upper()} {path} — публичный маршрут помечен security"
|
|
||||||
elif not has_bearer:
|
|
||||||
missing.append(f"{method.upper()} {path}")
|
|
||||||
assert missing == [], f"маршруты без схемы {BEARER_SCHEME} (Authorize в Swagger не подставит токен): {missing}"
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user