fix v2 hardening review findings

This commit is contained in:
ajnart committed 2026-07-28 15:28:58 +02:00
1 parent b82a6a7abc
commit a7eae3ff81
35 files changed
+1147 -277

No files matched your search

+19 -13
View File
@@ -6,19 +6,19 @@ import { stringify as stringifySuperJson } from "superjson";
import type { StartedTestContainer } from "testcontainers";
import { describe, test } from "vitest";
import { eq } from "@homarr/db";
import { eq } from "drizzle-orm";
import {
customWidgetDefinitions,
customWidgetSecrets,
legacyCustomWidgetDefinitions,
legacyCustomWidgetSecrets,
} from "@homarr/db/schema";
import type { HomarrCustomWidgetV2Input } from "@homarr/custom-widgets/core";
} from "../packages/db/schema/sqlite";
import type { HomarrCustomWidgetV2Input } from "../packages/custom-widgets/src/core";
import { createHomarrContainer } from "./shared/create-homarr-container";
import { createSqliteDbFileAsync } from "./shared/e2e-db";
import type { MockApiServer } from "./shared/mock-api-server";
import { startMockApiServerAsync } from "./shared/mock-api-server";
import { exposeHostPortToContainersAsync, startMockApiServerAsync } from "./shared/mock-api-server";
import { seedAdminUserAsync } from "./shared/seed-admin-user";
const adminCredentials = { username: "admin", password: "Comp(exP4sswOrd" };
@@ -122,7 +122,7 @@ const startWorkshopMockServerAsync = async (widget: HomarrCustomWidgetV2Input):
await new Promise<void>((resolve, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", () => {
server.listen(0, "0.0.0.0", () => {
server.off("error", reject);
resolve();
});
@@ -130,14 +130,20 @@ const startWorkshopMockServerAsync = async (widget: HomarrCustomWidgetV2Input):
const address = server.address();
if (!address || typeof address === "string") throw new Error("Workshop mock server did not bind to a TCP port");
return {
url: `http://127.0.0.1:${address.port}`,
close: () =>
new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
server.closeAllConnections();
}),
};
try {
return {
url: await exposeHostPortToContainersAsync(address.port),
close: () =>
new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
server.closeAllConnections();
}),
};
} catch (error) {
server.closeAllConnections();
server.close();
throw error;
}
};
const loginAsAdmin = async (browser: Browser, baseUrl: string, clipboard = false) => {
+47 -44
View File
@@ -1,4 +1,5 @@
import { chromium } from "@playwright/test";
import type { Browser } from "@playwright/test";
import { describe, test } from "vitest";
import { OnboardingActions } from "./shared/actions/onboarding-actions";
@@ -16,31 +17,32 @@ describe("Onboarding", () => {
},
}).start();
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
const actions = new OnboardingActions(page, db);
const assertions = new OnboardingAssertions(page, db);
let browser: Browser | undefined;
try {
browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
const actions = new OnboardingActions(page, db);
const assertions = new OnboardingAssertions(page, db);
// Act
await page.goto(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`);
await actions.startOnboardingAsync("scratch");
await actions.processUserStepAsync({
username: "admin",
password: "Comp(exP4sswOrd",
confirmPassword: "Comp(exP4sswOrd",
});
await actions.processSettingsStepAsync();
await actions.processIntegrationsStepAsync();
// Act
await page.goto(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`);
await actions.startOnboardingAsync("scratch");
await actions.processUserStepAsync({
username: "admin",
password: "Comp(exP4sswOrd",
confirmPassword: "Comp(exP4sswOrd",
});
await actions.processSettingsStepAsync();
await actions.processIntegrationsStepAsync();
// Assert
await assertions.assertFinishStepVisibleAsync();
await assertions.assertUserAndAdminGroupInsertedAsync("admin");
await assertions.assertDbOnboardingStepAsync("finish");
// Cleanup
await browser.close();
await homarrContainer.stop();
// Assert
await assertions.assertFinishStepVisibleAsync();
await assertions.assertUserAndAdminGroupInsertedAsync("admin");
await assertions.assertDbOnboardingStepAsync("finish");
} finally {
await Promise.allSettled([browser?.close(), homarrContainer.stop()]);
}
}, 60_000);
test("External provider onboarding setup should be successful", async () => {
@@ -60,28 +62,29 @@ describe("Onboarding", () => {
}).start();
const externalGroupName = "oidc-admins";
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
const actions = new OnboardingActions(page, db);
const assertions = new OnboardingAssertions(page, db);
let browser: Browser | undefined;
try {
browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
const actions = new OnboardingActions(page, db);
const assertions = new OnboardingAssertions(page, db);
// Act
await page.goto(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`);
await actions.startOnboardingAsync("scratch");
await actions.processExternalGroupStepAsync({
name: externalGroupName,
});
await actions.processSettingsStepAsync();
await actions.processIntegrationsStepAsync();
// Act
await page.goto(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`);
await actions.startOnboardingAsync("scratch");
await actions.processExternalGroupStepAsync({
name: externalGroupName,
});
await actions.processSettingsStepAsync();
await actions.processIntegrationsStepAsync();
// Assert
await assertions.assertFinishStepVisibleAsync();
await assertions.assertExternalGroupInsertedAsync(externalGroupName);
await assertions.assertDbOnboardingStepAsync("finish");
// Cleanup
await browser.close();
await homarrContainer.stop();
// Assert
await assertions.assertFinishStepVisibleAsync();
await assertions.assertExternalGroupInsertedAsync(externalGroupName);
await assertions.assertDbOnboardingStepAsync("finish");
} finally {
await Promise.allSettled([browser?.close(), homarrContainer.stop()]);
}
}, 60_000);
});
+1 -1
View File
@@ -32,7 +32,7 @@ export class OnboardingActions {
}
public async processUserStepAsync(input: { username: string; password: string; confirmPassword: string }) {
await this.page.waitForSelector("text=administrator user");
await this.page.getByLabel("Username").waitFor({ state: "visible" });
await this.page.getByLabel("Username").fill(input.username);
await this.page.getByLabel("Password", { exact: true }).fill(input.password);
+14 -11
View File
@@ -10,6 +10,19 @@ export interface MockApiServer {
close: () => Promise<void>;
}
export async function exposeHostPortToContainersAsync(port: number): Promise<string> {
await TestContainers.exposeHostPorts(port);
const client = await getContainerRuntimeClient();
const forwarder = (await client.container.list()).find(
(container) => container.State === "running" && container.Labels[TESTCONTAINERS_SSHD_LABEL] === "true",
);
const forwarderAddress = forwarder
? Object.values(forwarder.NetworkSettings.Networks).find((network) => network.IPAddress)?.IPAddress
: undefined;
if (!forwarderAddress) throw new Error("Failed to locate the Testcontainers host-port forwarder");
return `http://${forwarderAddress}:${port}`;
}
export async function startMockApiServerAsync(responseBody: unknown): Promise<MockApiServer> {
const body = JSON.stringify(responseBody);
const server: Server = await new Promise((resolve) => {
@@ -26,18 +39,8 @@ export async function startMockApiServerAsync(responseBody: unknown): Promise<Mo
}
try {
await TestContainers.exposeHostPorts(address.port);
const client = await getContainerRuntimeClient();
const forwarder = (await client.container.list()).find(
(container) => container.State === "running" && container.Labels[TESTCONTAINERS_SSHD_LABEL] === "true",
);
const forwarderAddress = forwarder
? Object.values(forwarder.NetworkSettings.Networks).find((network) => network.IPAddress)?.IPAddress
: undefined;
if (!forwarderAddress) throw new Error("Failed to locate the Testcontainers host-port forwarder");
return {
url: `http://${forwarderAddress}:${address.port}`,
url: await exposeHostPortToContainersAsync(address.port),
close: () =>
new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));