mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-11 02:04:34 +00:00
Gateway: add SecretRef support for gateway.auth.token with auth-mode guardrails (#35094)
This commit is contained in:
283
src/commands/gateway-install-token.test.ts
Normal file
283
src/commands/gateway-install-token.test.ts
Normal file
@@ -0,0 +1,283 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
|
||||
const readConfigFileSnapshotMock = vi.hoisted(() => vi.fn());
|
||||
const writeConfigFileMock = vi.hoisted(() => vi.fn());
|
||||
const resolveSecretInputRefMock = vi.hoisted(() =>
|
||||
vi.fn((): { ref: unknown } => ({ ref: undefined })),
|
||||
);
|
||||
const hasConfiguredSecretInputMock = vi.hoisted(() =>
|
||||
vi.fn((value: unknown) => {
|
||||
if (typeof value === "string") {
|
||||
return value.trim().length > 0;
|
||||
}
|
||||
return value != null;
|
||||
}),
|
||||
);
|
||||
const resolveGatewayAuthMock = vi.hoisted(() =>
|
||||
vi.fn(() => ({
|
||||
mode: "token",
|
||||
token: undefined,
|
||||
password: undefined,
|
||||
allowTailscale: false,
|
||||
})),
|
||||
);
|
||||
const shouldRequireGatewayTokenForInstallMock = vi.hoisted(() => vi.fn(() => true));
|
||||
const resolveSecretRefValuesMock = vi.hoisted(() => vi.fn());
|
||||
const secretRefKeyMock = vi.hoisted(() => vi.fn(() => "env:default:OPENCLAW_GATEWAY_TOKEN"));
|
||||
const randomTokenMock = vi.hoisted(() => vi.fn(() => "generated-token"));
|
||||
|
||||
vi.mock("../config/config.js", () => ({
|
||||
readConfigFileSnapshot: readConfigFileSnapshotMock,
|
||||
writeConfigFile: writeConfigFileMock,
|
||||
}));
|
||||
|
||||
vi.mock("../config/types.secrets.js", () => ({
|
||||
resolveSecretInputRef: resolveSecretInputRefMock,
|
||||
hasConfiguredSecretInput: hasConfiguredSecretInputMock,
|
||||
}));
|
||||
|
||||
vi.mock("../gateway/auth.js", () => ({
|
||||
resolveGatewayAuth: resolveGatewayAuthMock,
|
||||
}));
|
||||
|
||||
vi.mock("../gateway/auth-install-policy.js", () => ({
|
||||
shouldRequireGatewayTokenForInstall: shouldRequireGatewayTokenForInstallMock,
|
||||
}));
|
||||
|
||||
vi.mock("../secrets/ref-contract.js", () => ({
|
||||
secretRefKey: secretRefKeyMock,
|
||||
}));
|
||||
|
||||
vi.mock("../secrets/resolve.js", () => ({
|
||||
resolveSecretRefValues: resolveSecretRefValuesMock,
|
||||
}));
|
||||
|
||||
vi.mock("./onboard-helpers.js", () => ({
|
||||
randomToken: randomTokenMock,
|
||||
}));
|
||||
|
||||
const { resolveGatewayInstallToken } = await import("./gateway-install-token.js");
|
||||
|
||||
describe("resolveGatewayInstallToken", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
readConfigFileSnapshotMock.mockResolvedValue({ exists: false, valid: true, config: {} });
|
||||
resolveSecretInputRefMock.mockReturnValue({ ref: undefined });
|
||||
hasConfiguredSecretInputMock.mockImplementation((value: unknown) => {
|
||||
if (typeof value === "string") {
|
||||
return value.trim().length > 0;
|
||||
}
|
||||
return value != null;
|
||||
});
|
||||
resolveSecretRefValuesMock.mockResolvedValue(new Map());
|
||||
shouldRequireGatewayTokenForInstallMock.mockReturnValue(true);
|
||||
resolveGatewayAuthMock.mockReturnValue({
|
||||
mode: "token",
|
||||
token: undefined,
|
||||
password: undefined,
|
||||
allowTailscale: false,
|
||||
});
|
||||
randomTokenMock.mockReturnValue("generated-token");
|
||||
});
|
||||
|
||||
it("uses plaintext gateway.auth.token when configured", async () => {
|
||||
const result = await resolveGatewayInstallToken({
|
||||
config: {
|
||||
gateway: { auth: { token: "config-token" } },
|
||||
} as OpenClawConfig,
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
token: "config-token",
|
||||
tokenRefConfigured: false,
|
||||
unavailableReason: undefined,
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("validates SecretRef token but does not persist resolved plaintext", async () => {
|
||||
const tokenRef = { source: "env", provider: "default", id: "OPENCLAW_GATEWAY_TOKEN" };
|
||||
resolveSecretInputRefMock.mockReturnValue({ ref: tokenRef });
|
||||
resolveSecretRefValuesMock.mockResolvedValue(
|
||||
new Map([["env:default:OPENCLAW_GATEWAY_TOKEN", "resolved-token"]]),
|
||||
);
|
||||
|
||||
const result = await resolveGatewayInstallToken({
|
||||
config: {
|
||||
gateway: { auth: { mode: "token", token: tokenRef } },
|
||||
} as OpenClawConfig,
|
||||
env: { OPENCLAW_GATEWAY_TOKEN: "resolved-token" } as NodeJS.ProcessEnv,
|
||||
});
|
||||
|
||||
expect(result.token).toBeUndefined();
|
||||
expect(result.tokenRefConfigured).toBe(true);
|
||||
expect(result.unavailableReason).toBeUndefined();
|
||||
expect(result.warnings.some((message) => message.includes("SecretRef-managed"))).toBeTruthy();
|
||||
});
|
||||
|
||||
it("returns unavailable reason when token SecretRef is unresolved in token mode", async () => {
|
||||
resolveSecretInputRefMock.mockReturnValue({
|
||||
ref: { source: "env", provider: "default", id: "MISSING_GATEWAY_TOKEN" },
|
||||
});
|
||||
resolveSecretRefValuesMock.mockRejectedValue(new Error("missing env var"));
|
||||
|
||||
const result = await resolveGatewayInstallToken({
|
||||
config: {
|
||||
gateway: { auth: { mode: "token", token: "${MISSING_GATEWAY_TOKEN}" } },
|
||||
} as OpenClawConfig,
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
});
|
||||
|
||||
expect(result.token).toBeUndefined();
|
||||
expect(result.unavailableReason).toContain("gateway.auth.token SecretRef is configured");
|
||||
});
|
||||
|
||||
it("returns unavailable reason when token and password are both configured and mode is unset", async () => {
|
||||
const result = await resolveGatewayInstallToken({
|
||||
config: {
|
||||
gateway: {
|
||||
auth: {
|
||||
token: "token-value",
|
||||
password: "password-value",
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
autoGenerateWhenMissing: true,
|
||||
persistGeneratedToken: true,
|
||||
});
|
||||
|
||||
expect(result.token).toBeUndefined();
|
||||
expect(result.unavailableReason).toContain("gateway.auth.mode is unset");
|
||||
expect(result.unavailableReason).toContain("openclaw config set gateway.auth.mode token");
|
||||
expect(result.unavailableReason).toContain("openclaw config set gateway.auth.mode password");
|
||||
expect(writeConfigFileMock).not.toHaveBeenCalled();
|
||||
expect(resolveSecretRefValuesMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("auto-generates token when no source exists and auto-generation is enabled", async () => {
|
||||
const result = await resolveGatewayInstallToken({
|
||||
config: {
|
||||
gateway: { auth: { mode: "token" } },
|
||||
} as OpenClawConfig,
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
autoGenerateWhenMissing: true,
|
||||
});
|
||||
|
||||
expect(result.token).toBe("generated-token");
|
||||
expect(result.unavailableReason).toBeUndefined();
|
||||
expect(
|
||||
result.warnings.some((message) => message.includes("without saving to config")),
|
||||
).toBeTruthy();
|
||||
expect(writeConfigFileMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("persists auto-generated token when requested", async () => {
|
||||
const result = await resolveGatewayInstallToken({
|
||||
config: {
|
||||
gateway: { auth: { mode: "token" } },
|
||||
} as OpenClawConfig,
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
autoGenerateWhenMissing: true,
|
||||
persistGeneratedToken: true,
|
||||
});
|
||||
|
||||
expect(result.warnings.some((message) => message.includes("saving to config"))).toBeTruthy();
|
||||
expect(writeConfigFileMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
gateway: {
|
||||
auth: {
|
||||
mode: "token",
|
||||
token: "generated-token",
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("drops generated plaintext when config changes to SecretRef before persist", async () => {
|
||||
readConfigFileSnapshotMock.mockResolvedValue({
|
||||
exists: true,
|
||||
valid: true,
|
||||
config: {
|
||||
gateway: {
|
||||
auth: {
|
||||
token: "${OPENCLAW_GATEWAY_TOKEN}",
|
||||
},
|
||||
},
|
||||
},
|
||||
issues: [],
|
||||
});
|
||||
resolveSecretInputRefMock.mockReturnValueOnce({ ref: undefined }).mockReturnValueOnce({
|
||||
ref: { source: "env", provider: "default", id: "OPENCLAW_GATEWAY_TOKEN" },
|
||||
});
|
||||
|
||||
const result = await resolveGatewayInstallToken({
|
||||
config: {
|
||||
gateway: { auth: { mode: "token" } },
|
||||
} as OpenClawConfig,
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
autoGenerateWhenMissing: true,
|
||||
persistGeneratedToken: true,
|
||||
});
|
||||
|
||||
expect(result.token).toBeUndefined();
|
||||
expect(
|
||||
result.warnings.some((message) => message.includes("skipping plaintext token persistence")),
|
||||
).toBeTruthy();
|
||||
expect(writeConfigFileMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not auto-generate when inferred mode has password SecretRef configured", async () => {
|
||||
shouldRequireGatewayTokenForInstallMock.mockReturnValue(false);
|
||||
|
||||
const result = await resolveGatewayInstallToken({
|
||||
config: {
|
||||
gateway: {
|
||||
auth: {
|
||||
password: { source: "env", provider: "default", id: "GATEWAY_PASSWORD" },
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
providers: {
|
||||
default: { source: "env" },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
autoGenerateWhenMissing: true,
|
||||
persistGeneratedToken: true,
|
||||
});
|
||||
|
||||
expect(result.token).toBeUndefined();
|
||||
expect(result.unavailableReason).toBeUndefined();
|
||||
expect(result.warnings.some((message) => message.includes("Auto-generated"))).toBe(false);
|
||||
expect(writeConfigFileMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips token SecretRef resolution when token auth is not required", async () => {
|
||||
const tokenRef = { source: "env", provider: "default", id: "OPENCLAW_GATEWAY_TOKEN" };
|
||||
resolveSecretInputRefMock.mockReturnValue({ ref: tokenRef });
|
||||
shouldRequireGatewayTokenForInstallMock.mockReturnValue(false);
|
||||
|
||||
const result = await resolveGatewayInstallToken({
|
||||
config: {
|
||||
gateway: {
|
||||
auth: {
|
||||
mode: "password",
|
||||
token: tokenRef,
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
});
|
||||
|
||||
expect(resolveSecretRefValuesMock).not.toHaveBeenCalled();
|
||||
expect(result.unavailableReason).toBeUndefined();
|
||||
expect(result.warnings).toEqual([]);
|
||||
expect(result.token).toBeUndefined();
|
||||
expect(result.tokenRefConfigured).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user