Files
sencho/backend/src/services/network/networkDeleteGuards.ts
T
Anso 8980910153 feat: node-scoped Networking operator page (#1603)
* feat: add node-scoped Networking operator page

Adds a Networking view with overview, topology, inventory, and findings.

Shared aggregate reads back the page; Resources keeps prune and redirects here.

Includes fail-closed network delete guards, operator docs, and /nodes/:slug/networking routing.

* fix: rename unused variable n to _n to satisfy no-unused-vars lint

* fix: keep top bar search clickable when nav grows

* feat: complete Compose-first Networking Phase 2 operator assistant

* fix: move networking action visibility helper out of component module

* feat(networking): complete Compose-first Networking operator page

Finish the node-scoped Networking page (Overview, Networks, Topology,
Findings) with design-system parity and correct finding semantics.

- Rebuild detail sheets on SystemSheet/SheetSection; align the tab band,
  masthead, and mobile tone with Fleet and Security.
- Encode the host-mode and exposure severity matrix; fix collision counts
  so intentional shared externals are not flagged; add one typed drift
  predicate shared by inventory, topology, badges, and overview counts.
- Preserve per-container attachments and IPs on topology node clicks;
  drawer-only click with an explicit logs action; ownership and boolean
  filters; bound large graphs before layout.
- Aggregate cached Compose Doctor findings into the Findings tab with
  honest source labels, structural merge and dedupe, staleness
  reconciliation, and a shared exposure-context helper both engines use.
- Networks tab: privacy-safe service search, precise ownership counts,
  schema v3 with version-2 adapters on every endpoint, pre-confirm delete
  reasons, and the shared sortable table with an internal scroll region.
- Interop: Fleet node-card networking signal with pending-intent
  navigation, stack-to-node backlink, and Dossier/Drift deep links.
- Enrich sanitized inspect with an allowlisted connected-container list;
  fetch topology once and filter client-side.
- Docs and tests across every new finding kind, adapter, and flow.

* fix(networking): correct drift count, exposure fail-soft, and inspect crash paths

Address code-review findings on the Networking page implementation:
- Fix the Overview drift count to use the shared drift-kind predicate instead
  of a hardcoded list that omitted external-network-missing.
- Gate Compose Doctor's unclassified-exposure and reverse-proxy-undocumented
  rules on exposure-context availability, so a DB read failure no longer
  fabricates findings (mirrors the live engine's existing fail-soft behavior).
- Guard the per-stack exposure-intent read in topology aggregation so a
  transient DB failure degrades to unknown intent instead of failing the
  whole response.
- Harden the network detail drawer against a partial inspect payload from an
  older remote node, and log the real error instead of a bare catch.
- Remove now-duplicated severity-rank and drift-kind helpers in favor of the
  shared modules; drop dead backend-only exports; widen the frontend schema
  version type to a plain number instead of casting past a literal type.
- Add coverage for the delete-guard precedence, the full host-mode severity
  matrix, the schema-2 compatibility adapter, and the sanitized connected-
  container allowlist; tighten two tests that were not exercising the
  behavior they claimed to.

* fix: add missing onOpenNodeNetworking prop to FleetView experimental test

The added required prop on FleetViewProps broke the merge-build when the
test file (on main but not on this branch) was compiled against the
updated FleetView interface.
2026-07-14 20:18:10 -04:00

72 lines
2.2 KiB
TypeScript

/**
* Fail-closed guards for admin network delete on /api/system/networks/delete.
*/
import SelfIdentityService from '../SelfIdentityService';
import type { DependencySnapshot } from '../DockerController';
import type { StackNetworkFacts } from './types';
import type { NetworkingNetworkBase } from './networkingTypes';
export type NetworkDeleteBlockCode =
| 'system-network'
| 'sencho-owned'
| 'attached'
| 'stack-declared'
| 'stack-declaration-unknown';
export interface NetworkDeleteGuardResult {
blocked: boolean;
code?: NetworkDeleteBlockCode;
error?: string;
}
export function evaluateNetworkDeleteGuard(
networkId: string,
snapshot: DependencySnapshot,
stackFacts: StackNetworkFacts[],
baseRow?: NetworkingNetworkBase,
): NetworkDeleteGuardResult {
const net = baseRow
?? snapshot.networks.find(n => n.id === networkId || n.name === networkId);
if (!net) {
return { blocked: false };
}
if (net.isSystem) {
return { blocked: true, code: 'system-network', error: 'System networks cannot be deleted.' };
}
const self = SelfIdentityService.getInstance();
if (self.isOwnNetwork(net.id) || self.isOwnNetwork(net.name)) {
return { blocked: true, code: 'sencho-owned', error: 'Sencho-managed networks cannot be deleted.' };
}
const connected = snapshot.containers.some(c =>
c.networks.some(a => a.id === net.id || a.name === net.name),
);
if (connected) {
return { blocked: true, code: 'attached', error: 'Detach all containers before deleting this network.' };
}
const hasUnrenderable = stackFacts.some(f => !f.renderable);
const declaredNames = new Set<string>();
for (const facts of stackFacts) {
if (!facts.renderable) continue;
for (const n of facts.networks) declaredNames.add(n.name);
}
if (declaredNames.has(net.name)) {
return { blocked: true, code: 'stack-declared', error: 'This network is declared by a Compose stack.' };
}
if (hasUnrenderable && (net.composeProject || net.stack)) {
return {
blocked: true,
code: 'stack-declaration-unknown',
error: 'Cannot verify stack declarations while one or more stacks failed to render.',
};
}
return { blocked: false };
}