diff --git a/gns3server/api/routes/compute/dependencies/authentication.py b/gns3server/api/routes/compute/dependencies/authentication.py index 377a89dc6..5e725b3d8 100644 --- a/gns3server/api/routes/compute/dependencies/authentication.py +++ b/gns3server/api/routes/compute/dependencies/authentication.py @@ -26,12 +26,29 @@ from gns3server.config import Config from typing import Optional, Union log = logging.getLogger(__name__) -security = HTTPBasic() +security = HTTPBasic(auto_error=False) def compute_authentication(credentials: Optional[HTTPBasicCredentials] = Depends(security)) -> None: + """ + Authenticate compute requests. + + Returns None if authentication is disabled or if authentication succeeds + Raises HTTPException if authentication is required but credentials are invalid + """ server_settings = Config.instance().settings.Server + + if not server_settings.enable_http_auth: + return None + + if credentials is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid compute username or password", + headers={"WWW-Authenticate": "Basic"}, + ) + username = secrets.compare_digest(credentials.username, server_settings.compute_username) password = secrets.compare_digest(credentials.password, server_settings.compute_password.get_secret_value()) if not (username and password): @@ -44,6 +61,12 @@ def compute_authentication(credentials: Optional[HTTPBasicCredentials] = Depends async def ws_compute_authentication(websocket: WebSocket) -> Union[None, WebSocket]: """ """ + + server_settings = Config.instance().settings.Server + + if not server_settings.enable_http_auth: + await websocket.accept() + return websocket await websocket.accept() @@ -68,7 +91,6 @@ async def ws_compute_authentication(websocket: WebSocket) -> Union[None, WebSock 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): diff --git a/gns3server/config_samples/gns3_server.conf b/gns3server/config_samples/gns3_server.conf index 0a99ea338..92142bab5 100644 --- a/gns3server/config_samples/gns3_server.conf +++ b/gns3server/config_samples/gns3_server.conf @@ -91,6 +91,9 @@ udp_end_port_range = 30000 ; uBridge executable location, default: search in PATH ;ubridge_path = ubridge +; Option to enable or disable compute HTTP authentication +enable_http_auth = True + ; Username for compute HTTP authentication, "gns3" is the default if not specified compute_username = gns3 ; Password for compute HTTP authentication, a randomly generated password is used if not specified diff --git a/gns3server/schemas/config.py b/gns3server/schemas/config.py index 34ea90690..4d0426b0e 100644 --- a/gns3server/schemas/config.py +++ b/gns3server/schemas/config.py @@ -114,6 +114,7 @@ class BuiltinSymbolTheme(str, Enum): class ServerSettings(BaseModel): local: bool = False + enable_http_auth: bool = True name: str = f"{socket.gethostname()} (controller)" protocol: ServerProtocol = ServerProtocol.http host: str = "0.0.0.0" diff --git a/gns3server/server.py b/gns3server/server.py index 6ae18fdfb..be2b62564 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -240,7 +240,10 @@ class Server: self._set_config_defaults_from_command_line(args) config = Config.instance().settings - if not config.Server.compute_password.get_secret_value(): + + if not config.Server.enable_http_auth: + log.info("Compute authentication is disabled") + elif not config.Server.compute_password.get_secret_value(): alphabet = string.ascii_letters + string.digits + string.punctuation generated_password = ''.join(secrets.choice(alphabet) for _ in range(16)) config.Server.compute_password = SecretStr(generated_password) diff --git a/tests/api/routes/compute/test_compute.py b/tests/api/routes/compute/test_compute.py index aa70ff6b9..04a38b2b8 100644 --- a/tests/api/routes/compute/test_compute.py +++ b/tests/api/routes/compute/test_compute.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from unittest.mock import patch, MagicMock import pytest from fastapi import FastAPI, status @@ -22,6 +23,8 @@ from httpx import AsyncClient from gns3server.version import __version__ from gns3server.compute.project import Project +from gns3server.config import Config +from gns3server.schemas.config import ServerSettings pytestmark = pytest.mark.asyncio @@ -71,3 +74,20 @@ class TestComputeRoutes: response = await compute_client.get(app.url_path_for("compute:compute_statistics")) assert response.status_code == status.HTTP_200_OK + + + async def test_compute_auth_disabled(self, app: FastAPI, compute_client: AsyncClient) -> None: + + mock_settings = MagicMock() + mock_server_settings = MagicMock() + mock_server_settings.enable_http_auth = False + mock_server_settings.compute_username = "gns3" + mock_server_settings.compute_password.get_secret_value.return_value = "testpass" + mock_settings.settings.Server = mock_server_settings + + with patch("gns3server.api.routes.compute.dependencies.authentication.Config.instance", return_value=mock_settings): + response = await compute_client.get( + app.url_path_for("compute:compute_version"), + auth=("wrong_user", "wrong_password") + ) + assert response.status_code == status.HTTP_200_OK