110 lines
6.5 KiB
Python
110 lines
6.5 KiB
Python
"""Local credential protocol checks only; no real credentials or network."""
|
|
import os
|
|
import secrets
|
|
import shlex
|
|
import stat
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from harness.tests.common.impl_contract_docs import read_contract
|
|
|
|
ROOT = Path(__file__).resolve().parents[3]
|
|
POLICY = ROOT / "harness/operations/repository-cache.md"
|
|
REWORK = ROOT / "harness/agents/impl/skills/gitea-issue-rework/SKILL.md"
|
|
HEADING = "### Non-file Git credential helper"
|
|
|
|
|
|
class GitCredentialRecipeTests(unittest.TestCase):
|
|
def recipe(self):
|
|
policy = POLICY.read_text(encoding="utf-8")
|
|
self.assertTrue(HEADING in policy, "canonical non-file credential recipe is missing")
|
|
section = policy.split(HEADING, 1)[1].split("\n## ", 1)[0]
|
|
block = section.split("```sh\n", 1)[1].split("```", 1)[0]
|
|
return section, shlex.split(block.replace("\\\n", ""))
|
|
|
|
def test_canonical_nonfile_recipe_preserves_permission_and_delivery_contract(self):
|
|
section, command = self.recipe()
|
|
self.assertEqual(command[0], "git")
|
|
self.assertEqual(command[1:3], ["-c", "credential.helper="])
|
|
helper = command[4]
|
|
self.assertTrue(helper.startswith("credential.helper=!"))
|
|
self.assertIn("GIT_ROLE_USERNAME", helper)
|
|
self.assertIn("GIT_ROLE_TOKEN", helper)
|
|
self.assertIn("credential.interactive=false", command)
|
|
self.assertIn("remote.origin.mirror=false", command)
|
|
self.assertEqual(command[-4:], ["push", "--dry-run", "$VERIFIED_REPOSITORY_URL", "$VERIFIED_REFSPEC"])
|
|
for required in ("0600", "GIT_TERMINAL_PROMPT=0", "GIT_ASKPASS=/bin/false", "SSH_ASKPASS=/bin/false",
|
|
"same-UID", "store", "erase", "stdout", "readback"):
|
|
self.assertIn(required, section)
|
|
policy = POLICY.read_text(encoding="utf-8")
|
|
self.assertIn("Lock 파일과 임시 credential helper는 실행 role 소유와 mode `0600`을 요구한다", policy)
|
|
delivery = read_contract(REWORK).split("### 7. 기존 PR branch 전달", 1)[1].split("###", 1)[0]
|
|
self.assertIn("#non-file-git-credential-helper", delivery)
|
|
self.assertNotIn("필요한 temporary askpass helper", delivery)
|
|
|
|
def test_nonfile_get_missing_variables_and_store_erase_do_not_persist_credentials(self):
|
|
_, documented = self.recipe()
|
|
options = documented[:documented.index("push")]
|
|
with tempfile.TemporaryDirectory(dir=os.environ.get("TMPDIR", "/opt/data/cache/scratch")) as d:
|
|
root = Path(d)
|
|
token = secrets.token_hex(24)
|
|
env = {"PATH": os.environ["PATH"], "HOME": str(root), "GIT_CONFIG_NOSYSTEM": "1",
|
|
"GIT_CONFIG_GLOBAL": os.devnull, "GIT_TERMINAL_PROMPT": "0",
|
|
"GIT_ASKPASS": "/bin/false", "SSH_ASKPASS": "/bin/false",
|
|
"GIT_ROLE_USERNAME": "gitea-impl-agent", "GIT_ROLE_TOKEN": token}
|
|
initialized = subprocess.run(["git", "init", "-q"], cwd=root, env=env,
|
|
capture_output=True, timeout=10)
|
|
self.assertEqual(initialized.returncode, 0)
|
|
config = root / ".git/config"
|
|
before, mode = config.read_bytes(), stat.S_IMODE(config.stat().st_mode)
|
|
request = "protocol=https\nhost=example.invalid\n\n"
|
|
|
|
def invoke(action, packet=request, child_env=env):
|
|
# Values travel only in the private child env/protocol, never argv.
|
|
self.assertFalse(any(token in arg for arg in options))
|
|
result = subprocess.run([*options, "credential", action], cwd=root, env=child_env,
|
|
input=packet, text=True, capture_output=True, timeout=10)
|
|
self.assertFalse(token in result.stderr, "credential exposed outside private protocol")
|
|
return result
|
|
|
|
valid = invoke("fill")
|
|
self.assertEqual(valid.returncode, 0)
|
|
fields = dict(line.split("=", 1) for line in valid.stdout.splitlines() if "=" in line)
|
|
self.assertTrue(fields.get("username") == env["GIT_ROLE_USERNAME"], "username binding mismatch")
|
|
self.assertTrue(fields.get("password") == token, "synthetic credential binding mismatch")
|
|
for missing in ("GIT_ROLE_TOKEN", "GIT_ROLE_USERNAME"):
|
|
with self.subTest(missing=missing):
|
|
incomplete = dict(env)
|
|
incomplete.pop(missing)
|
|
rejected = invoke("fill", child_env=incomplete)
|
|
self.assertNotEqual(rejected.returncode, 0)
|
|
self.assertTrue(rejected.stdout == "", "failed credential lookup emitted data")
|
|
packet = ("protocol=https\nhost=example.invalid\nusername=" + env["GIT_ROLE_USERNAME"]
|
|
+ "\npassword=" + token + "\n\n")
|
|
for action in ("approve", "reject"):
|
|
with self.subTest(action=action):
|
|
result = invoke(action, packet=packet)
|
|
self.assertEqual(result.returncode, 0)
|
|
self.assertTrue(result.stdout == "", "store/erase emitted credential data")
|
|
self.assertTrue(config.read_bytes() == before, "Git config changed")
|
|
self.assertEqual(stat.S_IMODE(config.stat().st_mode), mode)
|
|
self.assertFalse(token.encode() in config.read_bytes(), "credential persisted")
|
|
self.assertEqual(sorted(path.name for path in root.iterdir()), [".git"])
|
|
|
|
def test_mode0600_direct_askpass_fails_without_permission_relaxation(self):
|
|
with tempfile.TemporaryDirectory(dir=os.environ.get("TMPDIR", "/opt/data/cache/scratch")) as d:
|
|
root = Path(d)
|
|
askpass = root / "askpass.sh"
|
|
askpass.write_text("#!/bin/sh\nexit 0\n")
|
|
askpass.chmod(0o600)
|
|
env = {"PATH": os.environ["PATH"], "HOME": str(root), "GIT_CONFIG_NOSYSTEM": "1",
|
|
"GIT_CONFIG_GLOBAL": os.devnull, "GIT_TERMINAL_PROMPT": "0", "GIT_ASKPASS": str(askpass)}
|
|
result = subprocess.run(["git", "-c", "credential.helper=", "credential", "fill"],
|
|
cwd=root, env=env, input="protocol=https\nhost=example.invalid\n\n",
|
|
text=True, capture_output=True, timeout=10)
|
|
self.assertEqual(result.returncode, 128)
|
|
self.assertIn("Permission denied", result.stderr)
|
|
self.assertTrue(result.stdout == "", "failed credential lookup emitted data")
|
|
self.assertEqual(stat.S_IMODE(askpass.stat().st_mode), 0o600)
|