mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 08:51:38 +00:00
fix(gateway): keep configured local auth authoritative (#114462)
* fix(gateway): align configured local credentials * test(gateway): align local credential coverage * chore(gateway): leave release note to release process
This commit is contained in:
committed by
GitHub
parent
c56689416e
commit
fd6e042d87
@@ -297,7 +297,7 @@ Security note:
|
||||
|
||||
- `--token` and `--password` can be visible in local process listings on some systems. Prefer `--token-file`/`--password-file` or environment variables (`OPENCLAW_GATEWAY_TOKEN`, `OPENCLAW_GATEWAY_PASSWORD`).
|
||||
- Gateway auth resolution follows the shared contract used by other Gateway clients:
|
||||
- local mode: env (`OPENCLAW_GATEWAY_*`) then `gateway.auth.*`, falling back to `gateway.remote.*` only when `gateway.auth.*` is unset (a configured-but-unresolved local SecretRef fails closed instead of silently falling back)
|
||||
- local mode: `gateway.auth.*` then env (`OPENCLAW_GATEWAY_*`), falling back to `gateway.remote.*` only when `gateway.auth.*` is unset (a configured-but-unresolved local SecretRef fails closed instead of silently falling back)
|
||||
- remote mode: `gateway.remote.*` with env/config fallback per remote precedence rules
|
||||
- `--url` is override-safe and does not reuse implicit config/env credentials; pass explicit `--token`/`--password` (or file variants)
|
||||
|
||||
|
||||
@@ -95,12 +95,12 @@ Gateway credential resolution follows one shared contract across call/probe/stat
|
||||
- CLI `--url` never reuses implicit config/env credentials.
|
||||
- Env `OPENCLAW_GATEWAY_URL` may use env credentials only (`OPENCLAW_GATEWAY_TOKEN` / `OPENCLAW_GATEWAY_PASSWORD`).
|
||||
- Local mode defaults:
|
||||
- token: `OPENCLAW_GATEWAY_TOKEN` -> `gateway.auth.token` -> `gateway.remote.token` (remote fallback only when the local token is unset)
|
||||
- password: `OPENCLAW_GATEWAY_PASSWORD` -> `gateway.auth.password` -> `gateway.remote.password` (remote fallback only when the local password is unset)
|
||||
- token: `gateway.auth.token` -> `OPENCLAW_GATEWAY_TOKEN` -> `gateway.remote.token` (remote fallback only when the local token is unset)
|
||||
- password: `gateway.auth.password` -> `OPENCLAW_GATEWAY_PASSWORD` -> `gateway.remote.password` (remote fallback only when the local password is unset)
|
||||
- Remote mode defaults:
|
||||
- token: `gateway.remote.token` -> `OPENCLAW_GATEWAY_TOKEN` -> `gateway.auth.token`
|
||||
- password: `OPENCLAW_GATEWAY_PASSWORD` -> `gateway.remote.password` -> `gateway.auth.password`
|
||||
- Node-host local-mode exception: `gateway.remote.token` / `gateway.remote.password` are ignored.
|
||||
- Node-host local-mode exception: environment credentials stay first and `gateway.remote.token` / `gateway.remote.password` are ignored because node commands target an explicit host and port.
|
||||
- Remote probe/status token checks are strict by default: they use `gateway.remote.token` only (no local token fallback) when targeting remote mode.
|
||||
- Gateway env overrides use `OPENCLAW_GATEWAY_*` only.
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ SecretRefs are validated only on effectively active surfaces:
|
||||
- `gateway.remote.url` is configured
|
||||
- `gateway.tailscale.mode` is `serve` or `funnel`
|
||||
- In local mode without those remote surfaces: `gateway.remote.token` is active when token auth can win and no env/auth token is configured; `gateway.remote.password` is active only when password auth can win and no env/auth password is configured.
|
||||
- `gateway.auth.token` SecretRef is inactive for startup auth resolution when `OPENCLAW_GATEWAY_TOKEN` is set, because env token input wins for that runtime.
|
||||
- Active `gateway.auth.token` / `gateway.auth.password` SecretRefs stay authoritative over `OPENCLAW_GATEWAY_TOKEN` / `OPENCLAW_GATEWAY_PASSWORD`; environment credentials are fallbacks when the corresponding local config input is absent.
|
||||
|
||||
</Accordion>
|
||||
|
||||
|
||||
@@ -371,7 +371,7 @@ describe("registerQrCli", () => {
|
||||
expect(resolveCommandSecretRefsViaGateway).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses OPENCLAW_GATEWAY_PASSWORD without resolving local password SecretRef", async () => {
|
||||
it("does not let OPENCLAW_GATEWAY_PASSWORD mask a local password SecretRef", async () => {
|
||||
vi.stubEnv("OPENCLAW_GATEWAY_PASSWORD", "password-from-env");
|
||||
loadConfig.mockReturnValue(
|
||||
createLocalGatewayConfigWithAuth(
|
||||
@@ -379,9 +379,9 @@ describe("registerQrCli", () => {
|
||||
),
|
||||
);
|
||||
|
||||
await runQr(["--setup-code-only"]);
|
||||
|
||||
expectLoggedLocalSetupCode();
|
||||
await expectQrExit(["--setup-code-only"]);
|
||||
const output = runtimeError.mock.calls.map((call) => readRuntimeCallText(call)).join("\n");
|
||||
expect(output).toContain("MISSING_LOCAL_GATEWAY_PASSWORD");
|
||||
expect(resolveCommandSecretRefsViaGateway).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -166,10 +166,10 @@ async function collectMacLaunchctlGatewayEnvOverrideWarning(
|
||||
"- Host-wide launchctl gateway auth overrides detected.",
|
||||
"- Current managed Gateway installs do not need these values unless config intentionally references the env var.",
|
||||
envToken && envTokenKey
|
||||
? `- \`${envTokenKey}\` is set; it can make local clients use a different token than gateway.auth.token.`
|
||||
? `- \`${envTokenKey}\` is set; explicit environment URL or node-host targets can use a different token than gateway.auth.token.`
|
||||
: undefined,
|
||||
envPassword
|
||||
? `- \`${envPasswordKey ?? "OPENCLAW_GATEWAY_PASSWORD"}\` is set; it can make local clients use a different password than gateway.auth.password.`
|
||||
? `- \`${envPasswordKey ?? "OPENCLAW_GATEWAY_PASSWORD"}\` is set; explicit environment URL or node-host targets can use a different password than gateway.auth.password.`
|
||||
: undefined,
|
||||
"- Clear overrides and restart the app/gateway:",
|
||||
envTokenKey ? ` launchctl unsetenv ${envTokenKey}` : undefined,
|
||||
|
||||
@@ -198,7 +198,7 @@ describe("noteSecurityWarnings gateway exposure", () => {
|
||||
await noteSecurityWarnings(cfg);
|
||||
const message = lastMessage();
|
||||
expect(message).toContain("OPENCLAW_GATEWAY_TOKEN conflicts with gateway.auth.token");
|
||||
expect(message).toContain("Direct local Gateway clients commonly prefer the env token");
|
||||
expect(message).toContain("Configured local Gateway clients");
|
||||
expect(message).toContain("~/.openclaw/.env");
|
||||
});
|
||||
|
||||
|
||||
@@ -36,8 +36,10 @@ async function materializeDoctorGatewayAuthRefs(
|
||||
cfg,
|
||||
env,
|
||||
mode: cfg.gateway?.auth?.mode,
|
||||
hasTokenCandidate: Boolean(normalizeOptionalString(env.OPENCLAW_GATEWAY_TOKEN)),
|
||||
hasPasswordCandidate: Boolean(normalizeOptionalString(env.OPENCLAW_GATEWAY_PASSWORD)),
|
||||
hasTokenOverride: false,
|
||||
hasPasswordOverride: false,
|
||||
hasTokenFallback: Boolean(normalizeOptionalString(env.OPENCLAW_GATEWAY_TOKEN)),
|
||||
hasPasswordFallback: Boolean(normalizeOptionalString(env.OPENCLAW_GATEWAY_PASSWORD)),
|
||||
};
|
||||
if (!canMaterializeGatewayAuthSecretRefsWithoutExec(materializeParams)) {
|
||||
return cfg;
|
||||
|
||||
@@ -108,8 +108,6 @@ async function resolveBrowserHatchTarget(
|
||||
config,
|
||||
env,
|
||||
modeOverride: "local",
|
||||
localTokenPrecedence: "config-first",
|
||||
localPasswordPrecedence: "config-first",
|
||||
});
|
||||
const auth = resolveGatewayAuth({
|
||||
authConfig: {
|
||||
|
||||
@@ -19,8 +19,10 @@ type GatewayAuthSecretRefResolutionParams = {
|
||||
cfg: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
mode?: GatewayAuthConfig["mode"];
|
||||
hasPasswordCandidate: boolean;
|
||||
hasTokenCandidate: boolean;
|
||||
hasPasswordOverride: boolean;
|
||||
hasTokenOverride: boolean;
|
||||
hasPasswordFallback: boolean;
|
||||
hasTokenFallback: boolean;
|
||||
};
|
||||
|
||||
/** Check whether a local Gateway auth input is configured directly or through defaults. */
|
||||
@@ -35,12 +37,14 @@ export function hasConfiguredGatewayAuthSecretInput(
|
||||
function shouldResolveGatewayAuthSecretRef(params: {
|
||||
mode?: GatewayAuthConfig["mode"];
|
||||
path: GatewayAuthSecretInputPath;
|
||||
hasPasswordCandidate: boolean;
|
||||
hasTokenCandidate: boolean;
|
||||
hasPasswordOverride: boolean;
|
||||
hasTokenOverride: boolean;
|
||||
hasPasswordFallback: boolean;
|
||||
hasTokenFallback: boolean;
|
||||
}): boolean {
|
||||
const isTokenPath = params.path === "gateway.auth.token";
|
||||
const hasPathCandidate = isTokenPath ? params.hasTokenCandidate : params.hasPasswordCandidate;
|
||||
if (hasPathCandidate) {
|
||||
const hasPathOverride = isTokenPath ? params.hasTokenOverride : params.hasPasswordOverride;
|
||||
if (hasPathOverride) {
|
||||
return false;
|
||||
}
|
||||
if (params.mode === (isTokenPath ? "token" : "password")) {
|
||||
@@ -56,8 +60,10 @@ function shouldResolveGatewayAuthSecretRef(params: {
|
||||
return !isTokenPath;
|
||||
}
|
||||
// With implicit mode, resolve the side that does not already have a concrete
|
||||
// candidate so token and password defaults do not both get materialized.
|
||||
return isTokenPath ? !params.hasPasswordCandidate : !params.hasTokenCandidate;
|
||||
// competing credential so token and password defaults do not both get materialized.
|
||||
return isTokenPath
|
||||
? !(params.hasPasswordOverride || params.hasPasswordFallback)
|
||||
: !(params.hasTokenOverride || params.hasTokenFallback);
|
||||
}
|
||||
|
||||
function shouldResolveGatewayTokenSecretRef(
|
||||
@@ -66,8 +72,10 @@ function shouldResolveGatewayTokenSecretRef(
|
||||
return shouldResolveGatewayAuthSecretRef({
|
||||
mode: params.mode,
|
||||
path: "gateway.auth.token",
|
||||
hasPasswordCandidate: params.hasPasswordCandidate,
|
||||
hasTokenCandidate: params.hasTokenCandidate,
|
||||
hasPasswordOverride: params.hasPasswordOverride,
|
||||
hasTokenOverride: params.hasTokenOverride,
|
||||
hasPasswordFallback: params.hasPasswordFallback,
|
||||
hasTokenFallback: params.hasTokenFallback,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -77,8 +85,10 @@ function shouldResolveGatewayPasswordSecretRef(
|
||||
return shouldResolveGatewayAuthSecretRef({
|
||||
mode: params.mode,
|
||||
path: "gateway.auth.password",
|
||||
hasPasswordCandidate: params.hasPasswordCandidate,
|
||||
hasTokenCandidate: params.hasTokenCandidate,
|
||||
hasPasswordOverride: params.hasPasswordOverride,
|
||||
hasTokenOverride: params.hasTokenOverride,
|
||||
hasPasswordFallback: params.hasPasswordFallback,
|
||||
hasTokenFallback: params.hasTokenFallback,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -187,8 +197,10 @@ async function resolveGatewayPasswordSecretRef(params: {
|
||||
cfg: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
mode?: GatewayAuthConfig["mode"];
|
||||
hasPasswordCandidate: boolean;
|
||||
hasTokenCandidate: boolean;
|
||||
hasPasswordOverride: boolean;
|
||||
hasTokenOverride: boolean;
|
||||
hasPasswordFallback: boolean;
|
||||
hasTokenFallback: boolean;
|
||||
}): Promise<OpenClawConfig> {
|
||||
return resolveGatewayAuthSecretRef({
|
||||
cfg: params.cfg,
|
||||
@@ -212,9 +224,11 @@ export async function materializeGatewayAuthSecretRefs(
|
||||
cfg: cfgWithToken,
|
||||
env: params.env,
|
||||
mode: params.mode,
|
||||
hasPasswordCandidate: params.hasPasswordCandidate,
|
||||
hasTokenCandidate:
|
||||
params.hasTokenCandidate ||
|
||||
hasPasswordOverride: params.hasPasswordOverride,
|
||||
hasTokenOverride: params.hasTokenOverride,
|
||||
hasPasswordFallback: params.hasPasswordFallback,
|
||||
hasTokenFallback:
|
||||
params.hasTokenFallback ||
|
||||
hasConfiguredGatewayAuthSecretInput(cfgWithToken, "gateway.auth.token"),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,6 +18,39 @@ function remoteGatewayConfig(remote?: GatewayRemoteConfig): OpenClawConfig {
|
||||
}
|
||||
|
||||
describe("resolveGatewayInteractiveSurfaceAuth", () => {
|
||||
it("keeps configured local password ahead of OPENCLAW_GATEWAY_PASSWORD", async () => {
|
||||
await expect(
|
||||
resolveGatewayInteractiveSurfaceAuth({
|
||||
config: {
|
||||
gateway: {
|
||||
mode: "local",
|
||||
auth: { mode: "password", password: "config-password" }, // pragma: allowlist secret
|
||||
},
|
||||
},
|
||||
env: { OPENCLAW_GATEWAY_PASSWORD: "env-password" }, // pragma: allowlist secret
|
||||
surface: "local",
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
token: undefined,
|
||||
password: "config-password", // pragma: allowlist secret
|
||||
failureReason: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to OPENCLAW_GATEWAY_PASSWORD without configured local password", async () => {
|
||||
await expect(
|
||||
resolveGatewayInteractiveSurfaceAuth({
|
||||
config: { gateway: { mode: "local", auth: { mode: "password" } } },
|
||||
env: { OPENCLAW_GATEWAY_PASSWORD: "env-password" }, // pragma: allowlist secret
|
||||
surface: "local",
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
token: undefined,
|
||||
password: "env-password", // pragma: allowlist secret
|
||||
failureReason: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses OPENCLAW_GATEWAY_TOKEN as remote interactive fallback", async () => {
|
||||
await expect(
|
||||
resolveGatewayInteractiveSurfaceAuth({
|
||||
|
||||
@@ -275,17 +275,16 @@ export async function resolveGatewayInteractiveSurfaceAuth(params: {
|
||||
};
|
||||
|
||||
const resolvePassword = async () => {
|
||||
const localPassword =
|
||||
explicitPassword || envPassword
|
||||
? { value: explicitPassword ?? envPassword }
|
||||
: await resolveGatewayCredential({
|
||||
config: params.config,
|
||||
env,
|
||||
diagnostics,
|
||||
path: "gateway.auth.password",
|
||||
value: params.config.gateway?.auth?.password,
|
||||
});
|
||||
const password = explicitPassword ?? envPassword ?? localPassword.value;
|
||||
const localPassword = explicitPassword
|
||||
? { value: explicitPassword }
|
||||
: await resolveGatewayCredential({
|
||||
config: params.config,
|
||||
env,
|
||||
diagnostics,
|
||||
path: "gateway.auth.password",
|
||||
value: params.config.gateway?.auth?.password,
|
||||
});
|
||||
const password = explicitPassword ?? localPassword.value ?? envPassword;
|
||||
return {
|
||||
password,
|
||||
failureReason: password
|
||||
|
||||
@@ -63,9 +63,9 @@ export function resolveGatewayAuthTokenSourceConflict(params: {
|
||||
const title = `${GATEWAY_ENV_TOKEN} conflicts with gateway.auth.token`;
|
||||
const detail =
|
||||
`${GATEWAY_ENV_TOKEN} is set while gateway.auth.token uses a different configured source. ` +
|
||||
"Direct local Gateway clients commonly prefer the env token, while the managed gateway service " +
|
||||
"prefers gateway.auth.token. If the values differ, CLI/RPC calls can fail to authenticate " +
|
||||
"with the running gateway.";
|
||||
"Configured local Gateway clients and the managed gateway service prefer gateway.auth.token. " +
|
||||
"Environment credentials remain active for explicit environment URL and node-host targets, " +
|
||||
"so a stale value can still authenticate against the wrong target.";
|
||||
const remediation =
|
||||
`Remove ${GATEWAY_ENV_TOKEN} from the shell, ~/.openclaw/.env, or launchctl env if gateway.auth.token is intended, ` +
|
||||
`or point gateway.auth.token at \${${GATEWAY_ENV_TOKEN}} if the env var should be canonical.`;
|
||||
|
||||
@@ -2335,7 +2335,7 @@ describe("callGateway password resolution", () => {
|
||||
expectedPassword: "secret",
|
||||
},
|
||||
{
|
||||
label: "prefers env password over local config password",
|
||||
label: "prefers local config password over env password",
|
||||
envPassword: "from-env",
|
||||
config: {
|
||||
gateway: {
|
||||
@@ -2344,7 +2344,7 @@ describe("callGateway password resolution", () => {
|
||||
auth: { password: "from-config" },
|
||||
},
|
||||
},
|
||||
expectedPassword: "from-env",
|
||||
expectedPassword: "from-config",
|
||||
},
|
||||
{
|
||||
label: "uses remote password in remote mode when env is unset",
|
||||
@@ -2392,7 +2392,7 @@ describe("callGateway password resolution", () => {
|
||||
expect(lastClientOptions?.password).toBe("resolved-local-ref-password");
|
||||
});
|
||||
|
||||
it("does not resolve local password ref when env password takes precedence", async () => {
|
||||
it("does not let env password mask an unresolved local password ref", async () => {
|
||||
process.env.OPENCLAW_GATEWAY_PASSWORD = "from-env";
|
||||
getRuntimeConfig.mockReturnValue({
|
||||
gateway: {
|
||||
@@ -2410,9 +2410,7 @@ describe("callGateway password resolution", () => {
|
||||
},
|
||||
} as unknown as OpenClawConfig);
|
||||
|
||||
await callGateway({ method: "health" });
|
||||
|
||||
expect(lastClientOptions?.password).toBe("from-env");
|
||||
await expect(callGateway({ method: "health" })).rejects.toThrow("gateway.auth.password");
|
||||
});
|
||||
|
||||
it("does not resolve local password ref when token auth can win", async () => {
|
||||
|
||||
@@ -653,8 +653,7 @@ type ResolvedGatewayCallContext = {
|
||||
remoteUrl?: string;
|
||||
explicitAuth: ExplicitGatewayAuth;
|
||||
modeOverride?: GatewayCredentialMode;
|
||||
localTokenPrecedence?: GatewayCredentialPrecedence;
|
||||
localPasswordPrecedence?: GatewayCredentialPrecedence;
|
||||
localPrecedence?: GatewayCredentialPrecedence;
|
||||
remoteTokenPrecedence?: GatewayRemoteCredentialPrecedence;
|
||||
remotePasswordPrecedence?: GatewayRemoteCredentialPrecedence;
|
||||
remoteTokenFallback?: GatewayRemoteCredentialFallback;
|
||||
@@ -760,8 +759,7 @@ async function resolveGatewayCredentialsWithEnv(
|
||||
urlOverrideSource: context.urlOverrideSource,
|
||||
env,
|
||||
modeOverride: context.modeOverride,
|
||||
localTokenPrecedence: context.localTokenPrecedence,
|
||||
localPasswordPrecedence: context.localPasswordPrecedence,
|
||||
localPrecedence: context.localPrecedence,
|
||||
remoteTokenPrecedence: context.remoteTokenPrecedence,
|
||||
remotePasswordPrecedence: context.remotePasswordPrecedence,
|
||||
remoteTokenFallback: context.remoteTokenFallback,
|
||||
|
||||
@@ -44,7 +44,7 @@ const DEFAULT_ENV = {
|
||||
describe("resolveGatewayConnectionAuth", () => {
|
||||
const cases: ConnectionAuthCase[] = [
|
||||
{
|
||||
name: "local mode defaults to env-first token/password",
|
||||
name: "local mode defaults to config-first token/password",
|
||||
cfgLocal: cfg({
|
||||
gateway: {
|
||||
mode: "local",
|
||||
@@ -59,52 +59,29 @@ describe("resolveGatewayConnectionAuth", () => {
|
||||
},
|
||||
}),
|
||||
env: DEFAULT_ENV,
|
||||
expected: {
|
||||
token: "env-token",
|
||||
password: "env-password", // pragma: allowlist secret
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "local mode supports config-first token/password",
|
||||
cfgLocal: 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",
|
||||
name: "local mode supports explicit env-first token/password",
|
||||
cfgLocal: cfg({
|
||||
gateway: {
|
||||
mode: "local",
|
||||
auth: {},
|
||||
remote: {
|
||||
token: "remote-token",
|
||||
password: "remote-password", // pragma: allowlist secret
|
||||
auth: {
|
||||
token: "config-token",
|
||||
password: "config-password", // pragma: allowlist secret
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: DEFAULT_ENV,
|
||||
options: {
|
||||
localTokenPrecedence: "env-first",
|
||||
localPasswordPrecedence: "config-first", // pragma: allowlist secret
|
||||
localPrecedence: "env-first",
|
||||
},
|
||||
expected: {
|
||||
token: "env-token",
|
||||
password: "remote-password", // pragma: allowlist secret
|
||||
password: "env-password", // pragma: allowlist secret
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -220,6 +197,20 @@ describe("resolveGatewayConnectionAuth", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves an env-template local token through the configured auth path", async () => {
|
||||
await expect(
|
||||
resolveGatewayConnectionAuth({
|
||||
config: cfg({
|
||||
gateway: {
|
||||
mode: "local",
|
||||
auth: { mode: "token", token: "${OPENCLAW_GATEWAY_TOKEN}" },
|
||||
},
|
||||
}),
|
||||
env: { OPENCLAW_GATEWAY_TOKEN: "env-token" },
|
||||
}),
|
||||
).resolves.toEqual({ token: "env-token", password: undefined });
|
||||
});
|
||||
|
||||
it("resolves config-first token SecretRef even when OPENCLAW env token exists", async () => {
|
||||
const config = cfg({
|
||||
gateway: {
|
||||
@@ -242,7 +233,6 @@ describe("resolveGatewayConnectionAuth", () => {
|
||||
const resolved = await resolveGatewayConnectionAuth({
|
||||
config,
|
||||
env,
|
||||
localTokenPrecedence: "config-first",
|
||||
});
|
||||
expect(resolved).toEqual({
|
||||
token: "config-first-token",
|
||||
@@ -273,7 +263,6 @@ describe("resolveGatewayConnectionAuth", () => {
|
||||
const resolved = await resolveGatewayConnectionAuth({
|
||||
config,
|
||||
env,
|
||||
localPasswordPrecedence: "config-first", // pragma: allowlist secret
|
||||
});
|
||||
expect(resolved).toEqual({
|
||||
token: undefined,
|
||||
@@ -303,7 +292,6 @@ describe("resolveGatewayConnectionAuth", () => {
|
||||
resolveGatewayConnectionAuth({
|
||||
config,
|
||||
env,
|
||||
localTokenPrecedence: "config-first",
|
||||
}),
|
||||
).rejects.toThrow("gateway.auth.token");
|
||||
});
|
||||
@@ -331,7 +319,6 @@ describe("resolveGatewayConnectionAuth", () => {
|
||||
resolveGatewayConnectionAuth({
|
||||
config,
|
||||
env,
|
||||
localPasswordPrecedence: "config-first", // pragma: allowlist secret
|
||||
}),
|
||||
).rejects.toThrow("gateway.auth.password");
|
||||
});
|
||||
|
||||
@@ -64,16 +64,6 @@ export function trimCredentialToUndefined(value: unknown): string | undefined {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
/** True when the process env supplies a nonempty Gateway token candidate. */
|
||||
export function hasGatewayTokenEnvCandidate(env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
return Boolean(trimToUndefined(env.OPENCLAW_GATEWAY_TOKEN));
|
||||
}
|
||||
|
||||
/** True when the process env supplies a nonempty Gateway password candidate. */
|
||||
export function hasGatewayPasswordEnvCandidate(env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
return Boolean(trimToUndefined(env.OPENCLAW_GATEWAY_PASSWORD));
|
||||
}
|
||||
|
||||
/** Classify one configured credential input without resolving secret refs. */
|
||||
function resolveConfiguredGatewayCredentialInput(params: {
|
||||
value: unknown;
|
||||
@@ -139,7 +129,6 @@ export function createGatewayCredentialPlan(params: {
|
||||
(authMode !== "token" && authMode !== "none" && !tokenCanWin);
|
||||
const localTokenSurfaceActive =
|
||||
localTokenCanWin &&
|
||||
!envToken &&
|
||||
(authMode === "token" ||
|
||||
(authMode === undefined && !(envPassword || localPassword.configured)));
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ function withGatewayAuthEnv<T>(env: NodeJS.ProcessEnv, fn: () => T): T {
|
||||
describe("gateway credential precedence coverage", () => {
|
||||
const cases: TestCase[] = [
|
||||
{
|
||||
name: "local mode: env overrides config for call/probe/status, auth remains config-first",
|
||||
name: "local mode keeps configured auth aligned across client and server surfaces",
|
||||
cfg: {
|
||||
gateway: {
|
||||
mode: "local",
|
||||
@@ -69,8 +69,8 @@ describe("gateway credential precedence coverage", () => {
|
||||
OPENCLAW_GATEWAY_PASSWORD: "env-password", // pragma: allowlist secret
|
||||
} as NodeJS.ProcessEnv,
|
||||
expected: {
|
||||
call: { token: "env-token", password: "env-password" }, // pragma: allowlist secret
|
||||
probe: { token: "env-token", password: "env-password" }, // pragma: allowlist secret
|
||||
call: { token: "config-token", password: "config-password" }, // pragma: allowlist secret
|
||||
probe: { token: "config-token", password: "config-password" }, // pragma: allowlist secret
|
||||
status: { token: "config-token", password: "config-password" }, // pragma: allowlist secret
|
||||
auth: { token: "config-token", password: "config-password" }, // pragma: allowlist secret
|
||||
},
|
||||
@@ -103,7 +103,7 @@ describe("gateway credential precedence coverage", () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "local mode in gateway service runtime uses config-first token precedence",
|
||||
name: "gateway service runtime uses the same local credential policy",
|
||||
cfg: {
|
||||
gateway: {
|
||||
mode: "local",
|
||||
@@ -119,8 +119,8 @@ describe("gateway credential precedence coverage", () => {
|
||||
OPENCLAW_SERVICE_KIND: "gateway",
|
||||
} as NodeJS.ProcessEnv,
|
||||
expected: {
|
||||
call: { token: "config-token", password: "env-password" }, // pragma: allowlist secret
|
||||
probe: { token: "config-token", password: "env-password" }, // pragma: allowlist secret
|
||||
call: { token: "config-token", password: "config-password" }, // pragma: allowlist secret
|
||||
probe: { token: "config-token", password: "config-password" }, // pragma: allowlist secret
|
||||
status: { token: "config-token", password: "config-password" }, // pragma: allowlist secret
|
||||
auth: { token: "config-token", password: "config-password" }, // pragma: allowlist secret
|
||||
},
|
||||
|
||||
@@ -29,8 +29,7 @@ type GatewayCredentialSecretInputOptions = {
|
||||
urlOverrideSource?: "cli" | "env";
|
||||
env?: NodeJS.ProcessEnv;
|
||||
modeOverride?: GatewayCredentialMode;
|
||||
localTokenPrecedence?: GatewayCredentialPrecedence;
|
||||
localPasswordPrecedence?: GatewayCredentialPrecedence;
|
||||
localPrecedence?: GatewayCredentialPrecedence;
|
||||
remoteTokenPrecedence?: GatewayRemoteCredentialPrecedence;
|
||||
remotePasswordPrecedence?: GatewayRemoteCredentialPrecedence;
|
||||
remoteTokenFallback?: GatewayRemoteCredentialFallback;
|
||||
@@ -101,8 +100,7 @@ function resolveGatewayCredentialsFromConfigOptions(params: {
|
||||
urlOverride: options.urlOverride,
|
||||
urlOverrideSource: options.urlOverrideSource,
|
||||
modeOverride: options.modeOverride,
|
||||
localTokenPrecedence: options.localTokenPrecedence,
|
||||
localPasswordPrecedence: options.localPasswordPrecedence,
|
||||
localPrecedence: options.localPrecedence,
|
||||
remoteTokenPrecedence: options.remoteTokenPrecedence,
|
||||
remotePasswordPrecedence: options.remotePasswordPrecedence ?? "env-first", // pragma: allowlist secret
|
||||
remoteTokenFallback: options.remoteTokenFallback,
|
||||
|
||||
@@ -188,15 +188,15 @@ describe("resolveGatewayCredentialsFromConfig", () => {
|
||||
expectEnvGatewayCredentials(resolved);
|
||||
});
|
||||
|
||||
it("uses local-mode environment values before local config", () => {
|
||||
it("uses local config before local-mode environment values", () => {
|
||||
const resolved = resolveGatewayCredentialsFor({
|
||||
mode: "local",
|
||||
auth: DEFAULT_GATEWAY_AUTH,
|
||||
});
|
||||
expectEnvGatewayCredentials(resolved);
|
||||
expect(resolved).toEqual(DEFAULT_GATEWAY_AUTH);
|
||||
});
|
||||
|
||||
it("uses config-first local token precedence inside gateway service runtime", () => {
|
||||
it("does not let the gateway service marker change local credential precedence", () => {
|
||||
const resolved = resolveGatewayCredentialsFromConfig({
|
||||
cfg: cfg({
|
||||
gateway: {
|
||||
@@ -212,10 +212,19 @@ describe("resolveGatewayCredentialsFromConfig", () => {
|
||||
});
|
||||
expect(resolved).toEqual({
|
||||
token: "config-token",
|
||||
password: "env-password", // pragma: allowlist secret
|
||||
password: "config-password", // pragma: allowlist secret
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps env ahead of remote fallback when local auth is missing", () => {
|
||||
const resolved = resolveGatewayCredentialsFor({
|
||||
mode: "local",
|
||||
auth: {},
|
||||
remote: DEFAULT_REMOTE_AUTH,
|
||||
});
|
||||
expectEnvGatewayCredentials(resolved);
|
||||
});
|
||||
|
||||
it("falls back to remote credentials in local mode when local auth is missing", () => {
|
||||
const resolved = resolveLocalGatewayCredentials({
|
||||
remote: DEFAULT_REMOTE_AUTH,
|
||||
@@ -250,26 +259,23 @@ describe("resolveGatewayCredentialsFromConfig", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("treats env-template local tokens as SecretRefs instead of plaintext", () => {
|
||||
const resolved = resolveGatewayCredentialsFromConfig({
|
||||
cfg: cfg({
|
||||
gateway: {
|
||||
mode: "local",
|
||||
auth: {
|
||||
mode: "token",
|
||||
token: "${OPENCLAW_GATEWAY_TOKEN}",
|
||||
it("fails closed on env-template local tokens in the synchronous resolver", () => {
|
||||
expect(() =>
|
||||
resolveGatewayCredentialsFromConfig({
|
||||
cfg: cfg({
|
||||
gateway: {
|
||||
mode: "local",
|
||||
auth: {
|
||||
mode: "token",
|
||||
token: "${OPENCLAW_GATEWAY_TOKEN}",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {
|
||||
OPENCLAW_GATEWAY_TOKEN: "env-token",
|
||||
} as NodeJS.ProcessEnv,
|
||||
}),
|
||||
env: {
|
||||
OPENCLAW_GATEWAY_TOKEN: "env-token",
|
||||
} as NodeJS.ProcessEnv,
|
||||
});
|
||||
|
||||
expect(resolved).toEqual({
|
||||
token: "env-token",
|
||||
password: undefined,
|
||||
});
|
||||
).toThrow("gateway.auth.token");
|
||||
});
|
||||
|
||||
it("throws when env-template local token SecretRef is unresolved in token mode", () => {
|
||||
|
||||
@@ -7,11 +7,7 @@ import {
|
||||
trimCredentialToUndefined,
|
||||
trimToUndefined,
|
||||
} from "./credential-planner.js";
|
||||
export {
|
||||
hasGatewayPasswordEnvCandidate,
|
||||
hasGatewayTokenEnvCandidate,
|
||||
trimToUndefined,
|
||||
} from "./credential-planner.js";
|
||||
export { trimToUndefined } from "./credential-planner.js";
|
||||
|
||||
export type ExplicitGatewayAuth = {
|
||||
token?: string;
|
||||
@@ -112,25 +108,33 @@ export function resolveGatewayCredentialsFromValues(params: {
|
||||
|
||||
function resolveLocalGatewayCredentials(params: {
|
||||
plan: GatewayCredentialPlan;
|
||||
env: NodeJS.ProcessEnv;
|
||||
localTokenPrecedence: GatewayCredentialPrecedence;
|
||||
localPasswordPrecedence: GatewayCredentialPrecedence;
|
||||
localPrecedence: GatewayCredentialPrecedence;
|
||||
}): ResolvedGatewayCredentials {
|
||||
const fallbackToken = params.plan.localToken.configured
|
||||
const tokenConfigFallback = params.plan.localToken.configured
|
||||
? params.plan.localToken.value
|
||||
: params.plan.remoteToken.value;
|
||||
const fallbackPassword = params.plan.localPassword.configured
|
||||
const passwordConfigFallback = params.plan.localPassword.configured
|
||||
? params.plan.localPassword.value
|
||||
: params.plan.authMode === "trusted-proxy"
|
||||
? undefined
|
||||
: params.plan.remotePassword.value;
|
||||
const localResolved = resolveGatewayCredentialsFromValues({
|
||||
configToken: fallbackToken,
|
||||
configPassword: fallbackPassword,
|
||||
env: params.env,
|
||||
tokenPrecedence: params.localTokenPrecedence,
|
||||
passwordPrecedence: params.localPasswordPrecedence,
|
||||
});
|
||||
const token =
|
||||
params.localPrecedence === "config-first"
|
||||
? firstDefined([
|
||||
params.plan.localToken.value,
|
||||
params.plan.envToken,
|
||||
params.plan.localToken.configured ? undefined : params.plan.remoteToken.value,
|
||||
])
|
||||
: firstDefined([params.plan.envToken, tokenConfigFallback]);
|
||||
const password =
|
||||
params.localPrecedence === "config-first"
|
||||
? firstDefined([
|
||||
params.plan.localPassword.value,
|
||||
params.plan.envPassword,
|
||||
params.plan.localPassword.configured ? undefined : passwordConfigFallback,
|
||||
])
|
||||
: firstDefined([params.plan.envPassword, passwordConfigFallback]);
|
||||
const localResolved = { token, password };
|
||||
const localPasswordCanWin =
|
||||
params.plan.authMode === "password" ||
|
||||
params.plan.authMode === "trusted-proxy" ||
|
||||
@@ -146,7 +150,7 @@ function resolveLocalGatewayCredentials(params: {
|
||||
// unresolved secret ref that would otherwise be the active local credential.
|
||||
if (
|
||||
params.plan.localToken.refPath &&
|
||||
params.localTokenPrecedence === "config-first" &&
|
||||
params.localPrecedence === "config-first" &&
|
||||
!params.plan.localToken.value &&
|
||||
Boolean(params.plan.envToken) &&
|
||||
localTokenCanWin
|
||||
@@ -155,7 +159,7 @@ function resolveLocalGatewayCredentials(params: {
|
||||
}
|
||||
if (
|
||||
params.plan.localPassword.refPath &&
|
||||
params.localPasswordPrecedence === "config-first" && // pragma: allowlist secret
|
||||
params.localPrecedence === "config-first" && // pragma: allowlist secret
|
||||
!params.plan.localPassword.value &&
|
||||
Boolean(params.plan.envPassword) &&
|
||||
localPasswordCanWin
|
||||
@@ -265,8 +269,7 @@ export function resolveGatewayCredentialsFromConfig(params: {
|
||||
urlOverride?: string;
|
||||
urlOverrideSource?: "cli" | "env";
|
||||
modeOverride?: GatewayCredentialMode;
|
||||
localTokenPrecedence?: GatewayCredentialPrecedence;
|
||||
localPasswordPrecedence?: GatewayCredentialPrecedence;
|
||||
localPrecedence?: GatewayCredentialPrecedence;
|
||||
remoteTokenPrecedence?: GatewayRemoteCredentialPrecedence;
|
||||
remotePasswordPrecedence?: GatewayRemoteCredentialPrecedence;
|
||||
remoteTokenFallback?: GatewayRemoteCredentialFallback;
|
||||
@@ -300,17 +303,10 @@ export function resolveGatewayCredentialsFromConfig(params: {
|
||||
});
|
||||
const mode: GatewayCredentialMode = params.modeOverride ?? plan.configuredMode;
|
||||
|
||||
const localTokenPrecedence =
|
||||
params.localTokenPrecedence ??
|
||||
(env.OPENCLAW_SERVICE_KIND === "gateway" ? "config-first" : "env-first");
|
||||
const localPasswordPrecedence = params.localPasswordPrecedence ?? "env-first";
|
||||
|
||||
if (mode === "local") {
|
||||
return resolveLocalGatewayCredentials({
|
||||
plan,
|
||||
env,
|
||||
localTokenPrecedence,
|
||||
localPasswordPrecedence,
|
||||
localPrecedence: params.localPrecedence ?? "config-first",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -227,16 +227,32 @@ describe("ensureGatewayStartupAuth", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses OPENCLAW_GATEWAY_TOKEN without resolving configured token SecretRef", async () => {
|
||||
it("keeps configured token SecretRef ahead of OPENCLAW_GATEWAY_TOKEN", async () => {
|
||||
const configuredToken = gatewayEnvSecretRef("GW_TOKEN");
|
||||
await expectResolvedToken({
|
||||
cfg: createMissingGatewayTokenSecretRefConfig(),
|
||||
cfg: gatewayAuthConfigWithDefaultEnvProvider({
|
||||
mode: "token",
|
||||
token: configuredToken,
|
||||
}),
|
||||
env: {
|
||||
GW_TOKEN: "token-from-config-ref",
|
||||
OPENCLAW_GATEWAY_TOKEN: "token-from-env",
|
||||
} as NodeJS.ProcessEnv,
|
||||
expectedToken: "token-from-env",
|
||||
expectedToken: "token-from-config-ref",
|
||||
expectedConfiguredToken: configuredToken,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not let OPENCLAW_GATEWAY_TOKEN mask an unresolved configured token ref", async () => {
|
||||
await expect(
|
||||
runStartupAuth({
|
||||
cfg: createMissingGatewayTokenSecretRefConfig(),
|
||||
env: { OPENCLAW_GATEWAY_TOKEN: "token-from-env" } as NodeJS.ProcessEnv,
|
||||
persist: true,
|
||||
}),
|
||||
).rejects.toThrow(/MISSING_GW_TOKEN/i);
|
||||
});
|
||||
|
||||
it("fails when gateway.auth.token SecretRef is active and unresolved", async () => {
|
||||
await expect(
|
||||
runStartupAuth({
|
||||
@@ -260,19 +276,35 @@ describe("ensureGatewayStartupAuth", () => {
|
||||
expect(mocks.replaceConfigFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses OPENCLAW_GATEWAY_PASSWORD without resolving configured password SecretRef", async () => {
|
||||
it("keeps configured password SecretRef ahead of OPENCLAW_GATEWAY_PASSWORD", async () => {
|
||||
const configuredPassword = gatewayEnvSecretRef("GW_PASSWORD");
|
||||
const result = await runStartupAuth({
|
||||
cfg: gatewayAuthConfigWithDefaultEnvProvider({
|
||||
mode: "password",
|
||||
password: gatewayEnvSecretRef("MISSING_GW_PASSWORD"),
|
||||
password: configuredPassword,
|
||||
}),
|
||||
env: {
|
||||
GW_PASSWORD: "password-from-config-ref", // pragma: allowlist secret
|
||||
OPENCLAW_GATEWAY_PASSWORD: "password-from-env", // pragma: allowlist secret
|
||||
} as NodeJS.ProcessEnv,
|
||||
persist: true,
|
||||
});
|
||||
|
||||
expectResolvedPassword(result, "password-from-env");
|
||||
expectResolvedPassword(result, "password-from-config-ref");
|
||||
expect(result.cfg.gateway?.auth?.password).toEqual(configuredPassword);
|
||||
});
|
||||
|
||||
it("does not let OPENCLAW_GATEWAY_PASSWORD mask an unresolved configured password ref", async () => {
|
||||
await expect(
|
||||
runStartupAuth({
|
||||
cfg: gatewayAuthConfigWithDefaultEnvProvider({
|
||||
mode: "password",
|
||||
password: gatewayEnvSecretRef("MISSING_GW_PASSWORD"),
|
||||
}),
|
||||
env: { OPENCLAW_GATEWAY_PASSWORD: "password-from-env" } as NodeJS.ProcessEnv,
|
||||
persist: true,
|
||||
}),
|
||||
).rejects.toThrow(/MISSING_GW_PASSWORD/i);
|
||||
});
|
||||
|
||||
it("does not resolve gateway.auth.password SecretRef when token mode is explicit", async () => {
|
||||
|
||||
@@ -11,11 +11,7 @@ import {
|
||||
} from "./auth-config-utils.js";
|
||||
import { assertExplicitGatewayAuthModeWhenBothConfigured } from "./auth-mode-policy.js";
|
||||
import { resolveGatewayAuth, type ResolvedGatewayAuth } from "./auth.js";
|
||||
import {
|
||||
hasGatewayPasswordEnvCandidate,
|
||||
hasGatewayTokenEnvCandidate,
|
||||
trimToUndefined,
|
||||
} from "./credentials.js";
|
||||
import { trimToUndefined } from "./credentials.js";
|
||||
import { assertGatewayAuthNotKnownWeak } from "./known-weak-gateway-secrets.js";
|
||||
|
||||
const HOOKS_GATEWAY_AUTH_REUSE_WARNING =
|
||||
@@ -144,12 +140,8 @@ function hasGatewayTokenOverrideCandidate(params: { authOverride?: GatewayAuthCo
|
||||
}
|
||||
|
||||
function hasGatewayPasswordOverrideCandidate(params: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
authOverride?: GatewayAuthConfig;
|
||||
}): boolean {
|
||||
if (hasGatewayPasswordEnvCandidate(params.env)) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
typeof params.authOverride?.password === "string" &&
|
||||
params.authOverride.password.trim().length > 0
|
||||
@@ -185,22 +177,27 @@ export async function ensureGatewayStartupAuth(params: {
|
||||
cfg: params.cfg,
|
||||
env,
|
||||
mode: explicitMode,
|
||||
hasTokenCandidate:
|
||||
hasGatewayTokenOverrideCandidate({ authOverride: params.authOverride }) ||
|
||||
hasGatewayTokenEnvCandidate(env),
|
||||
hasPasswordCandidate:
|
||||
hasGatewayPasswordOverrideCandidate({ env, authOverride: params.authOverride }) ||
|
||||
hasTokenOverride: hasGatewayTokenOverrideCandidate({ authOverride: params.authOverride }),
|
||||
hasPasswordOverride: hasGatewayPasswordOverrideCandidate({
|
||||
authOverride: params.authOverride,
|
||||
}),
|
||||
hasTokenFallback: Boolean(trimToUndefined(env.OPENCLAW_GATEWAY_TOKEN)),
|
||||
hasPasswordFallback:
|
||||
Boolean(trimToUndefined(env.OPENCLAW_GATEWAY_PASSWORD)) ||
|
||||
hasConfiguredGatewayAuthSecretInput(params.cfg, "gateway.auth.password"),
|
||||
}),
|
||||
resolveGatewayPasswordSecretRefValue({
|
||||
cfg: params.cfg,
|
||||
env,
|
||||
mode: explicitMode,
|
||||
hasPasswordCandidate: hasGatewayPasswordOverrideCandidate({
|
||||
env,
|
||||
hasPasswordOverride: hasGatewayPasswordOverrideCandidate({
|
||||
authOverride: params.authOverride,
|
||||
}),
|
||||
hasTokenCandidate: hasGatewayTokenCandidate({
|
||||
hasTokenOverride: hasGatewayTokenOverrideCandidate({
|
||||
authOverride: params.authOverride,
|
||||
}),
|
||||
hasPasswordFallback: Boolean(trimToUndefined(env.OPENCLAW_GATEWAY_PASSWORD)),
|
||||
hasTokenFallback: hasGatewayTokenCandidate({
|
||||
cfg: params.cfg,
|
||||
env,
|
||||
authOverride: params.authOverride,
|
||||
|
||||
@@ -285,8 +285,7 @@ describe("runNodeHost", () => {
|
||||
},
|
||||
},
|
||||
env: process.env,
|
||||
localTokenPrecedence: "env-first",
|
||||
localPasswordPrecedence: "env-first",
|
||||
localPrecedence: "env-first",
|
||||
remoteTokenPrecedence: "env-first",
|
||||
remotePasswordPrecedence: "env-first",
|
||||
});
|
||||
|
||||
@@ -158,8 +158,7 @@ async function resolveNodeHostGatewayCredentials(params: {
|
||||
return await resolveGatewayConnectionAuth({
|
||||
config: configForResolution,
|
||||
env: params.env,
|
||||
localTokenPrecedence: "env-first",
|
||||
localPasswordPrecedence: "env-first", // pragma: allowlist secret
|
||||
localPrecedence: "env-first",
|
||||
remoteTokenPrecedence: "env-first",
|
||||
remotePasswordPrecedence: "env-first", // pragma: allowlist secret
|
||||
});
|
||||
|
||||
@@ -365,17 +365,6 @@ describe("pairing setup code", () => {
|
||||
},
|
||||
expectedAuthLabel: "password",
|
||||
},
|
||||
{
|
||||
name: "uses OPENCLAW_GATEWAY_PASSWORD without resolving configured password SecretRef",
|
||||
auth: {
|
||||
mode: "password",
|
||||
password: { source: "env", provider: "default", id: "MISSING_GW_PASSWORD" },
|
||||
} as const,
|
||||
env: {
|
||||
OPENCLAW_GATEWAY_PASSWORD: "password-from-env", // pragma: allowlist secret
|
||||
},
|
||||
expectedAuthLabel: "password",
|
||||
},
|
||||
{
|
||||
name: "does not resolve gateway.auth.password SecretRef in token mode",
|
||||
auth: {
|
||||
@@ -419,6 +408,20 @@ describe("pairing setup code", () => {
|
||||
options: { env: {} },
|
||||
expectedError: "MISSING_GW_TOKEN",
|
||||
},
|
||||
{
|
||||
name: "does not let OPENCLAW_GATEWAY_PASSWORD mask a configured password SecretRef",
|
||||
config: createCustomGatewayConfig(
|
||||
{
|
||||
mode: "password",
|
||||
password: { source: "env", provider: "default", id: "MISSING_GW_PASSWORD" },
|
||||
},
|
||||
defaultEnvSecretProviderConfig,
|
||||
),
|
||||
options: {
|
||||
env: { OPENCLAW_GATEWAY_PASSWORD: "password-from-env" },
|
||||
},
|
||||
expectedError: "MISSING_GW_PASSWORD",
|
||||
},
|
||||
] as const)("$name", async ({ config, options, expectedError }) => {
|
||||
await expectResolvedSetupFailureCase({ config, options, expectedError });
|
||||
});
|
||||
|
||||
@@ -419,8 +419,10 @@ export async function resolvePairingSetupFromConfig(
|
||||
cfg,
|
||||
env,
|
||||
mode: cfg.gateway?.auth?.mode,
|
||||
hasTokenCandidate: Boolean(normalizeOptionalString(env.OPENCLAW_GATEWAY_TOKEN)),
|
||||
hasPasswordCandidate: Boolean(normalizeOptionalString(env.OPENCLAW_GATEWAY_PASSWORD)),
|
||||
hasTokenOverride: false,
|
||||
hasPasswordOverride: false,
|
||||
hasTokenFallback: Boolean(normalizeOptionalString(env.OPENCLAW_GATEWAY_TOKEN)),
|
||||
hasPasswordFallback: Boolean(normalizeOptionalString(env.OPENCLAW_GATEWAY_PASSWORD)),
|
||||
});
|
||||
const authLabel = resolvePairingSetupAuthLabel(cfgForAuth, env);
|
||||
if (authLabel.error) {
|
||||
|
||||
@@ -43,7 +43,7 @@ describe("evaluateGatewayAuthSurfaceStates", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("marks gateway.auth.token inactive when env token is configured", () => {
|
||||
it("keeps gateway.auth.token active when env token is configured", () => {
|
||||
const states = evaluate(
|
||||
{
|
||||
gateway: {
|
||||
@@ -58,8 +58,8 @@ describe("evaluateGatewayAuthSurfaceStates", () => {
|
||||
|
||||
expectGatewayState(states["gateway.auth.token"], {
|
||||
hasSecretRef: true,
|
||||
active: false,
|
||||
reason: "gateway token env var is configured.",
|
||||
active: true,
|
||||
reason: 'gateway.auth.mode is "token".',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -137,9 +137,7 @@ export function evaluateGatewayAuthSurfaceStates(params: {
|
||||
return "gateway.auth is not configured.";
|
||||
}
|
||||
if (plan.authMode === "token") {
|
||||
return plan.envToken
|
||||
? "gateway token env var is configured."
|
||||
: 'gateway.auth.mode is "token".';
|
||||
return 'gateway.auth.mode is "token".';
|
||||
}
|
||||
if (
|
||||
plan.authMode === "password" ||
|
||||
|
||||
@@ -39,10 +39,10 @@ describe("security audit gateway auth selection", () => {
|
||||
expectedAuth: { token: "local-token-abc123" },
|
||||
},
|
||||
{
|
||||
name: "prefers env token over local config token",
|
||||
name: "prefers local config token over env token",
|
||||
cfg: { gateway: { mode: "local", auth: { token: "local-token" } } },
|
||||
env: { token: "env-token" },
|
||||
expectedAuth: { token: "env-token" },
|
||||
expectedAuth: { token: "local-token" },
|
||||
},
|
||||
{
|
||||
name: "uses local auth when gateway.mode is undefined (default)",
|
||||
|
||||
@@ -248,8 +248,10 @@ async function materializeAuditGatewayAuthRefs(params: {
|
||||
cfg: params.cfg,
|
||||
env: params.env,
|
||||
mode: params.cfg.gateway?.auth?.mode,
|
||||
hasTokenCandidate: Boolean(normalizeOptionalString(params.env.OPENCLAW_GATEWAY_TOKEN)),
|
||||
hasPasswordCandidate: Boolean(normalizeOptionalString(params.env.OPENCLAW_GATEWAY_PASSWORD)),
|
||||
hasTokenOverride: false,
|
||||
hasPasswordOverride: false,
|
||||
hasTokenFallback: Boolean(normalizeOptionalString(params.env.OPENCLAW_GATEWAY_TOKEN)),
|
||||
hasPasswordFallback: Boolean(normalizeOptionalString(params.env.OPENCLAW_GATEWAY_PASSWORD)),
|
||||
};
|
||||
if (!canMaterializeGatewayAuthSecretRefsWithoutExec(materializeParams)) {
|
||||
return params.cfg;
|
||||
|
||||
@@ -314,7 +314,7 @@ describe("resolveGatewayConnection", () => {
|
||||
expect(result.token).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps normal TUI local password mode env precedence by default", async () => {
|
||||
it("keeps configured local password ahead of the ambient env password", async () => {
|
||||
loadConfig.mockReturnValue({
|
||||
gateway: {
|
||||
mode: "local",
|
||||
@@ -327,7 +327,7 @@ describe("resolveGatewayConnection", () => {
|
||||
|
||||
await withEnvAsync({ OPENCLAW_GATEWAY_PASSWORD: "env-password" }, async () => {
|
||||
const result = await resolveGatewayConnection({});
|
||||
expect(result.password).toBe("env-password");
|
||||
expect(result.password).toBe("config-password");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user