Compare commits

...
Author SHA1 Message Date
ajnart 9699255180 fix(wud): authenticate HTTP requests and validate container access 2026-09-12 10:50:27 +02:00
3 changed files with 16 additions and 17 deletions

No files matched your search

@@ -24,12 +24,14 @@ The What's Up Docker integration lets Homarr connect to your [WUD](https://githu
<AddingIntegration /> <AddingIntegration />
:::info URL configuration :::info URL configuration
Use the root URL of your WUD instance, for example `https://wud.example.com`. Homarr calls the `/api/app` and `/api/containers` endpoints under that URL automatically. Use the root URL of your WUD instance, for example `https://wud.example.com`. Homarr uses `/api/containers` under that URL for both connection testing and widget data.
::: :::
### Secrets ### Secrets
WUD does not require authentication by default. If your instance is protected with [Basic Authentication](https://github.com/getwud/wud/blob/main/docs/configuration/authentications/basic/README.md), you can provide a username and password. WUD 9 requires authentication. Enter your WUD username and password in Homarr; the initial admin credentials configured with `WUD_AUTH_ADMIN_USER` and `WUD_AUTH_ADMIN_PASSWORD` work with Basic authentication. Older WUD versions without authentication can leave both fields empty.
Credentials are sent to the configured URL over HTTP or HTTPS. Use HTTPS outside a trusted internal network.
<IntegrationSecrets <IntegrationSecrets
secrets={[ secrets={[
@@ -40,7 +42,7 @@ WUD does not require authentication by default. If your instance is protected wi
{ {
credentials: ["username", "password"], credentials: ["username", "password"],
steps: [ steps: [
"Open your WUD instance's configuration and configure a WUD_AUTH_BASIC_* user as described in the WUD documentation", "Use your WUD account credentials; older versions can use a configured WUD_AUTH_BASIC_* user",
"Enter that username and password in Homarr — Basic authentication is handled automatically", "Enter that username and password in Homarr — Basic authentication is handled automatically",
], ],
}, },
@@ -198,7 +198,7 @@ describe("WudIntegration authentication", () => {
expect(requestInit?.timeout).toBe(10_000); expect(requestInit?.timeout).toBe(10_000);
}); });
test("does not send credentials over a non-HTTPS URL", async () => { test("sends configured Basic auth credentials over an HTTP URL", async () => {
mockFetch.mockResolvedValue( mockFetch.mockResolvedValue(
new Response(JSON.stringify(sampleContainersResponse), { new Response(JSON.stringify(sampleContainersResponse), {
status: 200, status: 200,
@@ -209,7 +209,7 @@ describe("WudIntegration authentication", () => {
const integration = new WudIntegration({ const integration = new WudIntegration({
id: "test-wud-http", id: "test-wud-http",
name: "Test WUD HTTP", name: "Test WUD HTTP",
url: "http://wud.example.com", url: "http://wud:3000",
externalUrl: null, externalUrl: null,
decryptedSecrets: [ decryptedSecrets: [
{ kind: "username", value: TEST_USERNAME }, { kind: "username", value: TEST_USERNAME },
@@ -221,7 +221,7 @@ describe("WudIntegration authentication", () => {
const [, requestInit] = mockFetch.mock.calls[0] ?? []; const [, requestInit] = mockFetch.mock.calls[0] ?? [];
const headers = (requestInit?.headers ?? {}) as Record<string, string>; const headers = (requestInit?.headers ?? {}) as Record<string, string>;
expect(headers.Authorization).toBeUndefined(); expect(headers.Authorization).toBe(`Basic ${Buffer.from(`${TEST_USERNAME}:${TEST_PASSWORD}`).toString("base64")}`);
}); });
}); });
@@ -238,15 +238,16 @@ describe("WudIntegration testing endpoint", () => {
fetchAsync: vi.fn().mockResolvedValue(response), fetchAsync: vi.fn().mockResolvedValue(response),
}) as unknown as IntegrationTestingInput; }) as unknown as IntegrationTestingInput;
test("returns success when /api/app responds 200", async () => { test("returns success when /api/containers responds 200", async () => {
const input = makeInput(new Response(JSON.stringify({ name: "wud", version: "5.0.0" }), { status: 200 })); const input = makeInput(new Response(JSON.stringify(sampleContainersResponse), { status: 200 }));
const result = await invokeTesting(createIntegration(), input); const result = await invokeTesting(createIntegration(), input);
expect(result.success).toBe(true); expect(result.success).toBe(true);
expect(String(vi.mocked(input.fetchAsync).mock.calls[0]?.[0])).toBe(`${TEST_URL}/api/containers`);
}); });
test("returns a status-code failure when /api/app responds non-OK", async () => { test("returns a status-code failure when /api/containers responds non-OK", async () => {
const input = makeInput(new Response("unauthorized", { status: 401 })); const input = makeInput(new Response("unauthorized", { status: 401 }));
const result = await invokeTesting(createIntegration(), input); const result = await invokeTesting(createIntegration(), input);
@@ -255,7 +256,7 @@ describe("WudIntegration testing endpoint", () => {
}); });
test("passes the Basic auth header through to fetchAsync when configured", async () => { test("passes the Basic auth header through to fetchAsync when configured", async () => {
const response = new Response(JSON.stringify({ name: "wud", version: "5.0.0" }), { status: 200 }); const response = new Response(JSON.stringify(sampleContainersResponse), { status: 200 });
const input = makeInput(response); const input = makeInput(response);
await invokeTesting(createIntegrationWithBasicAuth(), input); await invokeTesting(createIntegrationWithBasicAuth(), input);
@@ -14,7 +14,7 @@ const CONTAINERS_REQUEST_TIMEOUT_MS = 10_000;
export class WudIntegration extends Integration { export class WudIntegration extends Integration {
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> { protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const response = await input.fetchAsync(this.url("/api/app"), { const response = await input.fetchAsync(this.url("/api/containers"), {
headers: this.getAuthHeaders(), headers: this.getAuthHeaders(),
}); });
@@ -23,10 +23,10 @@ export class WudIntegration extends Integration {
} }
try { try {
await response.json(); await parseWudContainersResponseAsync(response);
} catch (error) { } catch (error) {
return TestConnectionError.ParseResult( return TestConnectionError.ParseResult(
new ParseError("Invalid WUD app response", { new ParseError("Invalid WUD containers response", {
cause: error instanceof Error ? error : new Error(String(error)), cause: error instanceof Error ? error : new Error(String(error)),
}), }),
); );
@@ -55,10 +55,6 @@ export class WudIntegration extends Integration {
return undefined; return undefined;
} }
if (this.url("/api/app").protocol !== "https:") {
return undefined;
}
const credentials = Buffer.from(`${this.getSecretValue("username")}:${this.getSecretValue("password")}`).toString( const credentials = Buffer.from(`${this.getSecretValue("username")}:${this.getSecretValue("password")}`).toString(
"base64", "base64",
); );