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:
@@ -10,8 +10,49 @@ the optional ``[web]`` extra is not installed.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import click
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from ci_orchestrator.config import Config
|
||||
|
||||
_LOOPBACK_HOSTNAMES = {"localhost", ""}
|
||||
|
||||
|
||||
def _is_loopback(host: str) -> bool:
|
||||
"""True if ``host`` is a loopback address / ``localhost``.
|
||||
|
||||
Unresolvable hostnames are treated as NON-loopback (conservative — we
|
||||
would rather refuse an ambiguous bind than expose the dashboard).
|
||||
"""
|
||||
if host in _LOOPBACK_HOSTNAMES:
|
||||
return True
|
||||
try:
|
||||
return ipaddress.ip_address(host).is_loopback
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def _bind_guard_error(host: str, config: Config) -> str | None:
|
||||
"""Refuse an unauthenticated bind on a non-loopback address.
|
||||
|
||||
When ``[web.oidc]`` is not configured the dashboard runs in OPEN mode
|
||||
(no login — see ``web/auth.py``). That is only safe on loopback. Binding
|
||||
a LAN/any address in open mode would expose host-management actions
|
||||
unauthenticated, so it is refused here. Returns an error message to abort
|
||||
with, or ``None`` when the bind is allowed.
|
||||
"""
|
||||
if config.web.oidc is None and not _is_loopback(host):
|
||||
return (
|
||||
f"refusing to bind {host!r} without authentication: [web.oidc] is "
|
||||
"not configured, so the dashboard would be served unauthenticated "
|
||||
"on a non-loopback address. Configure [web.oidc] (Gitea OIDC) or "
|
||||
"bind 127.0.0.1."
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@click.group("web")
|
||||
def web() -> None:
|
||||
@@ -49,6 +90,10 @@ def serve(host: str | None, port: int | None) -> None:
|
||||
bind_host = host if host is not None else config.web.bind_host
|
||||
bind_port = port if port is not None else config.web.bind_port
|
||||
|
||||
guard_error = _bind_guard_error(bind_host, config)
|
||||
if guard_error is not None:
|
||||
raise click.ClickException(guard_error)
|
||||
|
||||
app = create_app(config)
|
||||
uvicorn.run(app, host=bind_host, port=bind_port)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user