- 新测试 cloud/tests/test_chv_muse_p3.py 30 项:替身过期、真 token 泄露面、SSRF 挡截、deny/ask 路径、authd-seed 拆分、wake 预审、 客内 authd 守护进程、git credential helper 端到端(真 git fill) - 8 个旧测试按新 authd 行为改写(未删测试、未放宽断言): GITEA_TOKEN 改走 authd-seed,build_ssh_command 不再 source 进 codex 环境;wake 测试补 Sentinel 白名单 + 假解析器 - 7 处超 50 行函数拆到 50 行内(不改行为);补 authd/sentinel 公共 API 注解,mypy 在改动文件上干净 - 真问题修复:git 默认不把 path 发给 helper,chv-sync boot 加 credential.useHttpPath=true,否则 authd 路径每次都拒绝 - 全量 cloud 套件 1897 项 OK(skipped=8) 真机待验证(未做,等第三棒让出 WSL 测试机):见 docs/chv-muse-p3-gap.md §3.6 清单
108 lines
3.9 KiB
Python
Executable File
108 lines
3.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""git credential helper:向 chv-authd 按次取凭证(feat-chv-muse-p3)。
|
||
|
||
git credential 协议:git 把 `protocol/host/path`(每行 `key=value`,
|
||
空行结束)写进本进程 stdin;本进程向 authd 的 Unix socket 发
|
||
一次 JSON 请求,拿到用户名/口令后按协议回给 git(属性 + 空行)。
|
||
|
||
口令只进 git 子进程内存:本进程不写文件、不打日志(拒绝时只
|
||
报 kind,不含值);`password_expiry_utc` 按释放的 expires_at
|
||
回给 git,git 在该时间后重新问 helper(不缓存过期凭证)。
|
||
authd 拒绝 / socket 不通时本进程 exit 1——git 操作随之失败
|
||
(fail-closed,绝不回落成“无凭证也照推”)。
|
||
|
||
用法(chv-sync boot 配好后 git 自动调,不用手工跑):
|
||
git config --global credential.helper /usr/local/bin/git-credential-chv-authd
|
||
git config --global credential.useHttpPath true
|
||
(后者不可少:git 默认不把 path 发给 helper,没有 path 本进程
|
||
无法推断仓库与操作,会直接拒绝。)
|
||
socket 路径可用 CHV_AUTHD_SOCKET 覆盖(测试用 /tmp 下的假 socket)。
|
||
"""
|
||
|
||
import json
|
||
import os
|
||
import socket
|
||
import sys
|
||
|
||
DEFAULT_SOCKET_PATH = "/run/chv-authd/authd.sock"
|
||
SOCKET_TIMEOUT_SECONDS = 5
|
||
|
||
|
||
def _read_credential_request():
|
||
"""读 git credential 协议的请求属性(空行结束)→ dict。"""
|
||
attrs = {}
|
||
for line in sys.stdin:
|
||
line = line.rstrip("\n")
|
||
if not line:
|
||
break
|
||
if "=" in line:
|
||
key, value = line.split("=", 1)
|
||
attrs[key.strip()] = value.strip()
|
||
return attrs
|
||
|
||
|
||
def _ask_authd(socket_path, host, path):
|
||
"""向 authd 要一次凭证:返回 (username, password, expires_at)。
|
||
拒绝/通信失败即 RuntimeError(只含 kind,不含值)。"""
|
||
req = {"connector": "gitea", "host": host, "path": path}
|
||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||
sock.settimeout(SOCKET_TIMEOUT_SECONDS)
|
||
try:
|
||
sock.connect(socket_path)
|
||
sock.sendall((json.dumps(req) + "\n").encode("utf-8"))
|
||
chunks = []
|
||
while True:
|
||
data = sock.recv(65536)
|
||
if not data:
|
||
break
|
||
chunks.append(data)
|
||
if b"\n" in data:
|
||
break
|
||
except OSError as exc:
|
||
raise RuntimeError("authd_unreachable:%s" % exc)
|
||
finally:
|
||
sock.close()
|
||
try:
|
||
resp = json.loads(b"".join(chunks).decode("utf-8"))
|
||
except ValueError:
|
||
raise RuntimeError("authd_bad_response")
|
||
if not resp.get("ok"):
|
||
raise RuntimeError("authd_refused:%s"
|
||
% resp.get("error", "refused"))
|
||
return (resp["username"], resp["password"],
|
||
resp.get("expires_at"))
|
||
|
||
|
||
def main(argv=None):
|
||
"""helper 入口:读协议 → 问 authd → 回属性。拒绝即 exit 1。"""
|
||
attrs = _read_credential_request()
|
||
host = attrs.get("host", "")
|
||
path = attrs.get("path", "")
|
||
if attrs.get("protocol") != "https" or not host or not path:
|
||
print("git-credential-chv-authd: 请求不完整(须 protocol=https "
|
||
"+ host + path),拒绝", file=sys.stderr)
|
||
return 1
|
||
if not path.startswith("/"):
|
||
path = "/" + path
|
||
# git 的 info/refs 路径带 ?service=... 查询串:Sentinel 的
|
||
# L7 规则按干净路径匹配,这里先去掉查询串再问 authd。
|
||
path = path.split("?", 1)[0]
|
||
socket_path = os.environ.get("CHV_AUTHD_SOCKET") \
|
||
or DEFAULT_SOCKET_PATH
|
||
try:
|
||
username, password, expires_at = _ask_authd(
|
||
socket_path, host, path)
|
||
except RuntimeError as exc:
|
||
print("git-credential-chv-authd: %s" % exc, file=sys.stderr)
|
||
return 1
|
||
print("username=%s" % username)
|
||
print("password=%s" % password)
|
||
if expires_at:
|
||
print("password_expiry_utc=%d" % int(expires_at))
|
||
print()
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|