mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-20 01:01:20 +00:00
Add persistent TLS bypass warning banners
This commit is contained in:
parent
5aaa8d98b2
commit
7e4f1f474e
9 changed files with 409 additions and 0 deletions
|
|
@ -7,6 +7,7 @@ import {
|
|||
formLabel,
|
||||
formSelect,
|
||||
} from '@/components/shared/Form';
|
||||
import { TlsVerificationWarningBanner } from '@/components/shared/TlsVerificationWarningBanner';
|
||||
import { MonitoredSystemAdmissionPreview } from '../../MonitoredSystemAdmissionPreview';
|
||||
import type { TrueNASConnection } from '@/api/truenas';
|
||||
import type { TrueNASSettingsPanelState } from '../../useTrueNASSettingsPanelState';
|
||||
|
|
@ -191,6 +192,12 @@ export const TrueNASCredentialSlot: Component<TrueNASCredentialSlotProps> = (pro
|
|||
<span class={formHelpText}>Optional certificate pin for HTTPS connections.</span>
|
||||
</label>
|
||||
<div class="space-y-3 rounded-md border border-border bg-surface-alt px-4 py-3">
|
||||
<Show when={props.state.form().insecureSkipVerify}>
|
||||
<TlsVerificationWarningBanner
|
||||
subject="this TrueNAS connection"
|
||||
remediation="Install a trusted certificate or configure the TLS fingerprint before using this in production."
|
||||
/>
|
||||
</Show>
|
||||
<label class="flex items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
formHelpText,
|
||||
formLabel,
|
||||
} from '@/components/shared/Form';
|
||||
import { TlsVerificationWarningBanner } from '@/components/shared/TlsVerificationWarningBanner';
|
||||
import { MonitoredSystemAdmissionPreview } from '../../MonitoredSystemAdmissionPreview';
|
||||
import type { VMwareConnection } from '@/api/vmware';
|
||||
import type { VMwareSettingsPanelState } from '../../useVMwareSettingsPanelState';
|
||||
|
|
@ -141,6 +142,12 @@ export const VMwareCredentialSlot: Component<VMwareCredentialSlotProps> = (props
|
|||
</div>
|
||||
|
||||
<div class="space-y-3 rounded-md border border-border bg-surface-alt px-4 py-3">
|
||||
<Show when={props.state.form().insecureSkipVerify}>
|
||||
<TlsVerificationWarningBanner
|
||||
subject="this vCenter connection"
|
||||
remediation="Install a trusted certificate for vCenter before using this in production."
|
||||
/>
|
||||
</Show>
|
||||
<label class="flex items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import connectionEditorSource from '../ConnectionEditor/ConnectionEditor.tsx?raw
|
|||
import addressProbeStepSource from '../ConnectionEditor/AddressProbeStep.tsx?raw';
|
||||
import connectionEditorStateSource from '../ConnectionEditor/useConnectionEditor.ts?raw';
|
||||
import nodeCredentialSlotSource from '../ConnectionEditor/CredentialSlots/NodeCredentialSlot.tsx?raw';
|
||||
import trueNASCredentialSlotSource from '../ConnectionEditor/CredentialSlots/TrueNASCredentialSlot.tsx?raw';
|
||||
import vmwareCredentialSlotSource from '../ConnectionEditor/CredentialSlots/VMwareCredentialSlot.tsx?raw';
|
||||
import diagnosticsResultsPanelSource from '../DiagnosticsResultsPanel.tsx?raw';
|
||||
import diagnosticsModelSource from '../diagnosticsModel.ts?raw';
|
||||
|
||||
|
|
@ -149,6 +151,18 @@ describe('settings architecture guardrails', () => {
|
|||
expect(nodeCredentialSlotSource).toContain('<NodeModalMonitoringSection');
|
||||
expect(nodeCredentialSlotSource).toContain('<NodeModalStatusFooter');
|
||||
expect(nodeCredentialSlotSource).not.toContain('<Dialog');
|
||||
|
||||
expect(vmwareCredentialSlotSource).toContain('TlsVerificationWarningBanner');
|
||||
expect(vmwareCredentialSlotSource).toContain('subject="this vCenter connection"');
|
||||
expect(vmwareCredentialSlotSource).toContain(
|
||||
'Install a trusted certificate for vCenter before using this in production.',
|
||||
);
|
||||
|
||||
expect(trueNASCredentialSlotSource).toContain('TlsVerificationWarningBanner');
|
||||
expect(trueNASCredentialSlotSource).toContain('subject="this TrueNAS connection"');
|
||||
expect(trueNASCredentialSlotSource).toContain(
|
||||
'Install a trusted certificate or configure the TLS fingerprint before using this in production.',
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps diagnostics commercial funnel rendering on the shared results/model boundary', () => {
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ import summaryMetricCardSource from '@/components/shared/SummaryMetricCard.tsx?r
|
|||
import summaryPanelSource from '@/components/shared/SummaryPanel.tsx?raw';
|
||||
import summarySynchronizedReadoutSource from '@/components/shared/SummarySynchronizedReadout.tsx?raw';
|
||||
import tagBadgesSource from '@/components/shared/TagBadges.tsx?raw';
|
||||
import tlsVerificationWarningBannerSource from '@/components/shared/TlsVerificationWarningBanner.tsx?raw';
|
||||
import commandPaletteStateSource from '@/components/shared/useCommandPaletteState.ts?raw';
|
||||
import columnPickerStateSource from '@/components/shared/useColumnPickerState.ts?raw';
|
||||
import tagInputStateSource from '@/components/shared/useTagInputState.ts?raw';
|
||||
|
|
@ -587,6 +588,14 @@ describe('shared primitive guardrails', () => {
|
|||
expect(reportingPanelSource).not.toContain('rounded-md border border-blue-200 bg-blue-50 p-6');
|
||||
});
|
||||
|
||||
it('keeps TLS verification warnings in the shared primitive boundary', () => {
|
||||
expect(tlsVerificationWarningBannerSource).toContain('role="alert"');
|
||||
expect(tlsVerificationWarningBannerSource).toContain('TLS verification disabled.');
|
||||
expect(tlsVerificationWarningBannerSource).toContain('controlled lab environments');
|
||||
expect(tlsVerificationWarningBannerSource).toContain('Install a trusted certificate');
|
||||
expect(tlsVerificationWarningBannerSource).not.toContain('CalloutCard');
|
||||
});
|
||||
|
||||
it('keeps shared fleet limit banner copy on the monitored-system commercial term', () => {
|
||||
expect(monitoredSystemLimitWarningBannerModelSource).toContain(
|
||||
'@/utils/monitoredSystemPresentation',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
import { splitProps, type JSX } from 'solid-js';
|
||||
|
||||
interface TlsVerificationWarningBannerProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
||||
subject: string;
|
||||
remediation?: string;
|
||||
}
|
||||
|
||||
const bannerClass =
|
||||
'rounded-md border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900 dark:border-amber-700 dark:bg-amber-950/40 dark:text-amber-200';
|
||||
|
||||
export function TlsVerificationWarningBanner(props: TlsVerificationWarningBannerProps) {
|
||||
const [local, rest] = splitProps(props, ['subject', 'remediation', 'class']);
|
||||
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
class={`${bannerClass} ${local.class ?? ''}`.trim()}
|
||||
{...rest}
|
||||
>
|
||||
<span class="font-medium">TLS verification disabled.</span>{' '}
|
||||
Pulse will accept untrusted certificates for {local.subject}. Use this only for
|
||||
controlled lab environments.{' '}
|
||||
{local.remediation ?? 'Install a trusted certificate before using this in production.'}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TlsVerificationWarningBanner;
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import { cleanup, render, screen } from '@solidjs/testing-library';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { TlsVerificationWarningBanner } from '../TlsVerificationWarningBanner';
|
||||
|
||||
describe('TlsVerificationWarningBanner', () => {
|
||||
afterEach(() => cleanup());
|
||||
|
||||
it('renders the shared TLS risk copy with the supplied subject', () => {
|
||||
render(() => <TlsVerificationWarningBanner subject="this connection" />);
|
||||
|
||||
expect(screen.getByRole('alert')).toHaveTextContent(
|
||||
'TLS verification disabled. Pulse will accept untrusted certificates for this connection. Use this only for controlled lab environments. Install a trusted certificate before using this in production.',
|
||||
);
|
||||
});
|
||||
|
||||
it('renders custom remediation guidance when provided', () => {
|
||||
render(() => (
|
||||
<TlsVerificationWarningBanner
|
||||
subject="this endpoint"
|
||||
remediation="Install a trusted certificate on the endpoint before using this in production."
|
||||
/>
|
||||
));
|
||||
|
||||
expect(screen.getByRole('alert')).toHaveTextContent(
|
||||
'Install a trusted certificate on the endpoint before using this in production.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { Show } from 'solid-js';
|
||||
|
||||
import { SettingsPanel } from '@/components/shared/SettingsPanel';
|
||||
import { TlsVerificationWarningBanner } from '@/components/shared/TlsVerificationWarningBanner';
|
||||
import { Toggle } from '@/components/shared/Toggle';
|
||||
import {
|
||||
formControl,
|
||||
|
|
@ -207,6 +208,13 @@ export function AlertAppriseDestinationsSection(props: AlertAppriseDestinationsS
|
|||
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>
|
||||
{ALERT_DESTINATIONS_APPRISE_TLS_LABEL}
|
||||
</label>
|
||||
<Show when={props.config.skipTlsVerify}>
|
||||
<TlsVerificationWarningBanner
|
||||
class="mb-3"
|
||||
subject="this Apprise API endpoint"
|
||||
remediation="Install a trusted certificate on the Apprise server before using this in production."
|
||||
/>
|
||||
</Show>
|
||||
<label class="inline-flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import alertOverridesStateSource from '@/features/alerts/useAlertOverridesState.
|
|||
import alertDestinationsModelSource from '@/features/alerts/alertDestinationsModel.ts?raw';
|
||||
import alertDestinationsStateSource from '@/features/alerts/useAlertDestinationsState.ts?raw';
|
||||
import alertDestinationsTabStateSource from '@/features/alerts/useAlertDestinationsTabState.ts?raw';
|
||||
import alertAppriseDestinationsSectionSource from '@/features/alerts/AlertAppriseDestinationsSection.tsx?raw';
|
||||
import alertWebhookDestinationsStateSource from '@/features/alerts/useAlertWebhookDestinationsState.ts?raw';
|
||||
import alertAcknowledgementStateSource from '@/features/alerts/useAlertAcknowledgementState.ts?raw';
|
||||
import alertHistoryAdministrationCardSource from '@/features/alerts/AlertHistoryAdministrationCard.tsx?raw';
|
||||
|
|
@ -424,6 +425,11 @@ describe('tab path helpers', () => {
|
|||
expect(alertDestinationsTabSource).not.toContain('ALERT_DESTINATIONS_EMAIL_PANEL_TITLE');
|
||||
expect(alertDestinationsTabSource).not.toContain('ALERT_DESTINATIONS_APPRISE_PANEL_TITLE');
|
||||
expect(alertDestinationsTabSource).not.toContain('getAlertWebhooksSectionTitle');
|
||||
expect(alertAppriseDestinationsSectionSource).toContain('TlsVerificationWarningBanner');
|
||||
expect(alertAppriseDestinationsSectionSource).toContain('subject="this Apprise API endpoint"');
|
||||
expect(alertAppriseDestinationsSectionSource).toContain(
|
||||
'Install a trusted certificate on the Apprise server before using this in production.',
|
||||
);
|
||||
expect(alertHistoryTabSource).not.toContain('useAlertIncidentTimelineState');
|
||||
expect(alertHistoryTabSource).not.toContain('AlertsAPI.getHistory');
|
||||
expect(alertHistoryTabSource).not.toContain('AlertsAPI.getIncidentsForResource');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,301 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { expect, test as base } from "@playwright/test";
|
||||
import { createAuthenticatedStorageState } from "./helpers";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
type WorkerFixtures = {
|
||||
authStorageStatePath: string;
|
||||
};
|
||||
|
||||
const VMWARE_SCREENSHOT_PATH = path.resolve(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"tmp",
|
||||
"vmware-tls-warning-banner.png",
|
||||
);
|
||||
const TRUENAS_SCREENSHOT_PATH = path.resolve(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"tmp",
|
||||
"truenas-tls-warning-banner.png",
|
||||
);
|
||||
const APPRISE_SCREENSHOT_PATH = path.resolve(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"tmp",
|
||||
"apprise-tls-warning-banner.png",
|
||||
);
|
||||
|
||||
const buildSafeTrueNASAdmissionPreview = () => ({
|
||||
current_count: 1,
|
||||
projected_count: 1,
|
||||
additional_count: 0,
|
||||
limit: 10,
|
||||
would_exceed_limit: false,
|
||||
effect: "attaches_existing",
|
||||
current_systems: [],
|
||||
projected_systems: [],
|
||||
current_system: null,
|
||||
projected_system: null,
|
||||
});
|
||||
|
||||
const buildSafeVMwareAdmissionPreview = () => ({
|
||||
current_count: 1,
|
||||
projected_count: 1,
|
||||
additional_count: 0,
|
||||
limit: 10,
|
||||
would_exceed_limit: false,
|
||||
effect: "attaches_existing",
|
||||
current_systems: [],
|
||||
projected_systems: [],
|
||||
current_system: null,
|
||||
projected_system: null,
|
||||
});
|
||||
|
||||
const test = base.extend<{}, WorkerFixtures>({
|
||||
storageState: async ({ authStorageStatePath }, use) => {
|
||||
await use(authStorageStatePath);
|
||||
},
|
||||
authStorageStatePath: [
|
||||
async ({ browser }, use, workerInfo) => {
|
||||
const storageStatePath = path.resolve(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"tmp",
|
||||
"playwright-auth",
|
||||
`tls-warning-banners-${workerInfo.project.name}.json`,
|
||||
);
|
||||
fs.mkdirSync(path.dirname(storageStatePath), { recursive: true });
|
||||
await createAuthenticatedStorageState(browser, storageStatePath);
|
||||
try {
|
||||
await use(storageStatePath);
|
||||
} finally {
|
||||
fs.rmSync(storageStatePath, { force: true });
|
||||
}
|
||||
},
|
||||
{ scope: "worker" },
|
||||
],
|
||||
});
|
||||
|
||||
async function mockVMwareConnections(page: import("@playwright/test").Page) {
|
||||
await page.route("**/api/vmware/connections**", async (route) => {
|
||||
const request = route.request();
|
||||
const pathname = new URL(request.url()).pathname;
|
||||
|
||||
if (pathname === "/api/vmware/connections" && request.method() === "GET") {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify([]),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
pathname === "/api/vmware/connections/preview" &&
|
||||
request.method() === "POST"
|
||||
) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(buildSafeVMwareAdmissionPreview()),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await route.continue();
|
||||
});
|
||||
}
|
||||
|
||||
async function mockTrueNASConnections(page: import("@playwright/test").Page) {
|
||||
await page.route("**/api/truenas/connections**", async (route) => {
|
||||
const request = route.request();
|
||||
const pathname = new URL(request.url()).pathname;
|
||||
|
||||
if (pathname === "/api/truenas/connections" && request.method() === "GET") {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify([]),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
pathname === "/api/truenas/connections/preview" &&
|
||||
request.method() === "POST"
|
||||
) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(buildSafeTrueNASAdmissionPreview()),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await route.continue();
|
||||
});
|
||||
}
|
||||
|
||||
async function mockAlertDestinations(page: import("@playwright/test").Page) {
|
||||
await page.route("**/api/alerts/config", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
enabled: true,
|
||||
activationState: "active",
|
||||
overrides: {},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route("**/api/alerts/active", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify([]),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route("**/api/notifications/email", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
enabled: false,
|
||||
provider: "",
|
||||
server: "",
|
||||
port: 587,
|
||||
username: "",
|
||||
password: "",
|
||||
from: "",
|
||||
to: [],
|
||||
tls: false,
|
||||
startTLS: false,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route("**/api/notifications/apprise", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
enabled: true,
|
||||
mode: "http",
|
||||
targets: [],
|
||||
cliPath: "apprise",
|
||||
timeoutSeconds: 15,
|
||||
serverUrl: "https://apprise.lab.local",
|
||||
configKey: "",
|
||||
apiKey: "",
|
||||
apiKeyHeader: "X-API-KEY",
|
||||
skipTlsVerify: false,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route("**/api/notifications/webhooks", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify([]),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test.describe("TLS verification warning banners", () => {
|
||||
test.setTimeout(180_000);
|
||||
|
||||
test("keeps a persistent warning visible while editing a VMware connection", async ({
|
||||
page,
|
||||
}) => {
|
||||
await mockVMwareConnections(page);
|
||||
|
||||
await page.goto("/settings/infrastructure", {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
await page.waitForURL(/\/settings\/infrastructure/, {
|
||||
timeout: 15_000,
|
||||
});
|
||||
|
||||
await page.getByRole("button", { name: "Add infrastructure" }).click();
|
||||
await page.getByRole("button", { name: "VMware" }).click();
|
||||
await page.getByLabel("Skip TLS verification").check();
|
||||
|
||||
const warning = page.getByRole("alert").filter({
|
||||
hasText: "this vCenter connection",
|
||||
});
|
||||
await expect(warning).toBeVisible();
|
||||
await expect(warning).toContainText(
|
||||
"Install a trusted certificate for vCenter before using this in production.",
|
||||
);
|
||||
|
||||
fs.mkdirSync(path.dirname(VMWARE_SCREENSHOT_PATH), { recursive: true });
|
||||
await page.screenshot({ path: VMWARE_SCREENSHOT_PATH, fullPage: true });
|
||||
});
|
||||
|
||||
test("keeps a persistent warning visible while editing a TrueNAS connection", async ({
|
||||
page,
|
||||
}) => {
|
||||
await mockTrueNASConnections(page);
|
||||
|
||||
await page.goto("/settings/infrastructure", {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
await page.waitForURL(/\/settings\/infrastructure/, {
|
||||
timeout: 15_000,
|
||||
});
|
||||
|
||||
await page.getByRole("button", { name: "Add infrastructure" }).click();
|
||||
await page.getByRole("button", { name: "TrueNAS" }).click();
|
||||
await page.getByLabel("Skip TLS verification").check();
|
||||
|
||||
const warning = page.getByRole("alert").filter({
|
||||
hasText: "this TrueNAS connection",
|
||||
});
|
||||
await expect(warning).toBeVisible();
|
||||
await expect(warning).toContainText(
|
||||
"Install a trusted certificate or configure the TLS fingerprint before using this in production.",
|
||||
);
|
||||
|
||||
fs.mkdirSync(path.dirname(TRUENAS_SCREENSHOT_PATH), { recursive: true });
|
||||
await page.screenshot({ path: TRUENAS_SCREENSHOT_PATH, fullPage: true });
|
||||
});
|
||||
|
||||
test("keeps a persistent warning visible while enabling Apprise self-signed certificates", async ({
|
||||
page,
|
||||
}) => {
|
||||
await mockAlertDestinations(page);
|
||||
|
||||
await page.goto("/alerts/destinations", {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
await page.waitForURL(/\/alerts\/destinations/, { timeout: 15_000 });
|
||||
|
||||
await expect(
|
||||
page.getByRole("heading", { level: 1, name: "Notification Destinations" }),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByLabel("Allow self-signed certificates").check();
|
||||
|
||||
const warning = page.getByRole("alert").filter({
|
||||
hasText: "this Apprise API endpoint",
|
||||
});
|
||||
await expect(warning).toBeVisible();
|
||||
await expect(warning).toContainText(
|
||||
"Install a trusted certificate on the Apprise server before using this in production.",
|
||||
);
|
||||
|
||||
fs.mkdirSync(path.dirname(APPRISE_SCREENSHOT_PATH), { recursive: true });
|
||||
await page.screenshot({ path: APPRISE_SCREENSHOT_PATH, fullPage: true });
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue