Skip to content

Commit 9fff2b7

Browse files
committed
fix(gateway): detect SecretRef auth rotations
1 parent 8a2207f commit 9fff2b7

3 files changed

Lines changed: 47 additions & 13 deletions

File tree

src/gateway/server-methods/config-write-flow.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
writeRestartSentinel,
1313
} from "../../infra/restart-sentinel.js";
1414
import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js";
15+
import { getActiveSecretsRuntimeSnapshot } from "../../secrets/runtime.js";
1516
import { resolveEffectiveSharedGatewayAuth } from "../auth.js";
1617
import { buildGatewayReloadPlan, resolveGatewayReloadSettings } from "../config-reload.js";
1718
import { formatControlPlaneActor, type ControlPlaneActor } from "../control-plane-audit.js";
@@ -46,6 +47,16 @@ export function didSharedGatewayAuthChange(prev: OpenClawConfig, next: OpenClawC
4647
return prevAuth.mode !== nextAuth.mode || !isDeepStrictEqual(prevAuth.secret, nextAuth.secret);
4748
}
4849

50+
export function didActiveSharedGatewayAuthChange(params: {
51+
fallbackPrev: OpenClawConfig;
52+
next: OpenClawConfig;
53+
}): boolean {
54+
return didSharedGatewayAuthChange(
55+
getActiveSecretsRuntimeSnapshot()?.config ?? params.fallbackPrev,
56+
params.next,
57+
);
58+
}
59+
4960
function queueSharedGatewayAuthDisconnect(
5061
shouldDisconnect: boolean,
5162
context?: GatewayRequestContext,

src/gateway/server-methods/config.shared-auth.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ vi.mock("../../config/runtime-schema.js", () => ({
4141
}));
4242

4343
vi.mock("../../secrets/runtime.js", () => ({
44+
getActiveSecretsRuntimeSnapshot: () => null,
4445
prepareSecretsRuntimeSnapshot: prepareSecretsRuntimeSnapshotMock,
4546
}));
4647

@@ -69,7 +70,11 @@ beforeEach(() => {
6970
ok: true,
7071
config,
7172
}));
72-
prepareSecretsRuntimeSnapshotMock.mockResolvedValue(undefined);
73+
prepareSecretsRuntimeSnapshotMock.mockImplementation(
74+
async ({ config }: { config: OpenClawConfig }) => ({
75+
config,
76+
}),
77+
);
7378
restartSentinelMocks.writeRestartSentinel.mockClear();
7479
});
7580

src/gateway/server-methods/config.ts

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ import { loadGatewayRuntimeConfigSchema } from "../../config/runtime-schema.js";
1818
import { lookupConfigSchema, type ConfigSchemaResponse } from "../../config/schema.js";
1919
import type { ConfigValidationIssue, OpenClawConfig } from "../../config/types.openclaw.js";
2020
import { formatErrorMessage } from "../../infra/errors.js";
21-
import { prepareSecretsRuntimeSnapshot } from "../../secrets/runtime.js";
21+
import {
22+
prepareSecretsRuntimeSnapshot,
23+
type PreparedSecretsRuntimeSnapshot,
24+
} from "../../secrets/runtime.js";
2225
import { diffConfigPaths } from "../config-reload.js";
2326
import {
2427
formatControlPlaneActor,
@@ -40,6 +43,7 @@ import {
4043
import { resolveBaseHashParam } from "./base-hash.js";
4144
import {
4245
commitGatewayConfigWrite,
46+
didActiveSharedGatewayAuthChange,
4347
didSharedGatewayAuthChange,
4448
resolveGatewayConfigPath,
4549
resolveGatewayConfigRestartWriteResult,
@@ -234,13 +238,12 @@ function summarizeConfigValidationIssues(issues: ReadonlyArray<ConfigValidationI
234238
async function ensureResolvableSecretRefsOrRespond(params: {
235239
config: OpenClawConfig;
236240
respond: RespondFn;
237-
}): Promise<boolean> {
241+
}): Promise<PreparedSecretsRuntimeSnapshot | null> {
238242
try {
239-
await prepareSecretsRuntimeSnapshot({
243+
return await prepareSecretsRuntimeSnapshot({
240244
config: params.config,
241245
includeAuthStoreRefs: false,
242246
});
243-
return true;
244247
} catch (error) {
245248
const details = formatErrorMessage(error);
246249
params.respond(
@@ -251,7 +254,7 @@ async function ensureResolvableSecretRefsOrRespond(params: {
251254
`invalid config: active SecretRef resolution failed (${details})`,
252255
),
253256
);
254-
return false;
257+
return null;
255258
}
256259
}
257260

@@ -415,7 +418,11 @@ export const configHandlers: GatewayRequestHandlers = {
415418
);
416419
return;
417420
}
418-
if (!(await ensureResolvableSecretRefsOrRespond({ config: validated.config, respond }))) {
421+
const preparedSecretsSnapshot = await ensureResolvableSecretRefsOrRespond({
422+
config: validated.config,
423+
respond,
424+
});
425+
if (!preparedSecretsSnapshot) {
419426
return;
420427
}
421428
const changedPaths = diffConfigPaths(snapshot.config, validated.config);
@@ -447,10 +454,12 @@ export const configHandlers: GatewayRequestHandlers = {
447454
);
448455
// Compare before the write so we invalidate clients authenticated against the
449456
// previous shared secret immediately after the config update succeeds.
450-
const disconnectSharedAuthClients = didSharedGatewayAuthChange(
451-
snapshot.config,
452-
validated.config,
453-
);
457+
const disconnectSharedAuthClients =
458+
didSharedGatewayAuthChange(snapshot.config, validated.config) ||
459+
didActiveSharedGatewayAuthChange({
460+
fallbackPrev: snapshot.config,
461+
next: preparedSecretsSnapshot.config,
462+
});
454463
const writeResult = await commitGatewayConfigWrite({
455464
snapshot,
456465
writeOptions,
@@ -497,7 +506,11 @@ export const configHandlers: GatewayRequestHandlers = {
497506
if (!parsed) {
498507
return;
499508
}
500-
if (!(await ensureResolvableSecretRefsOrRespond({ config: parsed.config, respond }))) {
509+
const preparedSecretsSnapshot = await ensureResolvableSecretRefsOrRespond({
510+
config: parsed.config,
511+
respond,
512+
});
513+
if (!preparedSecretsSnapshot) {
501514
return;
502515
}
503516
const changedPaths = diffConfigPaths(snapshot.config, parsed.config);
@@ -507,7 +520,12 @@ export const configHandlers: GatewayRequestHandlers = {
507520
);
508521
// Compare before the write so we invalidate clients authenticated against the
509522
// previous shared secret immediately after the config update succeeds.
510-
const disconnectSharedAuthClients = didSharedGatewayAuthChange(snapshot.config, parsed.config);
523+
const disconnectSharedAuthClients =
524+
didSharedGatewayAuthChange(snapshot.config, parsed.config) ||
525+
didActiveSharedGatewayAuthChange({
526+
fallbackPrev: snapshot.config,
527+
next: preparedSecretsSnapshot.config,
528+
});
511529
const writeResult = await commitGatewayConfigWrite({
512530
snapshot,
513531
writeOptions,

0 commit comments

Comments
 (0)