Files
codex-app-server/cloud/tests/test_chv_muse_p3.py
T
host 9f593d62ef fix(chv-muse-r11fix): authd 层 3 socket 用 group 权限、层 4 裸 .git 当 pull
层 3:AuthdServer 不再 0700/0600 一刀切。socket 共享组取显式
socket_group 或第一个非 root 放行 uid 的主组(build_server 透
传;生产默认 allowed_uids=(0,1000) 派生 chv 主组),目录 0750
+ socket 0660 + chgrp;解析不出(uid 无 passwd 条目)回落
root-only,绝不放 0666(探针 fixprobe8 用的 0666 不上生产)。
层 4:_operation_for_path 加裸 .git → GET/pull;Sentinel
gitea 规则加 (GET, suffix, .git),POST 裸 .git 仍拒。语义已定:
裸 .git 一律当 pull(只读默认),push 只走显式 git-receive-pack。
测试:Sentinel 允许 GET/拒绝 POST 裸 .git、helper 端到端裸路径
取凭证、组 gid 派生与回落、显式组落地 0750/0660。
2026-11-07 03:58:45 +00:00

812 lines
35 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""对齐 Muse P3 测试:authd 替身凭证 + Sentinel 式出口审批。
只用标准库与 mock,绝不启动真实 VM、不联网、不含真实 token
(测试值全是假值)。覆盖:
- authd 保险库:白名单入库、作用域绑定、单次释放、替身过期、
forget 吊销、repr/审计里绝无 token 值;
- 铸 token / 吊销(假 HTTP 客户端);
- Sentinel:SSRF 挡截、deny/ask 路径、L7 路径规则、严格能力
(绑定/过期/次数)、待审批队列;
- seed 拆分:split_authd_env、write_seed 拒收 guest-env 里的
GITEA_TOKEN、authd-seed 0600;
- wake 预审:allow/ask/deny、无同步跳过;
- 客内 authd 守护进程(真 socket + 真 seed):放凭证、拒绝
路径/主机/仓库、审计无值;
- git credential helper 端到端:真 git credential fill + 假
authd socket(守护进程真跑在测试进程内)。
"""
import importlib.util
import json
import os
import socket
import subprocess
import sys
import tempfile
import threading
import unittest
from pathlib import Path
try:
from cloud.chv.authd import (
AuthdRefusal,
CredentialVault,
mint_gitea_token,
revoke_gitea_token,
surrogate_fingerprint,
)
from cloud.chv.gitea_sync import split_authd_env
from cloud.chv.hostkeys import AUTHD_SEED_PATH, write_seed
from cloud.chv.sentinel import (
ApprovalQueue,
EgressRequest,
SentinelPolicy,
)
from cloud.sandbox import ChvFailure, CloudHypervisorExecutor
except ImportError:
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.chv.authd import (
AuthdRefusal,
CredentialVault,
mint_gitea_token,
revoke_gitea_token,
surrogate_fingerprint,
)
from cloud.chv.gitea_sync import split_authd_env
from cloud.chv.hostkeys import AUTHD_SEED_PATH, write_seed
from cloud.chv.sentinel import (
ApprovalQueue,
EgressRequest,
SentinelPolicy,
)
from cloud.sandbox import ChvFailure, CloudHypervisorExecutor
REPO_ROOT = Path(__file__).resolve().parents[2]
TOKEN = "fake-token-123"
PUBLIC_IP = "93.184.216.34"
FAKE_SYNC = {
"GITEA_URL": "https://gitea.tose.top",
"GITEA_USER": "admin",
"GITEA_TOKEN": TOKEN,
"SYNC_REPO": "admin/chv-sync-test",
"SYNC_BRANCH": "main",
"GITEA_EMAIL": "admin@tose.top",
}
def _public_resolver(host, port):
return [PUBLIC_IP]
def _load_guest_authd():
"""chv-guest/authd.py(守护进程)按文件加载,不走包名。"""
path = REPO_ROOT / "chv-guest" / "authd.py"
spec = importlib.util.spec_from_file_location(
"chv_guest_authd", path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
# -- (1) authd 保险库 ----------------------------------------------------
class VaultTest(unittest.TestCase):
def setUp(self):
self.now = [1000.0]
self.vault = CredentialVault(clock=lambda: self.now[0])
def _stored(self):
self.vault.store("gitea", "GITEA_TOKEN", TOKEN,
host="gitea.tose.top",
repo="admin/chv-sync-test")
def test_store_rejects_unknown_connector_key_empty(self):
with self.assertRaises(AuthdRefusal) as ctx:
self.vault.store("github", "GITEA_TOKEN", TOKEN)
self.assertEqual(ctx.exception.kind, "unknown_connector")
with self.assertRaises(AuthdRefusal) as ctx:
self.vault.store("gitea", "OTHER_KEY", TOKEN)
self.assertEqual(ctx.exception.kind,
"credential_not_allowlisted")
with self.assertRaises(AuthdRefusal) as ctx:
self.vault.store("gitea", "GITEA_TOKEN", " ")
self.assertEqual(ctx.exception.kind, "empty_credential")
self.assertFalse(self.vault.has("gitea", "GITEA_TOKEN"))
def test_release_single_use_and_scope_binding(self):
self._stored()
rel = self.vault.release("gitea", "GITEA_TOKEN",
"gitea.tose.top",
"admin/chv-sync-test", "push")
self.assertEqual(self.vault.consume(rel.release_id), TOKEN)
with self.assertRaises(AuthdRefusal) as ctx:
self.vault.consume(rel.release_id)
self.assertEqual(ctx.exception.kind, "release_unknown")
with self.assertRaises(AuthdRefusal) as ctx:
self.vault.release("gitea", "GITEA_TOKEN",
"other.example",
"admin/chv-sync-test", "push")
self.assertEqual(ctx.exception.kind, "scope_mismatch")
with self.assertRaises(AuthdRefusal) as ctx:
self.vault.release("gitea", "GITEA_TOKEN",
"gitea.tose.top",
"admin/other-repo", "push")
self.assertEqual(ctx.exception.kind, "scope_mismatch")
with self.assertRaises(AuthdRefusal) as ctx:
self.vault.release("gitea", "GITEA_TOKEN",
"gitea.tose.top",
"admin/chv-sync-test", "delete")
self.assertEqual(ctx.exception.kind, "operation_not_allowed")
def test_release_expires_after_ttl(self):
# 替身过期:300 秒 TTL,301 秒后兑现即废。
self._stored()
rel = self.vault.release("gitea", "GITEA_TOKEN",
"gitea.tose.top",
"admin/chv-sync-test", "pull")
self.now[0] += 301
with self.assertRaises(AuthdRefusal) as ctx:
self.vault.consume(rel.release_id)
self.assertEqual(ctx.exception.kind, "release_expired")
def test_release_unexpired_still_works(self):
self._stored()
rel = self.vault.release("gitea", "GITEA_TOKEN",
"gitea.tose.top",
"admin/chv-sync-test", "clone")
self.now[0] += 299
self.assertEqual(self.vault.consume(rel.release_id), TOKEN)
def test_forget_revokes_pending_releases(self):
self._stored()
rel = self.vault.release("gitea", "GITEA_TOKEN",
"gitea.tose.top",
"admin/chv-sync-test", "pull")
self.assertTrue(self.vault.forget("gitea", "GITEA_TOKEN"))
self.assertFalse(self.vault.has("gitea", "GITEA_TOKEN"))
self.assertFalse(self.vault.forget("gitea", "GITEA_TOKEN"))
with self.assertRaises(AuthdRefusal) as ctx:
self.vault.consume(rel.release_id)
self.assertEqual(ctx.exception.kind, "release_unknown")
with self.assertRaises(AuthdRefusal) as ctx:
self.vault.release("gitea", "GITEA_TOKEN",
"gitea.tose.top",
"admin/chv-sync-test", "pull")
self.assertEqual(ctx.exception.kind, "credential_not_stored")
def test_repr_and_audit_never_contain_value(self):
# 真 token 泄露面:repr/审计记录里绝无 token 值。
self._stored()
rel = self.vault.release("gitea", "GITEA_TOKEN",
"gitea.tose.top",
"admin/chv-sync-test", "push")
for text in (repr(self.vault), repr(rel),
json.dumps(self.vault.audit_release(rel)),
json.dumps(self.vault.audit_refusal(
"scope_mismatch", "gitea", "GITEA_TOKEN"))):
self.assertNotIn(TOKEN, text)
self.assertNotEqual(surrogate_fingerprint(TOKEN), TOKEN)
self.assertEqual(len(surrogate_fingerprint(TOKEN)), 8)
self.assertEqual(surrogate_fingerprint(TOKEN),
surrogate_fingerprint(TOKEN))
class MintRevokeTest(unittest.TestCase):
def test_mint_returns_new_token_and_failure_is_refusal(self):
def fake_post(url, headers, payload):
self.assertIn("/users/admin/tokens", url)
self.assertEqual(payload["scopes"], ["write:repository"])
return 201, {"sha1": "new-short-token"}
self.assertEqual(
mint_gitea_token("https://gitea.tose.top/api/v1", "admin",
TOKEN, "chv-test", post=fake_post),
"new-short-token")
with self.assertRaises(AuthdRefusal) as ctx:
mint_gitea_token("https://gitea.tose.top/api/v1", "admin",
TOKEN, "chv-test",
post=lambda u, h, p: (403, {}))
self.assertEqual(ctx.exception.kind, "mint_failed")
with self.assertRaises(AuthdRefusal):
mint_gitea_token("", "admin", TOKEN, "x")
def test_revoke_statuses(self):
base = ("https://gitea.tose.top/api/v1", "admin", TOKEN)
self.assertTrue(revoke_gitea_token(
*base, 49, delete=lambda u, h: 204))
self.assertTrue(revoke_gitea_token(
*base, 49, delete=lambda u, h: 404))
self.assertFalse(revoke_gitea_token(
*base, 49, delete=lambda u, h: 500))
self.assertFalse(revoke_gitea_token("", "admin", TOKEN, 49))
# -- (2) Sentinel 决策 ----------------------------------------------------
class SentinelTest(unittest.TestCase):
def _req(self, host="gitea.tose.top", method="GET",
scheme="https", port=443, path="/api/v1/version"):
return EgressRequest("gitea", method, scheme, host, port, path)
def test_allow_public_ip_allowlisted_host(self):
policy = SentinelPolicy(allow_hosts={"gitea.tose.top"})
dec = policy.evaluate(self._req(), resolve=_public_resolver)
self.assertEqual(dec.verdict, "allow")
self.assertIsNotNone(dec.capability)
self.assertNotIn(TOKEN, json.dumps(dec.audit(self._req())))
def test_ssrf_private_ip_denied_even_when_allowlisted(self):
# SSRF 挡截:白名单主机解析到私网 IP 仍 deny。
policy = SentinelPolicy(allow_hosts={"gitea.tose.top"})
for ip in ("10.0.0.5", "192.168.1.1", "127.0.0.1",
"169.254.1.2", "100.64.0.9", "::1", "0.0.0.0"):
dec = policy.evaluate(
self._req(), resolve=lambda h, p, ip=ip: [ip])
self.assertEqual(dec.verdict, "deny", ip)
self.assertIn("ssrf_private_ip", dec.reasons[0])
def test_dns_failure_is_deny_not_ask(self):
policy = SentinelPolicy(allow_hosts={"gitea.tose.top"})
dec = policy.evaluate(
self._req(), resolve=lambda h, p: (_ for _ in ()).throw(
OSError("boom")))
self.assertEqual(dec.verdict, "deny")
self.assertEqual(dec.reasons, ["dns_resolution_failed"])
dec = policy.evaluate(self._req(), resolve=lambda h, p: [])
self.assertEqual(dec.verdict, "deny")
def test_unknown_host_is_ask_not_allow(self):
policy = SentinelPolicy(allow_hosts={"gitea.tose.top"})
dec = policy.evaluate(self._req(host="other.example"),
resolve=_public_resolver)
self.assertEqual(dec.verdict, "ask")
self.assertEqual(dec.reasons, ["host_not_allowlisted"])
self.assertIsNone(dec.capability)
def test_deny_paths(self):
policy = SentinelPolicy(allow_hosts={"gitea.tose.top"},
deny_hosts={"bad.example"})
cases = [
(EgressRequest("github", "GET", "https", "gitea.tose.top",
443, "/api/v1/version"),
"unknown_connector"),
(self._req(scheme="http"), "scheme_not_allowed"),
(self._req(port=80), "port_not_allowed"),
(self._req(host="bad.example"), "host_denied"),
(self._req(path="/login"), "path_not_allowed"),
# round-11 真机修复(层 4):裸 .git 只放 GET,POST 仍拒。
(self._req(method="POST", path="/admin/r.git"),
"path_not_allowed"),
]
for req, reason in cases:
dec = policy.evaluate(req, resolve=_public_resolver)
self.assertEqual(dec.verdict, "deny", reason)
self.assertEqual(dec.reasons, [reason])
# git 智能 HTTP 三条路径放行,裸 .git 只放 GET(层 4)。
for method, path in (
("GET", "/admin/r.git/info/refs"),
("GET", "/admin/r.git"),
("POST", "/admin/r.git/git-upload-pack"),
("POST", "/admin/r.git/git-receive-pack")):
dec = policy.evaluate(
self._req(method=method, path=path),
resolve=_public_resolver)
self.assertEqual(dec.verdict, "allow", path)
def test_capability_binding_expiry_and_uses(self):
policy = SentinelPolicy(allow_hosts={"gitea.tose.top"})
dec = policy.evaluate(self._req(), resolve=_public_resolver,
now=1000.0)
cap = dec.capability
req = self._req()
self.assertTrue(cap.matches(req, now=1000.0))
self.assertFalse(cap.matches(self._req(host="other.example"),
now=1000.0))
self.assertFalse(cap.matches(self._req(method="POST"),
now=1000.0))
self.assertFalse(cap.matches(self._req(path="/login"),
now=1000.0))
self.assertFalse(cap.matches(req, now=1000.0 + 301))
self.assertTrue(cap.use(req, now=1000.0))
self.assertFalse(cap.use(req, now=1000.0))
def test_from_env_and_approval_queue(self):
policy = SentinelPolicy.from_env(
{"CHV_SENTINEL_ALLOW_HOSTS": "a.example, b.example "},
extra_allow_hosts={"c.example"})
self.assertEqual(policy.allow_hosts,
{"a.example", "b.example", "c.example"})
queue = ApprovalQueue(clock=lambda: 1000.0)
req = self._req(host="other.example")
pid = queue.record(req, ["host_not_allowlisted"])
pending = queue.list_pending()
self.assertEqual(len(pending), 1)
self.assertEqual(pending[0]["pending_id"], pid)
self.assertEqual(pending[0]["ts"], 1000.0)
self.assertTrue(queue.resolve(pid, "deny"))
self.assertFalse(queue.resolve(pid, "deny"))
self.assertFalse(queue.resolve("nope", "allow"))
self.assertEqual(queue.list_pending(), [])
# -- (3) seed 拆分 ---------------------------------------------------------
class SeedSplitTest(unittest.TestCase):
def test_split_authd_env(self):
authd, rest = split_authd_env(
{"GITEA_TOKEN": TOKEN, "SUB2API_API_KEY": "gw",
"OTHER": "1"})
self.assertEqual(authd, {"GITEA_TOKEN": TOKEN})
self.assertEqual(rest, {"SUB2API_API_KEY": "gw", "OTHER": "1"})
self.assertEqual(split_authd_env(None), ({}, {}))
self.assertEqual(split_authd_env({}), ({}, {}))
def test_write_seed_refuses_token_in_guest_env(self):
with tempfile.TemporaryDirectory() as tmp:
seed_dir = os.path.join(tmp, "seed")
with self.assertRaises(ValueError):
write_seed(seed_dir, "vm1", "PRIV",
"ssh-ed25519 AAAAx",
secret_env={"GITEA_TOKEN": TOKEN})
with self.assertRaises(ValueError):
write_seed(seed_dir, "vm1", "PRIV",
"ssh-ed25519 AAAAx",
guest_env={"GITEA_TOKEN": TOKEN})
with self.assertRaises(ValueError):
write_seed(seed_dir, "vm1", "PRIV",
"ssh-ed25519 AAAAx",
authd_env={"SUB2API_API_KEY": "gw"})
def test_write_seed_authd_entry_is_separate_0600(self):
import yaml
with tempfile.TemporaryDirectory() as tmp:
seed_dir = os.path.join(tmp, "seed")
write_seed(seed_dir, "vm1", "PRIV", "ssh-ed25519 AAAAx",
secret_env={"SUB2API_API_KEY": "gw-key"},
guest_env={"GITEA_URL": "https://gitea.tose.top"},
authd_env={"GITEA_TOKEN": TOKEN})
with open(os.path.join(seed_dir, "user-data")) as fh:
parsed = yaml.safe_load(fh.read())
entries = {e["path"]: e for e in parsed["write_files"]}
self.assertEqual(entries[AUTHD_SEED_PATH]["permissions"],
"0600")
self.assertIn("GITEA_TOKEN=" + TOKEN,
entries[AUTHD_SEED_PATH]["content"])
self.assertNotIn("GITEA_TOKEN",
entries["/etc/chv/guest-env"]["content"])
def test_write_seed_authd_entry_carries_sync_keys(self):
# round-11 真机修复(层 2):authd-seed 须同时有
# GITEA_URL/GITEA_USER/SYNC_REPO,build_server 才会放行
#(只 GITEA_TOKEN 时 ValueError,探针 fixprobe3 即从
# guest-env 补这三键);三键只在 guest-env 里有才抄。
import yaml
with tempfile.TemporaryDirectory() as tmp:
seed_dir = os.path.join(tmp, "seed")
write_seed(seed_dir, "vm1", "PRIV", "ssh-ed25519 AAAAx",
guest_env={
"GITEA_URL": "https://gitea.tose.top",
"GITEA_USER": "admin",
"SYNC_REPO": "admin/chv-sync-test",
"SYNC_BRANCH": "main"},
authd_env={"GITEA_TOKEN": TOKEN})
with open(os.path.join(seed_dir, "user-data")) as fh:
parsed = yaml.safe_load(fh.read())
entries = {e["path"]: e for e in parsed["write_files"]}
content = entries[AUTHD_SEED_PATH]["content"]
for key in ("GITEA_URL=https://gitea.tose.top",
"GITEA_USER=admin",
"SYNC_REPO=admin/chv-sync-test",
"GITEA_TOKEN="):
self.assertIn(key, content)
# 非同步键不进 authd-seed。
self.assertNotIn("SYNC_BRANCH", content)
def test_write_seed_authd_entry_without_sync_keys(self):
# guest-env 里没有三键时 authd-seed 只装密钥,不编默认值。
import yaml
with tempfile.TemporaryDirectory() as tmp:
seed_dir = os.path.join(tmp, "seed")
write_seed(seed_dir, "vm1", "PRIV", "ssh-ed25519 AAAAx",
authd_env={"GITEA_TOKEN": TOKEN})
with open(os.path.join(seed_dir, "user-data")) as fh:
parsed = yaml.safe_load(fh.read())
entries = {e["path"]: e for e in parsed["write_files"]}
content = entries[AUTHD_SEED_PATH]["content"]
self.assertIn("GITEA_TOKEN=", content)
self.assertNotIn("GITEA_URL", content)
self.assertNotIn("GITEA_USER", content)
self.assertNotIn("SYNC_REPO", content)
# -- (4) wake 预审 -----------------------------------------------------------
class _FakeResult:
def __init__(self, returncode=0, stdout="", stderr=""):
self.returncode = returncode
self.stdout = stdout
self.stderr = stderr
class _FakeProc:
def __init__(self, alive=True):
self._alive = alive
def poll(self):
return None if self._alive else 1
def terminate(self):
self._alive = False
def wait(self, timeout=None):
return 0
def kill(self):
self._alive = False
def _write_file(path, content=b"x"):
with open(path, "wb") as fh:
fh.write(content)
os.chmod(path, 0o755)
return path
class WakeGateTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.dir = self.tmp.name
self.binary = _write_file(
os.path.join(self.dir, "cloud-hypervisor"))
self.ch_remote = _write_file(os.path.join(self.dir, "ch-remote"))
self.kernel = _write_file(os.path.join(self.dir, "vmlinux"))
self.rootfs = _write_file(os.path.join(self.dir, "disk.raw"),
b"R" * 100)
self.ssh_key = _write_file(os.path.join(self.dir, "id_chv"))
self.kvm = _write_file(os.path.join(self.dir, "kvm"))
self.cpuinfo = os.path.join(self.dir, "cpuinfo")
with open(self.cpuinfo, "w") as fh:
fh.write("flags : fpu vmx sse\n")
self.state_dir = os.path.join(self.dir, "state")
self.audit = []
self.launches = []
def tearDown(self):
self.tmp.cleanup()
def _runner(self, cmd, timeout=None):
if cmd and cmd[0] == "ssh-keygen":
priv = cmd[cmd.index("-f") + 1]
with open(priv, "w") as fh:
fh.write("-----BEGIN OPENSSH PRIVATE KEY-----\nFAKE\n")
os.chmod(priv, 0o600)
with open(priv + ".pub", "w") as fh:
fh.write("ssh-ed25519 AAAAfake1 vm-key\n")
return _FakeResult(0, "", "")
if cmd and cmd[0] == "genisoimage":
out = cmd[cmd.index("-output") + 1]
with open(out, "wb") as fh:
fh.write(b"ISO")
return _FakeResult(0, "", "")
return _FakeResult(0, "", "")
def _make_exec(self, env_extra=None, resolver=None):
env = {"CHV_EGRESS_PROXY": "http://10.0.0.1:8888",
"CHV_HOST_KEY_MODE": "provisioned",
"CHV_SSH_FORWARD": "1"}
if env_extra:
env.update(env_extra)
ex = CloudHypervisorExecutor(
binary=self.binary, ch_remote=self.ch_remote,
kernel=self.kernel, rootfs=self.rootfs,
ssh_key=self.ssh_key, kvm_path=self.kvm,
cpuinfo_path=self.cpuinfo, state_dir=self.state_dir,
runner=self._runner,
launcher=lambda cmd: self.launches.append(list(cmd))
or _FakeProc(alive=True),
api_client=lambda s, m, p, payload=None: (200, {}),
sleeper=lambda s: None, env=env,
sentinel_resolver=resolver)
ex.audit_callback = lambda e, t, d, k=None: self.audit.append(
(e, t.get("id") if isinstance(t, dict) else t, d, k))
return ex
def test_wake_allowlisted_gitea_passes_gate(self):
ex = self._make_exec(
env_extra={"CHV_SENTINEL_ALLOW_HOSTS": "gitea.tose.top"},
resolver=_public_resolver)
out = ex.wake_vm({"id": "gate1", "gitea_sync": dict(FAKE_SYNC)})
self.assertEqual(out["state"], "running")
kinds = [a[0] for a in self.audit]
self.assertIn("sentinel_egress_allow", kinds)
def test_wake_unknown_gitea_host_is_ask_and_fail_closed(self):
ex = self._make_exec(resolver=_public_resolver)
sync = dict(FAKE_SYNC)
sync["GITEA_URL"] = "https://other.example"
with self.assertRaises(ChvFailure) as ctx:
ex.wake_vm({"id": "gate2", "gitea_sync": sync})
self.assertEqual(ctx.exception.kind, "sentinel_egress_denied")
self.assertEqual(self.launches, [])
kinds = [a[0] for a in self.audit]
self.assertIn("sentinel_egress_ask", kinds)
pending = ex._sentinel_queue.list_pending()
self.assertEqual(len(pending), 1)
self.assertEqual(pending[0]["host"], "other.example")
def test_wake_gitea_resolving_to_private_ip_is_deny(self):
ex = self._make_exec(
env_extra={"CHV_SENTINEL_ALLOW_HOSTS": "gitea.tose.top"},
resolver=lambda h, p: ["10.0.0.5"])
with self.assertRaises(ChvFailure) as ctx:
ex.wake_vm({"id": "gate3", "gitea_sync": dict(FAKE_SYNC)})
self.assertEqual(ctx.exception.kind, "sentinel_egress_denied")
kinds = [a[0] for a in self.audit]
self.assertIn("sentinel_egress_denied", kinds)
self.assertEqual(ex._sentinel_queue.list_pending(), [])
def test_wake_without_sync_skips_gate(self):
ex = self._make_exec(resolver=_public_resolver)
out = ex.wake_vm({"id": "gate4"})
self.assertEqual(out["state"], "running")
kinds = [a[0] for a in self.audit]
self.assertNotIn("sentinel_egress_allow", kinds)
self.assertNotIn("sentinel_egress_denied", kinds)
self.assertNotIn("sentinel_egress_ask", kinds)
# -- (5) 客内 authd 守护进程 + helper 端到端 -------------------------------
class GuestAuthdTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.dir = self.tmp.name
self.seed_path = os.path.join(self.dir, "authd-seed")
with open(self.seed_path, "w") as fh:
fh.write("GITEA_URL=https://gitea.tose.top\n"
"GITEA_USER=admin\n"
"GITEA_TOKEN=" + TOKEN + "\n"
"SYNC_REPO=admin/chv-sync-test\n")
self.socket_path = os.path.join(self.dir, "authd.sock")
self.log_path = os.path.join(self.dir, "authd.log")
mod = _load_guest_authd()
self.server = mod.build_server(
seed_path=self.seed_path, socket_path=self.socket_path,
log_path=self.log_path, lib_dir=str(REPO_ROOT),
allowed_uids=[os.getuid()], resolve=_public_resolver)
self.thread = threading.Thread(
target=self.server.serve_forever, daemon=True)
self.thread.start()
def tearDown(self):
self.server.shutdown()
self.server.server_close()
self.tmp.cleanup()
def _ask(self, req):
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.settimeout(5)
try:
sock.connect(self.socket_path)
sock.sendall((json.dumps(req) + "\n").encode("utf-8"))
buf = b""
while b"\n" not in buf:
chunk = sock.recv(4096)
if not chunk:
break
buf += chunk
return json.loads(buf.decode("utf-8"))
finally:
sock.close()
def test_release_for_clone_path(self):
resp = self._ask({"connector": "gitea",
"host": "gitea.tose.top",
"path": "/admin/chv-sync-test.git/info/refs"})
self.assertTrue(resp["ok"])
self.assertEqual(resp["username"], "admin")
self.assertEqual(resp["password"], TOKEN)
self.assertGreater(resp["expires_at"], 0)
def test_bare_git_path_is_pull(self):
# round-11 真机修复(层 4):裸 .git 路径一律当 pull。
resp = self._ask({"connector": "gitea",
"host": "gitea.tose.top",
"path": "/admin/chv-sync-test.git"})
self.assertTrue(resp["ok"])
self.assertEqual(resp["username"], "admin")
self.assertEqual(resp["password"], TOKEN)
def test_socket_uses_group_perms(self):
# round-11 真机修复(层 3):socket 用 group 权限(目录
# 0750、socket 0660),不用探针的 0666;放行 uid 无
# passwd 条目时才回落 root-only(见 SocketGroupTest)。
import stat
mod = _load_guest_authd()
if mod._socket_group_gid([os.getuid()]) is None:
self.skipTest("放行 uid 无 passwd 条目,组权限不可解析")
self.assertEqual(
stat.S_IMODE(os.stat(self.socket_path).st_mode), 0o660)
self.assertEqual(
stat.S_IMODE(os.stat(self.dir).st_mode), 0o750)
def test_second_request_gets_fresh_release(self):
for _ in range(2):
resp = self._ask({"connector": "gitea",
"host": "gitea.tose.top",
"path": "/admin/chv-sync-test.git/"
"git-upload-pack"})
self.assertTrue(resp["ok"])
self.assertEqual(resp["password"], TOKEN)
def test_refused_paths_hosts_and_repos(self):
cases = [
({"connector": "github", "host": "gitea.tose.top",
"path": "/admin/chv-sync-test.git/info/refs"},
"bad_request_fields"),
({"connector": "gitea", "host": "gitea.tose.top",
"path": "/admin/chv-sync-test.git/foo"},
"path_not_allowed"),
({"connector": "gitea", "host": "other.example",
"path": "/admin/chv-sync-test.git/info/refs"},
"sentinel_ask"),
({"connector": "gitea", "host": "gitea.tose.top",
"path": "/foo.git/info/refs"}, "repo_unparseable"),
]
for req, error in cases:
resp = self._ask(req)
self.assertFalse(resp["ok"], req)
self.assertEqual(resp["error"], error, req)
def test_audit_log_never_contains_value(self):
self._ask({"connector": "gitea", "host": "gitea.tose.top",
"path": "/admin/chv-sync-test.git/info/refs"})
self._ask({"connector": "gitea", "host": "gitea.tose.top",
"path": "/admin/chv-sync-test.git/foo"})
with open(self.log_path) as fh:
self.assertNotIn(TOKEN, fh.read())
def test_incomplete_or_missing_seed_fails_fast(self):
mod = _load_guest_authd()
bad_seed = os.path.join(self.dir, "bad-seed")
with open(bad_seed, "w") as fh:
fh.write("GITEA_URL=https://gitea.tose.top\n")
with self.assertRaises(ValueError):
mod.build_server(seed_path=bad_seed,
socket_path=os.path.join(self.dir, "s2"),
log_path=self.log_path,
lib_dir=str(REPO_ROOT),
allowed_uids=[os.getuid()],
resolve=_public_resolver)
with self.assertRaises(FileNotFoundError):
mod.build_server(seed_path=os.path.join(self.dir, "nope"),
socket_path=os.path.join(self.dir, "s3"),
log_path=self.log_path,
lib_dir=str(REPO_ROOT),
allowed_uids=[os.getuid()],
resolve=_public_resolver)
class SocketGroupTest(unittest.TestCase):
"""round-11 真机修复(层 3):socket 共享组 gid 的派生与回落。"""
def test_explicit_group_and_derivation(self):
mod = _load_guest_authd()
# 显式 int 即 gid;显式组名解析;不存在的组名 → None。
self.assertEqual(mod._socket_group_gid([0, 1000], 1234), 1234)
self.assertEqual(mod._socket_group_gid([0, 1000], "root"), 0)
self.assertIsNone(
mod._socket_group_gid([0, 1000], "no-such-group-xyz"))
# 只剩 root → 无组可派,回落 root-only。
self.assertIsNone(mod._socket_group_gid([0]))
self.assertIsNone(mod._socket_group_gid([]))
# 派生:第一个非 root 放行 uid 的主组(用 passwd 里第一个
# 非 root 条目,不依赖当前测试进程的 uid——本机测试以 root
# 跑,os.getuid() 本身就是 0,派生不到组)。
import pwd
others = [p for p in pwd.getpwall() if p.pw_uid != 0]
if not others:
self.skipTest("passwd 里无非 root 用户")
first = sorted(others, key=lambda p: p.pw_uid)[0]
self.assertEqual(
mod._socket_group_gid([0, first.pw_uid]), first.pw_gid)
def test_server_applies_group_perms_with_explicit_group(self):
# 显式给组时服务器落地 0750/0660(本机以 root 跑测试,
# allowed_uids 只有 root,派生不到组,须显式给才能覆到
# AuthdServer 的落盘路径)。
import stat
mod = _load_guest_authd()
with tempfile.TemporaryDirectory() as tmp:
seed_path = os.path.join(tmp, "authd-seed")
with open(seed_path, "w") as fh:
fh.write("GITEA_URL=https://gitea.tose.top\n"
"GITEA_USER=admin\n"
"GITEA_TOKEN=" + TOKEN + "\n"
"SYNC_REPO=admin/chv-sync-test\n")
socket_path = os.path.join(tmp, "authd.sock")
log_path = os.path.join(tmp, "authd.log")
server = mod.build_server(
seed_path=seed_path, socket_path=socket_path,
log_path=log_path, lib_dir=str(REPO_ROOT),
allowed_uids=[os.getuid()], resolve=_public_resolver,
socket_group=os.getgid())
try:
self.assertEqual(server.socket_gid, os.getgid())
self.assertEqual(
stat.S_IMODE(os.stat(socket_path).st_mode), 0o660)
self.assertEqual(
stat.S_IMODE(os.stat(tmp).st_mode), 0o750)
finally:
server.server_close()
class HelperE2ETest(unittest.TestCase):
"""helper + 真 git credential fill(假 authd socket 在后台)。"""
HELPER = REPO_ROOT / "chv-guest" / "git-credential-chv-authd"
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.dir = self.tmp.name
self.guest = GuestAuthdTest("test_release_for_clone_path")
self.guest.setUp()
self.socket_path = self.guest.socket_path
def tearDown(self):
self.guest.tearDown()
self.tmp.cleanup()
def _helper_env(self):
env = dict(os.environ)
env["CHV_AUTHD_SOCKET"] = self.socket_path
return env
def test_helper_returns_credential(self):
proc = subprocess.run(
[sys.executable, str(self.HELPER)],
input="protocol=https\nhost=gitea.tose.top\n"
"path=/admin/chv-sync-test.git/info/refs"
"?service=git-upload-pack\n\n",
capture_output=True, text=True, env=self._helper_env(),
check=False)
self.assertEqual(proc.returncode, 0, proc.stderr)
self.assertIn("username=admin", proc.stdout)
self.assertIn("password=" + TOKEN, proc.stdout)
self.assertIn("password_expiry_utc=", proc.stdout)
def test_helper_refusal_is_exit_1(self):
proc = subprocess.run(
[sys.executable, str(self.HELPER)],
input="protocol=https\nhost=gitea.tose.top\n"
"path=/admin/chv-sync-test.git/foo\n\n",
capture_output=True, text=True, env=self._helper_env(),
check=False)
self.assertEqual(proc.returncode, 1)
self.assertIn("path_not_allowed", proc.stderr)
def test_helper_returns_credential_for_bare_git_path(self):
# round-11 真机修复(层 4):裸 .git 路径一律当 pull,
# helper 须照常返回凭证(git 智能 HTTP 发现请求只发裸路径)。
proc = subprocess.run(
[sys.executable, str(self.HELPER)],
input="protocol=https\nhost=gitea.tose.top\n"
"path=/admin/chv-sync-test.git\n\n",
capture_output=True, text=True, env=self._helper_env(),
check=False)
self.assertEqual(proc.returncode, 0, proc.stderr)
self.assertIn("username=admin", proc.stdout)
self.assertIn("password=" + TOKEN, proc.stdout)
def test_git_credential_fill_end_to_end(self):
env = self._helper_env()
env["GIT_CONFIG_NOSYSTEM"] = "1"
proc = subprocess.run(
["git", "-c", "credential.helper=%s" % self.HELPER,
"-c", "credential.useHttpPath=true",
"credential", "fill"],
input="protocol=https\nhost=gitea.tose.top\n"
"path=/admin/chv-sync-test.git/info/refs\n\n",
capture_output=True, text=True, env=env, cwd=self.dir,
check=False)
self.assertEqual(proc.returncode, 0, proc.stderr)
self.assertIn("password=" + TOKEN, proc.stdout)
if __name__ == "__main__":
unittest.main()