feat(chv): 对齐 Muse P3 收尾——authd 替身凭证 + Sentinel 出口审批
- 新测试 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 清单
This commit is contained in:
1 parent
06c3e172fe
commit
a90cf78d84
15 files changed
+2370
-93
No files matched your search
Executable
+107
@@ -0,0 +1,107 @@
|
||||
#!/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())
|
||||
Reference in new issue
Block a user