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:
@@ -532,6 +532,31 @@ export function updateLinuxAdminSettings(payload: {
|
||||
});
|
||||
}
|
||||
|
||||
export interface HostMgmtAccess {
|
||||
host_id: number;
|
||||
has_override: boolean;
|
||||
user: string | null;
|
||||
password_set: boolean;
|
||||
password_hint: string | null;
|
||||
effective_source: string;
|
||||
effective_configured: boolean;
|
||||
effective_user: string | null;
|
||||
}
|
||||
|
||||
export function fetchHostAccess(hostId: number): Promise<HostMgmtAccess> {
|
||||
return apiFetch<HostMgmtAccess>(`/api/v1/hosts/${hostId}/access`);
|
||||
}
|
||||
|
||||
export function updateHostAccess(
|
||||
hostId: number,
|
||||
payload: { user?: string | null; password?: string | null; clear?: boolean },
|
||||
): Promise<HostMgmtAccess> {
|
||||
return apiFetch<HostMgmtAccess>(`/api/v1/hosts/${hostId}/access`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export interface HostSshActionResult {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */
|
||||
export const APP_NAME = "Security Alert Center";
|
||||
export const APP_VERSION = "0.5.16";
|
||||
export const APP_VERSION = "0.5.17";
|
||||
export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;
|
||||
|
||||
@@ -69,6 +69,52 @@
|
||||
<dd>{{ host.event_count ?? 0 }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<section v-if="showAgentConfig" class="host-mgmt-access">
|
||||
<h3>{{ isWindowsHost ? "WinRM / доступ к хосту" : "SSH / доступ к хосту" }}</h3>
|
||||
<p class="muted">
|
||||
Переопределение для этого ПК. Если пусто — используются глобальные учётные данные из
|
||||
<RouterLink to="/settings">Настройки</RouterLink>
|
||||
({{ isWindowsHost ? "Windows admin" : "Linux admin" }}).
|
||||
Сейчас effective:
|
||||
<strong>{{ accessEffectiveLabel }}</strong>
|
||||
</p>
|
||||
<form class="agent-config-form" @submit.prevent="saveHostAccess">
|
||||
<label class="config-field">
|
||||
Логин
|
||||
<input
|
||||
v-model="accessForm.user"
|
||||
type="text"
|
||||
:placeholder="isWindowsHost ? 'COMPUTER\\user или .\\Administrator' : 'root'"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</label>
|
||||
<label class="config-field">
|
||||
Пароль
|
||||
<input
|
||||
v-model="accessForm.password"
|
||||
type="password"
|
||||
:placeholder="accessForm.passwordSet ? '•••• (оставьте пустым, чтобы не менять)' : ''"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</label>
|
||||
<div class="host-actions-row">
|
||||
<button type="submit" :disabled="savingAccess">
|
||||
{{ savingAccess ? "Сохранение…" : "Сохранить доступ" }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="secondary"
|
||||
:disabled="savingAccess || !accessForm.hasOverride"
|
||||
@click="clearHostAccess"
|
||||
>
|
||||
Сбросить (глобальные)
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="accessMessage" :class="accessOk ? 'success' : 'error'">{{ accessMessage }}</p>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div v-if="isWindowsHost" class="host-actions">
|
||||
<button
|
||||
type="button"
|
||||
@@ -296,6 +342,8 @@ import {
|
||||
import {
|
||||
apiFetch,
|
||||
fetchHost,
|
||||
fetchHostAccess,
|
||||
updateHostAccess,
|
||||
fetchRecentEvents,
|
||||
testHostWinRm,
|
||||
testHostSsh,
|
||||
@@ -303,6 +351,7 @@ import {
|
||||
patchHostAgentConfig,
|
||||
type EventListResponse,
|
||||
type HostDetail,
|
||||
type HostMgmtAccess,
|
||||
} from "../api";
|
||||
import { emitHostPatch } from "../utils/hostPatchBus";
|
||||
import HostSessionsPanel from "../components/HostSessionsPanel.vue";
|
||||
@@ -354,6 +403,32 @@ const configForm = reactive({
|
||||
serverDisplayName: "",
|
||||
getInventory: true,
|
||||
});
|
||||
const savingAccess = ref(false);
|
||||
const accessMessage = ref("");
|
||||
const accessOk = ref(false);
|
||||
const accessForm = reactive({
|
||||
user: "",
|
||||
password: "",
|
||||
passwordSet: false,
|
||||
hasOverride: false,
|
||||
effectiveSource: "",
|
||||
effectiveUser: "",
|
||||
effectiveConfigured: false,
|
||||
});
|
||||
const accessEffectiveLabel = computed(() => {
|
||||
if (!accessForm.effectiveConfigured) {
|
||||
return "не настроено";
|
||||
}
|
||||
const src =
|
||||
accessForm.effectiveSource === "host"
|
||||
? "override хоста"
|
||||
: accessForm.effectiveSource === "db"
|
||||
? "глобальные (Settings)"
|
||||
: accessForm.effectiveSource === "env"
|
||||
? "env"
|
||||
: accessForm.effectiveSource;
|
||||
return `${accessForm.effectiveUser || "—"} · ${src}`;
|
||||
});
|
||||
let sshTestHideTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let winRmTestHideTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let sshProbeSeq = 0;
|
||||
@@ -678,6 +753,67 @@ async function saveAgentConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
async function applyAccessView(view: HostMgmtAccess) {
|
||||
accessForm.user = view.user || "";
|
||||
accessForm.password = "";
|
||||
accessForm.passwordSet = view.password_set;
|
||||
accessForm.hasOverride = view.has_override;
|
||||
accessForm.effectiveSource = view.effective_source;
|
||||
accessForm.effectiveUser = view.effective_user || "";
|
||||
accessForm.effectiveConfigured = view.effective_configured;
|
||||
}
|
||||
|
||||
async function loadHostAccess() {
|
||||
accessMessage.value = "";
|
||||
try {
|
||||
const view = await fetchHostAccess(hostId.value);
|
||||
applyAccessView(view);
|
||||
} catch (e) {
|
||||
accessOk.value = false;
|
||||
accessMessage.value = e instanceof Error ? e.message : "Не удалось загрузить доступ хоста";
|
||||
}
|
||||
}
|
||||
|
||||
async function saveHostAccess() {
|
||||
savingAccess.value = true;
|
||||
accessMessage.value = "";
|
||||
try {
|
||||
const payload: { user?: string | null; password?: string | null } = {
|
||||
user: accessForm.user,
|
||||
};
|
||||
if (accessForm.password.trim()) {
|
||||
payload.password = accessForm.password;
|
||||
}
|
||||
const view = await updateHostAccess(hostId.value, payload);
|
||||
applyAccessView(view);
|
||||
accessOk.value = true;
|
||||
accessMessage.value = view.has_override
|
||||
? "Сохранено (override хоста)"
|
||||
: "Сохранено (логин очищен — глобальные настройки)";
|
||||
} catch (e) {
|
||||
accessOk.value = false;
|
||||
accessMessage.value = e instanceof Error ? e.message : "Ошибка сохранения доступа";
|
||||
} finally {
|
||||
savingAccess.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function clearHostAccess() {
|
||||
savingAccess.value = true;
|
||||
accessMessage.value = "";
|
||||
try {
|
||||
const view = await updateHostAccess(hostId.value, { clear: true });
|
||||
applyAccessView(view);
|
||||
accessOk.value = true;
|
||||
accessMessage.value = "Override сброшен — используются глобальные настройки";
|
||||
} catch (e) {
|
||||
accessOk.value = false;
|
||||
accessMessage.value = e instanceof Error ? e.message : "Ошибка сброса доступа";
|
||||
} finally {
|
||||
savingAccess.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadHost() {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
@@ -692,6 +828,7 @@ async function loadHost() {
|
||||
try {
|
||||
const detail = await fetchHost(hostId.value);
|
||||
applyHostDetail(detail);
|
||||
void loadHostAccess();
|
||||
if (isLinuxHostDetail(detail)) {
|
||||
void probeSshOnOpen();
|
||||
}
|
||||
@@ -848,6 +985,49 @@ watch(
|
||||
color: #9aa4b2;
|
||||
}
|
||||
|
||||
.host-mgmt-access {
|
||||
margin: 1rem 0 1.25rem;
|
||||
padding: 0.85rem 1rem;
|
||||
border: 1px solid #2a3340;
|
||||
border-radius: 8px;
|
||||
background: #141a22;
|
||||
}
|
||||
|
||||
.host-mgmt-access h3 {
|
||||
margin: 0 0 0.35rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.host-mgmt-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.65rem;
|
||||
margin-top: 0.75rem;
|
||||
max-width: 28rem;
|
||||
}
|
||||
|
||||
.host-mgmt-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.host-mgmt-field input {
|
||||
padding: 0.4rem 0.55rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #3a4554;
|
||||
background: #0f1419;
|
||||
color: #e8eef5;
|
||||
}
|
||||
|
||||
.host-mgmt-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.host-actions {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
|
||||
@@ -126,8 +126,9 @@
|
||||
<section class="card settings-card settings-policy">
|
||||
<h2>Windows (qwinsta / logoff)</h2>
|
||||
<p class="settings-hint settings-hint-top">
|
||||
Доменный admin для удалённых команд на Windows-хостах (qwinsta, logoff через агент).
|
||||
Глобальный (доменный) admin для удалённых команд на Windows-хостах (qwinsta, logoff, WinRM-update).
|
||||
Формат логина: <code>ДОМЕН\пользователь</code> (например <code>B26\papatramp</code>).
|
||||
Для отдельного ПК (home/workgroup) задайте override в карточке <strong>Хосты → доступ к хосту</strong>.
|
||||
Пока запись не создана в UI — используются <code>SAC_WIN_ADMIN_*</code> из <code>sac-api.env</code>.
|
||||
</p>
|
||||
<form class="settings-form" @submit.prevent="saveWinAdminSettings">
|
||||
@@ -186,8 +187,9 @@
|
||||
<section class="card settings-card settings-policy">
|
||||
<h2>Linux (SSH / обновление ssh-monitor)</h2>
|
||||
<p class="settings-hint settings-hint-top">
|
||||
SSH-учётка для удалённого запуска <code>/opt/scripts/update_ssh_monitor.sh</code> с карточки Linux-хоста.
|
||||
Глобальная SSH-учётка для удалённого запуска <code>/opt/scripts/update_ssh_monitor.sh</code> с карточки Linux-хоста.
|
||||
Рекомендуется <code>root</code> или пользователь с <code>sudo NOPASSWD</code>.
|
||||
Для отдельного сервера задайте override в карточке <strong>Хосты → доступ к хосту</strong>.
|
||||
Пока запись не создана в UI — используются <code>SAC_LINUX_ADMIN_*</code> из <code>sac-api.env</code>.
|
||||
</p>
|
||||
<form class="settings-form" @submit.prevent="saveLinuxAdminSettings">
|
||||
|
||||
Reference in New Issue
Block a user