From 565ab68951dd174e478a2af85f92e8b89b7ed22a Mon Sep 17 00:00:00 2001 From: xydt-610 <381152212@qq.com> Date: Sat, 21 Mar 2026 11:56:31 +0800 Subject: [PATCH] fix(gateway): retain operator scopes for non-local token-auth clients --- .../server/ws-connection/connect-policy.test.ts | 17 +++++++++++++++++ .../server/ws-connection/connect-policy.ts | 11 ++++++++++- .../server/ws-connection/message-handler.ts | 3 ++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/gateway/server/ws-connection/connect-policy.test.ts b/src/gateway/server/ws-connection/connect-policy.test.ts index a7baa7f73c1..c17f2938974 100644 --- a/src/gateway/server/ws-connection/connect-policy.test.ts +++ b/src/gateway/server/ws-connection/connect-policy.test.ts @@ -141,6 +141,23 @@ describe("ws connect policy", () => { }).kind, ).toBe("reject-unauthorized"); + // #51396: non-local token-auth backend clients retain scopes when authOk + // (primary auth) succeeded, even if sharedAuthOk (probe) disagrees. + expect( + evaluateMissingDeviceIdentity({ + hasDeviceIdentity: false, + role: "operator", + isControlUi: false, + controlUiAuthPolicy: policy, + trustedProxyAuthOk: false, + sharedAuthOk: false, + authOk: true, + authMethod: "token", + hasSharedAuth: true, + isLocalClient: false, + }).kind, + ).toBe("allow"); + expect( evaluateMissingDeviceIdentity({ hasDeviceIdentity: false, diff --git a/src/gateway/server/ws-connection/connect-policy.ts b/src/gateway/server/ws-connection/connect-policy.ts index caf4551a714..77f69f49c72 100644 --- a/src/gateway/server/ws-connection/connect-policy.ts +++ b/src/gateway/server/ws-connection/connect-policy.ts @@ -89,6 +89,7 @@ export function evaluateMissingDeviceIdentity(params: { trustedProxyAuthOk?: boolean; sharedAuthOk: boolean; authOk: boolean; + authMethod?: string; hasSharedAuth: boolean; isLocalClient: boolean; }): MissingDeviceIdentityDecision { @@ -116,7 +117,15 @@ export function evaluateMissingDeviceIdentity(params: { return { kind: "reject-control-ui-insecure-auth" }; } } - if (roleCanSkipDeviceIdentity(params.role, params.sharedAuthOk)) { + // Operator with shared auth (token/password) skips device identity. + // Also allow when token/password auth succeeded directly (#51396: non-local + // token-auth clients were incorrectly getting scopes stripped). + const sharedAuthSufficient = + params.sharedAuthOk || + (params.role === "operator" && + params.authOk && + (params.authMethod === "token" || params.authMethod === "password")); + if (roleCanSkipDeviceIdentity(params.role, sharedAuthSufficient)) { return { kind: "allow" }; } if (!params.authOk && params.hasSharedAuth) { diff --git a/src/gateway/server/ws-connection/message-handler.ts b/src/gateway/server/ws-connection/message-handler.ts index 80aa6437342..a6d5481f04b 100644 --- a/src/gateway/server/ws-connection/message-handler.ts +++ b/src/gateway/server/ws-connection/message-handler.ts @@ -528,12 +528,13 @@ export function attachGatewayWsMessageHandler(params: { trustedProxyAuthOk, sharedAuthOk, authOk, + authMethod, hasSharedAuth, isLocalClient, }); // Shared token/password auth can bypass pairing for trusted operators. // Device-less clients only keep self-declared scopes on the explicit - // allow path, including trusted token-authenticated backend operators. + // allow path, including trusted token-authenticated backend operators (#51396). if (!device && decision.kind !== "allow") { clearUnboundScopes(); }