mirror of https://github.com/openclaw/openclaw.git
fix(mattermost): harden callback auth bypass and default callback port
This commit is contained in:
parent
1a2fb8fc20
commit
f03358edb0
|
|
@ -236,7 +236,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
|
|||
const envPortRaw = process.env.OPENCLAW_GATEWAY_PORT?.trim();
|
||||
const envPort = envPortRaw ? Number.parseInt(envPortRaw, 10) : NaN;
|
||||
const gatewayPort =
|
||||
Number.isFinite(envPort) && envPort > 0 ? envPort : (cfg.gateway?.port ?? 3015);
|
||||
Number.isFinite(envPort) && envPort > 0 ? envPort : (cfg.gateway?.port ?? 18789);
|
||||
|
||||
const callbackUrl = resolveCallbackUrl({
|
||||
config: slashConfig,
|
||||
|
|
|
|||
|
|
@ -90,6 +90,8 @@ function resolveMattermostSlashCallbackPaths(
|
|||
configSnapshot: ReturnType<typeof loadConfig>,
|
||||
): Set<string> {
|
||||
const callbackPaths = new Set<string>([MATTERMOST_SLASH_CALLBACK_PATH]);
|
||||
const isMattermostCommandCallbackPath = (path: string): boolean =>
|
||||
path === MATTERMOST_SLASH_CALLBACK_PATH || path.startsWith("/api/channels/mattermost/");
|
||||
|
||||
const normalizeCallbackPath = (value: unknown): string => {
|
||||
const trimmed = typeof value === "string" ? value.trim() : "";
|
||||
|
|
@ -109,7 +111,7 @@ function resolveMattermostSlashCallbackPaths(
|
|||
}
|
||||
try {
|
||||
const pathname = new URL(trimmed).pathname;
|
||||
if (pathname) {
|
||||
if (pathname && isMattermostCommandCallbackPath(pathname)) {
|
||||
callbackPaths.add(pathname);
|
||||
}
|
||||
} catch {
|
||||
|
|
@ -123,7 +125,10 @@ function resolveMattermostSlashCallbackPaths(
|
|||
return;
|
||||
}
|
||||
const commands = raw as Record<string, unknown>;
|
||||
callbackPaths.add(normalizeCallbackPath(commands.callbackPath));
|
||||
const callbackPath = normalizeCallbackPath(commands.callbackPath);
|
||||
if (isMattermostCommandCallbackPath(callbackPath)) {
|
||||
callbackPaths.add(callbackPath);
|
||||
}
|
||||
tryAddCallbackUrlPath(commands.callbackUrl);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
withGatewayServer,
|
||||
withGatewayTempConfig,
|
||||
} from "./server-http.test-harness.js";
|
||||
import { withTempConfig } from "./test-temp-config.js";
|
||||
|
||||
type PluginRequestHandler = (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
|
||||
|
||||
|
|
@ -216,6 +217,93 @@ describe("gateway plugin HTTP auth boundary", () => {
|
|||
});
|
||||
});
|
||||
|
||||
test("allows unauthenticated Mattermost slash callback routes while keeping other channel routes protected", async () => {
|
||||
const handlePluginRequest = vi.fn(async (req: IncomingMessage, res: ServerResponse) => {
|
||||
const pathname = new URL(req.url ?? "/", "http://localhost").pathname;
|
||||
if (pathname === "/api/channels/mattermost/command") {
|
||||
res.statusCode = 200;
|
||||
res.end("ok:mm-callback");
|
||||
return true;
|
||||
}
|
||||
if (pathname === "/api/channels/nostr/default/profile") {
|
||||
res.statusCode = 200;
|
||||
res.end("ok:nostr");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
await withTempConfig({
|
||||
cfg: {
|
||||
gateway: { trustedProxies: [] },
|
||||
channels: {
|
||||
mattermost: {
|
||||
commands: { callbackPath: "/api/channels/mattermost/command" },
|
||||
},
|
||||
},
|
||||
},
|
||||
prefix: "openclaw-plugin-http-auth-mm-callback-",
|
||||
run: async () => {
|
||||
const server = createTestGatewayServer({
|
||||
resolvedAuth: AUTH_TOKEN,
|
||||
overrides: { handlePluginRequest },
|
||||
});
|
||||
|
||||
const slashCallback = await sendRequest(server, {
|
||||
path: "/api/channels/mattermost/command",
|
||||
method: "POST",
|
||||
});
|
||||
expect(slashCallback.res.statusCode).toBe(200);
|
||||
expect(slashCallback.getBody()).toBe("ok:mm-callback");
|
||||
|
||||
const otherChannelUnauthed = await sendRequest(server, {
|
||||
path: "/api/channels/nostr/default/profile",
|
||||
});
|
||||
expect(otherChannelUnauthed.res.statusCode).toBe(401);
|
||||
expect(otherChannelUnauthed.getBody()).toContain("Unauthorized");
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("does not bypass auth when mattermost callbackPath points to non-mattermost channel routes", async () => {
|
||||
const handlePluginRequest = vi.fn(async (req: IncomingMessage, res: ServerResponse) => {
|
||||
const pathname = new URL(req.url ?? "/", "http://localhost").pathname;
|
||||
if (pathname === "/api/channels/nostr/default/profile") {
|
||||
res.statusCode = 200;
|
||||
res.end("ok:nostr");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
await withTempConfig({
|
||||
cfg: {
|
||||
gateway: { trustedProxies: [] },
|
||||
channels: {
|
||||
mattermost: {
|
||||
commands: { callbackPath: "/api/channels/nostr/default/profile" },
|
||||
},
|
||||
},
|
||||
},
|
||||
prefix: "openclaw-plugin-http-auth-mm-misconfig-",
|
||||
run: async () => {
|
||||
const server = createTestGatewayServer({
|
||||
resolvedAuth: AUTH_TOKEN,
|
||||
overrides: { handlePluginRequest },
|
||||
});
|
||||
|
||||
const unauthenticated = await sendRequest(server, {
|
||||
path: "/api/channels/nostr/default/profile",
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
expect(unauthenticated.res.statusCode).toBe(401);
|
||||
expect(unauthenticated.getBody()).toContain("Unauthorized");
|
||||
expect(handlePluginRequest).not.toHaveBeenCalled();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("keeps wildcard plugin handlers ungated when auth enforcement predicate excludes their paths", async () => {
|
||||
const handlePluginRequest = vi.fn(async (req: IncomingMessage, res: ServerResponse) => {
|
||||
const pathname = new URL(req.url ?? "/", "http://localhost").pathname;
|
||||
|
|
|
|||
Loading…
Reference in New Issue