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 />
:::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
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
secrets={[
@@ -40,7 +42,7 @@ WUD does not require authentication by default. If your instance is protected wi
{
credentials: ["username", "password"],
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",
],
},
@@ -198,7 +198,7 @@ describe("WudIntegration authentication", () => {
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(
new Response(JSON.stringify(sampleContainersResponse), {
status: 200,
@@ -209,7 +209,7 @@ describe("WudIntegration authentication", () => {
const integration = new WudIntegration({
id: "test-wud-http",
name: "Test WUD HTTP",
url: "http://wud.example.com",
url: "http://wud:3000",
externalUrl: null,
decryptedSecrets: [
{ kind: "username", value: TEST_USERNAME },
@@ -221,7 +221,7 @@ describe("WudIntegration authentication", () => {
const [, requestInit] = mockFetch.mock.calls[0] ?? [];
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),
}) as unknown as IntegrationTestingInput;
test("returns success when /api/app responds 200", async () => {
const input = makeInput(new Response(JSON.stringify({ name: "wud", version: "5.0.0" }), { status: 200 }));
test("returns success when /api/containers responds 200", async () => {
const input = makeInput(new Response(JSON.stringify(sampleContainersResponse), { status: 200 }));
const result = await invokeTesting(createIntegration(), input);
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 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 () => {
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);
await invokeTesting(createIntegrationWithBasicAuth(), input);
@@ -14,7 +14,7 @@ const CONTAINERS_REQUEST_TIMEOUT_MS = 10_000;
export class WudIntegration extends Integration {
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(),
});
@@ -23,10 +23,10 @@ export class WudIntegration extends Integration {
}
try {
await response.json();
await parseWudContainersResponseAsync(response);
} catch (error) {
return TestConnectionError.ParseResult(
new ParseError("Invalid WUD app response", {
new ParseError("Invalid WUD containers response", {
cause: error instanceof Error ? error : new Error(String(error)),
}),
);
@@ -55,10 +55,6 @@ export class WudIntegration extends Integration {
return undefined;
}
if (this.url("/api/app").protocol !== "https:") {
return undefined;
}
const credentials = Buffer.from(`${this.getSecretValue("username")}:${this.getSecretValue("password")}`).toString(
"base64",
);