fix(network): защитить ключи WireGuard и режим просмотра операций
This commit is contained in:
1 parent
c8f9c83f45
commit
a404da768d
23 files changed
+1648
-36
No files matched your search
@@ -1497,7 +1497,7 @@ function FiltersPage() {
|
||||
const allTunnels = !isLive ? greTunnels : (greByServer[selectedServerId] ?? [])
|
||||
const selectedServer = allServers.find(s => s.id === selectedServerId) ?? allServers[0]
|
||||
const totalRules = rulesets.reduce((s, r) => s + r.rules.length, 0)
|
||||
const mutationsLocked = movePending || (isLive && (applyBusy || historyRestoring || rulesReading || readyServerId !== selectedServerId || liveStale || liveLoadState !== "idle"))
|
||||
const mutationsLocked = (isLive && !can("mm:filters:write")) || movePending || (isLive && (applyBusy || historyRestoring || rulesReading || readyServerId !== selectedServerId || liveStale || liveLoadState !== "idle"))
|
||||
|
||||
const filterRailItems = useMemo<ServerTileItem[]>(() => (
|
||||
allServers.map((s) => ({
|
||||
@@ -1548,6 +1548,7 @@ function FiltersPage() {
|
||||
nextRules: FilterRule[] | (() => Promise<FilterRule[]>),
|
||||
source: "apply" | "copy" = "apply",
|
||||
) => {
|
||||
if (isLive && !can("mm:filters:write")) return { ok: false, error: "Нет прав на изменение фильтров" }
|
||||
if (!mounted.current) return { ok: false, error: "Источник данных изменён" }
|
||||
if (applyLock.current || applyBusy || historyRestoring) return { ok: false, error: "Дождитесь завершения текущей операции" }
|
||||
if (isLive && (liveStale || liveLoadState !== "idle" || (serverId === selectedServerId && readyServerId !== serverId))) {
|
||||
@@ -1864,6 +1865,7 @@ function FiltersPage() {
|
||||
}
|
||||
banner={
|
||||
<>
|
||||
{isLive && !can("mm:filters:write") && <p role="status" className="border-b px-6 py-2 text-sm text-muted-foreground">Режим просмотра. Изменение фильтров недоступно.</p>}
|
||||
{isLive && liveLoadState === "error" && (
|
||||
<div className="shrink-0 border-b border-destructive/30 bg-destructive/10 px-6 py-2.5 text-xs text-destructive flex items-center gap-2">
|
||||
<AlertCircleIcon className="size-3.5 shrink-0" />
|
||||
|
||||
@@ -2169,7 +2169,7 @@ function FirewallPageInner() {
|
||||
|
||||
const scopedFailures = isLive ? failures.filter(f => (selectedServerId === ALL_SERVERS_ID || f.serverId === selectedServerId) && (!f.family || f.family === ipFamily)) : []
|
||||
const firewallIncomplete = scopedFailures.length > 0
|
||||
const firewallWriteBlocked = firewallIncomplete || (isLive && !readTrusted)
|
||||
const firewallWriteBlocked = (isLive && !can("mm:network:write")) || firewallIncomplete || (isLive && !readTrusted)
|
||||
const scopedSliceTimes = sliceTimes.filter(slice => (selectedServerId === ALL_SERVERS_ID || slice.serverId === selectedServerId) && slice.family === ipFamily)
|
||||
const freshness = firewallTimeRange(scopedSliceTimes)
|
||||
const totalEnabled = familyRules.filter((r) => r.enabled).length
|
||||
@@ -2530,6 +2530,7 @@ function FirewallPageInner() {
|
||||
showAll
|
||||
allCount={displayServers.length}
|
||||
loading={isLive && dataLoading && displayServers.length === 0}
|
||||
banner={isLive && !can("mm:network:write") ? <p role="status" className="border-b px-4 py-2 text-sm text-muted-foreground">Режим просмотра. Изменение конфигурации недоступно.</p> : undefined}
|
||||
header={
|
||||
<PageHeader
|
||||
crumbs={[{ label: "Управление" }, { label: "Firewall" }]}
|
||||
|
||||
@@ -257,7 +257,7 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
||||
|
||||
|
||||
const historyServerId = selectedServerId === ALL_SERVERS_ID ? null : selectedServerId
|
||||
const mutationsLocked = !isLive || mutateBusy || dataLoading || !hasLoaded || liveStale || historyRestoring
|
||||
const mutationsLocked = !isLive || !can("mm:network:write") || mutateBusy || dataLoading || !hasLoaded || liveStale || historyRestoring
|
||||
function canManage(serverId: string) {
|
||||
return !mutationsLocked && !failedServerIds.includes(serverId) && liveServers.some(s => s.id === serverId && s.enabled)
|
||||
}
|
||||
@@ -548,6 +548,7 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
||||
showAll
|
||||
allCount={displayServers.length}
|
||||
loading={isLive && dataLoading && displayServers.length === 0}
|
||||
banner={isLive && !can("mm:network:write") ? <p role="status" className="border-b px-4 py-2 text-sm text-muted-foreground">Режим просмотра. Изменение конфигурации недоступно.</p> : undefined}
|
||||
header={
|
||||
<PageHeader
|
||||
crumbs={[{ label: "Управление" }, { label: "GRE-туннели" }]}
|
||||
|
||||
@@ -320,7 +320,7 @@ function IpsecPage({ selectedServerId, setSelectedServerId, workspaceTab, setWor
|
||||
const failedServerIds = new Set(failures.map(f => f.serverId))
|
||||
const scopeUnknown = missingSelectedServer || isLive && (!hasLoaded || !!dataError || (effectiveServerId === ALL_SERVERS_ID ? failures.length > 0 : failedServerIds.has(effectiveServerId)))
|
||||
|
||||
const canManage = (serverId: string) => isLive && hasLoaded && !dataError && !inventoryError &&
|
||||
const canManage = (serverId: string) => isLive && can("mm:network:write") && hasLoaded && !dataError && !inventoryError &&
|
||||
!loading && !busy && !historyRestoring && !failedServerIds.has(serverId) && liveServers.some(s => s.id === serverId)
|
||||
const assertDeviceReady = (serverId: string) => {
|
||||
if (operationLock.current || !canManage(serverId)) throw new Error("Сначала подтвердите актуальное состояние устройства: повторите опрос IPsec")
|
||||
@@ -637,6 +637,7 @@ function IpsecPage({ selectedServerId, setSelectedServerId, workspaceTab, setWor
|
||||
showAll
|
||||
allCount={displayClients.length}
|
||||
loading={isLive && loading && displayServers.length === 0}
|
||||
banner={isLive && !can("mm:network:write") ? <p role="status" className="border-b px-4 py-2 text-sm text-muted-foreground">Режим просмотра. Изменение конфигурации недоступно.</p> : undefined}
|
||||
header={
|
||||
<PageHeader
|
||||
crumbs={[{ label: "Управление" }, { label: "IPsec / IKEv2" }]}
|
||||
|
||||
@@ -184,6 +184,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
|
||||
}) {
|
||||
const { mode, backendUrl } = useDataSource()
|
||||
const isLive = mode === "live"
|
||||
const canWrite = !isLive || can("mm:network:write")
|
||||
|
||||
const [liveIfaces, setLiveIfaces] = useState<WgIfaceWithServer[]>([])
|
||||
const [liveServers, setLiveServers] = useState<Server[]>([])
|
||||
@@ -446,6 +447,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
|
||||
setCreateError(null)
|
||||
setBusy(true)
|
||||
try {
|
||||
if (!can("mm:network:write")) throw new Error("Нет прав на изменение сети или экспорт ключей")
|
||||
const ep = parseEndpoint(form.peerEndpoint)
|
||||
await createWireGuardInterface(backendUrl, {
|
||||
serverId: form.serverId,
|
||||
@@ -495,6 +497,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
|
||||
setImportError(null)
|
||||
setBusy(true)
|
||||
try {
|
||||
if (!can("mm:network:write")) throw new Error("Нет прав на изменение сети или экспорт ключей")
|
||||
const res = await importWireGuard(backendUrl, {
|
||||
serverId: args.serverId,
|
||||
content: args.content,
|
||||
@@ -538,6 +541,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
|
||||
setTogglePending(context)
|
||||
setToggleError(null)
|
||||
try {
|
||||
if (!can("mm:network:write")) throw new Error("Нет прав на изменение сети или экспорт ключей")
|
||||
const result = await patchWireGuardInterface(backendUrl, iface.serverId, iface.rosId, {
|
||||
disabled: iface.enabled,
|
||||
})
|
||||
@@ -564,6 +568,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
|
||||
setDeleteBusy(true)
|
||||
setDeleteError(null)
|
||||
try {
|
||||
if (!can("mm:network:write")) throw new Error("Нет прав на изменение сети или экспорт ключей")
|
||||
if (pendingDelete.kind === "iface") {
|
||||
if (!pendingDelete.iface.rosId) {
|
||||
if (active.current) toast.info("Доступно только в live-режиме")
|
||||
@@ -599,6 +604,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
|
||||
setPeerError(null)
|
||||
setBusy(true)
|
||||
try {
|
||||
if (!can("mm:network:write")) throw new Error("Нет прав на изменение сети или экспорт ключей")
|
||||
const ep = parseEndpoint(form.endpoint)
|
||||
await createWireGuardPeer(backendUrl, {
|
||||
serverId: peerIface.serverId,
|
||||
@@ -630,6 +636,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
|
||||
exportPending.current = epoch
|
||||
setExportBusy(true)
|
||||
try {
|
||||
if (!can("mm:network:write")) throw new Error("Нет прав на изменение сети или экспорт ключей")
|
||||
const peer = format === "peer-conf" ? selectWgExportPeer(exportIface.peers, exportPeerId) : undefined
|
||||
if (format === "peer-conf" && !peer) throw new Error("Выбранный пир не найден. Обновите список и выберите пира заново.")
|
||||
const res = await exportWireGuard(backendUrl, {
|
||||
@@ -671,7 +678,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
|
||||
}
|
||||
|
||||
const emptyCreateAction = (
|
||||
<Button size="sm" disabled={!!urlError || !catalogKnown || loadError} onClick={() => { setCreateError(null); setCreateOpen(true) }}>
|
||||
<Button size="sm" disabled={!canWrite || !!urlError || !catalogKnown || loadError} onClick={() => { setCreateError(null); setCreateOpen(true) }}>
|
||||
<PlusIcon className="size-4" />
|
||||
Новый интерфейс
|
||||
</Button>
|
||||
@@ -687,6 +694,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
|
||||
showAll
|
||||
allCount={completeScope ? displayIfaces.length : undefined}
|
||||
loading={isLive && loading && displayServers.length === 0}
|
||||
banner={!canWrite ? <p role="status" className="border-b px-4 py-2 text-sm text-muted-foreground">Режим просмотра. Изменение конфигурации и экспорт ключей недоступны.</p> : undefined}
|
||||
header={
|
||||
<PageHeader
|
||||
crumbs={[{ label: "Управление" }, { label: "WireGuard" }]}
|
||||
@@ -719,11 +727,11 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button size="sm" variant="outline" disabled={!!urlError || !catalogKnown || loadError} onClick={() => { setImportError(null); setImportOpen(true) }}>
|
||||
<Button size="sm" variant="outline" disabled={!canWrite || !!urlError || !catalogKnown || loadError} onClick={() => { setImportError(null); setImportOpen(true) }}>
|
||||
<UploadIcon className="size-4" />
|
||||
Импорт
|
||||
</Button>
|
||||
<Button size="sm" disabled={!!urlError || !catalogKnown || loadError} onClick={() => { setCreateError(null); setCreateOpen(true) }}>
|
||||
<Button size="sm" disabled={!canWrite || !!urlError || !catalogKnown || loadError} onClick={() => { setCreateError(null); setCreateOpen(true) }}>
|
||||
<PlusIcon className="size-4" />
|
||||
Новый интерфейс
|
||||
</Button>
|
||||
@@ -843,15 +851,16 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
|
||||
/>
|
||||
{catalogKnown && !urlError && (completeScope || filteredIfaces.length > 0) ? <WireguardDataGrid
|
||||
interfaces={filteredIfaces}
|
||||
canWrite={canWrite}
|
||||
compactServer={compactServer}
|
||||
emptyAction={emptyCreateAction}
|
||||
onExport={(iface) => openExport(iface, "rsc")}
|
||||
onAddPeer={selectedSourceError ? undefined : iface => { setPeerError(null); setPeerIface(iface) }}
|
||||
onToggle={selectedSourceError ? undefined : handleToggle}
|
||||
onAddPeer={!canWrite || selectedSourceError ? undefined : iface => { setPeerError(null); setPeerIface(iface) }}
|
||||
onToggle={!canWrite || selectedSourceError ? undefined : handleToggle}
|
||||
toggleBusy={togglePending !== null}
|
||||
onDelete={selectedSourceError ? undefined : (iface) => setPendingDelete({ kind: "iface", iface })}
|
||||
onDeletePeer={selectedSourceError ? undefined : (iface, peerId) => setPendingDelete({ kind: "peer", iface, peerId })}
|
||||
onExportPeer={(iface, peerId) => openExport(iface, "peer", peerId)}
|
||||
onDelete={!canWrite || selectedSourceError ? undefined : (iface) => setPendingDelete({ kind: "iface", iface })}
|
||||
onDeletePeer={!canWrite || selectedSourceError ? undefined : (iface, peerId) => setPendingDelete({ kind: "peer", iface, peerId })}
|
||||
onExportPeer={canWrite ? (iface, peerId) => openExport(iface, "peer", peerId) : undefined}
|
||||
/> : <p role="status" className="px-5 py-8 text-sm text-muted-foreground">Каталог интерфейсов пока неизвестен</p>}
|
||||
</DataPageCard>
|
||||
</TabsContent>
|
||||
@@ -869,22 +878,22 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
|
||||
compactServer={compactServer}
|
||||
emptyAction={
|
||||
scopedIfaces.length === 1 ? (
|
||||
<Button size="sm" disabled={selectedSourceError} onClick={() => { setPeerError(null); setPeerIface(scopedIfaces[0]) }}>
|
||||
<Button size="sm" disabled={!canWrite || selectedSourceError} onClick={() => { setPeerError(null); setPeerIface(scopedIfaces[0]) }}>
|
||||
<PlusIcon className="size-4" />
|
||||
Добавить пира
|
||||
</Button>
|
||||
) : emptyCreateAction
|
||||
}
|
||||
onDeletePeer={selectedSourceError ? undefined : (row) => {
|
||||
onDeletePeer={!canWrite || selectedSourceError ? undefined : (row) => {
|
||||
const iface = scopedIfaces.find((i) => i.id === row.ifaceId && i.serverId === row.serverId)
|
||||
if (!iface) return
|
||||
setPendingDelete({ kind: "peer", iface, peerId: row.id })
|
||||
}}
|
||||
onExportPeer={(row) => {
|
||||
onExportPeer={canWrite ? (row) => {
|
||||
const iface = scopedIfaces.find((i) => i.id === row.ifaceId && i.serverId === row.serverId)
|
||||
if (!iface) return
|
||||
openExport(iface, "peer", row.id)
|
||||
}}
|
||||
} : undefined}
|
||||
/> : <p role="status" className="px-5 py-8 text-sm text-muted-foreground">Каталог пиров пока неизвестен</p>}
|
||||
</DataPageCard>
|
||||
</TabsContent>
|
||||
|
||||
Reference in new issue
Block a user