Files
Nexus/server/application/services/files_sudoers_service.py
T
Nexus Agent 84b388fae2 fix(sync): rsync 推送非 root 自动 sudo 提权
文件推送与文件管理对齐:非 root 默认 auto_sudo 时使用 sudo -n rsync;
新增批量 sudoers 补丁脚本以补全 rsync 白名单。
2026-06-11 23:20:45 +08:00

141 lines
5.0 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.
"""Shared Nexus files sudoers template and remote install helpers."""
from __future__ import annotations
import asyncio
import shlex
import textwrap
from server.domain.models import Server
from server.infrastructure.ssh.asyncssh_pool import exec_ssh_command, ssh_pool
from server.infrastructure.database.crypto import decrypt_value
from server.infrastructure.ssh.remote_shell import exec_ssh_command_with_fallback
NEXUS_FILES_SUDOERS_PATH = "/etc/sudoers.d/nexus-files"
def build_nexus_files_sudoers(ssh_user: str) -> str:
"""Return sudoers.d fragment for file manager + rsync push elevation."""
user = (ssh_user or "deploy").strip() or "deploy"
return textwrap.dedent(f"""\
# nexus-files: configured by Nexus (file manager + rsync push)
{user} ALL=(root) NOPASSWD: \\
/bin/ls, /usr/bin/ls, \\
/bin/cat, /usr/bin/cat, \\
/usr/bin/tee, /bin/tee, \\
/bin/mkdir, /usr/bin/mkdir, \\
/bin/cp, /usr/bin/cp, \\
/bin/mv, /usr/bin/mv, \\
/bin/rm, /usr/bin/rm, \\
/bin/chmod, /usr/bin/chmod, \\
/bin/chown, /usr/bin/chown, \\
/bin/bash, /usr/bin/bash, \\
/usr/bin/rsync, /bin/rsync
""")
async def probe_rsync_sudo(server: Server, *, timeout: int = 15) -> bool:
"""True when ``sudo -n rsync`` works on the target."""
ssh_user = (server.username or "root").strip() or "root"
if ssh_user == "root":
result = await exec_ssh_command(server, "rsync --version", timeout=timeout)
return result.get("exit_code") == 0
result = await exec_ssh_command(server, "sudo -n rsync --version", timeout=timeout)
return result.get("exit_code") == 0
async def _write_sudoers_with_password(
server: Server,
content: str,
sudoers_path: str,
plain_password: str,
) -> dict:
write_cmd = (
f"printf %s {shlex.quote(content)}"
f" | sudo -S tee {shlex.quote(sudoers_path)} > /dev/null"
f" && sudo chmod 440 {shlex.quote(sudoers_path)}"
f" && sudo visudo -cf {shlex.quote(sudoers_path)}"
)
conn = None
try:
conn = await ssh_pool.acquire(server)
result = await asyncio.wait_for(
conn.run(write_cmd, input=plain_password + "\n", timeout=20),
timeout=25,
)
exit_code = result.exit_status if result.exit_status is not None else -1
stderr_out = (result.stderr or "").strip()
stdout_out = (result.stdout or "").strip()
if exit_code != 0:
detail = stderr_out or stdout_out or f"exit {exit_code}"
return {"ok": False, "action": "password_sudo_failed", "detail": detail[:500]}
return {"ok": True, "action": "patched_password_sudo"}
finally:
if conn is not None:
await ssh_pool.release(server.id)
async def install_nexus_files_sudoers(server: Server, *, dry_run: bool = False) -> dict:
"""Ensure ``/etc/sudoers.d/nexus-files`` includes rsync (and file ops whitelist).
Returns ``{ok, action, server_id, server_name, ssh_user, detail?}``.
"""
ssh_user = (server.username or "root").strip() or "root"
base = {
"server_id": server.id,
"server_name": server.name,
"ssh_user": ssh_user,
}
if ssh_user == "root":
return {**base, "ok": True, "action": "skip_root"}
if await probe_rsync_sudo(server):
return {**base, "ok": True, "action": "already_ok"}
if dry_run:
return {**base, "ok": True, "action": "would_patch"}
content = build_nexus_files_sudoers(ssh_user)
sudoers_path = NEXUS_FILES_SUDOERS_PATH
if server.password:
plain = decrypt_value(server.password)
outcome = await _write_sudoers_with_password(server, content, sudoers_path, plain)
if not outcome["ok"]:
return {**base, **outcome}
if await probe_rsync_sudo(server):
return {**base, **outcome}
return {
**base,
"ok": False,
"action": "written_but_rsync_probe_failed",
"detail": "sudoers 已写入但 sudo -n rsync 仍失败",
}
write_inner = (
f"printf %s {shlex.quote(content)}"
f" | tee {shlex.quote(sudoers_path)} > /dev/null"
f" && chmod 440 {shlex.quote(sudoers_path)}"
f" && visudo -cf {shlex.quote(sudoers_path)}"
)
result = await exec_ssh_command_with_fallback(server, write_inner, timeout=30)
if result.get("exit_code") != 0:
detail = (result.get("stderr") or result.get("stdout") or "")[:500]
return {
**base,
"ok": False,
"action": "nopasswd_write_failed",
"detail": detail or "无法写入 sudoers(需存储 SSH 密码或已配置 bash 免密 sudo",
}
if await probe_rsync_sudo(server):
return {**base, "ok": True, "action": "patched_nopasswd"}
return {
**base,
"ok": False,
"action": "written_but_rsync_probe_failed",
"detail": "sudoers 已写入但 sudo -n rsync 仍失败,请检查 visudo 与白名单路径",
}