Files
Nexus/server/application/services/files_sudoers_service.py
T

141 lines
5.0 KiB
Python
Raw Normal View History

"""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 与白名单路径",
}