From 7f2ac4e358fbc07caae7f77fe5adb5503dbe76e9 Mon Sep 17 00:00:00 2001 From: Daniil Date: Thu, 30 Apr 2026 01:00:04 +0300 Subject: [PATCH] feat: add swagger auth --- cpv3/infrastructure/settings.py | 2 + cpv3/main.py | 60 +++++++++++++++++++++++--- tests/integration/test_swagger_auth.py | 57 ++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 tests/integration/test_swagger_auth.py diff --git a/cpv3/infrastructure/settings.py b/cpv3/infrastructure/settings.py index 1dc8590..2a3d9aa 100644 --- a/cpv3/infrastructure/settings.py +++ b/cpv3/infrastructure/settings.py @@ -16,6 +16,8 @@ class Settings(BaseSettings): # App debug: bool = Field(default=True, alias="DEBUG") + swagger_auth_login: str = Field(default="", alias="SWAGGER_AUTH_LOGIN") + swagger_auth_password: str = Field(default="", alias="SWAGGER_AUTH_PASSWORD") cors_allowed_origins: list[str] = Field( default_factory=lambda: [ "http://localhost:3000", diff --git a/cpv3/main.py b/cpv3/main.py index 24116b6..6e41976 100644 --- a/cpv3/main.py +++ b/cpv3/main.py @@ -1,18 +1,26 @@ from __future__ import annotations -from fastapi import FastAPI -from fastapi.middleware.cors import CORSMiddleware +from secrets import compare_digest +from typing import Annotated + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.middleware.cors import CORSMiddleware +from fastapi.openapi.docs import get_swagger_ui_html +from fastapi.security import HTTPBasic, HTTPBasicCredentials -from cpv3.infrastructure.settings import get_settings from cpv3.api.v1.router import api_router +from cpv3.infrastructure.settings import get_settings + +ERROR_SWAGGER_AUTH_INVALID = "Invalid Swagger credentials" settings = get_settings() +swagger_security = HTTPBasic() app = FastAPI( title="Coffee Project Backend API", version="0.0.0", - openapi_url="/api/schema/", - docs_url="/api/schema/swagger/", + openapi_url=None, + docs_url=None, redoc_url=None, ) @@ -24,5 +32,47 @@ app.add_middleware( allow_headers=["*"], ) + +def verify_swagger_auth( + credentials: Annotated[HTTPBasicCredentials, Depends(swagger_security)], +) -> None: + login_matches = compare_digest( + credentials.username, + settings.swagger_auth_login, + ) + password_matches = compare_digest( + credentials.password, + settings.swagger_auth_password, + ) + + if not settings.swagger_auth_login or not settings.swagger_auth_password: + login_matches = False + password_matches = False + + if not login_matches or not password_matches: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_SWAGGER_AUTH_INVALID, + headers={"WWW-Authenticate": "Basic"}, + ) + + +@app.get("/api/schema/", include_in_schema=False) +async def openapi_schema( + _: Annotated[None, Depends(verify_swagger_auth)], +): + return app.openapi() + + +@app.get("/api/schema/swagger/", include_in_schema=False) +async def swagger_ui( + _: Annotated[None, Depends(verify_swagger_auth)], +): + return get_swagger_ui_html( + openapi_url="/api/schema/", + title="Coffee Project Backend API - Swagger UI", + ) + + # Include the versioned API router app.include_router(api_router) diff --git a/tests/integration/test_swagger_auth.py b/tests/integration/test_swagger_auth.py new file mode 100644 index 0000000..182e0be --- /dev/null +++ b/tests/integration/test_swagger_auth.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +import importlib + +import pytest +from httpx import ASGITransport, AsyncClient, BasicAuth + +from cpv3.infrastructure.settings import get_settings + + +@pytest.fixture +def swagger_app(monkeypatch): + monkeypatch.setenv("SWAGGER_AUTH_LOGIN", "docs-user") + monkeypatch.setenv("SWAGGER_AUTH_PASSWORD", "docs-password") + get_settings.cache_clear() + + import cpv3.main + + module = importlib.reload(cpv3.main) + yield module.app + + get_settings.cache_clear() + importlib.reload(cpv3.main) + + +async def test_swagger_ui_requires_basic_auth(swagger_app): + async with AsyncClient( + transport=ASGITransport(app=swagger_app), + base_url="http://test", + ) as client: + response = await client.get("/api/schema/swagger/") + + assert response.status_code == 401 + assert response.headers["www-authenticate"] == "Basic" + + +async def test_openapi_schema_requires_basic_auth(swagger_app): + async with AsyncClient( + transport=ASGITransport(app=swagger_app), + base_url="http://test", + ) as client: + response = await client.get("/api/schema/") + + assert response.status_code == 401 + assert response.headers["www-authenticate"] == "Basic" + + +async def test_swagger_credentials_come_from_environment(swagger_app): + async with AsyncClient( + transport=ASGITransport(app=swagger_app), + base_url="http://test", + auth=BasicAuth("docs-user", "docs-password"), + ) as client: + response = await client.get("/api/schema/swagger/") + + assert response.status_code == 200 + assert "Swagger UI" in response.text