Gateway: add SecretRef support for gateway.auth.token with auth-mode guardrails (#35094)
This commit is contained in:
@@ -17,24 +17,45 @@ const ensureDevGatewayConfig = vi.fn(async (_opts?: unknown) => {});
|
||||
const runGatewayLoop = vi.fn(async ({ start }: { start: () => Promise<unknown> }) => {
|
||||
await start();
|
||||
});
|
||||
const configState = vi.hoisted(() => ({
|
||||
cfg: {} as Record<string, unknown>,
|
||||
snapshot: { exists: false } as Record<string, unknown>,
|
||||
}));
|
||||
|
||||
const { runtimeErrors, defaultRuntime, resetRuntimeCapture } = createCliRuntimeCapture();
|
||||
|
||||
vi.mock("../../config/config.js", () => ({
|
||||
getConfigPath: () => "/tmp/openclaw-test-missing-config.json",
|
||||
loadConfig: () => ({}),
|
||||
readConfigFileSnapshot: async () => ({ exists: false }),
|
||||
loadConfig: () => configState.cfg,
|
||||
readConfigFileSnapshot: async () => configState.snapshot,
|
||||
resolveStateDir: () => "/tmp",
|
||||
resolveGatewayPort: () => 18789,
|
||||
}));
|
||||
|
||||
vi.mock("../../gateway/auth.js", () => ({
|
||||
resolveGatewayAuth: (params: { authConfig?: { token?: string }; env?: NodeJS.ProcessEnv }) => ({
|
||||
mode: "token",
|
||||
token: params.authConfig?.token ?? params.env?.OPENCLAW_GATEWAY_TOKEN,
|
||||
password: undefined,
|
||||
allowTailscale: false,
|
||||
}),
|
||||
resolveGatewayAuth: (params: {
|
||||
authConfig?: { mode?: string; token?: unknown; password?: unknown };
|
||||
authOverride?: { mode?: string; token?: unknown; password?: unknown };
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}) => {
|
||||
const mode = params.authOverride?.mode ?? params.authConfig?.mode ?? "token";
|
||||
const token =
|
||||
(typeof params.authOverride?.token === "string" ? params.authOverride.token : undefined) ??
|
||||
(typeof params.authConfig?.token === "string" ? params.authConfig.token : undefined) ??
|
||||
params.env?.OPENCLAW_GATEWAY_TOKEN;
|
||||
const password =
|
||||
(typeof params.authOverride?.password === "string"
|
||||
? params.authOverride.password
|
||||
: undefined) ??
|
||||
(typeof params.authConfig?.password === "string" ? params.authConfig.password : undefined) ??
|
||||
params.env?.OPENCLAW_GATEWAY_PASSWORD;
|
||||
return {
|
||||
mode,
|
||||
token,
|
||||
password,
|
||||
allowTailscale: false,
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../gateway/server.js", () => ({
|
||||
@@ -106,6 +127,8 @@ describe("gateway run option collisions", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
resetRuntimeCapture();
|
||||
configState.cfg = {};
|
||||
configState.snapshot = { exists: false };
|
||||
startGatewayServer.mockClear();
|
||||
setGatewayWsLogStyle.mockClear();
|
||||
setVerbose.mockClear();
|
||||
@@ -190,4 +213,30 @@ describe("gateway run option collisions", () => {
|
||||
'Invalid --auth (use "none", "token", "password", or "trusted-proxy")',
|
||||
);
|
||||
});
|
||||
|
||||
it("allows password mode preflight when password is configured via SecretRef", async () => {
|
||||
configState.cfg = {
|
||||
gateway: {
|
||||
auth: {
|
||||
mode: "password",
|
||||
password: { source: "env", provider: "default", id: "OPENCLAW_GATEWAY_PASSWORD" },
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
defaults: {
|
||||
env: "default",
|
||||
},
|
||||
},
|
||||
};
|
||||
configState.snapshot = { exists: true, parsed: configState.cfg };
|
||||
|
||||
await runGatewayCli(["gateway", "run", "--allow-unconfigured"]);
|
||||
|
||||
expect(startGatewayServer).toHaveBeenCalledWith(
|
||||
18789,
|
||||
expect.objectContaining({
|
||||
bind: "loopback",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
resolveStateDir,
|
||||
resolveGatewayPort,
|
||||
} from "../../config/config.js";
|
||||
import { hasConfiguredSecretInput } from "../../config/types.secrets.js";
|
||||
import { resolveGatewayAuth } from "../../gateway/auth.js";
|
||||
import { startGatewayServer } from "../../gateway/server.js";
|
||||
import type { GatewayWsLogStyle } from "../../gateway/ws-logging.js";
|
||||
@@ -308,9 +309,22 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
|
||||
const passwordValue = resolvedAuth.password;
|
||||
const hasToken = typeof tokenValue === "string" && tokenValue.trim().length > 0;
|
||||
const hasPassword = typeof passwordValue === "string" && passwordValue.trim().length > 0;
|
||||
const tokenConfigured =
|
||||
hasToken ||
|
||||
hasConfiguredSecretInput(
|
||||
authOverride?.token ?? cfg.gateway?.auth?.token,
|
||||
cfg.secrets?.defaults,
|
||||
);
|
||||
const passwordConfigured =
|
||||
hasPassword ||
|
||||
hasConfiguredSecretInput(
|
||||
authOverride?.password ?? cfg.gateway?.auth?.password,
|
||||
cfg.secrets?.defaults,
|
||||
);
|
||||
const hasSharedSecret =
|
||||
(resolvedAuthMode === "token" && hasToken) || (resolvedAuthMode === "password" && hasPassword);
|
||||
const canBootstrapToken = resolvedAuthMode === "token" && !hasToken;
|
||||
(resolvedAuthMode === "token" && tokenConfigured) ||
|
||||
(resolvedAuthMode === "password" && passwordConfigured);
|
||||
const canBootstrapToken = resolvedAuthMode === "token" && !tokenConfigured;
|
||||
const authHints: string[] = [];
|
||||
if (miskeys.hasGatewayToken) {
|
||||
authHints.push('Found "gateway.token" in config. Use "gateway.auth.token" instead.');
|
||||
@@ -320,7 +334,7 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
|
||||
'"gateway.remote.token" is for remote CLI calls; it does not enable local gateway auth.',
|
||||
);
|
||||
}
|
||||
if (resolvedAuthMode === "password" && !hasPassword) {
|
||||
if (resolvedAuthMode === "password" && !passwordConfigured) {
|
||||
defaultRuntime.error(
|
||||
[
|
||||
"Gateway auth is set to password, but no password is configured.",
|
||||
|
||||
Reference in New Issue
Block a user