From c620cd14989a7024f98d1984d97110ef4e78771e Mon Sep 17 00:00:00 2001 From: Arif Dogan Date: Tue, 11 Nov 2025 03:13:44 +0100 Subject: [PATCH 1/4] feat: add endpoint access permissions via auth_dependency parameter Add optional auth_dependency parameter to Radar class that allows users to secure the dashboard and API endpoints with any FastAPI dependency function. Changes: - Add auth_dependency parameter to Radar.__init__() - Apply auth dependency to API router via dependencies parameter - Apply auth dependency to dashboard route via dependencies parameter - Add comprehensive documentation with examples (HTTP Basic, Bearer, Custom) - Add commented example in example_app.py showing HTTP Basic auth usage This provides maximum flexibility while maintaining backward compatibility. Users can implement any authentication mechanism (OAuth2, JWT, API keys, etc.) using standard FastAPI dependency patterns. Fixes #27 --- README.md | 76 ++++++++++++++++++++++++++++++++++++++++++ example_app.py | 19 +++++++++++ fastapi_radar/api.py | 11 ++++-- fastapi_radar/radar.py | 14 ++++++-- 4 files changed, 114 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ef9671e..6ead4c1 100644 --- a/README.md +++ b/README.md @@ -95,9 +95,85 @@ radar = Radar( exclude_paths=["/health"], # Paths to exclude from monitoring theme="auto", # Dashboard theme: "light", "dark", or "auto" db_path="/path/to/db", # Custom path for radar.duckdb file (default: current directory) + auth_dependency=None, # Optional: Authentication dependency for dashboard and API access ) ``` +## Securing the Dashboard + +By default, FastAPI Radar is accessible without authentication. For production environments, you should add authentication to protect your monitoring dashboard: + +### HTTP Basic Authentication + +```python +from fastapi import FastAPI, Depends, HTTPException, status +from fastapi.security import HTTPBasic, HTTPBasicCredentials +from fastapi_radar import Radar +import secrets + +app = FastAPI() +security = HTTPBasic() + +def verify_credentials(credentials: HTTPBasicCredentials = Depends(security)): + correct_username = secrets.compare_digest(credentials.username, "admin") + correct_password = secrets.compare_digest(credentials.password, "secret") + if not (correct_username and correct_password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid credentials", + headers={"WWW-Authenticate": "Basic"}, + ) + return credentials + +radar = Radar(app, auth_dependency=verify_credentials) +radar.create_tables() +``` + +### Bearer Token Authentication + +```python +from fastapi import FastAPI, Depends, HTTPException, status +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from fastapi_radar import Radar + +app = FastAPI() +security = HTTPBearer() + +def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)): + if credentials.credentials != "your-secret-token": + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid token", + ) + return credentials + +radar = Radar(app, auth_dependency=verify_token) +radar.create_tables() +``` + +### Custom Authentication + +You can use any FastAPI dependency for authentication: + +```python +from fastapi import FastAPI, Depends, HTTPException, Request +from fastapi_radar import Radar + +app = FastAPI() + +async def custom_auth(request: Request): + # Your custom authentication logic + api_key = request.headers.get("X-API-Key") + if api_key != "your-api-key": + raise HTTPException(status_code=401, detail="Unauthorized") + return True + +radar = Radar(app, auth_dependency=custom_auth) +radar.create_tables() +``` + +The `auth_dependency` parameter accepts any FastAPI dependency function, giving you full flexibility to implement OAuth2, JWT, API keys, or any other authentication mechanism. + ### Custom Database Location By default, FastAPI Radar stores its monitoring data in a `radar.duckdb` file in your current working directory. You can customize this location using the `db_path` parameter: diff --git a/example_app.py b/example_app.py index d46187f..103bb0f 100644 --- a/example_app.py +++ b/example_app.py @@ -95,6 +95,24 @@ class Config: version="1.0.0", ) +# Optional: Setup authentication for Radar dashboard +# Uncomment the following lines to secure the dashboard with HTTP Basic Auth +# from fastapi.security import HTTPBasic, HTTPBasicCredentials +# import secrets +# +# security = HTTPBasic() +# +# def verify_radar_credentials(credentials: HTTPBasicCredentials = Depends(security)): +# correct_username = secrets.compare_digest(credentials.username, "admin") +# correct_password = secrets.compare_digest(credentials.password, "secret") +# if not (correct_username and correct_password): +# raise HTTPException( +# status_code=401, +# detail="Invalid credentials", +# headers={"WWW-Authenticate": "Basic"}, +# ) +# return credentials + # Initialize Radar - automatically adds middleware and mounts dashboard radar = Radar( app, @@ -102,6 +120,7 @@ class Config: dashboard_path="/__radar", slow_query_threshold=50, theme="auto", + # auth_dependency=verify_radar_credentials, # Uncomment to enable authentication ) radar.create_tables() diff --git a/fastapi_radar/api.py b/fastapi_radar/api.py index 9803c6b..7665833 100644 --- a/fastapi_radar/api.py +++ b/fastapi_radar/api.py @@ -1,7 +1,7 @@ """API endpoints for FastAPI Radar dashboard.""" from datetime import datetime, timedelta, timezone -from typing import Any, Dict, List, Optional, Union +from typing import Any, Callable, Dict, List, Optional, Union import uuid from fastapi import APIRouter, Depends, HTTPException, Query @@ -142,8 +142,13 @@ class TraceDetail(BaseModel): spans: List[WaterfallSpan] -def create_api_router(get_session_context) -> APIRouter: - router = APIRouter(prefix="/__radar/api", tags=["radar"]) +def create_api_router(get_session_context, auth_dependency: Optional[Callable] = None) -> APIRouter: + # Build dependencies list for the router + dependencies = [] + if auth_dependency: + dependencies.append(Depends(auth_dependency)) + + router = APIRouter(prefix="/__radar/api", tags=["radar"], dependencies=dependencies) def get_db(): """Dependency function for FastAPI to get database session.""" diff --git a/fastapi_radar/radar.py b/fastapi_radar/radar.py index 558addd..4c2708d 100644 --- a/fastapi_radar/radar.py +++ b/fastapi_radar/radar.py @@ -5,7 +5,7 @@ import sys import multiprocessing from pathlib import Path -from typing import List, Optional, Union +from typing import Callable, List, Optional, Union import asyncio from fastapi import FastAPI @@ -61,6 +61,7 @@ def __init__( service_name: str = "fastapi-app", include_in_schema: bool = True, db_path: Optional[str] = None, + auth_dependency: Optional[Callable] = None, ): self.app = app self.db_engine = db_engine @@ -74,6 +75,7 @@ def __init__( self.enable_tracing = enable_tracing self.service_name = service_name self.db_path = db_path + self.auth_dependency = auth_dependency self.query_capture = None if dashboard_path not in self.exclude_paths: @@ -209,12 +211,12 @@ def _setup_query_capture(self) -> None: def _setup_api(self, include_in_schema: bool) -> None: """Mount API endpoints.""" - api_router = create_api_router(self.get_session) + api_router = create_api_router(self.get_session, self.auth_dependency) self.app.include_router(api_router, include_in_schema=include_in_schema) def _setup_dashboard(self, include_in_schema: bool) -> None: """Mount dashboard static files.""" - from fastapi import Request + from fastapi import Depends, Request from fastapi.responses import FileResponse dashboard_dir = Path(__file__).parent / "dashboard" / "dist" @@ -231,9 +233,15 @@ def _setup_dashboard(self, include_in_schema: bool) -> None: print(" npm run build") print("=" * 60 + "\n") + # Prepare dependencies list for the route + dependencies = [] + if self.auth_dependency: + dependencies.append(Depends(self.auth_dependency)) + @self.app.get( f"{self.dashboard_path}/{{full_path:path}}", include_in_schema=include_in_schema, + dependencies=dependencies, ) async def serve_dashboard(request: Request, full_path: str = ""): if full_path and any( From 2072de433a4d3621af3f54d1fd68556923759d86 Mon Sep 17 00:00:00 2001 From: Arif Dogan Date: Tue, 11 Nov 2025 03:24:38 +0100 Subject: [PATCH 2/4] black formatting --- fastapi_radar/api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fastapi_radar/api.py b/fastapi_radar/api.py index 7665833..8313756 100644 --- a/fastapi_radar/api.py +++ b/fastapi_radar/api.py @@ -142,7 +142,9 @@ class TraceDetail(BaseModel): spans: List[WaterfallSpan] -def create_api_router(get_session_context, auth_dependency: Optional[Callable] = None) -> APIRouter: +def create_api_router( + get_session_context, auth_dependency: Optional[Callable] = None +) -> APIRouter: # Build dependencies list for the router dependencies = [] if auth_dependency: From e08af6fa466d9d726815905592f1d764d7a91c80 Mon Sep 17 00:00:00 2001 From: Arif Dogan Date: Tue, 11 Nov 2025 03:26:41 +0100 Subject: [PATCH 3/4] docs: add lean authentication examples Add three minimal examples demonstrating different auth approaches: - HTTP Basic authentication (example_auth_app.py) - Bearer token authentication (example_bearer_auth.py) - Custom API key authentication (example_custom_auth.py) Each example is self-contained and demonstrates how to use the auth_dependency parameter with different authentication methods. --- example_auth_app.py | 74 ++++++++++++++++++++++++++++++++++++++++++ example_bearer_auth.py | 48 +++++++++++++++++++++++++++ example_custom_auth.py | 45 +++++++++++++++++++++++++ 3 files changed, 167 insertions(+) create mode 100644 example_auth_app.py create mode 100644 example_bearer_auth.py create mode 100644 example_custom_auth.py diff --git a/example_auth_app.py b/example_auth_app.py new file mode 100644 index 0000000..7ab961f --- /dev/null +++ b/example_auth_app.py @@ -0,0 +1,74 @@ +"""Minimal example showing how to secure FastAPI Radar with authentication.""" + +import secrets +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import HTTPBasic, HTTPBasicCredentials +from sqlalchemy import create_engine + +from fastapi_radar import Radar + +# Create FastAPI app +app = FastAPI(title="FastAPI Radar with Authentication") + +# Setup HTTP Basic Authentication +security = HTTPBasic() + + +def verify_radar_access(credentials: HTTPBasicCredentials = Depends(security)): + """Verify credentials for Radar dashboard access.""" + correct_username = secrets.compare_digest(credentials.username, "admin") + correct_password = secrets.compare_digest(credentials.password, "secret") + + if not (correct_username and correct_password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid credentials", + headers={"WWW-Authenticate": "Basic"}, + ) + return credentials + + +# Setup database (optional) +engine = create_engine("sqlite:///./app.db") + +# Initialize Radar with authentication +radar = Radar( + app, + db_engine=engine, + auth_dependency=verify_radar_access, # Secure the dashboard +) +radar.create_tables() + + +# Your regular API endpoints (not protected by Radar auth) +@app.get("/") +async def root(): + return { + "message": "Public API endpoint", + "dashboard": "Visit /__radar (requires auth: admin/secret)", + } + + +@app.get("/public") +async def public_endpoint(): + return {"message": "This endpoint is public"} + + +if __name__ == "__main__": + import uvicorn + + print("\n" + "=" * 60) + print("🔒 FastAPI Radar with Authentication") + print("=" * 60) + print("\nCredentials:") + print(" Username: admin") + print(" Password: secret") + print("\nEndpoints:") + print(" API (public): http://localhost:8000") + print(" Dashboard: http://localhost:8000/__radar (protected)") + print("\nTry accessing the dashboard:") + print(" Browser: http://localhost:8000/__radar") + print(" CLI: curl -u admin:secret http://localhost:8000/__radar/api/stats") + print("=" * 60 + "\n") + + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/example_bearer_auth.py b/example_bearer_auth.py new file mode 100644 index 0000000..6e8ee82 --- /dev/null +++ b/example_bearer_auth.py @@ -0,0 +1,48 @@ +"""Minimal example showing Bearer token authentication for FastAPI Radar.""" + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials + +from fastapi_radar import Radar + +app = FastAPI(title="FastAPI Radar with Bearer Token") +security = HTTPBearer() + + +def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)): + """Verify bearer token for Radar access.""" + if credentials.credentials != "my-secret-token-123": + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid token", + ) + return credentials + + +# Initialize Radar with token authentication +radar = Radar(app, auth_dependency=verify_token) +radar.create_tables() + + +@app.get("/") +async def root(): + return { + "message": "Use Bearer token to access dashboard", + "token": "my-secret-token-123", + "dashboard": "/__radar", + } + + +if __name__ == "__main__": + import uvicorn + + print("\n" + "=" * 60) + print("🔑 FastAPI Radar with Bearer Token") + print("=" * 60) + print("\nToken: my-secret-token-123") + print("\nAccess dashboard:") + print(' curl -H "Authorization: Bearer my-secret-token-123" \\') + print(" http://localhost:8000/__radar/api/stats") + print("=" * 60 + "\n") + + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/example_custom_auth.py b/example_custom_auth.py new file mode 100644 index 0000000..c0d33f0 --- /dev/null +++ b/example_custom_auth.py @@ -0,0 +1,45 @@ +"""Minimal example showing custom authentication for FastAPI Radar.""" + +from fastapi import FastAPI, HTTPException, Request + +from fastapi_radar import Radar + +app = FastAPI(title="FastAPI Radar with Custom Auth") + + +async def verify_api_key(request: Request): + """Custom authentication using API key from header.""" + api_key = request.headers.get("X-API-Key") + + if api_key != "radar-secret-key": + raise HTTPException(status_code=401, detail="Invalid or missing API key") + + return True + + +# Initialize Radar with custom authentication +radar = Radar(app, auth_dependency=verify_api_key) +radar.create_tables() + + +@app.get("/") +async def root(): + return { + "message": "Use X-API-Key header to access dashboard", + "header": "X-API-Key: radar-secret-key", + } + + +if __name__ == "__main__": + import uvicorn + + print("\n" + "=" * 60) + print("🔐 FastAPI Radar with Custom API Key") + print("=" * 60) + print("\nAPI Key: radar-secret-key") + print("\nAccess dashboard:") + print(' curl -H "X-API-Key: radar-secret-key" \\') + print(" http://localhost:8000/__radar/api/stats") + print("=" * 60 + "\n") + + uvicorn.run(app, host="0.0.0.0", port=8000) From c7297a1462e11d5526b2ea32f6b8d983f27678d5 Mon Sep 17 00:00:00 2001 From: Arif Dogan Date: Tue, 11 Nov 2025 03:28:38 +0100 Subject: [PATCH 4/4] chore: bump version to 0.3.4 --- fastapi_radar/__init__.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastapi_radar/__init__.py b/fastapi_radar/__init__.py index 27cadb1..a547a83 100644 --- a/fastapi_radar/__init__.py +++ b/fastapi_radar/__init__.py @@ -3,5 +3,5 @@ from .radar import Radar from .background import track_background_task -__version__ = "0.3.3" +__version__ = "0.3.4" __all__ = ["Radar", "track_background_task"] diff --git a/pyproject.toml b/pyproject.toml index 814304c..8611e9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "fastapi-radar" -version = "0.3.3" +version = "0.3.4" description = "A debugging dashboard for FastAPI applications with real-time monitoring" readme = "README.md" requires-python = ">=3.9"