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()