feat(webui): auth and deployment boundary (#435)
Add bind-host assessment that refuses 0.0.0.0/:: without override, warns on non-loopback binds, and documents internal-only MVP serving. Health endpoint exposes deployment metadata; docs cover Access/VPN/WARP and runtime env assumptions without embedding secrets in the client. Closes #435
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
"""Tests for web UI deployment and auth boundary (#435)."""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from webui.app import create_app
|
||||
from webui.deployment_boundary import (
|
||||
assess_bind_host,
|
||||
runtime_assumptions,
|
||||
scan_text_for_client_secrets,
|
||||
)
|
||||
|
||||
|
||||
class TestBindAssessment(unittest.TestCase):
|
||||
def test_loopback_is_safe(self):
|
||||
for host in ("127.0.0.1", "localhost", "::1"):
|
||||
with self.subTest(host=host):
|
||||
result = assess_bind_host(host)
|
||||
self.assertEqual(result.disposition, "safe")
|
||||
|
||||
def test_all_interfaces_refused_by_default(self):
|
||||
for host in ("0.0.0.0", "::", "*"):
|
||||
with self.subTest(host=host):
|
||||
result = assess_bind_host(host)
|
||||
self.assertEqual(result.disposition, "refuse")
|
||||
|
||||
def test_all_interfaces_allowed_with_override(self):
|
||||
prior = os.environ.pop("WEBUI_ALLOW_PUBLIC_BIND", None)
|
||||
try:
|
||||
os.environ["WEBUI_ALLOW_PUBLIC_BIND"] = "1"
|
||||
result = assess_bind_host("0.0.0.0")
|
||||
self.assertEqual(result.disposition, "warn")
|
||||
finally:
|
||||
os.environ.pop("WEBUI_ALLOW_PUBLIC_BIND", None)
|
||||
if prior is not None:
|
||||
os.environ["WEBUI_ALLOW_PUBLIC_BIND"] = prior
|
||||
|
||||
def test_non_loopback_warns_without_override(self):
|
||||
prior = os.environ.pop("WEBUI_ALLOW_REMOTE_BIND", None)
|
||||
try:
|
||||
os.environ.pop("WEBUI_ALLOW_REMOTE_BIND", None)
|
||||
result = assess_bind_host("192.168.1.10")
|
||||
self.assertEqual(result.disposition, "warn")
|
||||
finally:
|
||||
if prior is not None:
|
||||
os.environ["WEBUI_ALLOW_REMOTE_BIND"] = prior
|
||||
|
||||
def test_runtime_assumptions_exclude_secrets(self):
|
||||
assumptions = runtime_assumptions()
|
||||
blob = " ".join(str(v) for v in assumptions.values())
|
||||
self.assertNotIn("GITEA_TOKEN", blob)
|
||||
self.assertIn("internal-operator-console", assumptions["deployment_mode"])
|
||||
|
||||
|
||||
class TestDeploymentRoutes(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.client = TestClient(create_app(bind_host="127.0.0.1"))
|
||||
|
||||
def test_health_includes_deployment_metadata(self):
|
||||
response = self.client.get("/health")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json()
|
||||
self.assertIn("deployment", data)
|
||||
deployment = data["deployment"]
|
||||
self.assertEqual(deployment["mode"], "internal-operator-console")
|
||||
self.assertEqual(deployment["mvp_auth"], "none")
|
||||
self.assertEqual(deployment["bind"]["disposition"], "safe")
|
||||
self.assertIn("runtime_assumptions", deployment)
|
||||
|
||||
def test_rendered_pages_contain_no_embedded_secrets(self):
|
||||
for path in ("/", "/projects", "/prompts", "/queue"):
|
||||
with self.subTest(path=path):
|
||||
text = self.client.get(path).text
|
||||
self.assertEqual(scan_text_for_client_secrets(text), [])
|
||||
|
||||
|
||||
class TestStartupRefusal(unittest.TestCase):
|
||||
def test_refuses_all_interface_bind(self):
|
||||
env = {**os.environ, "WEBUI_HOST": "0.0.0.0"}
|
||||
env.pop("WEBUI_ALLOW_PUBLIC_BIND", None)
|
||||
repo_root = Path(__file__).resolve().parent.parent
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "webui"],
|
||||
cwd=repo_root,
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=3,
|
||||
)
|
||||
self.assertEqual(result.returncode, 1)
|
||||
|
||||
|
||||
class TestClientSecretScanner(unittest.TestCase):
|
||||
def test_detects_token_like_content(self):
|
||||
findings = scan_text_for_client_secrets("var x = GITEA_TOKEN_PRGS")
|
||||
self.assertTrue(findings)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user