"""HTTP routes for Agent enrollment and bootstrap.""" from typing import Annotated from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response, status from app.agent_enrollment.dependencies import get_agent_enrollment_service from app.agent_enrollment.exceptions import ( AgentEnrollmentConflict, AgentEnrollmentCredentialCollision, AgentEnrollmentNotFound, AgentEnrollmentTokenInvalid, ) from app.agent_enrollment.models import ( AgentCredentialTransitionCreateRequest, AgentCredentialTransitionCreateResponse, AgentCredentialTransitionRedeemRequest, AgentCredentialTransitionResponse, AgentEnrollmentCreateRequest, AgentEnrollmentCreateResponse, AgentEnrollmentListResponse, AgentEnrollRequest, AgentEnrollResponse, AgentWindowsPackageRequest, ) from app.agent_enrollment.service import AgentEnrollmentService from app.agent_packages.windows import ( AgentWindowsPackageError, AgentWindowsPackageSourceError, build_windows_agent_package, normalize_hub_url, ) from app.identity import require_permission router = APIRouter(tags=["agent-enrollment"]) EnrollmentService = Annotated[ AgentEnrollmentService, Depends(get_agent_enrollment_service), ] EnrollmentViewer = Annotated[ dict, Depends(require_permission("agents.view")), ] EnrollmentManager = Annotated[ dict, Depends(require_permission("agents.manage")), ] @router.post( "/api/agent-enrollments", response_model=AgentEnrollmentCreateResponse, status_code=status.HTTP_201_CREATED, ) def create_agent_enrollment( request: AgentEnrollmentCreateRequest, response: Response, current_user: EnrollmentManager, service: EnrollmentService, ) -> AgentEnrollmentCreateResponse: response.headers["Cache-Control"] = "no-store" try: enrollment = service.create_enrollment( request, actor_user_id=UUID(str(current_user["id"])), ) return AgentEnrollmentCreateResponse(data=enrollment) except AgentEnrollmentNotFound as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc except AgentEnrollmentConflict as exc: raise HTTPException(status_code=409, detail=str(exc)) from exc @router.post( "/api/agent-credential-transitions", response_model=AgentCredentialTransitionCreateResponse, status_code=status.HTTP_201_CREATED, ) def create_agent_credential_transition( request: AgentCredentialTransitionCreateRequest, response: Response, current_user: EnrollmentManager, service: EnrollmentService, ) -> AgentCredentialTransitionCreateResponse: response.headers["Cache-Control"] = "no-store" try: transition = service.create_credential_transition( request, actor_user_id=UUID(str(current_user["id"])), ) return AgentCredentialTransitionCreateResponse(data=transition) except AgentEnrollmentNotFound as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc except AgentEnrollmentConflict as exc: raise HTTPException(status_code=409, detail=str(exc)) from exc @router.get( "/api/agent-enrollments", response_model=AgentEnrollmentListResponse, ) def list_agent_enrollments( current_user: EnrollmentViewer, service: EnrollmentService, site_id: UUID | None = Query(default=None), agent_zone_id: UUID | None = Query(default=None), limit: int = Query(default=100, ge=1, le=500), ) -> AgentEnrollmentListResponse: del current_user return AgentEnrollmentListResponse( data=service.list_enrollments( site_id=site_id, agent_zone_id=agent_zone_id, limit=limit, ) ) @router.post("/api/agent-enrollments/windows-package") def download_windows_agent_package( package_request: AgentWindowsPackageRequest, http_request: Request, current_user: EnrollmentManager, ) -> Response: del current_user browser_origin = http_request.headers.get("origin", "").strip() try: hub_url = normalize_hub_url(browser_origin) except AgentWindowsPackageError as exc: raise HTTPException( status_code=400, detail=( "Windows Agent package downloads require " "a valid browser Origin" ), ) from exc try: package = build_windows_agent_package( enrollment_token=package_request.token, hub_url=hub_url, workflow="new_agent", ) except AgentWindowsPackageSourceError as exc: raise HTTPException(status_code=503, detail=str(exc)) from exc except AgentWindowsPackageError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc return Response( content=package, media_type="application/zip", headers={ "Cache-Control": "no-store", "Pragma": "no-cache", "Content-Disposition": ( 'attachment; filename="MinerOpsAgent-Windows.zip"' ), }, ) @router.post("/api/agent-credential-transitions/windows-package") def download_windows_agent_credential_transition_package( package_request: AgentWindowsPackageRequest, http_request: Request, current_user: EnrollmentManager, ) -> Response: del current_user browser_origin = http_request.headers.get("origin", "").strip() try: hub_url = normalize_hub_url(browser_origin) except AgentWindowsPackageError as exc: raise HTTPException( status_code=400, detail=( "Windows Agent package downloads require " "a valid browser Origin" ), ) from exc try: package = build_windows_agent_package( enrollment_token=package_request.token, hub_url=hub_url, workflow="existing_agent_credential_transition", ) except AgentWindowsPackageSourceError as exc: raise HTTPException(status_code=503, detail=str(exc)) from exc except AgentWindowsPackageError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc return Response( content=package, media_type="application/zip", headers={ "Cache-Control": "no-store", "Pragma": "no-cache", "Content-Disposition": ( 'attachment; filename="MinerOpsAgent-Credential-Upgrade-Windows.zip"' ), }, ) @router.post( "/api/agent/enroll", response_model=AgentEnrollResponse, status_code=status.HTTP_201_CREATED, ) def enroll_agent( request: AgentEnrollRequest, response: Response, service: EnrollmentService, ) -> AgentEnrollResponse: response.headers["Cache-Control"] = "no-store" try: return AgentEnrollResponse(data=service.redeem(request)) except AgentEnrollmentTokenInvalid as exc: raise HTTPException( status_code=401, detail="Invalid or expired enrollment token", ) from exc except AgentEnrollmentConflict as exc: raise HTTPException(status_code=409, detail=str(exc)) from exc @router.post( "/api/agent/credential-transition", response_model=AgentCredentialTransitionResponse, ) def transition_agent_credential( request: AgentCredentialTransitionRedeemRequest, response: Response, service: EnrollmentService, ) -> AgentCredentialTransitionResponse: response.headers["Cache-Control"] = "no-store" try: return AgentCredentialTransitionResponse( data=service.redeem_credential_transition(request) ) except AgentEnrollmentTokenInvalid as exc: raise HTTPException( status_code=401, detail="Invalid or expired credential transition token", ) from exc except (AgentEnrollmentConflict, AgentEnrollmentCredentialCollision) as exc: raise HTTPException(status_code=409, detail=str(exc)) from exc