diff --git a/Dockerfile b/Dockerfile index 7a123eb..11292ac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,6 +31,10 @@ RUN useradd --system --no-create-home --uid 1001 appuser USER appuser +# A deployment can run the image as a uid with no passwd entry. getpass, +# and so asyncssh, reads the login name from LOGNAME first. +ENV LOGNAME=appuser + EXPOSE 8780 HEALTHCHECK --interval=60s --timeout=5s --start-period=30s --retries=3 \ diff --git a/src/gateway/tests/test_server.py b/src/gateway/tests/test_server.py index a81beca..52ac3ff 100644 --- a/src/gateway/tests/test_server.py +++ b/src/gateway/tests/test_server.py @@ -12,6 +12,7 @@ from core.database import async_session_factory from gateway import server as gateway_server from gateway.settings import GatewaySettings +from gateway.tests.localprocess import LocalProcess from hosts.models import Host, HostStatus from providers.base import SandboxProcess, TerminalSize from providers.exceptions import ProviderTransportError @@ -169,8 +170,6 @@ async def answer(reader, writer): @pytest.fixture def local_provider(monkeypatch): - from gateway.tests.localprocess import LocalProcess - provider = SimpleNamespace(gateway_process_class=LocalProcess) monkeypatch.setattr(gateway_server, "get_vm_provider", lambda name: provider) return LocalProcess @@ -418,7 +417,9 @@ async def test_gateway_shares_one_tunnel_and_closes_it_with_the_caller( await asyncio.wait_for(tunnel.wait_closed(), 5) -async def test_gateway_reports_a_tunnel_that_fails_to_open(forwarding_caller, sandbox_provider): +async def test_gateway_fails_only_the_channel_when_the_tunnel_fails_to_open( + forwarding_caller, sandbox_provider +): async def refuse(name): raise ProviderTransportError("sandboxd is not running") @@ -427,3 +428,4 @@ async def refuse(name): await forwarding_caller.open_connection("127.0.0.1", 80) assert refusal.value.code == OPEN_CONNECT_FAILED + assert (await forwarding_caller.run("true", input="\n")).exit_status == 7 diff --git a/src/providers/docker_sbx/provider.py b/src/providers/docker_sbx/provider.py index 2d65163..ee089c0 100644 --- a/src/providers/docker_sbx/provider.py +++ b/src/providers/docker_sbx/provider.py @@ -156,7 +156,8 @@ async def delete_vm(self, name: str) -> None: async def open_gateway_tunnel(self, name: str) -> asyncssh.SSHClientConnection: # sandboxd authenticates the OS user on its local socket, and sbx itself - # trusts the host key on first use. No key crosses a network. + # trusts the host key on first use. No key crosses a network. asyncssh + # raises ValueError when the local login name is unknown. try: return await asyncssh.connect( f"{name}.sbx", @@ -168,7 +169,7 @@ async def open_gateway_tunnel(self, name: str) -> asyncssh.SSHClientConnection: agent_path=None, preferred_auth="none", ) - except (OSError, asyncssh.Error) as exc: + except (OSError, ValueError, asyncssh.Error) as exc: raise ProviderTransportError(f"sbx could not open a tunnel: {exc}") from exc async def build_template_image( diff --git a/src/providers/docker_sbx/tests/test_provider.py b/src/providers/docker_sbx/tests/test_provider.py index 9e6bb4a..c69238a 100644 --- a/src/providers/docker_sbx/tests/test_provider.py +++ b/src/providers/docker_sbx/tests/test_provider.py @@ -1,3 +1,4 @@ +import getpass from pathlib import Path from typing import Any from unittest.mock import AsyncMock, MagicMock @@ -278,3 +279,15 @@ async def test_delete_template_image_translates_a_missing_template(tmp_path): with pytest.raises(ProviderNotFoundError, match="was not found"): await provider.delete_template_image("drukbox-template:missing") + + +@pytest.mark.asyncio +async def test_open_gateway_tunnel_translates_an_unknown_local_login_name(tmp_path, monkeypatch): + def unknown_uid(): + raise KeyError("getpwuid(): uid not found: 1000") + + monkeypatch.setattr(getpass, "getuser", unknown_uid) + provider = _provider(_api_mock(), _settings(tmp_path)) + + with pytest.raises(ProviderTransportError): + await provider.open_gateway_tunnel("sb-test")