openclaw/src/gateway/connection-auth.test.ts

474 lines
13 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import {
resolveGatewayConnectionAuth,
resolveGatewayConnectionAuthFromConfig,
type GatewayConnectionAuthOptions,
} from "./connection-auth.js";
type ResolvedAuth = { token?: string; password?: string };
type ConnectionAuthCase = {
name: string;
cfg: OpenClawConfig;
env: NodeJS.ProcessEnv;
options?: Partial<Omit<GatewayConnectionAuthOptions, "config" | "env">>;
expected: ResolvedAuth;
};
function cfg(input: Partial<OpenClawConfig>): OpenClawConfig {
return input as OpenClawConfig;
}
function createRemoteModeConfig() {
return {
gateway: {
mode: "remote" as const,
auth: {
token: "local-token",
password: "local-password", // pragma: allowlist secret
},
remote: {
url: "wss://remote.example",
token: "remote-token",
password: "remote-password", // pragma: allowlist secret
},
},
};
}
function createUnresolvedLocalAuthConfig(params: {
mode: "token" | "password";
id: string;
remoteFallback: string;
}) {
return cfg({
gateway: {
mode: "local",
auth: {
mode: params.mode,
[params.mode]: { source: "env", provider: "default", id: params.id },
},
remote: {
[params.mode]: params.remoteFallback,
},
},
secrets: {
providers: {
default: { source: "env" },
},
},
});
}
async function expectFailClosedOnUnresolvedLocalAuth(config: OpenClawConfig, path: string) {
await expect(
resolveGatewayConnectionAuth({
config,
env: {} as NodeJS.ProcessEnv,
includeLegacyEnv: false,
}),
).rejects.toThrow(path);
expect(() =>
resolveGatewayConnectionAuthFromConfig({
cfg: config,
env: {} as NodeJS.ProcessEnv,
includeLegacyEnv: false,
}),
).toThrow(path);
}
const DEFAULT_ENV = {
OPENCLAW_GATEWAY_TOKEN: "env-token",
OPENCLAW_GATEWAY_PASSWORD: "env-password", // pragma: allowlist secret
} as NodeJS.ProcessEnv;
describe("resolveGatewayConnectionAuth", () => {
const cases: ConnectionAuthCase[] = [
{
name: "local mode defaults to env-first token/password",
cfg: cfg({
gateway: {
mode: "local",
auth: {
token: "config-token",
password: "config-password", // pragma: allowlist secret
},
remote: {
token: "remote-token",
password: "remote-password", // pragma: allowlist secret
},
},
}),
env: DEFAULT_ENV,
expected: {
token: "env-token",
password: "env-password", // pragma: allowlist secret
},
},
{
name: "local mode supports config-first token/password",
cfg: cfg({
gateway: {
mode: "local",
auth: {
token: "config-token",
password: "config-password", // pragma: allowlist secret
},
},
}),
env: DEFAULT_ENV,
options: {
localTokenPrecedence: "config-first",
localPasswordPrecedence: "config-first", // pragma: allowlist secret
},
expected: {
token: "config-token",
password: "config-password", // pragma: allowlist secret
},
},
{
name: "local mode precedence can mix env-first token with config-first password",
cfg: cfg({
gateway: {
mode: "local",
auth: {},
remote: {
token: "remote-token",
password: "remote-password", // pragma: allowlist secret
},
},
}),
env: DEFAULT_ENV,
options: {
localTokenPrecedence: "env-first",
localPasswordPrecedence: "config-first", // pragma: allowlist secret
},
expected: {
token: "env-token",
password: "remote-password", // pragma: allowlist secret
},
},
{
name: "remote mode defaults to remote-first token and env-first password",
cfg: cfg(createRemoteModeConfig()),
env: DEFAULT_ENV,
expected: {
token: "remote-token",
password: "env-password", // pragma: allowlist secret
},
},
{
name: "remote mode supports env-first token with remote-first password",
cfg: cfg(createRemoteModeConfig()),
env: DEFAULT_ENV,
options: {
remoteTokenPrecedence: "env-first",
remotePasswordPrecedence: "remote-first", // pragma: allowlist secret
},
expected: {
token: "env-token",
password: "remote-password", // pragma: allowlist secret
},
},
{
name: "remote-only fallback can suppress env/local password fallback",
cfg: cfg({
gateway: {
mode: "remote",
auth: {
token: "local-token",
password: "local-password", // pragma: allowlist secret
},
remote: {
url: "wss://remote.example",
token: "remote-token",
},
},
}),
env: DEFAULT_ENV,
options: {
remoteTokenFallback: "remote-only",
remotePasswordFallback: "remote-only", // pragma: allowlist secret
},
expected: {
token: "remote-token",
password: undefined,
},
},
{
name: "modeOverride can force remote precedence while config gateway.mode is local",
cfg: cfg({
gateway: {
mode: "local",
auth: {
token: "local-token",
password: "local-password", // pragma: allowlist secret
},
remote: {
url: "wss://remote.example",
token: "remote-token",
password: "remote-password", // pragma: allowlist secret
},
},
}),
env: DEFAULT_ENV,
options: {
modeOverride: "remote",
remoteTokenPrecedence: "remote-first",
remotePasswordPrecedence: "remote-first", // pragma: allowlist secret
},
expected: {
token: "remote-token",
password: "remote-password", // pragma: allowlist secret
},
},
{
name: "includeLegacyEnv controls CLAWDBOT fallback",
cfg: cfg({
gateway: {
mode: "local",
auth: {},
},
}),
env: {
CLAWDBOT_GATEWAY_TOKEN: "legacy-token",
CLAWDBOT_GATEWAY_PASSWORD: "legacy-password", // pragma: allowlist secret
} as NodeJS.ProcessEnv,
options: {
includeLegacyEnv: true,
},
expected: {
token: "legacy-token",
password: "legacy-password", // pragma: allowlist secret
},
},
];
it.each(cases)("$name", async ({ cfg, env, options, expected }) => {
const asyncResolved = await resolveGatewayConnectionAuth({
config: cfg,
env,
...options,
});
const syncResolved = resolveGatewayConnectionAuthFromConfig({
cfg,
env,
...options,
});
expect(asyncResolved).toEqual(expected);
expect(syncResolved).toEqual(expected);
});
it("can disable legacy env fallback", async () => {
const config = cfg({
gateway: {
mode: "local",
auth: {},
},
});
const env = {
CLAWDBOT_GATEWAY_TOKEN: "legacy-token",
CLAWDBOT_GATEWAY_PASSWORD: "legacy-password", // pragma: allowlist secret
} as NodeJS.ProcessEnv;
const resolved = await resolveGatewayConnectionAuth({
config,
env,
includeLegacyEnv: false,
});
expect(resolved).toEqual({
token: undefined,
password: undefined,
});
});
it("resolves local SecretRef token when legacy env is disabled", async () => {
const config = cfg({
gateway: {
mode: "local",
auth: {
token: { source: "env", provider: "default", id: "LOCAL_SECRET_TOKEN" },
},
},
secrets: {
providers: {
default: { source: "env" },
},
},
});
const env = {
CLAWDBOT_GATEWAY_TOKEN: "legacy-token",
LOCAL_SECRET_TOKEN: "resolved-from-secretref", // pragma: allowlist secret
} as NodeJS.ProcessEnv;
const resolved = await resolveGatewayConnectionAuth({
config,
env,
includeLegacyEnv: false,
});
expect(resolved).toEqual({
token: "resolved-from-secretref",
password: undefined,
});
});
it("resolves config-first token SecretRef even when OPENCLAW env token exists", async () => {
const config = cfg({
gateway: {
mode: "local",
auth: {
token: { source: "env", provider: "default", id: "CONFIG_FIRST_TOKEN" },
},
},
secrets: {
providers: {
default: { source: "env" },
},
},
});
const env = {
OPENCLAW_GATEWAY_TOKEN: "env-token",
CONFIG_FIRST_TOKEN: "config-first-token",
} as NodeJS.ProcessEnv;
const resolved = await resolveGatewayConnectionAuth({
config,
env,
includeLegacyEnv: false,
localTokenPrecedence: "config-first",
});
expect(resolved).toEqual({
token: "config-first-token",
password: undefined,
});
});
it("resolves config-first password SecretRef even when OPENCLAW env password exists", async () => {
const config = cfg({
gateway: {
mode: "local",
auth: {
mode: "password",
password: { source: "env", provider: "default", id: "CONFIG_FIRST_PASSWORD" },
},
},
secrets: {
providers: {
default: { source: "env" },
},
},
});
const env = {
OPENCLAW_GATEWAY_PASSWORD: "env-password", // pragma: allowlist secret
CONFIG_FIRST_PASSWORD: "config-first-password", // pragma: allowlist secret
} as NodeJS.ProcessEnv;
const resolved = await resolveGatewayConnectionAuth({
config,
env,
includeLegacyEnv: false,
localPasswordPrecedence: "config-first", // pragma: allowlist secret
});
expect(resolved).toEqual({
token: undefined,
password: "config-first-password", // pragma: allowlist secret
});
});
it("throws when config-first token SecretRef cannot resolve even if env token exists", async () => {
const config = cfg({
gateway: {
mode: "local",
auth: {
token: { source: "env", provider: "default", id: "MISSING_CONFIG_FIRST_TOKEN" },
},
},
secrets: {
providers: {
default: { source: "env" },
},
},
});
const env = {
OPENCLAW_GATEWAY_TOKEN: "env-token",
} as NodeJS.ProcessEnv;
await expect(
resolveGatewayConnectionAuth({
config,
env,
includeLegacyEnv: false,
localTokenPrecedence: "config-first",
}),
).rejects.toThrow("gateway.auth.token");
expect(() =>
resolveGatewayConnectionAuthFromConfig({
cfg: config,
env,
includeLegacyEnv: false,
localTokenPrecedence: "config-first",
}),
).toThrow("gateway.auth.token");
});
it("throws when config-first password SecretRef cannot resolve even if env password exists", async () => {
const config = cfg({
gateway: {
mode: "local",
auth: {
mode: "password",
password: { source: "env", provider: "default", id: "MISSING_CONFIG_FIRST_PASSWORD" },
},
},
secrets: {
providers: {
default: { source: "env" },
},
},
});
const env = {
OPENCLAW_GATEWAY_PASSWORD: "env-password", // pragma: allowlist secret
} as NodeJS.ProcessEnv;
await expect(
resolveGatewayConnectionAuth({
config,
env,
includeLegacyEnv: false,
localPasswordPrecedence: "config-first", // pragma: allowlist secret
}),
).rejects.toThrow("gateway.auth.password");
expect(() =>
resolveGatewayConnectionAuthFromConfig({
cfg: config,
env,
includeLegacyEnv: false,
localPasswordPrecedence: "config-first", // pragma: allowlist secret
}),
).toThrow("gateway.auth.password");
});
it("fails closed when local token SecretRef is unresolved and remote token fallback exists", async () => {
await expectFailClosedOnUnresolvedLocalAuth(
createUnresolvedLocalAuthConfig({
mode: "token",
id: "MISSING_LOCAL_TOKEN",
remoteFallback: "remote-token",
}),
"gateway.auth.token",
);
});
it("fails closed when local password SecretRef is unresolved and remote password fallback exists", async () => {
await expectFailClosedOnUnresolvedLocalAuth(
createUnresolvedLocalAuthConfig({
mode: "password",
id: "MISSING_LOCAL_PASSWORD",
remoteFallback: "remote-password", // pragma: allowlist secret
}),
"gateway.auth.password",
);
});
});