diff --git a/dev-requirements.txt b/dev-requirements.txt index 5e29ebc7..ac6470e5 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -4,3 +4,4 @@ pytest-timeout==2.1.0 pytest-asyncio==0.21.1 requests==2.31.0 httpx==0.24.1 +httpx_ws==0.4.2 diff --git a/gns3server/api/routes/compute/__init__.py b/gns3server/api/routes/compute/__init__.py index 4628dfd8..1922e31a 100644 --- a/gns3server/api/routes/compute/__init__.py +++ b/gns3server/api/routes/compute/__init__.py @@ -199,14 +199,12 @@ compute_api.include_router( compute_api.include_router( docker_nodes.router, - dependencies=[Depends(compute_authentication)], prefix="/projects/{project_id}/docker/nodes", tags=["Docker nodes"] ) compute_api.include_router( dynamips_nodes.router, - dependencies=[Depends(compute_authentication)], prefix="/projects/{project_id}/dynamips/nodes", tags=["Dynamips nodes"] ) @@ -234,7 +232,6 @@ compute_api.include_router( compute_api.include_router( iou_nodes.router, - dependencies=[Depends(compute_authentication)], prefix="/projects/{project_id}/iou/nodes", tags=["IOU nodes"]) @@ -247,28 +244,24 @@ compute_api.include_router( compute_api.include_router( qemu_nodes.router, - dependencies=[Depends(compute_authentication)], prefix="/projects/{project_id}/qemu/nodes", tags=["Qemu nodes"] ) compute_api.include_router( virtualbox_nodes.router, - dependencies=[Depends(compute_authentication)], prefix="/projects/{project_id}/virtualbox/nodes", tags=["VirtualBox nodes"] ) compute_api.include_router( vmware_nodes.router, - dependencies=[Depends(compute_authentication)], prefix="/projects/{project_id}/vmware/nodes", tags=["VMware nodes"] ) compute_api.include_router( vpcs_nodes.router, - dependencies=[Depends(compute_authentication)], prefix="/projects/{project_id}/vpcs/nodes", tags=["VPCS nodes"] ) diff --git a/gns3server/api/routes/compute/dependencies/authentication.py b/gns3server/api/routes/compute/dependencies/authentication.py index 5efb9927..377a89dc 100644 --- a/gns3server/api/routes/compute/dependencies/authentication.py +++ b/gns3server/api/routes/compute/dependencies/authentication.py @@ -15,12 +15,17 @@ # along with this program. If not, see . import secrets +import base64 +import binascii +import logging -from fastapi import Depends, HTTPException, status +from fastapi import Depends, HTTPException, WebSocket, status from fastapi.security import HTTPBasic, HTTPBasicCredentials +from fastapi.security.utils import get_authorization_scheme_param from gns3server.config import Config -from typing import Optional +from typing import Optional, Union +log = logging.getLogger(__name__) security = HTTPBasic() @@ -35,3 +40,44 @@ def compute_authentication(credentials: Optional[HTTPBasicCredentials] = Depends detail="Invalid compute username or password", headers={"WWW-Authenticate": "Basic"}, ) + +async def ws_compute_authentication(websocket: WebSocket) -> Union[None, WebSocket]: + """ + """ + + await websocket.accept() + + # handle basic HTTP authentication + invalid_user_credentials_exc = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Basic"}, + ) + + try: + authorization = websocket.headers.get("Authorization") + scheme, param = get_authorization_scheme_param(authorization) + if not authorization or scheme.lower() != "basic": + raise invalid_user_credentials_exc + try: + data = base64.b64decode(param).decode("ascii") + except (ValueError, UnicodeDecodeError, binascii.Error): + raise invalid_user_credentials_exc + + username, separator, password = data.partition(":") + if not separator: + raise invalid_user_credentials_exc + + server_settings = Config.instance().settings.Server + username = secrets.compare_digest(username, server_settings.compute_username) + password = secrets.compare_digest(password, server_settings.compute_password.get_secret_value()) + if not (username and password): + raise invalid_user_credentials_exc + + except HTTPException as e: + err_msg = f"Could not authenticate while connecting to compute WebSocket: {e.detail}" + websocket_error = {"action": "log.error", "event": {"message": err_msg}} + await websocket.send_json(websocket_error) + log.error(err_msg) + return await websocket.close(code=1008) + return websocket diff --git a/gns3server/api/routes/compute/docker_nodes.py b/gns3server/api/routes/compute/docker_nodes.py index e9a1e29f..11a2cf65 100644 --- a/gns3server/api/routes/compute/docker_nodes.py +++ b/gns3server/api/routes/compute/docker_nodes.py @@ -20,15 +20,18 @@ API routes for Docker nodes. import os -from fastapi import APIRouter, WebSocket, Depends, Body, Response, status +from fastapi import APIRouter, WebSocket, Depends, Body, status from fastapi.encoders import jsonable_encoder from fastapi.responses import StreamingResponse from uuid import UUID +from typing import Union from gns3server import schemas from gns3server.compute.docker import Docker from gns3server.compute.docker.docker_vm import DockerVM +from .dependencies.authentication import compute_authentication, ws_compute_authentication + responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Docker node"}} router = APIRouter(responses=responses) @@ -49,6 +52,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> DockerVM: response_model=schemas.Docker, status_code=status.HTTP_201_CREATED, responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Docker node"}}, + dependencies=[Depends(compute_authentication)] ) async def create_docker_node(project_id: UUID, node_data: schemas.DockerCreate) -> schemas.Docker: """ @@ -85,7 +89,11 @@ async def create_docker_node(project_id: UUID, node_data: schemas.DockerCreate) return container.asdict() -@router.get("/{node_id}", response_model=schemas.Docker) +@router.get( + "/{node_id}", + response_model=schemas.Docker, + dependencies=[Depends(compute_authentication)] +) def get_docker_node(node: DockerVM = Depends(dep_node)) -> schemas.Docker: """ Return a Docker node. @@ -94,7 +102,11 @@ def get_docker_node(node: DockerVM = Depends(dep_node)) -> schemas.Docker: return node.asdict() -@router.put("/{node_id}", response_model=schemas.Docker) +@router.put( + "/{node_id}", + response_model=schemas.Docker, + dependencies=[Depends(compute_authentication)] +) async def update_docker_node(node_data: schemas.DockerUpdate, node: DockerVM = Depends(dep_node)) -> schemas.Docker: """ Update a Docker node. @@ -131,7 +143,11 @@ async def update_docker_node(node_data: schemas.DockerUpdate, node: DockerVM = D return node.asdict() -@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/start", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def start_docker_node(node: DockerVM = Depends(dep_node)) -> None: """ Start a Docker node. @@ -140,7 +156,11 @@ async def start_docker_node(node: DockerVM = Depends(dep_node)) -> None: await node.start() -@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/stop", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def stop_docker_node(node: DockerVM = Depends(dep_node)) -> None: """ Stop a Docker node. @@ -149,7 +169,11 @@ async def stop_docker_node(node: DockerVM = Depends(dep_node)) -> None: await node.stop() -@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/suspend", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def suspend_docker_node(node: DockerVM = Depends(dep_node)) -> None: """ Suspend a Docker node. @@ -158,7 +182,11 @@ async def suspend_docker_node(node: DockerVM = Depends(dep_node)) -> None: await node.pause() -@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/reload", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def reload_docker_node(node: DockerVM = Depends(dep_node)) -> None: """ Reload a Docker node. @@ -167,7 +195,11 @@ async def reload_docker_node(node: DockerVM = Depends(dep_node)) -> None: await node.restart() -@router.post("/{node_id}/pause", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/pause", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def pause_docker_node(node: DockerVM = Depends(dep_node)) -> None: """ Pause a Docker node. @@ -176,7 +208,11 @@ async def pause_docker_node(node: DockerVM = Depends(dep_node)) -> None: await node.pause() -@router.post("/{node_id}/unpause", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/unpause", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def unpause_docker_node(node: DockerVM = Depends(dep_node)) -> None: """ Unpause a Docker node. @@ -185,7 +221,11 @@ async def unpause_docker_node(node: DockerVM = Depends(dep_node)) -> None: await node.unpause() -@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT) +@router.delete( + "/{node_id}", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def delete_docker_node(node: DockerVM = Depends(dep_node)) -> None: """ Delete a Docker node. @@ -194,7 +234,12 @@ async def delete_docker_node(node: DockerVM = Depends(dep_node)) -> None: await node.delete() -@router.post("/{node_id}/duplicate", response_model=schemas.Docker, status_code=status.HTTP_201_CREATED) +@router.post( + "/{node_id}/duplicate", + response_model=schemas.Docker, + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(compute_authentication)] +) async def duplicate_docker_node( destination_node_id: UUID = Body(..., embed=True), node: DockerVM = Depends(dep_node) @@ -211,6 +256,7 @@ async def duplicate_docker_node( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_201_CREATED, response_model=schemas.UDPNIO, + dependencies=[Depends(compute_authentication)] ) async def create_docker_node_nio( adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: DockerVM = Depends(dep_node) @@ -229,6 +275,7 @@ async def create_docker_node_nio( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_201_CREATED, response_model=schemas.UDPNIO, + dependencies=[Depends(compute_authentication)] ) async def update_docker_node_nio( adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: DockerVM = Depends(dep_node) @@ -245,7 +292,11 @@ async def update_docker_node_nio( return nio.asdict() -@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT) +@router.delete( + "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def delete_docker_node_nio( adapter_number: int, port_number: int, @@ -259,7 +310,10 @@ async def delete_docker_node_nio( await node.adapter_remove_nio_binding(adapter_number) -@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start") +@router.post( + "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start", + dependencies=[Depends(compute_authentication)] +) async def start_docker_node_capture( adapter_number: int, port_number: int, @@ -278,7 +332,8 @@ async def start_docker_node_capture( @router.post( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", - status_code=status.HTTP_204_NO_CONTENT + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] ) async def stop_docker_node_capture( adapter_number: int, @@ -293,7 +348,10 @@ async def stop_docker_node_capture( await node.stop_capture(adapter_number) -@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream") +@router.get( + "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream", + dependencies=[Depends(compute_authentication)] +) async def stream_pcap_file( adapter_number: int, port_number: int, @@ -310,15 +368,23 @@ async def stream_pcap_file( @router.websocket("/{node_id}/console/ws") -async def console_ws(websocket: WebSocket, node: DockerVM = Depends(dep_node)) -> None: +async def console_ws( + websocket: Union[None, WebSocket] = Depends(ws_compute_authentication), + node: DockerVM = Depends(dep_node) +) -> None: """ Console WebSocket. """ - await node.start_websocket_console(websocket) + if websocket: + await node.start_websocket_console(websocket) -@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/console/reset", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def reset_console(node: DockerVM = Depends(dep_node)) -> None: await node.reset_console() diff --git a/gns3server/api/routes/compute/dynamips_nodes.py b/gns3server/api/routes/compute/dynamips_nodes.py index 5f34f066..89be7f69 100644 --- a/gns3server/api/routes/compute/dynamips_nodes.py +++ b/gns3server/api/routes/compute/dynamips_nodes.py @@ -20,16 +20,18 @@ API routes for Dynamips nodes. import os -from fastapi import APIRouter, WebSocket, Depends, Response, status +from fastapi import APIRouter, WebSocket, Depends, status from fastapi.encoders import jsonable_encoder from fastapi.responses import StreamingResponse -from typing import List +from typing import List, Union from uuid import UUID from gns3server.compute.dynamips import Dynamips from gns3server.compute.dynamips.nodes.router import Router from gns3server import schemas +from .dependencies.authentication import compute_authentication, ws_compute_authentication + responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Dynamips node"}} router = APIRouter(responses=responses) @@ -53,6 +55,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> Router: response_model=schemas.Dynamips, status_code=status.HTTP_201_CREATED, responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Dynamips node"}}, + dependencies=[Depends(compute_authentication)] ) async def create_router(project_id: UUID, node_data: schemas.DynamipsCreate) -> schemas.Dynamips: """ @@ -84,7 +87,11 @@ async def create_router(project_id: UUID, node_data: schemas.DynamipsCreate) -> return vm.asdict() -@router.get("/{node_id}", response_model=schemas.Dynamips) +@router.get( + "/{node_id}", + response_model=schemas.Dynamips, + dependencies=[Depends(compute_authentication)] +) def get_router(node: Router = Depends(dep_node)) -> schemas.Dynamips: """ Return Dynamips router. @@ -93,7 +100,11 @@ def get_router(node: Router = Depends(dep_node)) -> schemas.Dynamips: return node.asdict() -@router.put("/{node_id}", response_model=schemas.Dynamips) +@router.put( + "/{node_id}", + response_model=schemas.Dynamips, + dependencies=[Depends(compute_authentication)] +) async def update_router(node_data: schemas.DynamipsUpdate, node: Router = Depends(dep_node)) -> schemas.Dynamips: """ Update a Dynamips router. @@ -104,7 +115,11 @@ async def update_router(node_data: schemas.DynamipsUpdate, node: Router = Depend return node.asdict() -@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT) +@router.delete( + "/{node_id}", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def delete_router(node: Router = Depends(dep_node)) -> None: """ Delete a Dynamips router. @@ -113,7 +128,11 @@ async def delete_router(node: Router = Depends(dep_node)) -> None: await Dynamips.instance().delete_node(node.id) -@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/start", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def start_router(node: Router = Depends(dep_node)) -> None: """ Start a Dynamips router. @@ -126,7 +145,11 @@ async def start_router(node: Router = Depends(dep_node)) -> None: await node.start() -@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/stop", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def stop_router(node: Router = Depends(dep_node)) -> None: """ Stop a Dynamips router. @@ -135,13 +158,21 @@ async def stop_router(node: Router = Depends(dep_node)) -> None: await node.stop() -@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/suspend", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def suspend_router(node: Router = Depends(dep_node)) -> None: await node.suspend() -@router.post("/{node_id}/resume", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/resume", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def resume_router(node: Router = Depends(dep_node)) -> None: """ Resume a suspended Dynamips router. @@ -150,7 +181,11 @@ async def resume_router(node: Router = Depends(dep_node)) -> None: await node.resume() -@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/reload", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def reload_router(node: Router = Depends(dep_node)) -> None: """ Reload a suspended Dynamips router. @@ -163,6 +198,7 @@ async def reload_router(node: Router = Depends(dep_node)) -> None: "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_201_CREATED, response_model=schemas.UDPNIO, + dependencies=[Depends(compute_authentication)] ) async def create_nio( adapter_number: int, @@ -183,6 +219,7 @@ async def create_nio( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_201_CREATED, response_model=schemas.UDPNIO, + dependencies=[Depends(compute_authentication)] ) async def update_nio( adapter_number: int, @@ -201,7 +238,11 @@ async def update_nio( return nio.asdict() -@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT) +@router.delete( + "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def delete_nio(adapter_number: int, port_number: int, node: Router = Depends(dep_node)) -> None: """ Delete a NIO (Network Input/Output) from the node. @@ -211,7 +252,10 @@ async def delete_nio(adapter_number: int, port_number: int, node: Router = Depen await nio.delete() -@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start") +@router.post( + "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start", + dependencies=[Depends(compute_authentication)] +) async def start_capture( adapter_number: int, port_number: int, @@ -228,7 +272,9 @@ async def start_capture( @router.post( - "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT + "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] ) async def stop_capture(adapter_number: int, port_number: int, node: Router = Depends(dep_node)) -> None: """ @@ -238,7 +284,10 @@ async def stop_capture(adapter_number: int, port_number: int, node: Router = Dep await node.stop_capture(adapter_number, port_number) -@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream") +@router.get( + "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream", + dependencies=[Depends(compute_authentication)] +) async def stream_pcap_file( adapter_number: int, port_number: int, @@ -253,7 +302,10 @@ async def stream_pcap_file( return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap") -@router.get("/{node_id}/idlepc_proposals") +@router.get( + "/{node_id}/idlepc_proposals", + dependencies=[Depends(compute_authentication)] +) async def get_idlepcs(node: Router = Depends(dep_node)) -> List[str]: """ Retrieve Dynamips idle-pc proposals @@ -263,7 +315,10 @@ async def get_idlepcs(node: Router = Depends(dep_node)) -> List[str]: return await node.get_idle_pc_prop() -@router.get("/{node_id}/auto_idlepc") +@router.get( + "/{node_id}/auto_idlepc", + dependencies=[Depends(compute_authentication)] +) async def get_auto_idlepc(node: Router = Depends(dep_node)) -> dict: """ Get an automatically guessed best idle-pc value. @@ -273,7 +328,12 @@ async def get_auto_idlepc(node: Router = Depends(dep_node)) -> dict: return {"idlepc": idlepc} -@router.post("/{node_id}/duplicate", response_model=schemas.Dynamips, status_code=status.HTTP_201_CREATED) +@router.post( + "/{node_id}/duplicate", + response_model=schemas.Dynamips, + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(compute_authentication)] +) async def duplicate_router(destination_node_id: UUID, node: Router = Depends(dep_node)) -> schemas.Dynamips: """ Duplicate a router. @@ -284,15 +344,24 @@ async def duplicate_router(destination_node_id: UUID, node: Router = Depends(dep @router.websocket("/{node_id}/console/ws") -async def console_ws(websocket: WebSocket, node: Router = Depends(dep_node)) -> None: +async def console_ws( + websocket: Union[None, WebSocket] = Depends(ws_compute_authentication), + node: Router = Depends(dep_node) + +) -> None: """ Console WebSocket. """ - await node.start_websocket_console(websocket) + if websocket: + await node.start_websocket_console(websocket) -@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/console/reset", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def reset_console(node: Router = Depends(dep_node)) -> None: await node.reset_console() diff --git a/gns3server/api/routes/compute/iou_nodes.py b/gns3server/api/routes/compute/iou_nodes.py index 5b74acc9..1b456a97 100644 --- a/gns3server/api/routes/compute/iou_nodes.py +++ b/gns3server/api/routes/compute/iou_nodes.py @@ -30,6 +30,8 @@ from gns3server import schemas from gns3server.compute.iou import IOU from gns3server.compute.iou.iou_vm import IOUVM +from .dependencies.authentication import compute_authentication, ws_compute_authentication + responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or IOU node"}} router = APIRouter(responses=responses) @@ -50,6 +52,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> IOUVM: response_model=schemas.IOU, status_code=status.HTTP_201_CREATED, responses={409: {"model": schemas.ErrorMessage, "description": "Could not create IOU node"}}, + dependencies=[Depends(compute_authentication)] ) async def create_iou_node(project_id: UUID, node_data: schemas.IOUCreate) -> schemas.IOU: """ @@ -82,7 +85,11 @@ async def create_iou_node(project_id: UUID, node_data: schemas.IOUCreate) -> sch return vm.asdict() -@router.get("/{node_id}", response_model=schemas.IOU) +@router.get( + "/{node_id}", + response_model=schemas.IOU, + dependencies=[Depends(compute_authentication)] +) def get_iou_node(node: IOUVM = Depends(dep_node)) -> schemas.IOU: """ Return an IOU node. @@ -91,7 +98,11 @@ def get_iou_node(node: IOUVM = Depends(dep_node)) -> schemas.IOU: return node.asdict() -@router.put("/{node_id}", response_model=schemas.IOU) +@router.put( + "/{node_id}", + response_model=schemas.IOU, + dependencies=[Depends(compute_authentication)] +) async def update_iou_node(node_data: schemas.IOUUpdate, node: IOUVM = Depends(dep_node)) -> schemas.IOU: """ Update an IOU node. @@ -112,7 +123,11 @@ async def update_iou_node(node_data: schemas.IOUUpdate, node: IOUVM = Depends(de return node.asdict() -@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT) +@router.delete( + "/{node_id}", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def delete_iou_node(node: IOUVM = Depends(dep_node)) -> None: """ Delete an IOU node. @@ -121,7 +136,12 @@ async def delete_iou_node(node: IOUVM = Depends(dep_node)) -> None: await IOU.instance().delete_node(node.id) -@router.post("/{node_id}/duplicate", response_model=schemas.IOU, status_code=status.HTTP_201_CREATED) +@router.post( + "/{node_id}/duplicate", + response_model=schemas.IOU, + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(compute_authentication)] +) async def duplicate_iou_node( destination_node_id: UUID = Body(..., embed=True), node: IOUVM = Depends(dep_node) @@ -134,7 +154,11 @@ async def duplicate_iou_node( return new_node.asdict() -@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/start", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def start_iou_node(start_data: schemas.IOUStart, node: IOUVM = Depends(dep_node)) -> None: """ Start an IOU node. @@ -148,7 +172,11 @@ async def start_iou_node(start_data: schemas.IOUStart, node: IOUVM = Depends(dep await node.start() -@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/stop", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def stop_iou_node(node: IOUVM = Depends(dep_node)) -> None: """ Stop an IOU node. @@ -157,7 +185,11 @@ async def stop_iou_node(node: IOUVM = Depends(dep_node)) -> None: await node.stop() -@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/stop", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) def suspend_iou_node(node: IOUVM = Depends(dep_node)) -> None: """ Suspend an IOU node. @@ -167,7 +199,11 @@ def suspend_iou_node(node: IOUVM = Depends(dep_node)) -> None: pass -@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/reload", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def reload_iou_node(node: IOUVM = Depends(dep_node)) -> None: """ Reload an IOU node. @@ -180,6 +216,7 @@ async def reload_iou_node(node: IOUVM = Depends(dep_node)) -> None: "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_201_CREATED, response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO], + dependencies=[Depends(compute_authentication)] ) async def create_iou_node_nio( adapter_number: int, @@ -200,6 +237,7 @@ async def create_iou_node_nio( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_201_CREATED, response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO], + dependencies=[Depends(compute_authentication)] ) async def update_iou_node_nio( adapter_number: int, @@ -218,7 +256,11 @@ async def update_iou_node_nio( return nio.asdict() -@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT) +@router.delete( + "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def delete_iou_node_nio(adapter_number: int, port_number: int, node: IOUVM = Depends(dep_node)) -> None: """ Delete a NIO (Network Input/Output) from the node. @@ -227,7 +269,10 @@ async def delete_iou_node_nio(adapter_number: int, port_number: int, node: IOUVM await node.adapter_remove_nio_binding(adapter_number, port_number) -@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start") +@router.post( + "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start", + dependencies=[Depends(compute_authentication)] +) async def start_iou_node_capture( adapter_number: int, port_number: int, @@ -244,7 +289,9 @@ async def start_iou_node_capture( @router.post( - "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT + "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] ) async def stop_iou_node_capture(adapter_number: int, port_number: int, node: IOUVM = Depends(dep_node)) -> None: """ @@ -254,7 +301,10 @@ async def stop_iou_node_capture(adapter_number: int, port_number: int, node: IOU await node.stop_capture(adapter_number, port_number) -@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream") +@router.get( + "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream", + dependencies=[Depends(compute_authentication)] +) async def stream_pcap_file( adapter_number: int, port_number: int, @@ -269,16 +319,26 @@ async def stream_pcap_file( return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap") -@router.websocket("/{node_id}/console/ws") -async def console_ws(websocket: WebSocket, node: IOUVM = Depends(dep_node)) -> None: +@router.websocket( + "/{node_id}/console/ws", +) +async def console_ws( + websocket: Union[None, WebSocket] = Depends(ws_compute_authentication), + node: IOUVM = Depends(dep_node) +) -> None: """ Console WebSocket. """ - await node.start_websocket_console(websocket) + if websocket: + await node.start_websocket_console(websocket) -@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/console/reset", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def reset_console(node: IOUVM = Depends(dep_node)) -> None: await node.reset_console() diff --git a/gns3server/api/routes/compute/notifications.py b/gns3server/api/routes/compute/notifications.py index 26a04b61..47b30f00 100644 --- a/gns3server/api/routes/compute/notifications.py +++ b/gns3server/api/routes/compute/notifications.py @@ -18,14 +18,13 @@ API routes for compute notifications. """ -import base64 -import binascii -from fastapi import APIRouter, WebSocket, WebSocketDisconnect, status, HTTPException -from fastapi.security.utils import get_authorization_scheme_param +from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect +from typing import Union from websockets.exceptions import ConnectionClosed, WebSocketException from gns3server.compute.notification_manager import NotificationManager +from .dependencies.authentication import ws_compute_authentication import logging @@ -35,53 +34,27 @@ router = APIRouter() @router.websocket("/notifications/ws") -async def project_ws_notifications(websocket: WebSocket) -> None: +async def project_ws_notifications(websocket: Union[None, WebSocket] = Depends(ws_compute_authentication)) -> None: """ Receive project notifications about the project from WebSocket. """ - await websocket.accept() - - # handle basic HTTP authentication - invalid_user_credentials_exc = HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid authentication credentials", - headers={"WWW-Authenticate": "Basic"}, - ) - - try: - authorization = websocket.headers.get("Authorization") - scheme, param = get_authorization_scheme_param(authorization) - if not authorization or scheme.lower() != "basic": - raise invalid_user_credentials_exc + if websocket: + log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to compute WebSocket") try: - data = base64.b64decode(param).decode("ascii") - except (ValueError, UnicodeDecodeError, binascii.Error): - raise invalid_user_credentials_exc - username, separator, password = data.partition(":") - if not separator: - raise invalid_user_credentials_exc - except invalid_user_credentials_exc as e: - websocket_error = {"action": "log.error", "event": {"message": f"Could not authenticate while connecting to " - f"compute WebSocket: {e.detail}"}} - await websocket.send_json(websocket_error) - return await websocket.close(code=1008) - - log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to compute WebSocket") - try: - with NotificationManager.instance().queue() as queue: - while True: - notification = await queue.get_json(5) - await websocket.send_text(notification) - except (ConnectionClosed, WebSocketDisconnect): - log.info(f"Client {websocket.client.host}:{websocket.client.port} has disconnected from compute WebSocket") - except WebSocketException as e: - log.warning(f"Error while sending to controller event to WebSocket client: {e}") - finally: - try: - await websocket.close() - except OSError: - pass # ignore OSError: [Errno 107] Transport endpoint is not connected + with NotificationManager.instance().queue() as queue: + while True: + notification = await queue.get_json(5) + await websocket.send_text(notification) + except (ConnectionClosed, WebSocketDisconnect): + log.info(f"Client {websocket.client.host}:{websocket.client.port} has disconnected from compute WebSocket") + except WebSocketException as e: + log.warning(f"Error while sending to controller event to WebSocket client: {e}") + finally: + try: + await websocket.close() + except OSError: + pass # ignore OSError: [Errno 107] Transport endpoint is not connected if __name__ == "__main__": diff --git a/gns3server/api/routes/compute/qemu_nodes.py b/gns3server/api/routes/compute/qemu_nodes.py index b1b95417..1689fabe 100644 --- a/gns3server/api/routes/compute/qemu_nodes.py +++ b/gns3server/api/routes/compute/qemu_nodes.py @@ -20,15 +20,17 @@ API routes for Qemu nodes. import os -from fastapi import APIRouter, WebSocket, Depends, Body, Path, Response, status +from fastapi import APIRouter, WebSocket, Depends, Body, Path, status from fastapi.encoders import jsonable_encoder from fastapi.responses import StreamingResponse +from typing import Union from uuid import UUID from gns3server import schemas from gns3server.compute.qemu import Qemu from gns3server.compute.qemu.qemu_vm import QemuVM +from .dependencies.authentication import compute_authentication, ws_compute_authentication responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Qemu node"}} @@ -50,6 +52,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> QemuVM: response_model=schemas.Qemu, status_code=status.HTTP_201_CREATED, responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Qemu node"}}, + dependencies=[Depends(compute_authentication)] ) async def create_qemu_node(project_id: UUID, node_data: schemas.QemuCreate) -> schemas.Qemu: """ @@ -78,7 +81,11 @@ async def create_qemu_node(project_id: UUID, node_data: schemas.QemuCreate) -> s return vm.asdict() -@router.get("/{node_id}", response_model=schemas.Qemu) +@router.get( + "/{node_id}", + response_model=schemas.Qemu, + dependencies=[Depends(compute_authentication)] +) def get_qemu_node(node: QemuVM = Depends(dep_node)) -> schemas.Qemu: """ Return a Qemu node. @@ -87,7 +94,11 @@ def get_qemu_node(node: QemuVM = Depends(dep_node)) -> schemas.Qemu: return node.asdict() -@router.put("/{node_id}", response_model=schemas.Qemu) +@router.put( + "/{node_id}", + response_model=schemas.Qemu, + dependencies=[Depends(compute_authentication)] +) async def update_qemu_node(node_data: schemas.QemuUpdate, node: QemuVM = Depends(dep_node)) -> schemas.Qemu: """ Update a Qemu node. @@ -103,7 +114,11 @@ async def update_qemu_node(node_data: schemas.QemuUpdate, node: QemuVM = Depends return node.asdict() -@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT) +@router.delete( + "/{node_id}", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def delete_qemu_node(node: QemuVM = Depends(dep_node)) -> None: """ Delete a Qemu node. @@ -112,7 +127,12 @@ async def delete_qemu_node(node: QemuVM = Depends(dep_node)) -> None: await Qemu.instance().delete_node(node.id) -@router.post("/{node_id}/duplicate", response_model=schemas.Qemu, status_code=status.HTTP_201_CREATED) +@router.post( + "/{node_id}/duplicate", + response_model=schemas.Qemu, + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(compute_authentication)] +) async def duplicate_qemu_node( destination_node_id: UUID = Body(..., embed=True), node: QemuVM = Depends(dep_node) @@ -127,7 +147,8 @@ async def duplicate_qemu_node( @router.post( "/{node_id}/disk_image/{disk_name}", - status_code=status.HTTP_204_NO_CONTENT + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] ) async def create_qemu_disk_image( disk_name: str, @@ -144,7 +165,8 @@ async def create_qemu_disk_image( @router.put( "/{node_id}/disk_image/{disk_name}", - status_code=status.HTTP_204_NO_CONTENT + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] ) async def update_qemu_disk_image( disk_name: str, @@ -161,7 +183,8 @@ async def update_qemu_disk_image( @router.delete( "/{node_id}/disk_image/{disk_name}", - status_code=status.HTTP_204_NO_CONTENT + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] ) async def delete_qemu_disk_image( disk_name: str, @@ -174,7 +197,11 @@ async def delete_qemu_disk_image( node.delete_disk_image(disk_name) -@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/start", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def start_qemu_node(node: QemuVM = Depends(dep_node)) -> None: """ Start a Qemu node. @@ -183,7 +210,11 @@ async def start_qemu_node(node: QemuVM = Depends(dep_node)) -> None: await node.start() -@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/stop", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def stop_qemu_node(node: QemuVM = Depends(dep_node)) -> None: """ Stop a Qemu node. @@ -192,7 +223,11 @@ async def stop_qemu_node(node: QemuVM = Depends(dep_node)) -> None: await node.stop() -@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/reload", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def reload_qemu_node(node: QemuVM = Depends(dep_node)) -> None: """ Reload a Qemu node. @@ -201,7 +236,11 @@ async def reload_qemu_node(node: QemuVM = Depends(dep_node)) -> None: await node.reload() -@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/suspend", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def suspend_qemu_node(node: QemuVM = Depends(dep_node)) -> None: """ Suspend a Qemu node. @@ -210,7 +249,11 @@ async def suspend_qemu_node(node: QemuVM = Depends(dep_node)) -> None: await node.suspend() -@router.post("/{node_id}/resume", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/resume", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def resume_qemu_node(node: QemuVM = Depends(dep_node)) -> None: """ Resume a Qemu node. @@ -223,6 +266,7 @@ async def resume_qemu_node(node: QemuVM = Depends(dep_node)) -> None: "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_201_CREATED, response_model=schemas.UDPNIO, + dependencies=[Depends(compute_authentication)] ) async def create_qemu_node_nio( *, @@ -245,6 +289,7 @@ async def create_qemu_node_nio( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_201_CREATED, response_model=schemas.UDPNIO, + dependencies=[Depends(compute_authentication)] ) async def update_qemu_node_nio( *, @@ -267,7 +312,11 @@ async def update_qemu_node_nio( return nio.asdict() -@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT) +@router.delete( + "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def delete_qemu_node_nio( adapter_number: int, port_number: int = Path(..., ge=0, le=0), @@ -281,7 +330,10 @@ async def delete_qemu_node_nio( await node.adapter_remove_nio_binding(adapter_number) -@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start") +@router.post( + "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start", + dependencies=[Depends(compute_authentication)] +) async def start_qemu_node_capture( *, adapter_number: int, @@ -300,7 +352,9 @@ async def start_qemu_node_capture( @router.post( - "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT + "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] ) async def stop_qemu_node_capture( adapter_number: int, @@ -315,7 +369,10 @@ async def stop_qemu_node_capture( await node.stop_capture(adapter_number) -@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream") +@router.get( + "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream", + dependencies=[Depends(compute_authentication)] +) async def stream_pcap_file( adapter_number: int, port_number: int = Path(..., ge=0, le=0), @@ -330,16 +387,26 @@ async def stream_pcap_file( return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap") -@router.websocket("/{node_id}/console/ws") -async def console_ws(websocket: WebSocket, node: QemuVM = Depends(dep_node)) -> None: +@router.websocket( + "/{node_id}/console/ws" +) +async def console_ws( + websocket: Union[None, WebSocket] = Depends(ws_compute_authentication), + node: QemuVM = Depends(dep_node) +) -> None: """ Console WebSocket. """ - await node.start_websocket_console(websocket) + if websocket: + await node.start_websocket_console(websocket) -@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/console/reset", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def reset_console(node: QemuVM = Depends(dep_node)) -> None: await node.reset_console() diff --git a/gns3server/api/routes/compute/virtualbox_nodes.py b/gns3server/api/routes/compute/virtualbox_nodes.py index f457cbff..f676025e 100644 --- a/gns3server/api/routes/compute/virtualbox_nodes.py +++ b/gns3server/api/routes/compute/virtualbox_nodes.py @@ -20,16 +20,19 @@ API routes for VirtualBox nodes. import os -from fastapi import APIRouter, WebSocket, Depends, Path, Response, status +from fastapi import APIRouter, WebSocket, Depends, Path, status from fastapi.encoders import jsonable_encoder from fastapi.responses import StreamingResponse from uuid import UUID +from typing import Union from gns3server import schemas from gns3server.compute.virtualbox import VirtualBox from gns3server.compute.virtualbox.virtualbox_error import VirtualBoxError from gns3server.compute.virtualbox.virtualbox_vm import VirtualBoxVM +from .dependencies.authentication import compute_authentication, ws_compute_authentication + responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or VirtualBox node"}} router = APIRouter(responses=responses, deprecated=True) @@ -50,6 +53,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> VirtualBoxVM: response_model=schemas.VirtualBox, status_code=status.HTTP_201_CREATED, responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VirtualBox node"}}, + dependencies=[Depends(compute_authentication)] ) async def create_virtualbox_node(project_id: UUID, node_data: schemas.VirtualBoxCreate) -> schemas.VirtualBox: """ @@ -82,7 +86,11 @@ async def create_virtualbox_node(project_id: UUID, node_data: schemas.VirtualBox return vm.asdict() -@router.get("/{node_id}", response_model=schemas.VirtualBox) +@router.get( + "/{node_id}", + response_model=schemas.VirtualBox, + dependencies=[Depends(compute_authentication)] +) def get_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> schemas.VirtualBox: """ Return a VirtualBox node. @@ -91,7 +99,11 @@ def get_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> schemas.Virtu return node.asdict() -@router.put("/{node_id}", response_model=schemas.VirtualBox) +@router.put( + "/{node_id}", + response_model=schemas.VirtualBox, + dependencies=[Depends(compute_authentication)] +) async def update_virtualbox_node( node_data: schemas.VirtualBoxUpdate, node: VirtualBoxVM = Depends(dep_node) @@ -136,7 +148,11 @@ async def update_virtualbox_node( return node.asdict() -@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT) +@router.delete( + "/{node_id}", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def delete_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None: """ Delete a VirtualBox node. @@ -145,7 +161,11 @@ async def delete_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None await VirtualBox.instance().delete_node(node.id) -@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/start", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def start_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None: """ Start a VirtualBox node. @@ -154,7 +174,11 @@ async def start_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None: await node.start() -@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/stop", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def stop_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None: """ Stop a VirtualBox node. @@ -163,7 +187,11 @@ async def stop_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None: await node.stop() -@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/suspend", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def suspend_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None: """ Suspend a VirtualBox node. @@ -172,7 +200,11 @@ async def suspend_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> Non await node.suspend() -@router.post("/{node_id}/resume", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/resume", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def resume_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None: """ Resume a VirtualBox node. @@ -181,7 +213,11 @@ async def resume_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None await node.resume() -@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/reload", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def reload_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None: """ Reload a VirtualBox node. @@ -194,6 +230,7 @@ async def reload_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_201_CREATED, response_model=schemas.UDPNIO, + dependencies=[Depends(compute_authentication)] ) async def create_virtualbox_node_nio( *, @@ -216,6 +253,7 @@ async def create_virtualbox_node_nio( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_201_CREATED, response_model=schemas.UDPNIO, + dependencies=[Depends(compute_authentication)] ) async def update_virtualbox_node_nio( *, @@ -238,7 +276,11 @@ async def update_virtualbox_node_nio( return nio.asdict() -@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT) +@router.delete( + "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def delete_virtualbox_node_nio( adapter_number: int, port_number: int = Path(..., ge=0, le=0), @@ -252,7 +294,10 @@ async def delete_virtualbox_node_nio( await node.adapter_remove_nio_binding(adapter_number) -@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start") +@router.post( + "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start", + dependencies=[Depends(compute_authentication)] +) async def start_virtualbox_node_capture( *, adapter_number: int, @@ -271,7 +316,9 @@ async def start_virtualbox_node_capture( @router.post( - "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT + "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] ) async def stop_virtualbox_node_capture( adapter_number: int, @@ -286,7 +333,10 @@ async def stop_virtualbox_node_capture( await node.stop_capture(adapter_number) -@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream") +@router.get( + "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream", + dependencies=[Depends(compute_authentication)] +) async def stream_pcap_file( adapter_number: int, port_number: int = Path(..., ge=0, le=0), @@ -302,8 +352,13 @@ async def stream_pcap_file( return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap") -@router.websocket("/{node_id}/console/ws") -async def console_ws(websocket: WebSocket, node: VirtualBoxVM = Depends(dep_node)) -> None: +@router.websocket( + "/{node_id}/console/ws" +) +async def console_ws( + websocket: Union[None, WebSocket] = Depends(ws_compute_authentication), + node: VirtualBoxVM = Depends(dep_node) +) -> None: """ Console WebSocket. """ @@ -311,7 +366,11 @@ async def console_ws(websocket: WebSocket, node: VirtualBoxVM = Depends(dep_node await node.start_websocket_console(websocket) -@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/console/reset", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def reset_console(node: VirtualBoxVM = Depends(dep_node)) -> None: await node.reset_console() diff --git a/gns3server/api/routes/compute/vmware_nodes.py b/gns3server/api/routes/compute/vmware_nodes.py index d7c38844..f1e3752c 100644 --- a/gns3server/api/routes/compute/vmware_nodes.py +++ b/gns3server/api/routes/compute/vmware_nodes.py @@ -20,16 +20,18 @@ API routes for VMware nodes. import os -from fastapi import APIRouter, WebSocket, Depends, Path, Response, status +from fastapi import APIRouter, WebSocket, Depends, Path, status from fastapi.encoders import jsonable_encoder from fastapi.responses import StreamingResponse from uuid import UUID +from typing import Union from gns3server import schemas from gns3server.compute.vmware import VMware -from gns3server.compute.project_manager import ProjectManager from gns3server.compute.vmware.vmware_vm import VMwareVM +from .dependencies.authentication import compute_authentication, ws_compute_authentication + responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or VMware node"}} router = APIRouter(responses=responses, deprecated=True) @@ -50,6 +52,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> VMwareVM: response_model=schemas.VMware, status_code=status.HTTP_201_CREATED, responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VMware node"}}, + dependencies=[Depends(compute_authentication)] ) async def create_vmware_node(project_id: UUID, node_data: schemas.VMwareCreate) -> schemas.VMware: """ @@ -76,7 +79,11 @@ async def create_vmware_node(project_id: UUID, node_data: schemas.VMwareCreate) return vm.asdict() -@router.get("/{node_id}", response_model=schemas.VMware) +@router.get( + "/{node_id}", + response_model=schemas.VMware, + dependencies=[Depends(compute_authentication)] +) def get_vmware_node(node: VMwareVM = Depends(dep_node)) -> schemas.VMware: """ Return a VMware node. @@ -85,7 +92,11 @@ def get_vmware_node(node: VMwareVM = Depends(dep_node)) -> schemas.VMware: return node.asdict() -@router.put("/{node_id}", response_model=schemas.VMware) +@router.put( + "/{node_id}", + response_model=schemas.VMware, + dependencies=[Depends(compute_authentication)] +) def update_vmware_node(node_data: schemas.VMwareUpdate, node: VMwareVM = Depends(dep_node)) -> schemas.VMware: """ Update a VMware node. @@ -102,7 +113,11 @@ def update_vmware_node(node_data: schemas.VMwareUpdate, node: VMwareVM = Depends return node.asdict() -@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT) +@router.delete( + "/{node_id}", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def delete_vmware_node(node: VMwareVM = Depends(dep_node)) -> None: """ Delete a VMware node. @@ -111,7 +126,11 @@ async def delete_vmware_node(node: VMwareVM = Depends(dep_node)) -> None: await VMware.instance().delete_node(node.id) -@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/start", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def start_vmware_node(node: VMwareVM = Depends(dep_node)) -> None: """ Start a VMware node. @@ -120,7 +139,11 @@ async def start_vmware_node(node: VMwareVM = Depends(dep_node)) -> None: await node.start() -@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/stop", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def stop_vmware_node(node: VMwareVM = Depends(dep_node)) -> None: """ Stop a VMware node. @@ -129,7 +152,11 @@ async def stop_vmware_node(node: VMwareVM = Depends(dep_node)) -> None: await node.stop() -@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/suspend", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def suspend_vmware_node(node: VMwareVM = Depends(dep_node)) -> None: """ Suspend a VMware node. @@ -138,7 +165,11 @@ async def suspend_vmware_node(node: VMwareVM = Depends(dep_node)) -> None: await node.suspend() -@router.post("/{node_id}/resume", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/resume", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def resume_vmware_node(node: VMwareVM = Depends(dep_node)) -> None: """ Resume a VMware node. @@ -147,7 +178,11 @@ async def resume_vmware_node(node: VMwareVM = Depends(dep_node)) -> None: await node.resume() -@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/reload", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def reload_vmware_node(node: VMwareVM = Depends(dep_node)) -> None: """ Reload a VMware node. @@ -160,6 +195,7 @@ async def reload_vmware_node(node: VMwareVM = Depends(dep_node)) -> None: "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_201_CREATED, response_model=schemas.UDPNIO, + dependencies=[Depends(compute_authentication)] ) async def create_vmware_node_nio( *, @@ -182,6 +218,7 @@ async def create_vmware_node_nio( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_201_CREATED, response_model=schemas.UDPNIO, + dependencies=[Depends(compute_authentication)] ) async def update_vmware_node_nio( *, @@ -202,7 +239,11 @@ async def update_vmware_node_nio( return nio.asdict() -@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT) +@router.delete( + "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def delete_vmware_node_nio( adapter_number: int, port_number: int = Path(..., ge=0, le=0), @@ -216,7 +257,10 @@ async def delete_vmware_node_nio( await node.adapter_remove_nio_binding(adapter_number) -@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start") +@router.post( + "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start", + dependencies=[Depends(compute_authentication)] +) async def start_vmware_node_capture( *, adapter_number: int, @@ -235,7 +279,9 @@ async def start_vmware_node_capture( @router.post( - "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT + "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] ) async def stop_vmware_node_capture( adapter_number: int, @@ -250,7 +296,10 @@ async def stop_vmware_node_capture( await node.stop_capture(adapter_number) -@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream") +@router.get( + "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream", + dependencies=[Depends(compute_authentication)] +) async def stream_pcap_file( adapter_number: int, port_number: int = Path(..., ge=0, le=0), @@ -266,7 +315,11 @@ async def stream_pcap_file( return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap") -@router.post("/{node_id}/interfaces/vmnet", status_code=status.HTTP_201_CREATED) +@router.post( + "/{node_id}/interfaces/vmnet", + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(compute_authentication)] +) def allocate_vmnet(node: VMwareVM = Depends(dep_node)) -> dict: """ Allocate a VMware VMnet interface on the server. @@ -280,16 +333,23 @@ def allocate_vmnet(node: VMwareVM = Depends(dep_node)) -> dict: @router.websocket("/{node_id}/console/ws") -async def console_ws(websocket: WebSocket, node: VMwareVM = Depends(dep_node)) -> None: +async def console_ws( + websocket: Union[None, WebSocket] = Depends(ws_compute_authentication), + node: VMwareVM = Depends(dep_node) +) -> None: """ Console WebSocket. """ - await node.start_websocket_console(websocket) + if websocket: + await node.start_websocket_console(websocket) -@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/console/reset", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def reset_console(node: VMwareVM = Depends(dep_node)) -> None: await node.reset_console() - diff --git a/gns3server/api/routes/compute/vpcs_nodes.py b/gns3server/api/routes/compute/vpcs_nodes.py index df4f82c3..143ceef8 100644 --- a/gns3server/api/routes/compute/vpcs_nodes.py +++ b/gns3server/api/routes/compute/vpcs_nodes.py @@ -20,15 +20,18 @@ API routes for VPCS nodes. import os -from fastapi import APIRouter, WebSocket, Depends, Body, Path, Response, status +from fastapi import APIRouter, WebSocket, Depends, Body, Path, status from fastapi.encoders import jsonable_encoder from fastapi.responses import StreamingResponse +from typing import Union from uuid import UUID from gns3server import schemas from gns3server.compute.vpcs import VPCS from gns3server.compute.vpcs.vpcs_vm import VPCSVM +from .dependencies.authentication import compute_authentication, ws_compute_authentication + responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or VMware node"}} router = APIRouter(responses=responses) @@ -49,6 +52,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> VPCSVM: response_model=schemas.VPCS, status_code=status.HTTP_201_CREATED, responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VMware node"}}, + dependencies=[Depends(compute_authentication)] ) async def create_vpcs_node(project_id: UUID, node_data: schemas.VPCSCreate) -> schemas.VPCS: """ @@ -69,7 +73,11 @@ async def create_vpcs_node(project_id: UUID, node_data: schemas.VPCSCreate) -> s return vm.asdict() -@router.get("/{node_id}", response_model=schemas.VPCS) +@router.get( + "/{node_id}", + response_model=schemas.VPCS, + dependencies=[Depends(compute_authentication)] +) def get_vpcs_node(node: VPCSVM = Depends(dep_node)) -> schemas.VPCS: """ Return a VPCS node. @@ -78,7 +86,11 @@ def get_vpcs_node(node: VPCSVM = Depends(dep_node)) -> schemas.VPCS: return node.asdict() -@router.put("/{node_id}", response_model=schemas.VPCS) +@router.put( + "/{node_id}", + response_model=schemas.VPCS, + dependencies=[Depends(compute_authentication)] +) def update_vpcs_node(node_data: schemas.VPCSUpdate, node: VPCSVM = Depends(dep_node)) -> schemas.VPCS: """ Update a VPCS node. @@ -92,7 +104,11 @@ def update_vpcs_node(node_data: schemas.VPCSUpdate, node: VPCSVM = Depends(dep_n return node.asdict() -@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT) +@router.delete( + "/{node_id}", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def delete_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None: """ Delete a VPCS node. @@ -101,7 +117,12 @@ async def delete_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None: await VPCS.instance().delete_node(node.id) -@router.post("/{node_id}/duplicate", response_model=schemas.VPCS, status_code=status.HTTP_201_CREATED) +@router.post( + "/{node_id}/duplicate", + response_model=schemas.VPCS, + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(compute_authentication)] +) async def duplicate_vpcs_node( destination_node_id: UUID = Body(..., embed=True), node: VPCSVM = Depends(dep_node)) -> None: @@ -113,7 +134,11 @@ async def duplicate_vpcs_node( return new_node.asdict() -@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/start", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def start_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None: """ Start a VPCS node. @@ -122,7 +147,11 @@ async def start_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None: await node.start() -@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/stop", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def stop_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None: """ Stop a VPCS node. @@ -131,7 +160,11 @@ async def stop_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None: await node.stop() -@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/suspend", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def suspend_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None: """ Suspend a VPCS node. @@ -141,7 +174,11 @@ async def suspend_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None: pass -@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/reload", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def reload_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None: """ Reload a VPCS node. @@ -154,6 +191,7 @@ async def reload_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None: "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_201_CREATED, response_model=schemas.UDPNIO, + dependencies=[Depends(compute_authentication)] ) async def create_vpcs_node_nio( *, @@ -176,6 +214,7 @@ async def create_vpcs_node_nio( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_201_CREATED, response_model=schemas.UDPNIO, + dependencies=[Depends(compute_authentication)] ) async def update_vpcs_node_nio( *, @@ -196,7 +235,11 @@ async def update_vpcs_node_nio( return nio.asdict() -@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT) +@router.delete( + "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) async def delete_vpcs_node_nio( *, adapter_number: int = Path(..., ge=0, le=0), @@ -211,7 +254,10 @@ async def delete_vpcs_node_nio( await node.port_remove_nio_binding(port_number) -@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start") +@router.post( + "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start", + dependencies=[Depends(compute_authentication)] +) async def start_vpcs_node_capture( *, adapter_number: int = Path(..., ge=0, le=0), @@ -230,7 +276,9 @@ async def start_vpcs_node_capture( @router.post( - "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT + "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] ) async def stop_vpcs_node_capture( *, @@ -246,13 +294,10 @@ async def stop_vpcs_node_capture( await node.stop_capture(port_number) -@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT) -async def reset_console(node: VPCSVM = Depends(dep_node)) -> None: - - await node.reset_console() - - -@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream") +@router.get( + "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream", + dependencies=[Depends(compute_authentication)] +) async def stream_pcap_file( *, adapter_number: int = Path(..., ge=0, le=0), @@ -269,10 +314,24 @@ async def stream_pcap_file( return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap") -@router.websocket("/{node_id}/console/ws") -async def console_ws(websocket: WebSocket, node: VPCSVM = Depends(dep_node)) -> None: +@router.websocket( + "/{node_id}/console/ws" +) +async def console_ws( + websocket: Union[None, WebSocket] = Depends(ws_compute_authentication), + node: VPCSVM = Depends(dep_node)) -> None: """ Console WebSocket. """ await node.start_websocket_console(websocket) + + +@router.post( + "/{node_id}/console/reset", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(compute_authentication)] +) +async def reset_console(node: VPCSVM = Depends(dep_node)) -> None: + + await node.reset_console() diff --git a/gns3server/api/routes/controller/dependencies/authentication.py b/gns3server/api/routes/controller/dependencies/authentication.py index ce49ab0a..05e4d4ae 100644 --- a/gns3server/api/routes/controller/dependencies/authentication.py +++ b/gns3server/api/routes/controller/dependencies/authentication.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import re +import logging from fastapi import Request, Query, Depends, HTTPException, WebSocket, status from fastapi.security import OAuth2PasswordBearer @@ -26,6 +26,7 @@ from gns3server.db.repositories.rbac import RbacRepository from gns3server.services import auth_service from .database import get_repository +log = logging.getLogger(__name__) oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/v3/access/users/login", auto_error=False) @@ -108,7 +109,9 @@ async def get_current_active_user_from_websocket( return user except HTTPException as e: - websocket_error = {"action": "log.error", "event": {"message": f"Could not authenticate while connecting to " - f"WebSocket: {e.detail}"}} + err_msg = f"Could not authenticate while connecting to controller WebSocket: {e.detail}" + websocket_error = {"action": "log.error", "event": {"message": err_msg}} await websocket.send_json(websocket_error) - await websocket.close(code=1008) + log.error(err_msg) + return await websocket.close(code=1008) + diff --git a/gns3server/api/routes/controller/nodes.py b/gns3server/api/routes/controller/nodes.py index 865ad683..13e84450 100644 --- a/gns3server/api/routes/controller/nodes.py +++ b/gns3server/api/routes/controller/nodes.py @@ -29,6 +29,7 @@ from typing import List, Callable from uuid import UUID from gns3server.controller import Controller +from gns3server.config import Config from gns3server.controller.node import Node from gns3server.controller.project import Project from gns3server.utils import force_unix_path @@ -510,16 +511,22 @@ async def post_file(file_path: str, request: Request, node: Node = Depends(dep_n # FIXME: response with correct status code (from compute) -@router.websocket("/{node_id}/console/ws", dependencies=[Depends(has_privilege_on_websocket("Node.Console"))]) -async def ws_console(websocket: WebSocket, node: Node = Depends(dep_node)) -> None: +@router.websocket("/{node_id}/console/ws") +async def ws_console( + websocket: WebSocket, + current_user: schemas.User = Depends(has_privilege_on_websocket("Node.Console")), + node: Node = Depends(dep_node) +) -> None: """ WebSocket console. Required privilege: Node.Console """ + if current_user is None: + return + compute = node.compute - await websocket.accept() log.info( f"New client {websocket.client.host}:{websocket.client.port} has connected to controller console WebSocket" ) @@ -557,9 +564,20 @@ async def ws_console(websocket: WebSocket, node: Node = Depends(dep_node)) -> No try: # receive WebSocket data from compute console WebSocket and forward to client. - async with HTTPClient.get_client().ws_connect(ws_console_compute_url) as ws_console_compute: - asyncio.ensure_future(ws_receive(ws_console_compute)) - async for msg in ws_console_compute: + log.info(f"Forwarding console WebSocket to '{ws_console_compute_url}'") + server_config = Config.instance().settings.Server + user = server_config.compute_username + password = server_config.compute_password + if not user: + raise ControllerForbiddenError("Compute username is not set") + user = user.strip() + if user and password: + auth = aiohttp.BasicAuth(user, password.get_secret_value(), "utf-8") + else: + auth = aiohttp.BasicAuth(user, "") + async with HTTPClient.get_client().ws_connect(ws_console_compute_url, auth=auth) as ws: + asyncio.ensure_future(ws_receive(ws)) + async for msg in ws: if msg.type == aiohttp.WSMsgType.TEXT: await websocket.send_text(msg.data) elif msg.type == aiohttp.WSMsgType.BINARY: diff --git a/gns3server/compute/base_node.py b/gns3server/compute/base_node.py index c877f5ff..20f437be 100644 --- a/gns3server/compute/base_node.py +++ b/gns3server/compute/base_node.py @@ -485,6 +485,11 @@ class BaseNode: :param ws: Websocket object """ + log.info( + f"New client {websocket.client.host}:{websocket.client.port} has connected to compute" + f" console WebSocket" + ) + if self.status != "started": raise NodeError(f"Node {self.name} is not started") @@ -492,20 +497,13 @@ class BaseNode: raise NodeError(f"Node {self.name} console type is not telnet") try: - (telnet_reader, telnet_writer) = await asyncio.open_connection( - self._manager.port_manager.console_host, self.console - ) + host = self._manager.port_manager.console_host + port = self.console + (telnet_reader, telnet_writer) = await asyncio.open_connection(host, port) + log.info(f"Connected to local Telnet server {host}:{port}") except ConnectionError as e: raise NodeError(f"Cannot connect to node {self.name} telnet server: {e}") - log.info("Connected to Telnet server") - - await websocket.accept() - log.info( - f"New client {websocket.client.host}:{websocket.client.port} has connected to compute" - f" console WebSocket" - ) - async def ws_forward(telnet_writer): try: diff --git a/tests/api/routes/test_routes.py b/tests/api/routes/test_routes.py index 7a7313ed..b960d6f4 100644 --- a/tests/api/routes/test_routes.py +++ b/tests/api/routes/test_routes.py @@ -20,6 +20,10 @@ from fastapi import FastAPI, status from fastapi.routing import APIRoute, APIWebSocketRoute from starlette.routing import Mount from httpx import AsyncClient +from httpx_ws import aconnect_ws +from httpx_ws.transport import ASGIWebSocketTransport + + pytestmark = pytest.mark.asyncio @@ -37,6 +41,7 @@ ALLOWED_CONTROLLER_ENDPOINTS = [ ("/v3/symbols/default_symbols", "GET") ] + # Controller endpoints have a OAuth2 bearer token authentication async def test_controller_endpoints_require_authentication(app: FastAPI, unauthorized_client: AsyncClient) -> None: @@ -47,7 +52,14 @@ async def test_controller_endpoints_require_authentication(app: FastAPI, unautho response = await getattr(unauthorized_client, method.lower())(route.path) assert response.status_code == status.HTTP_401_UNAUTHORIZED elif isinstance(route, APIWebSocketRoute): - pass # TODO: test websocket route authentication + params = {"token": "wrong_token"} + async with AsyncClient(base_url="http://test-api", transport=ASGIWebSocketTransport(app)) as client: + async with aconnect_ws(route.path, client, params=params) as ws: + json_notification = await ws.receive_json() + assert json_notification['event'] == { + 'message': 'Could not authenticate while connecting to controller WebSocket: Could not validate credentials' + } + # Compute endpoints have a basic HTTP authentication async def test_compute_endpoints_require_authentication(app: FastAPI, unauthorized_client: AsyncClient) -> None: @@ -55,9 +67,14 @@ async def test_compute_endpoints_require_authentication(app: FastAPI, unauthoriz for route in app.routes: if isinstance(route, Mount): for compute_route in route.routes: - if isinstance(compute_route, APIRoute): # APIWebSocketRoute + if isinstance(compute_route, APIRoute): for method in list(compute_route.methods): response = await getattr(unauthorized_client, method.lower())(route.path + compute_route.path) assert response.status_code == status.HTTP_401_UNAUTHORIZED elif isinstance(compute_route, APIWebSocketRoute): - pass # TODO: test websocket route authentication + async with AsyncClient(base_url="http://test-api", transport=ASGIWebSocketTransport(app)) as client: + async with aconnect_ws(route.path + compute_route.path, client, auth=("wrong_user", "password123")) as ws: + json_notification = await ws.receive_json() + assert json_notification['event'] == { + 'message': 'Could not authenticate while connecting to compute WebSocket: Could not validate credentials' + }