112 lines
3.9 KiB
Python
112 lines
3.9 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import unittest
|
|
from pathlib import Path
|
|
from urllib.parse import urlsplit
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
WORKFLOW = ROOT / ".gitea" / "workflows" / "managed-validation.yml"
|
|
CANONICAL_ORIGIN = "https://git.example.com"
|
|
|
|
|
|
def _origin_case_script() -> str:
|
|
text = WORKFLOW.read_text(encoding="utf-8")
|
|
match = re.search(
|
|
r'(?ms)^ case "\$\{GITHUB_SERVER_URL%/\}" in\n'
|
|
r'.*?^ esac$',
|
|
text,
|
|
)
|
|
if match is None:
|
|
raise AssertionError("managed validation has no GITHUB_SERVER_URL case policy")
|
|
# Remove workflow indentation before executing only the bounded origin policy.
|
|
return "\n".join(line[10:] for line in match.group(0).splitlines())
|
|
|
|
|
|
def _evaluate_origin(origin: str) -> subprocess.CompletedProcess[str]:
|
|
script = (
|
|
"set -euo pipefail\n"
|
|
+ _origin_case_script()
|
|
+ "\nprintf '%s\\n' \"${SOURCE_SERVER_URL}\"\n"
|
|
)
|
|
env = os.environ.copy()
|
|
env["GITHUB_SERVER_URL"] = origin
|
|
bash = shutil.which("bash") or "bash"
|
|
if os.name == "nt":
|
|
# System32/bash.exe starts WSL, which does not inherit arbitrary Windows environment
|
|
# variables. Git Bash runs natively and tests the exact environment used by this fixture.
|
|
git = shutil.which("git")
|
|
git_bash = Path(git).resolve().parents[1] / "bin" / "bash.exe" if git else None
|
|
if git_bash is None or not git_bash.is_file():
|
|
raise RuntimeError("Git Bash is required for Windows origin policy tests")
|
|
bash = str(git_bash)
|
|
return subprocess.run(
|
|
[bash, "--noprofile", "--norc", "-c", script],
|
|
cwd=ROOT,
|
|
env=env,
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
|
|
|
|
def _safe_origin(value: str) -> str:
|
|
parsed = urlsplit(value)
|
|
hostname = parsed.hostname or "(missing-host)"
|
|
port = f":{parsed.port}" if parsed.port is not None else ""
|
|
return f"{parsed.scheme or '(missing-scheme)'}://{hostname}{port}"
|
|
|
|
|
|
class SecurityOriginPolicyTests(unittest.TestCase):
|
|
def test_known_runner_and_public_origins_canonicalize_to_https(self) -> None:
|
|
accepted = (
|
|
"https://git.example.com",
|
|
"https://git.example.com/",
|
|
"http://git.example.com",
|
|
"http://git.example.com/",
|
|
"http://192.0.2.10:3000",
|
|
"http://192.0.2.10:3000/",
|
|
"http://gitea:3000",
|
|
"http://gitea:3000/",
|
|
"http://Gitea:3000",
|
|
"http://Gitea:3000/",
|
|
"http://gitea-runner-internal:3000",
|
|
"http://gitea-runner-internal:3000/",
|
|
)
|
|
for origin in accepted:
|
|
with self.subTest(origin=origin):
|
|
result = _evaluate_origin(origin)
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertEqual(result.stdout.strip(), CANONICAL_ORIGIN)
|
|
|
|
def test_unknown_http_and_https_origins_fail_closed(self) -> None:
|
|
for origin in (
|
|
"http://untrusted.invalid",
|
|
"https://untrusted.invalid",
|
|
"http://192.0.2.151:3000",
|
|
"http://gitea:3001",
|
|
):
|
|
with self.subTest(origin=origin):
|
|
result = _evaluate_origin(origin)
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertIn("Untrusted Gitea server URL", result.stderr)
|
|
|
|
def test_live_runner_origin_is_explicitly_covered_when_present(self) -> None:
|
|
origin = os.environ.get("GITHUB_SERVER_URL")
|
|
if not origin:
|
|
self.skipTest("GITHUB_SERVER_URL is only expected inside the managed runner")
|
|
result = _evaluate_origin(origin)
|
|
self.assertEqual(
|
|
result.returncode,
|
|
0,
|
|
f"managed runner origin is not covered: {_safe_origin(origin)}; {result.stderr.strip()}",
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|