Gateway: add SecretRef support for gateway.auth.token with auth-mode guardrails (#35094)

This commit is contained in:
Josh Avant
2026-03-05 12:53:56 -06:00
committed by GitHub
parent bc66a8fa81
commit 72cf9253fc
112 changed files with 5750 additions and 465 deletions

View File

@@ -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",
}),
);
});
});

View File

@@ -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.",