test_hosted_dashboard_security.py python
115 lines 3.7 KB
Raw
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 1 day ago
1 """Security tests for hosted governance dashboard (§HGD.12)."""
2
3 from __future__ import annotations
4
5 import pytest
6
7 from tests.fixtures.hosted_dashboard import start_test_hosted
8 from tools.hosted_dashboard.adapters.musehub import musehub_baseline_impossible
9 from tools.hosted_dashboard.http_client import ALLOWED_METHODS, UpstreamClient, UpstreamError
10 from tools.hosted_dashboard.scopes import refuse_write_scopes
11 from tools.hosted_dashboard.server import STATIC_ROOT
12
13
14 def test_mutating_methods_on_api() -> None:
15 handle, client, _ = start_test_hosted()
16 try:
17 for method in ("POST", "PUT", "PATCH", "DELETE"):
18 status, payload = client.request(method, "/api/org/summary")
19 assert status == 405
20 assert payload["error"] == "method_not_allowed"
21 finally:
22 handle.shutdown()
23
24
25 def test_upstream_client_methods_get_head_only() -> None:
26 assert ALLOWED_METHODS == frozenset({"GET", "HEAD"})
27 client = UpstreamClient(token=None)
28
29 def boom(method, url, headers, timeout):
30 raise AssertionError("should not transport")
31
32 client._transport = boom # type: ignore[method-assign]
33 with pytest.raises(UpstreamError) as exc:
34 client.request("POST", "https://api.github.com/user")
35 assert exc.value.token == "method_refused"
36
37
38 def test_rejected_scopes_refuse() -> None:
39 assert refuse_write_scopes(["administration"]) == "write_scope_refused"
40 assert refuse_write_scopes(["workflows"]) == "write_scope_refused"
41
42
43 def test_ssrf_disallowed_host_and_ip() -> None:
44 client = UpstreamClient(token=None)
45 for url in (
46 "https://169.254.169.254/latest/meta-data",
47 "https://127.0.0.1/repos",
48 "https://evil.example/x",
49 "http://[::1]/",
50 ):
51 with pytest.raises(UpstreamError) as exc:
52 client.get_json(url)
53 assert exc.value.token == "upstream_host_refused"
54
55
56 def test_path_traversal_in_params_refused() -> None:
57 handle, client, _ = start_test_hosted()
58 try:
59 status, payload = client.get("/api/repos/../etc/passwd/roadmap")
60 assert status in {400, 404}
61 finally:
62 handle.shutdown()
63
64
65 def test_missing_auth_401_except_health() -> None:
66 handle, client, _ = start_test_hosted()
67 try:
68 status, payload = client.get("/api/org/summary", auth=False)
69 assert status == 401
70 assert payload["error"] == "auth"
71 status, health = client.get("/api/health", auth=False)
72 assert status == 200
73 finally:
74 handle.shutdown()
75
76
77 def test_no_localstorage_viewer_token() -> None:
78 js = (STATIC_ROOT / "assets" / "dashboard.js").read_text(encoding="utf-8")
79 assert "localStorage" not in js
80 assert "sessionStorage" not in js
81 assert "document.cookie" not in js
82
83
84 def test_no_auth_disable_flag_in_cli_help() -> None:
85 from cli.main import build_parser
86
87 help_text = build_parser().format_help()
88 assert "--disable-auth" not in help_text
89 assert "hosted-dashboard" in help_text
90
91
92 def test_musehub_only_baseline_impossible() -> None:
93 assert musehub_baseline_impossible(github_contents_enabled=False) is True
94 assert musehub_baseline_impossible(github_contents_enabled=True) is False
95
96
97 def test_no_deploy_probe_urls_in_module() -> None:
98 from pathlib import Path
99
100 root = Path(__file__).resolve().parents[2] / "tools" / "hosted_dashboard"
101 text = ""
102 for path in root.rglob("*.py"):
103 text += path.read_text(encoding="utf-8")
104 assert "production_health" not in text
105 assert "http://prod" not in text
106 assert "https://prod" not in text
107
108
109 def test_deploy_route_not_registered() -> None:
110 handle, client, _ = start_test_hosted()
111 try:
112 status, payload = client.get("/api/deploy")
113 assert status == 404
114 finally:
115 handle.shutdown()
File History 1 commit
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 1 day ago