feat: per-host WinRM/SSH credentials override (0.5.17)

Allow host-level login/password for home/workgroup PCs while keeping global domain admins in Settings.
This commit is contained in:
2026-07-14 20:04:42 +10:00
parent 3d18f73965
commit 9883e6aab3
20 changed files with 613 additions and 64 deletions
+6 -4
View File
@@ -28,12 +28,12 @@ from app.services.host_sessions import (
mark_event_session_terminated,
terminate_session_for_event,
)
from app.services.linux_admin_settings import get_effective_linux_admin_config
from app.services.linux_admin_settings import get_effective_linux_admin_for_host
from app.services.ssh_connect import (
HostNotLinuxError as SshHostNotLinuxError,
HostTargetMissingError as SshHostTargetMissingError,
)
from app.services.win_admin_settings import get_effective_win_admin_config
from app.services.win_admin_settings import get_effective_win_admin_for_host
from app.services.winrm_connect import HostNotWindowsError, HostTargetMissingError
from app.services.ingest import ingest_event
from app.services.event_summary import event_to_summary
@@ -284,8 +284,10 @@ def post_event_terminate_session(
if event_session_terminated(event, db=db):
raise HTTPException(status_code=409, detail="Session already terminated for this event")
linux_cfg = get_effective_linux_admin_config(db)
win_cfg = get_effective_win_admin_config(db)
if event.host is None:
raise HTTPException(status_code=400, detail="Event has no host")
linux_cfg = get_effective_linux_admin_for_host(db, event.host)
win_cfg = get_effective_win_admin_for_host(db, event.host)
sid = (body.session_id if body else None) or event_session_id(event)
try:
+109 -16
View File
@@ -46,8 +46,12 @@ from app.services.host_sessions import (
terminate_windows_session,
)
from app.services.host_delete import delete_host_and_related
from app.services.linux_admin_settings import get_effective_linux_admin_config
from app.services.win_admin_settings import get_effective_win_admin_config
from app.services.host_mgmt_credentials import (
get_host_mgmt_access_view,
upsert_host_mgmt_credentials,
)
from app.services.linux_admin_settings import get_effective_linux_admin_for_host
from app.services.win_admin_settings import get_effective_win_admin_for_host
from app.services.winrm_connect import (
HostNotWindowsError,
HostTargetMissingError,
@@ -299,6 +303,23 @@ class HostAgentConfigResponse(BaseModel):
settings: dict[str, object]
class HostMgmtAccessResponse(BaseModel):
host_id: int
has_override: bool
user: str | None = None
password_set: bool = False
password_hint: str | None = None
effective_source: str
effective_configured: bool
effective_user: str | None = None
class HostMgmtAccessUpdate(BaseModel):
user: str | None = None
password: str | None = None
clear: bool = False
class HostRemoteActionStartResponse(BaseModel):
status: str
host_id: int
@@ -469,9 +490,12 @@ def test_host_winrm(
if host is None:
raise HTTPException(status_code=404, detail="Host not found")
cfg = get_effective_win_admin_config(db)
cfg = get_effective_win_admin_for_host(db, host)
if not cfg.configured:
raise HTTPException(status_code=400, detail="Windows domain admin is not configured")
raise HTTPException(
status_code=400,
detail="Windows admin is not configured (host override or Settings → Windows)",
)
try:
targets = iter_winrm_targets(host)
@@ -532,9 +556,12 @@ def test_host_ssh(
if host is None:
raise HTTPException(status_code=404, detail="Host not found")
cfg = get_effective_linux_admin_config(db)
cfg = get_effective_linux_admin_for_host(db, host)
if not cfg.configured:
raise HTTPException(status_code=400, detail="Linux SSH admin is not configured")
raise HTTPException(
status_code=400,
detail="Linux SSH admin is not configured (host override or Settings → Linux)",
)
response = _run_ssh_action_on_host(host, cfg, action=test_ssh_connection)
_set_ssh_admin_status(host, response.ok)
@@ -556,9 +583,12 @@ def update_host_agent_via_ssh(
if host is None:
raise HTTPException(status_code=404, detail="Host not found")
cfg = get_effective_linux_admin_config(db)
cfg = get_effective_linux_admin_for_host(db, host)
if not cfg.configured:
raise HTTPException(status_code=400, detail="Linux SSH admin is not configured")
raise HTTPException(
status_code=400,
detail="Linux SSH admin is not configured (host override or Settings → Linux)",
)
title = "Обновление ssh-monitor (SSH)"
try:
@@ -686,9 +716,12 @@ def list_host_sessions(
from app.services.winrm_connect import is_windows_host
if is_linux_host(host):
cfg = get_effective_linux_admin_config(db)
cfg = get_effective_linux_admin_for_host(db, host)
if not cfg.configured:
raise HTTPException(status_code=400, detail="Linux SSH admin is not configured")
raise HTTPException(
status_code=400,
detail="Linux SSH admin is not configured (host override or Settings → Linux)",
)
try:
sessions, result = list_linux_sessions(host, cfg)
except SshHostNotLinuxError as exc:
@@ -703,9 +736,12 @@ def list_host_sessions(
)
if is_windows_host(host):
cfg = get_effective_win_admin_config(db)
cfg = get_effective_win_admin_for_host(db, host)
if not cfg.configured:
raise HTTPException(status_code=400, detail="Windows domain admin is not configured")
raise HTTPException(
status_code=400,
detail="Windows admin is not configured (host override or Settings → Windows)",
)
try:
sessions, result = list_windows_sessions(host, cfg)
except HostNotWindowsError as exc:
@@ -743,9 +779,12 @@ def terminate_host_session(
raise HTTPException(status_code=422, detail="session_id is required")
if is_linux_host(host):
cfg = get_effective_linux_admin_config(db)
cfg = get_effective_linux_admin_for_host(db, host)
if not cfg.configured:
raise HTTPException(status_code=400, detail="Linux SSH admin is not configured")
raise HTTPException(
status_code=400,
detail="Linux SSH admin is not configured (host override or Settings → Linux)",
)
try:
result = terminate_linux_session(host, cfg, sid)
except SshHostNotLinuxError as exc:
@@ -761,9 +800,12 @@ def terminate_host_session(
)
if is_windows_host(host):
cfg = get_effective_win_admin_config(db)
cfg = get_effective_win_admin_for_host(db, host)
if not cfg.configured:
raise HTTPException(status_code=400, detail="Windows domain admin is not configured")
raise HTTPException(
status_code=400,
detail="Windows admin is not configured (host override or Settings → Windows)",
)
try:
result = terminate_windows_session(host, cfg, sid)
except HostNotWindowsError as exc:
@@ -803,6 +845,57 @@ def patch_host_agent_config(
)
@router.get("/{host_id}/access", response_model=HostMgmtAccessResponse)
def get_host_access(
host_id: int,
db: Session = Depends(get_db),
_user=Depends(require_admin),
) -> HostMgmtAccessResponse:
host = db.get(Host, host_id)
if host is None:
raise HTTPException(status_code=404, detail="Host not found")
view = get_host_mgmt_access_view(db, host)
return HostMgmtAccessResponse(
host_id=view.host_id,
has_override=view.has_override,
user=view.user,
password_set=view.password_set,
password_hint=view.password_hint,
effective_source=view.effective_source,
effective_configured=view.effective_configured,
effective_user=view.effective_user,
)
@router.put("/{host_id}/access", response_model=HostMgmtAccessResponse)
def put_host_access(
host_id: int,
body: HostMgmtAccessUpdate,
db: Session = Depends(get_db),
_user=Depends(require_admin),
) -> HostMgmtAccessResponse:
host = db.get(Host, host_id)
if host is None:
raise HTTPException(status_code=404, detail="Host not found")
view = upsert_host_mgmt_credentials(
db,
host,
user=body.user,
password=body.password,
clear=body.clear,
)
return HostMgmtAccessResponse(
host_id=view.host_id,
has_override=view.has_override,
user=view.user,
password_set=view.password_set,
password_hint=view.password_hint,
effective_source=view.effective_source,
effective_configured=view.effective_configured,
effective_user=view.effective_user,
)
@router.get("/{host_id}/agent-config", response_model=HostAgentConfigResponse)
def get_host_agent_config(
host_id: int,