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。
This commit is contained in:
1 parent
2d12179b9a
commit
9f593d62ef
3 files changed
+165
-15
No files matched your search
@@ -66,13 +66,16 @@ _BLOCKED_NETWORKS = tuple(
|
||||
|
||||
# 每连接器的 L7 路径策略:(方法, 匹配方式, 模式)。
|
||||
# gitea:API 只放 /api/v1/;git 智能 HTTP 只放三条(clone/pull/push
|
||||
# 的实际路径,见 git http-protocol)。其它路径(Web UI、登录页、
|
||||
# 的实际路径,见 git http-protocol)加裸 .git(round-11 真机修复
|
||||
# 层 4:git 智能 HTTP 的发现请求只发裸 .git 路径,裸路径只放 GET、
|
||||
# 一律当 pull;POST 裸 .git 仍拒绝)。其它路径(Web UI、登录页、
|
||||
# 任意管理接口)一律不在本连接器的能力内。
|
||||
_CONNECTOR_PATH_RULES = {
|
||||
"gitea": (
|
||||
("GET", "prefix", "/api/v1/"),
|
||||
("POST", "prefix", "/api/v1/"),
|
||||
("GET", "suffix", ".git/info/refs"),
|
||||
("GET", "suffix", ".git"),
|
||||
("POST", "suffix", ".git/git-upload-pack"),
|
||||
("POST", "suffix", ".git/git-receive-pack"),
|
||||
),
|
||||
|
||||
@@ -271,14 +271,18 @@ class SentinelTest(unittest.TestCase):
|
||||
(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 智能 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(
|
||||
@@ -598,6 +602,28 @@ class GuestAuthdTest(unittest.TestCase):
|
||||
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",
|
||||
@@ -655,6 +681,60 @@ class GuestAuthdTest(unittest.TestCase):
|
||||
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 在后台)。"""
|
||||
|
||||
@@ -699,6 +779,19 @@ class HelperE2ETest(unittest.TestCase):
|
||||
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"
|
||||
|
||||
Reference in new issue
Block a user