Files
gitea-agent/harness/tests/common/test_git_credential_recipe.py
T

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)