feat(dashboard): drop token auth, trust oauth2-proxy X-Forwarded-User

This commit is contained in:
Claude
2026-08-22 18:03:33 +00:00
parent c9809bce9b
commit 99255bb167
4 changed files with 141 additions and 203 deletions
+70 -98
View File
@@ -1,8 +1,9 @@
"""Tests for pilot/dashboard.py — stdlib HTTP server rendering dashboard HTML.
We spin up the server on an ephemeral port in setUp, drive it with
http.client, and tear it down in tearDown. Tests are split between
no-auth (default — env unset) and auth-on (PRAGENT_DASHBOARD_TOKEN set).
http.client, and tear it down in tearDown. Auth is now performed by
oauth2-proxy: the dashboard trusts `X-Forwarded-User` set by the proxy
and returns 401 (with a Basic challenge) when the header is missing.
The dashboard reads `PRAGENT_FEEDBACK_DB` and renders views via
`dashboard_data`. We seed an in-memory SQLite at `tmp_path` for each
@@ -69,15 +70,72 @@ def _post(port: int, path: str, body: bytes, headers: dict | None = None) -> tup
return r.status, h, body_b
class TestDashboardNoAuth(unittest.TestCase):
"""PRAGENT_DASHBOARD_TOKEN unset → no cookie check."""
class TestDashboardAuth(unittest.TestCase):
"""Auth gate: require X-Forwarded-User (set by oauth2-proxy).
When the header is missing every non-static route returns 401 with a
Basic challenge, which lets oauth2-proxy redirect the browser to
Logto. Static is exempt so the unauthenticated probe traffic doesn't
loop the proxy through the auth flow.
"""
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.db = f"{self.tmp.name}/f.db"
os.environ["PRAGENT_FEEDBACK_DB"] = self.db
os.environ["PRAGENT_DASHBOARD_TOKEN"] = ""
os.environ["DASHBOARD_PORT"] = str(0) # we override below
self.port = _free_port()
from http.server import ThreadingHTTPServer
self.srv = ThreadingHTTPServer(("127.0.0.1", self.port), dash.Handler)
self.thread = threading.Thread(target=self.srv.serve_forever, daemon=True)
self.thread.start()
def tearDown(self):
self.srv.shutdown()
self.srv.server_close()
self.thread.join(timeout=2)
for k in ("PRAGENT_FEEDBACK_DB", "DASHBOARD_PORT"):
os.environ.pop(k, None)
self.tmp.cleanup()
def test_anonymous_overview_returns_401_with_basic_challenge(self):
status, h, body = _get(self.port, "/")
self.assertEqual(status, 401)
self.assertEqual(h.get("WWW-Authenticate"), 'Basic realm="pragent-dashboard"')
self.assertEqual(body, b"unauthorized\n")
def test_anonymous_repo_returns_401(self):
status, _h, _body = _get(self.port, "/r/alpha/one")
self.assertEqual(status, 401)
def test_anonymous_post_returns_401(self):
status, _h, _body = _post(self.port, "/r/alpha/one/edit", b"x=1")
self.assertEqual(status, 401)
def test_authenticated_overview_succeeds(self):
status, h, body = _get(self.port, "/", headers={"X-Forwarded-User": "marcos@example.com"})
self.assertEqual(status, 200)
self.assertIn(b"Overview", body)
def test_static_does_not_require_auth(self):
status, h, body = _get(self.port, "/static/style.css")
self.assertEqual(status, 200)
self.assertIn("text/css", h.get("Content-Type", ""))
self.assertGreater(len(body), 50)
def test_empty_x_forwarded_user_treated_as_anonymous(self):
status, _h, _body = _get(self.port, "/", headers={"X-Forwarded-User": " "})
self.assertEqual(status, 401)
class TestDashboardRender(unittest.TestCase):
"""Render-only tests — X-Forwarded-User set, real seeded data."""
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.db = f"{self.tmp.name}/f.db"
os.environ["PRAGENT_FEEDBACK_DB"] = self.db
# Seed: 2 repos, a couple of reviews + findings each.
conn = feedback.init(self.db)
for repo, n_prs in (("alpha/one", 2), ("beta/two", 1)):
@@ -99,22 +157,21 @@ class TestDashboardNoAuth(unittest.TestCase):
conn.close()
self.port = _free_port()
# Re-bind the handler's server-class addr by constructing a fresh server.
from http.server import ThreadingHTTPServer
self.srv = ThreadingHTTPServer(("127.0.0.1", self.port), dash.Handler)
self.thread = threading.Thread(target=self.srv.serve_forever, daemon=True)
self.thread.start()
self.auth_hdr = {"X-Forwarded-User": "marcos@example.com"}
def tearDown(self):
self.srv.shutdown()
self.srv.server_close()
self.thread.join(timeout=2)
for k in ("PRAGENT_FEEDBACK_DB", "PRAGENT_DASHBOARD_TOKEN", "DASHBOARD_PORT"):
os.environ.pop(k, None)
os.environ.pop("PRAGENT_FEEDBACK_DB", None)
self.tmp.cleanup()
def test_overview_200_contains_repo_names(self):
status, _h, body = _get(self.port, "/")
status, _h, body = _get(self.port, "/", headers=self.auth_hdr)
self.assertEqual(status, 200)
text = body.decode()
self.assertIn("Overview", text)
@@ -122,7 +179,7 @@ class TestDashboardNoAuth(unittest.TestCase):
self.assertIn("beta/two", text)
def test_repo_page_200(self):
status, _h, body = _get(self.port, "/r/alpha/one")
status, _h, body = _get(self.port, "/r/alpha/one", headers=self.auth_hdr)
self.assertEqual(status, 200)
text = body.decode()
self.assertIn("alpha/one", text)
@@ -130,102 +187,17 @@ class TestDashboardNoAuth(unittest.TestCase):
self.assertIn("src/file_0.py", text)
def test_pr_page_200(self):
status, _h, body = _get(self.port, "/r/alpha/one/1")
status, _h, body = _get(self.port, "/r/alpha/one/1", headers=self.auth_hdr)
self.assertEqual(status, 200)
text = body.decode()
self.assertIn("alpha/one", text)
self.assertIn("#1", text)
self.assertIn("src/file_0.py", text)
def test_static_css_200(self):
status, h, body = _get(self.port, "/static/style.css")
self.assertEqual(status, 200)
self.assertIn("text/css", h.get("Content-Type", ""))
self.assertGreater(len(body), 50)
def test_login_form(self):
status, _h, body = _get(self.port, "/login")
self.assertEqual(status, 200)
self.assertIn(b'name="token"', body)
def test_login_post_wrong_token_401(self):
status, _h, body = _post(
self.port, "/login", b"token=wrong",
)
self.assertEqual(status, 401)
def test_unknown_route_404(self):
status, _h, _body = _get(self.port, "/no/such/route")
status, _h, _body = _get(self.port, "/no/such/route", headers=self.auth_hdr)
self.assertEqual(status, 404)
class TestDashboardWithAuth(unittest.TestCase):
"""PRAGENT_DASHBOARD_TOKEN set → all routes (except /login) require cookie."""
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.db = f"{self.tmp.name}/f.db"
os.environ["PRAGENT_FEEDBACK_DB"] = self.db
os.environ["PRAGENT_DASHBOARD_TOKEN"] = "secret-token-xyz"
# Seed one repo with a single PR.
conn = feedback.init(self.db)
rid = feedback.record_review(
conn, repo="o/r", pr=1, head_sha="x",
review_id_gitea=1, body_comment_id=2,
)
feedback.record_inline_finding(
conn, review_id=rid, repo="o/r", pr=1,
path="a.py", line=1, severity="low", problem="x",
)
conn.close()
self.port = _free_port()
from http.server import ThreadingHTTPServer
self.srv = ThreadingHTTPServer(("127.0.0.1", self.port), dash.Handler)
self.thread = threading.Thread(target=self.srv.serve_forever, daemon=True)
self.thread.start()
def tearDown(self):
self.srv.shutdown()
self.srv.server_close()
self.thread.join(timeout=2)
for k in ("PRAGENT_FEEDBACK_DB", "PRAGENT_DASHBOARD_TOKEN"):
os.environ.pop(k, None)
self.tmp.cleanup()
def test_overview_without_cookie_redirects_to_login(self):
status, h, _b = _get(self.port, "/")
self.assertEqual(status, 302)
self.assertIn("/login", h.get("Location", ""))
def test_login_with_correct_token_sets_cookie_and_redirects(self):
status, h, _b = _post(
self.port, "/login", b"token=secret-token-xyz",
)
self.assertEqual(status, 302)
self.assertIn("/", h.get("Location", ""))
sc = h.get("Set-Cookie", "")
self.assertIn("pragent_dash=secret-token-xyz", sc)
self.assertIn("HttpOnly", sc)
self.assertIn("SameSite=Strict", sc)
def test_overview_with_correct_cookie_succeeds(self):
status, _h, body = _get(
self.port, "/",
headers={"Cookie": "pragent_dash=secret-token-xyz"},
)
self.assertEqual(status, 200)
self.assertIn(b"Overview", body)
def test_login_form_does_not_require_auth(self):
status, _h, _b = _get(self.port, "/login")
self.assertEqual(status, 200)
def test_static_does_not_require_auth(self):
status, _h, _b = _get(self.port, "/static/style.css")
self.assertEqual(status, 200)
if __name__ == "__main__":
unittest.main()
unittest.main()