diff --git a/decnet/web/router/__init__.py b/decnet/web/router/__init__.py index 3061797..c732b34 100644 --- a/decnet/web/router/__init__.py +++ b/decnet/web/router/__init__.py @@ -25,7 +25,15 @@ from .swarm_updates import swarm_updates_router from .swarm_mgmt import swarm_mgmt_router from .system import system_router -api_router = APIRouter() +api_router = APIRouter( + # Every route under /api/v1 is auth-guarded (either by an explicit + # require_* Depends or by the global auth middleware). Document 401/403 + # here so the OpenAPI schema reflects reality for contract tests. + responses={ + 401: {"description": "Missing or invalid credentials"}, + 403: {"description": "Authenticated but not authorized"}, + }, +) # Authentication api_router.include_router(login_router) diff --git a/decnet/web/router/swarm/__init__.py b/decnet/web/router/swarm/__init__.py index a19a52f..7d3b4c2 100644 --- a/decnet/web/router/swarm/__init__.py +++ b/decnet/web/router/swarm/__init__.py @@ -18,7 +18,17 @@ from .api_check_hosts import router as check_hosts_router from .api_heartbeat import router as heartbeat_router from .api_list_deckies import router as list_deckies_router -swarm_router = APIRouter(prefix="/swarm") +swarm_router = APIRouter( + prefix="/swarm", + # Error responses that every swarm route can surface. Route-level + # `responses=` entries still override/extend these for route-specific + # codes (e.g. 409 on /enroll). + responses={ + 400: {"description": "Malformed request"}, + 403: {"description": "Peer cert missing or fingerprint mismatch"}, + 404: {"description": "Referenced host does not exist"}, + }, +) # Hosts swarm_router.include_router(enroll_host_router) diff --git a/decnet/web/router/swarm/api_decommission_host.py b/decnet/web/router/swarm/api_decommission_host.py index 966784f..7e6c669 100644 --- a/decnet/web/router/swarm/api_decommission_host.py +++ b/decnet/web/router/swarm/api_decommission_host.py @@ -26,6 +26,7 @@ router = APIRouter() "/hosts/{uuid}", status_code=status.HTTP_204_NO_CONTENT, tags=["Swarm Hosts"], + responses={404: {"description": "No host with this UUID is enrolled"}}, ) async def api_decommission_host( uuid: str, diff --git a/decnet/web/router/swarm/api_deploy_swarm.py b/decnet/web/router/swarm/api_deploy_swarm.py index ba77c01..1142df8 100644 --- a/decnet/web/router/swarm/api_deploy_swarm.py +++ b/decnet/web/router/swarm/api_deploy_swarm.py @@ -135,7 +135,15 @@ async def dispatch_decnet_config( return SwarmDeployResponse(results=list(results)) -@router.post("/deploy", response_model=SwarmDeployResponse, tags=["Swarm Deployments"]) +@router.post( + "/deploy", + response_model=SwarmDeployResponse, + tags=["Swarm Deployments"], + responses={ + 400: {"description": "Deployment mode must be 'swarm'"}, + 404: {"description": "A referenced host_uuid is not enrolled"}, + }, +) async def api_deploy_swarm( req: SwarmDeployRequest, repo: BaseRepository = Depends(get_repo), diff --git a/decnet/web/router/swarm/api_enroll_host.py b/decnet/web/router/swarm/api_enroll_host.py index 1e85c8e..b6edb89 100644 --- a/decnet/web/router/swarm/api_enroll_host.py +++ b/decnet/web/router/swarm/api_enroll_host.py @@ -29,6 +29,7 @@ router = APIRouter() response_model=SwarmEnrolledBundle, status_code=status.HTTP_201_CREATED, tags=["Swarm Hosts"], + responses={409: {"description": "A worker with this name is already enrolled"}}, ) async def api_enroll_host( req: SwarmEnrollRequest, diff --git a/decnet/web/router/swarm/api_get_host.py b/decnet/web/router/swarm/api_get_host.py index 292d357..556b6ee 100644 --- a/decnet/web/router/swarm/api_get_host.py +++ b/decnet/web/router/swarm/api_get_host.py @@ -10,7 +10,12 @@ from decnet.web.db.models import SwarmHostView router = APIRouter() -@router.get("/hosts/{uuid}", response_model=SwarmHostView, tags=["Swarm Hosts"]) +@router.get( + "/hosts/{uuid}", + response_model=SwarmHostView, + tags=["Swarm Hosts"], + responses={404: {"description": "No host with this UUID is enrolled"}}, +) async def api_get_host( uuid: str, repo: BaseRepository = Depends(get_repo), diff --git a/decnet/web/router/swarm/api_heartbeat.py b/decnet/web/router/swarm/api_heartbeat.py index f49e580..d1cdac1 100644 --- a/decnet/web/router/swarm/api_heartbeat.py +++ b/decnet/web/router/swarm/api_heartbeat.py @@ -96,7 +96,15 @@ async def _verify_peer_matches_host( return host -@router.post("/heartbeat", status_code=204, tags=["Swarm Health"]) +@router.post( + "/heartbeat", + status_code=204, + tags=["Swarm Health"], + responses={ + 403: {"description": "Peer cert missing, or its fingerprint does not match the host's pinned cert"}, + 404: {"description": "host_uuid is not enrolled"}, + }, +) async def heartbeat( req: HeartbeatRequest, request: Request, diff --git a/decnet/web/router/swarm/api_teardown_swarm.py b/decnet/web/router/swarm/api_teardown_swarm.py index f775c50..0cb68b1 100644 --- a/decnet/web/router/swarm/api_teardown_swarm.py +++ b/decnet/web/router/swarm/api_teardown_swarm.py @@ -21,7 +21,12 @@ log = get_logger("swarm.teardown") router = APIRouter() -@router.post("/teardown", response_model=SwarmDeployResponse, tags=["Swarm Deployments"]) +@router.post( + "/teardown", + response_model=SwarmDeployResponse, + tags=["Swarm Deployments"], + responses={404: {"description": "A targeted host does not exist"}}, +) async def api_teardown_swarm( req: SwarmTeardownRequest, repo: BaseRepository = Depends(get_repo),