mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 04:42:44 +00:00
fix(gateway): allow trusted-proxy control-ui auth to skip device pairing
Control UI connections authenticated via gateway.auth.mode=trusted-proxy were still forced through device pairing because pairing bypass only considered shared token/password auth (sharedAuthOk). In trusted-proxy deployments, this produced persistent "pairing required" failures despite valid trusted proxy headers. Treat authenticated trusted-proxy control-ui connections as pairing-bypass eligible and allow missing device identity in that mode. Fixes #25293 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
committed by
Peter Steinberger
parent
d84659f22f
commit
20523b918a
@@ -49,6 +49,7 @@ describe("ws connect policy", () => {
|
|||||||
role: "node",
|
role: "node",
|
||||||
isControlUi: false,
|
isControlUi: false,
|
||||||
controlUiAuthPolicy: policy,
|
controlUiAuthPolicy: policy,
|
||||||
|
trustedProxyAuthOk: false,
|
||||||
sharedAuthOk: true,
|
sharedAuthOk: true,
|
||||||
authOk: true,
|
authOk: true,
|
||||||
hasSharedAuth: true,
|
hasSharedAuth: true,
|
||||||
@@ -68,6 +69,7 @@ describe("ws connect policy", () => {
|
|||||||
role: "operator",
|
role: "operator",
|
||||||
isControlUi: true,
|
isControlUi: true,
|
||||||
controlUiAuthPolicy: controlUiStrict,
|
controlUiAuthPolicy: controlUiStrict,
|
||||||
|
trustedProxyAuthOk: false,
|
||||||
sharedAuthOk: true,
|
sharedAuthOk: true,
|
||||||
authOk: true,
|
authOk: true,
|
||||||
hasSharedAuth: true,
|
hasSharedAuth: true,
|
||||||
@@ -82,6 +84,7 @@ describe("ws connect policy", () => {
|
|||||||
role: "operator",
|
role: "operator",
|
||||||
isControlUi: true,
|
isControlUi: true,
|
||||||
controlUiAuthPolicy: controlUiStrict,
|
controlUiAuthPolicy: controlUiStrict,
|
||||||
|
trustedProxyAuthOk: false,
|
||||||
sharedAuthOk: true,
|
sharedAuthOk: true,
|
||||||
authOk: true,
|
authOk: true,
|
||||||
hasSharedAuth: true,
|
hasSharedAuth: true,
|
||||||
@@ -101,6 +104,7 @@ describe("ws connect policy", () => {
|
|||||||
role: "operator",
|
role: "operator",
|
||||||
isControlUi: true,
|
isControlUi: true,
|
||||||
controlUiAuthPolicy: controlUiNoInsecure,
|
controlUiAuthPolicy: controlUiNoInsecure,
|
||||||
|
trustedProxyAuthOk: false,
|
||||||
sharedAuthOk: true,
|
sharedAuthOk: true,
|
||||||
authOk: true,
|
authOk: true,
|
||||||
hasSharedAuth: true,
|
hasSharedAuth: true,
|
||||||
@@ -114,6 +118,7 @@ describe("ws connect policy", () => {
|
|||||||
role: "operator",
|
role: "operator",
|
||||||
isControlUi: false,
|
isControlUi: false,
|
||||||
controlUiAuthPolicy: policy,
|
controlUiAuthPolicy: policy,
|
||||||
|
trustedProxyAuthOk: false,
|
||||||
sharedAuthOk: true,
|
sharedAuthOk: true,
|
||||||
authOk: true,
|
authOk: true,
|
||||||
hasSharedAuth: true,
|
hasSharedAuth: true,
|
||||||
@@ -127,6 +132,7 @@ describe("ws connect policy", () => {
|
|||||||
role: "operator",
|
role: "operator",
|
||||||
isControlUi: false,
|
isControlUi: false,
|
||||||
controlUiAuthPolicy: policy,
|
controlUiAuthPolicy: policy,
|
||||||
|
trustedProxyAuthOk: false,
|
||||||
sharedAuthOk: false,
|
sharedAuthOk: false,
|
||||||
authOk: false,
|
authOk: false,
|
||||||
hasSharedAuth: true,
|
hasSharedAuth: true,
|
||||||
@@ -140,15 +146,31 @@ describe("ws connect policy", () => {
|
|||||||
role: "node",
|
role: "node",
|
||||||
isControlUi: false,
|
isControlUi: false,
|
||||||
controlUiAuthPolicy: policy,
|
controlUiAuthPolicy: policy,
|
||||||
|
trustedProxyAuthOk: false,
|
||||||
sharedAuthOk: true,
|
sharedAuthOk: true,
|
||||||
authOk: true,
|
authOk: true,
|
||||||
hasSharedAuth: true,
|
hasSharedAuth: true,
|
||||||
isLocalClient: false,
|
isLocalClient: false,
|
||||||
}).kind,
|
}).kind,
|
||||||
).toBe("reject-device-required");
|
).toBe("reject-device-required");
|
||||||
|
|
||||||
|
// Trusted-proxy authenticated Control UI should bypass device-identity gating.
|
||||||
|
expect(
|
||||||
|
evaluateMissingDeviceIdentity({
|
||||||
|
hasDeviceIdentity: false,
|
||||||
|
role: "operator",
|
||||||
|
isControlUi: true,
|
||||||
|
controlUiAuthPolicy: controlUiNoInsecure,
|
||||||
|
trustedProxyAuthOk: true,
|
||||||
|
sharedAuthOk: false,
|
||||||
|
authOk: true,
|
||||||
|
hasSharedAuth: false,
|
||||||
|
isLocalClient: false,
|
||||||
|
}).kind,
|
||||||
|
).toBe("allow");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("pairing bypass requires control-ui bypass + shared auth", () => {
|
test("pairing bypass requires control-ui bypass + shared auth (or trusted-proxy auth)", () => {
|
||||||
const bypass = resolveControlUiAuthPolicy({
|
const bypass = resolveControlUiAuthPolicy({
|
||||||
isControlUi: true,
|
isControlUi: true,
|
||||||
controlUiConfig: { dangerouslyDisableDeviceAuth: true },
|
controlUiConfig: { dangerouslyDisableDeviceAuth: true },
|
||||||
@@ -159,8 +181,9 @@ describe("ws connect policy", () => {
|
|||||||
controlUiConfig: undefined,
|
controlUiConfig: undefined,
|
||||||
deviceRaw: null,
|
deviceRaw: null,
|
||||||
});
|
});
|
||||||
expect(shouldSkipControlUiPairing(bypass, true)).toBe(true);
|
expect(shouldSkipControlUiPairing(bypass, true, false)).toBe(true);
|
||||||
expect(shouldSkipControlUiPairing(bypass, false)).toBe(false);
|
expect(shouldSkipControlUiPairing(bypass, false, false)).toBe(false);
|
||||||
expect(shouldSkipControlUiPairing(strict, true)).toBe(false);
|
expect(shouldSkipControlUiPairing(strict, true, false)).toBe(false);
|
||||||
|
expect(shouldSkipControlUiPairing(strict, false, true)).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,7 +35,11 @@ export function resolveControlUiAuthPolicy(params: {
|
|||||||
export function shouldSkipControlUiPairing(
|
export function shouldSkipControlUiPairing(
|
||||||
policy: ControlUiAuthPolicy,
|
policy: ControlUiAuthPolicy,
|
||||||
sharedAuthOk: boolean,
|
sharedAuthOk: boolean,
|
||||||
|
trustedProxyAuthOk = false,
|
||||||
): boolean {
|
): boolean {
|
||||||
|
if (trustedProxyAuthOk) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return policy.allowBypass && sharedAuthOk;
|
return policy.allowBypass && sharedAuthOk;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,6 +54,7 @@ export function evaluateMissingDeviceIdentity(params: {
|
|||||||
role: GatewayRole;
|
role: GatewayRole;
|
||||||
isControlUi: boolean;
|
isControlUi: boolean;
|
||||||
controlUiAuthPolicy: ControlUiAuthPolicy;
|
controlUiAuthPolicy: ControlUiAuthPolicy;
|
||||||
|
trustedProxyAuthOk?: boolean;
|
||||||
sharedAuthOk: boolean;
|
sharedAuthOk: boolean;
|
||||||
authOk: boolean;
|
authOk: boolean;
|
||||||
hasSharedAuth: boolean;
|
hasSharedAuth: boolean;
|
||||||
@@ -58,6 +63,9 @@ export function evaluateMissingDeviceIdentity(params: {
|
|||||||
if (params.hasDeviceIdentity) {
|
if (params.hasDeviceIdentity) {
|
||||||
return { kind: "allow" };
|
return { kind: "allow" };
|
||||||
}
|
}
|
||||||
|
if (params.isControlUi && params.trustedProxyAuthOk) {
|
||||||
|
return { kind: "allow" };
|
||||||
|
}
|
||||||
if (params.isControlUi && !params.controlUiAuthPolicy.allowBypass) {
|
if (params.isControlUi && !params.controlUiAuthPolicy.allowBypass) {
|
||||||
// Allow localhost Control UI connections when allowInsecureAuth is configured.
|
// Allow localhost Control UI connections when allowInsecureAuth is configured.
|
||||||
// Localhost has no network interception risk, and browser SubtleCrypto
|
// Localhost has no network interception risk, and browser SubtleCrypto
|
||||||
|
|||||||
@@ -427,11 +427,17 @@ export function attachGatewayWsMessageHandler(params: {
|
|||||||
if (!device) {
|
if (!device) {
|
||||||
clearUnboundScopes();
|
clearUnboundScopes();
|
||||||
}
|
}
|
||||||
|
const trustedProxyAuthOk =
|
||||||
|
isControlUi &&
|
||||||
|
resolvedAuth.mode === "trusted-proxy" &&
|
||||||
|
authOk &&
|
||||||
|
authMethod === "trusted-proxy";
|
||||||
const decision = evaluateMissingDeviceIdentity({
|
const decision = evaluateMissingDeviceIdentity({
|
||||||
hasDeviceIdentity: Boolean(device),
|
hasDeviceIdentity: Boolean(device),
|
||||||
role,
|
role,
|
||||||
isControlUi,
|
isControlUi,
|
||||||
controlUiAuthPolicy,
|
controlUiAuthPolicy,
|
||||||
|
trustedProxyAuthOk,
|
||||||
sharedAuthOk,
|
sharedAuthOk,
|
||||||
authOk,
|
authOk,
|
||||||
hasSharedAuth,
|
hasSharedAuth,
|
||||||
@@ -563,8 +569,13 @@ export function attachGatewayWsMessageHandler(params: {
|
|||||||
// In that case, don't force device pairing on first connect.
|
// In that case, don't force device pairing on first connect.
|
||||||
const skipPairingForOperatorSharedAuth =
|
const skipPairingForOperatorSharedAuth =
|
||||||
role === "operator" && sharedAuthOk && !isControlUi && !isWebchat;
|
role === "operator" && sharedAuthOk && !isControlUi && !isWebchat;
|
||||||
|
const trustedProxyAuthOk =
|
||||||
|
isControlUi &&
|
||||||
|
resolvedAuth.mode === "trusted-proxy" &&
|
||||||
|
authOk &&
|
||||||
|
authMethod === "trusted-proxy";
|
||||||
const skipPairing =
|
const skipPairing =
|
||||||
shouldSkipControlUiPairing(controlUiAuthPolicy, sharedAuthOk) ||
|
shouldSkipControlUiPairing(controlUiAuthPolicy, sharedAuthOk, trustedProxyAuthOk) ||
|
||||||
skipPairingForOperatorSharedAuth;
|
skipPairingForOperatorSharedAuth;
|
||||||
if (device && devicePublicKey && !skipPairing) {
|
if (device && devicePublicKey && !skipPairing) {
|
||||||
const formatAuditList = (items: string[] | undefined): string => {
|
const formatAuditList = (items: string[] | undefined): string => {
|
||||||
|
|||||||
Reference in New Issue
Block a user