From 59ed4406c8fa1206ea17afffe06fcc06efa4906e Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Wed, 16 Apr 2025 17:51:45 -0700 Subject: [PATCH 01/10] feat: add Swagger UI support - Introduced a new SwaggerUI handler to serve the Swagger UI interface. - Updated app configuration to include Swagger UI parameters and routes. - Enhanced Settings class with new fields for Swagger UI configuration. - Ensured compatibility with OpenAPI spec endpoint when using Swagger UI. --- src/stac_auth_proxy/app.py | 20 ++++++++++++- src/stac_auth_proxy/config.py | 9 +++++- src/stac_auth_proxy/handlers/__init__.py | 3 +- src/stac_auth_proxy/handlers/swagger_ui.py | 35 ++++++++++++++++++++++ 4 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 src/stac_auth_proxy/handlers/swagger_ui.py diff --git a/src/stac_auth_proxy/app.py b/src/stac_auth_proxy/app.py index 82264b6..82be235 100644 --- a/src/stac_auth_proxy/app.py +++ b/src/stac_auth_proxy/app.py @@ -13,7 +13,7 @@ from starlette_cramjam.middleware import CompressionMiddleware from .config import Settings -from .handlers import HealthzHandler, ReverseProxyHandler +from .handlers import HealthzHandler, ReverseProxyHandler, SwaggerUI from .middleware import ( AddProcessTimeHeaderMiddleware, ApplyCql2FilterMiddleware, @@ -68,6 +68,10 @@ async def lifespan(app: FastAPI): app = FastAPI( openapi_url=None, # Disable OpenAPI schema endpoint, we want to serve upstream's schema + swagger_ui_parameters={ + "usePkceWithAuthorizationCodeGrant": True, + "clientId": "stac-auth-proxy", + }, lifespan=lifespan, root_path=settings.root_path, ) @@ -78,6 +82,20 @@ async def lifespan(app: FastAPI): # Handlers (place catch-all proxy handler last) # + if settings.swagger_ui_url: + assert ( + settings.openapi_spec_endpoint + ), "openapi_spec_endpoint must be set when using swagger_ui_url" + app.add_route( + settings.swagger_ui_url, + SwaggerUI( + openapi_url=settings.openapi_spec_endpoint, + title=settings.swagger_ui_title, + init_oauth=settings.swagger_ui_init_oauth, + parameters=settings.swagger_ui_parameters, + ).route, + include_in_schema=False, + ) if settings.healthz_prefix: app.include_router( HealthzHandler(upstream_url=str(settings.upstream_url)).router, diff --git a/src/stac_auth_proxy/config.py b/src/stac_auth_proxy/config.py index ce6cf54..09c0562 100644 --- a/src/stac_auth_proxy/config.py +++ b/src/stac_auth_proxy/config.py @@ -45,9 +45,16 @@ class Settings(BaseSettings): check_conformance: bool = True enable_compression: bool = True - openapi_spec_endpoint: Optional[str] = Field(pattern=_PREFIX_PATTERN, default=None) + # OpenAPI / Swagger UI + openapi_spec_endpoint: Optional[str] = Field( + pattern=_PREFIX_PATTERN, default="/api" + ) openapi_auth_scheme_name: str = "oidcAuth" openapi_auth_scheme_override: Optional[dict] = None + swagger_ui_url: str = "/api.html" + swagger_ui_title: str = "STAC API" + swagger_ui_init_oauth: dict = Field(default_factory=dict) + swagger_ui_parameters: dict = Field(default_factory=dict) # Auth enable_authentication_extension: bool = True diff --git a/src/stac_auth_proxy/handlers/__init__.py b/src/stac_auth_proxy/handlers/__init__.py index 52c6c97..b0b6cf0 100644 --- a/src/stac_auth_proxy/handlers/__init__.py +++ b/src/stac_auth_proxy/handlers/__init__.py @@ -2,5 +2,6 @@ from .healthz import HealthzHandler from .reverse_proxy import ReverseProxyHandler +from .swagger_ui import SwaggerUI -__all__ = ["ReverseProxyHandler", "HealthzHandler"] +__all__ = ["ReverseProxyHandler", "HealthzHandler", "SwaggerUI"] diff --git a/src/stac_auth_proxy/handlers/swagger_ui.py b/src/stac_auth_proxy/handlers/swagger_ui.py new file mode 100644 index 0000000..9203bdd --- /dev/null +++ b/src/stac_auth_proxy/handlers/swagger_ui.py @@ -0,0 +1,35 @@ +"""Swagger UI handler.""" + +from dataclasses import dataclass, field + +from fastapi.openapi.docs import get_swagger_ui_html +from starlette.requests import Request +from starlette.responses import HTMLResponse + + +@dataclass +class SwaggerUI: + """Swagger UI handler.""" + + openapi_url: str + title: str = "STAC API" + # https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/ + init_oauth: dict = field(default_factory=dict) + # https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/ + parameters: dict = field(default_factory=dict) + oauth2_redirect_url: str = "/docs/oauth2-redirect" + + async def route(self, req: Request) -> HTMLResponse: + """Route handler.""" + root_path = req.scope.get("root_path", "").rstrip("/") + openapi_url = root_path + self.openapi_url + oauth2_redirect_url = self.oauth2_redirect_url + if oauth2_redirect_url: + oauth2_redirect_url = root_path + oauth2_redirect_url + return get_swagger_ui_html( + openapi_url=openapi_url, + title=f"{self.title} - Swagger UI", + oauth2_redirect_url=oauth2_redirect_url, + init_oauth=self.init_oauth, + swagger_ui_parameters=self.parameters, + ) From 244713c59e4fb0f6aad8743d34a63e398886c847 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Wed, 16 Apr 2025 22:44:35 -0700 Subject: [PATCH 02/10] Fix test --- tests/test_openapi.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_openapi.py b/tests/test_openapi.py index cef00d5..b4411ca 100644 --- a/tests/test_openapi.py +++ b/tests/test_openapi.py @@ -15,6 +15,7 @@ def test_no_openapi_spec_endpoint(source_api_server: str): app = app_factory( upstream_url=source_api_server, openapi_spec_endpoint=None, + swagger_ui_url=None, ) client = TestClient(app) response = client.get("/api") From 62fc7a06dc0f1f651f5eadea92a252f6e0a80ac2 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Wed, 16 Apr 2025 22:45:31 -0700 Subject: [PATCH 03/10] Correct types --- src/stac_auth_proxy/config.py | 4 ++-- src/stac_auth_proxy/handlers/swagger_ui.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/stac_auth_proxy/config.py b/src/stac_auth_proxy/config.py index 09c0562..8c994f8 100644 --- a/src/stac_auth_proxy/config.py +++ b/src/stac_auth_proxy/config.py @@ -51,8 +51,8 @@ class Settings(BaseSettings): ) openapi_auth_scheme_name: str = "oidcAuth" openapi_auth_scheme_override: Optional[dict] = None - swagger_ui_url: str = "/api.html" - swagger_ui_title: str = "STAC API" + swagger_ui_url: Optional[str] = "/api.html" + swagger_ui_title: Optional[str] = "STAC API" swagger_ui_init_oauth: dict = Field(default_factory=dict) swagger_ui_parameters: dict = Field(default_factory=dict) diff --git a/src/stac_auth_proxy/handlers/swagger_ui.py b/src/stac_auth_proxy/handlers/swagger_ui.py index 9203bdd..d56c6c6 100644 --- a/src/stac_auth_proxy/handlers/swagger_ui.py +++ b/src/stac_auth_proxy/handlers/swagger_ui.py @@ -1,6 +1,7 @@ """Swagger UI handler.""" from dataclasses import dataclass, field +from typing import Optional from fastapi.openapi.docs import get_swagger_ui_html from starlette.requests import Request @@ -12,7 +13,7 @@ class SwaggerUI: """Swagger UI handler.""" openapi_url: str - title: str = "STAC API" + title: Optional[str] = "STAC API" # https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/ init_oauth: dict = field(default_factory=dict) # https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/ From 08e21b56e032996da185776bca77df045d782c76 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Wed, 16 Apr 2025 22:50:42 -0700 Subject: [PATCH 04/10] Rm swagger parameters --- src/stac_auth_proxy/app.py | 1 - src/stac_auth_proxy/config.py | 1 - src/stac_auth_proxy/handlers/swagger_ui.py | 5 +---- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/stac_auth_proxy/app.py b/src/stac_auth_proxy/app.py index 82be235..4ff2824 100644 --- a/src/stac_auth_proxy/app.py +++ b/src/stac_auth_proxy/app.py @@ -92,7 +92,6 @@ async def lifespan(app: FastAPI): openapi_url=settings.openapi_spec_endpoint, title=settings.swagger_ui_title, init_oauth=settings.swagger_ui_init_oauth, - parameters=settings.swagger_ui_parameters, ).route, include_in_schema=False, ) diff --git a/src/stac_auth_proxy/config.py b/src/stac_auth_proxy/config.py index 8c994f8..2081d48 100644 --- a/src/stac_auth_proxy/config.py +++ b/src/stac_auth_proxy/config.py @@ -54,7 +54,6 @@ class Settings(BaseSettings): swagger_ui_url: Optional[str] = "/api.html" swagger_ui_title: Optional[str] = "STAC API" swagger_ui_init_oauth: dict = Field(default_factory=dict) - swagger_ui_parameters: dict = Field(default_factory=dict) # Auth enable_authentication_extension: bool = True diff --git a/src/stac_auth_proxy/handlers/swagger_ui.py b/src/stac_auth_proxy/handlers/swagger_ui.py index d56c6c6..1338825 100644 --- a/src/stac_auth_proxy/handlers/swagger_ui.py +++ b/src/stac_auth_proxy/handlers/swagger_ui.py @@ -14,10 +14,7 @@ class SwaggerUI: openapi_url: str title: Optional[str] = "STAC API" - # https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/ init_oauth: dict = field(default_factory=dict) - # https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/ - parameters: dict = field(default_factory=dict) oauth2_redirect_url: str = "/docs/oauth2-redirect" async def route(self, req: Request) -> HTMLResponse: @@ -32,5 +29,5 @@ async def route(self, req: Request) -> HTMLResponse: title=f"{self.title} - Swagger UI", oauth2_redirect_url=oauth2_redirect_url, init_oauth=self.init_oauth, - swagger_ui_parameters=self.parameters, + swagger_ui_parameters=None, ) From 0d999f7378aae5b18c891c64f185a6217ef988fb Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Wed, 16 Apr 2025 22:50:50 -0700 Subject: [PATCH 05/10] Add documentation --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 717d3ba..02b8bb5 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,18 @@ The application is configurable via environment variables. - **Type:** JSON object - **Required:** No, defaults to `null` (disabled) - **Example:** `{"type": "http", "scheme": "bearer", "bearerFormat": "JWT", "description": "Paste your raw JWT here. This API uses Bearer token authorization.\n"}` + - **`SWAGGER_UI_URL`**, path of Swagger UI, used for augmenting spec response with auth configuration + - **Type:** string or null + - **Required:** No, defaults to `/api.html` + - **Example:** `/api` + - **`SWAGGER_UI_TITLE`**, title of the Swagger UI + - **Type:** string + - **Required:** No, defaults to `STAC API` + - **Example:** `Foo API` + - **`SWAGGER_UI_INIT_OAUTH`**, initialization options for the [Swagger UI OAuth2 configuration](https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/) + - **Type:** JSON object + - **Required:** No, defaults to `null` (disabled) + - **Example:** `{"clientId": "stac-auth-proxy", "usePkceWithAuthorizationCodeGrant": true}` - Filtering - **`ITEMS_FILTER_CLS`**, CQL2 expression generator for item-level filtering - **Type:** JSON object with class configuration From 9f4d9257138e8fb8ae42437cd639cf1b795c15a5 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Wed, 16 Apr 2025 22:52:14 -0700 Subject: [PATCH 06/10] Cleanup --- src/stac_auth_proxy/app.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/stac_auth_proxy/app.py b/src/stac_auth_proxy/app.py index 4ff2824..8c6d17e 100644 --- a/src/stac_auth_proxy/app.py +++ b/src/stac_auth_proxy/app.py @@ -68,10 +68,6 @@ async def lifespan(app: FastAPI): app = FastAPI( openapi_url=None, # Disable OpenAPI schema endpoint, we want to serve upstream's schema - swagger_ui_parameters={ - "usePkceWithAuthorizationCodeGrant": True, - "clientId": "stac-auth-proxy", - }, lifespan=lifespan, root_path=settings.root_path, ) From 3e301bc26e4344951676d04c44d2add56bdbd29d Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Wed, 16 Apr 2025 22:57:58 -0700 Subject: [PATCH 07/10] Improve notes --- src/stac_auth_proxy/handlers/swagger_ui.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/stac_auth_proxy/handlers/swagger_ui.py b/src/stac_auth_proxy/handlers/swagger_ui.py index 1338825..641b2e0 100644 --- a/src/stac_auth_proxy/handlers/swagger_ui.py +++ b/src/stac_auth_proxy/handlers/swagger_ui.py @@ -1,4 +1,11 @@ -"""Swagger UI handler.""" +""" +In order to allow customization fo the Swagger UI's OAuth2 configuration, we support +overriding the default handler. This is useful for adding custom parameters such as +`usePkceWithAuthorizationCodeGrant` or `clientId`. + +See: +- https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/ +""" from dataclasses import dataclass, field from typing import Optional From e6921d4513c23ff6bfe3031b1160c6955c526996 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Wed, 16 Apr 2025 23:02:09 -0700 Subject: [PATCH 08/10] Simplify configuration, keep detailed config on handler class --- README.md | 6 +----- src/stac_auth_proxy/app.py | 4 ++-- src/stac_auth_proxy/config.py | 3 +-- src/stac_auth_proxy/handlers/swagger_ui.py | 3 ++- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 02b8bb5..b3eab89 100644 --- a/README.md +++ b/README.md @@ -145,14 +145,10 @@ The application is configurable via environment variables. - **Type:** JSON object - **Required:** No, defaults to `null` (disabled) - **Example:** `{"type": "http", "scheme": "bearer", "bearerFormat": "JWT", "description": "Paste your raw JWT here. This API uses Bearer token authorization.\n"}` - - **`SWAGGER_UI_URL`**, path of Swagger UI, used for augmenting spec response with auth configuration + - **`SWAGGER_UI_ENDPOINT`**, path of Swagger UI, used for augmenting spec response with auth configuration - **Type:** string or null - **Required:** No, defaults to `/api.html` - **Example:** `/api` - - **`SWAGGER_UI_TITLE`**, title of the Swagger UI - - **Type:** string - - **Required:** No, defaults to `STAC API` - - **Example:** `Foo API` - **`SWAGGER_UI_INIT_OAUTH`**, initialization options for the [Swagger UI OAuth2 configuration](https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/) - **Type:** JSON object - **Required:** No, defaults to `null` (disabled) diff --git a/src/stac_auth_proxy/app.py b/src/stac_auth_proxy/app.py index 8c6d17e..fe6f709 100644 --- a/src/stac_auth_proxy/app.py +++ b/src/stac_auth_proxy/app.py @@ -78,12 +78,12 @@ async def lifespan(app: FastAPI): # Handlers (place catch-all proxy handler last) # - if settings.swagger_ui_url: + if settings.swagger_ui_endpoint: assert ( settings.openapi_spec_endpoint ), "openapi_spec_endpoint must be set when using swagger_ui_url" app.add_route( - settings.swagger_ui_url, + settings.swagger_ui_endpoint, SwaggerUI( openapi_url=settings.openapi_spec_endpoint, title=settings.swagger_ui_title, diff --git a/src/stac_auth_proxy/config.py b/src/stac_auth_proxy/config.py index 2081d48..aa77142 100644 --- a/src/stac_auth_proxy/config.py +++ b/src/stac_auth_proxy/config.py @@ -51,8 +51,7 @@ class Settings(BaseSettings): ) openapi_auth_scheme_name: str = "oidcAuth" openapi_auth_scheme_override: Optional[dict] = None - swagger_ui_url: Optional[str] = "/api.html" - swagger_ui_title: Optional[str] = "STAC API" + swagger_ui_endpoint: Optional[str] = "/api.html" swagger_ui_init_oauth: dict = Field(default_factory=dict) # Auth diff --git a/src/stac_auth_proxy/handlers/swagger_ui.py b/src/stac_auth_proxy/handlers/swagger_ui.py index 641b2e0..1b885a2 100644 --- a/src/stac_auth_proxy/handlers/swagger_ui.py +++ b/src/stac_auth_proxy/handlers/swagger_ui.py @@ -22,6 +22,7 @@ class SwaggerUI: openapi_url: str title: Optional[str] = "STAC API" init_oauth: dict = field(default_factory=dict) + parameters: dict = field(default_factory=dict) oauth2_redirect_url: str = "/docs/oauth2-redirect" async def route(self, req: Request) -> HTMLResponse: @@ -36,5 +37,5 @@ async def route(self, req: Request) -> HTMLResponse: title=f"{self.title} - Swagger UI", oauth2_redirect_url=oauth2_redirect_url, init_oauth=self.init_oauth, - swagger_ui_parameters=None, + swagger_ui_parameters=self.parameters, ) From b1d72ce2ac0fdeea9b1b8cfe89d968b0ef88ad8d Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Wed, 16 Apr 2025 23:05:19 -0700 Subject: [PATCH 09/10] Fix tests --- src/stac_auth_proxy/app.py | 3 +-- src/stac_auth_proxy/config.py | 6 ++---- tests/test_openapi.py | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/stac_auth_proxy/app.py b/src/stac_auth_proxy/app.py index fe6f709..0682f68 100644 --- a/src/stac_auth_proxy/app.py +++ b/src/stac_auth_proxy/app.py @@ -81,12 +81,11 @@ async def lifespan(app: FastAPI): if settings.swagger_ui_endpoint: assert ( settings.openapi_spec_endpoint - ), "openapi_spec_endpoint must be set when using swagger_ui_url" + ), "openapi_spec_endpoint must be set when using swagger_ui_endpoint" app.add_route( settings.swagger_ui_endpoint, SwaggerUI( openapi_url=settings.openapi_spec_endpoint, - title=settings.swagger_ui_title, init_oauth=settings.swagger_ui_init_oauth, ).route, include_in_schema=False, diff --git a/src/stac_auth_proxy/config.py b/src/stac_auth_proxy/config.py index aa77142..c2ca946 100644 --- a/src/stac_auth_proxy/config.py +++ b/src/stac_auth_proxy/config.py @@ -46,12 +46,10 @@ class Settings(BaseSettings): enable_compression: bool = True # OpenAPI / Swagger UI - openapi_spec_endpoint: Optional[str] = Field( - pattern=_PREFIX_PATTERN, default="/api" - ) + openapi_spec_endpoint: Optional[str] = Field(pattern=_PREFIX_PATTERN, default=None) openapi_auth_scheme_name: str = "oidcAuth" openapi_auth_scheme_override: Optional[dict] = None - swagger_ui_endpoint: Optional[str] = "/api.html" + swagger_ui_endpoint: Optional[str] = None swagger_ui_init_oauth: dict = Field(default_factory=dict) # Auth diff --git a/tests/test_openapi.py b/tests/test_openapi.py index b4411ca..c8efee0 100644 --- a/tests/test_openapi.py +++ b/tests/test_openapi.py @@ -15,7 +15,7 @@ def test_no_openapi_spec_endpoint(source_api_server: str): app = app_factory( upstream_url=source_api_server, openapi_spec_endpoint=None, - swagger_ui_url=None, + swagger_ui_endpoint=None, ) client = TestClient(app) response = client.get("/api") From edea4aa7fc089d1511d5ce3f2372a3039e03ad35 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Wed, 16 Apr 2025 23:06:05 -0700 Subject: [PATCH 10/10] Rm unnecessary change --- tests/test_openapi.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_openapi.py b/tests/test_openapi.py index c8efee0..cef00d5 100644 --- a/tests/test_openapi.py +++ b/tests/test_openapi.py @@ -15,7 +15,6 @@ def test_no_openapi_spec_endpoint(source_api_server: str): app = app_factory( upstream_url=source_api_server, openapi_spec_endpoint=None, - swagger_ui_endpoint=None, ) client = TestClient(app) response = client.get("/api")