fix(web): refuse unauthenticated bind on non-loopback address (W5)
In open mode ([web.oidc] unset) the dashboard has no login. Binding a LAN/any address there would expose host-management unauthenticated, so ci web serve now aborts unless the bind host is loopback. Auth-configured deployments bind LAN as before.
This commit is contained in:
@@ -148,3 +148,71 @@ def test_web_group_registered_on_root_cli() -> None:
|
||||
def test_config_import_does_not_require_web_extra() -> None:
|
||||
"""``Config``/``load_config`` (imported lazily inside serve) is core, not [web]."""
|
||||
assert Config is not None
|
||||
|
||||
|
||||
# ── W5 security guard: no unauthenticated bind on a non-loopback address ──────
|
||||
|
||||
|
||||
def _cfg(*, oidc: bool, tmp: Path) -> Config:
|
||||
from ci_orchestrator.config import (
|
||||
BackendConfig,
|
||||
Paths,
|
||||
WebConfig,
|
||||
WebOidcConfig,
|
||||
)
|
||||
|
||||
web_oidc = (
|
||||
WebOidcConfig(issuer="https://gitea.lan", client_id="cid", redirect_url="http://x/cb")
|
||||
if oidc
|
||||
else None
|
||||
)
|
||||
return Config(
|
||||
paths=Paths(
|
||||
root=tmp,
|
||||
templates=tmp / "templates",
|
||||
build_vms=tmp / "build-vms",
|
||||
artifacts=tmp / "artifacts",
|
||||
keys=tmp / "keys",
|
||||
),
|
||||
backend=BackendConfig(),
|
||||
web=WebConfig(oidc=web_oidc),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("host", ["127.0.0.1", "::1", "localhost", ""])
|
||||
def test_is_loopback_true(host: str) -> None:
|
||||
assert web_module._is_loopback(host) is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("host", ["192.168.1.50", "10.0.0.5", "0.0.0.0", "gitea.lan"])
|
||||
def test_is_loopback_false(host: str) -> None:
|
||||
assert web_module._is_loopback(host) is False
|
||||
|
||||
|
||||
def test_bind_guard_blocks_lan_bind_without_oidc(tmp_path: Path) -> None:
|
||||
err = web_module._bind_guard_error("192.168.1.50", _cfg(oidc=False, tmp=tmp_path))
|
||||
assert err is not None
|
||||
assert "refusing to bind" in err
|
||||
|
||||
|
||||
def test_bind_guard_allows_lan_bind_with_oidc(tmp_path: Path) -> None:
|
||||
assert web_module._bind_guard_error("192.168.1.50", _cfg(oidc=True, tmp=tmp_path)) is None
|
||||
|
||||
|
||||
def test_bind_guard_allows_loopback_without_oidc(tmp_path: Path) -> None:
|
||||
assert web_module._bind_guard_error("127.0.0.1", _cfg(oidc=False, tmp=tmp_path)) is None
|
||||
|
||||
|
||||
def test_web_serve_refuses_lan_bind_without_auth(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""End-to-end: ``serve`` aborts (and never starts uvicorn) on an unsafe bind.
|
||||
|
||||
The autouse fixture points config at an empty tmp dir → ``web.oidc`` is
|
||||
``None`` (open mode), so a non-loopback ``--host`` must be refused.
|
||||
"""
|
||||
calls = _install_fake_uvicorn(monkeypatch)
|
||||
|
||||
result = CliRunner().invoke(cli, ["web", "serve", "--host", "10.0.0.5", "--port", "8800"])
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert "refusing to bind" in result.output
|
||||
assert calls == [], "uvicorn.run must never be called on a refused bind"
|
||||
|
||||
Reference in New Issue
Block a user