gateway: clear unbound scopes for trusted-proxy auth (#57692)

* gateway: clear unbound scopes for trusted-proxy auth

* gateway: isolate trusted-proxy scope test branch
This commit is contained in:
Jacob Tomlinson
2026-03-30 06:19:00 -07:00
committed by GitHub
parent 566fb73d9d
commit 8b88b927cb
3 changed files with 97 additions and 4 deletions

View File

@@ -3,6 +3,7 @@ import {
evaluateMissingDeviceIdentity,
isTrustedProxyControlUiOperatorAuth,
resolveControlUiAuthPolicy,
shouldClearUnboundScopesForMissingDeviceIdentity,
shouldSkipControlUiPairing,
} from "./connect-policy.js";
@@ -300,4 +301,72 @@ describe("ws connect policy", () => {
).toBe(tc.expected);
}
});
test("clears unbound scopes for device-less shared auth outside explicit preservation cases", () => {
const nonControlUi = resolveControlUiAuthPolicy({
isControlUi: false,
controlUiConfig: undefined,
deviceRaw: null,
});
const controlUi = resolveControlUiAuthPolicy({
isControlUi: true,
controlUiConfig: { allowInsecureAuth: true },
deviceRaw: null,
});
expect(
shouldClearUnboundScopesForMissingDeviceIdentity({
decision: { kind: "allow" },
controlUiAuthPolicy: nonControlUi,
preserveInsecureLocalControlUiScopes: false,
authMethod: "token",
}),
).toBe(true);
expect(
shouldClearUnboundScopesForMissingDeviceIdentity({
decision: { kind: "allow" },
controlUiAuthPolicy: nonControlUi,
preserveInsecureLocalControlUiScopes: false,
authMethod: "password",
}),
).toBe(true);
expect(
shouldClearUnboundScopesForMissingDeviceIdentity({
decision: { kind: "allow" },
controlUiAuthPolicy: nonControlUi,
preserveInsecureLocalControlUiScopes: false,
authMethod: "trusted-proxy",
}),
).toBe(true);
expect(
shouldClearUnboundScopesForMissingDeviceIdentity({
decision: { kind: "allow" },
controlUiAuthPolicy: nonControlUi,
preserveInsecureLocalControlUiScopes: false,
authMethod: undefined,
trustedProxyAuthOk: true,
}),
).toBe(true);
expect(
shouldClearUnboundScopesForMissingDeviceIdentity({
decision: { kind: "allow" },
controlUiAuthPolicy: controlUi,
preserveInsecureLocalControlUiScopes: true,
authMethod: "token",
}),
).toBe(false);
expect(
shouldClearUnboundScopesForMissingDeviceIdentity({
decision: { kind: "reject-device-required" },
controlUiAuthPolicy: nonControlUi,
preserveInsecureLocalControlUiScopes: false,
authMethod: undefined,
}),
).toBe(true);
});
});

View File

@@ -81,6 +81,26 @@ export type MissingDeviceIdentityDecision =
| { kind: "reject-unauthorized" }
| { kind: "reject-device-required" };
export function shouldClearUnboundScopesForMissingDeviceIdentity(params: {
decision: MissingDeviceIdentityDecision;
controlUiAuthPolicy: ControlUiAuthPolicy;
preserveInsecureLocalControlUiScopes: boolean;
authMethod: string | undefined;
trustedProxyAuthOk?: boolean;
}): boolean {
return (
params.decision.kind !== "allow" ||
(!params.controlUiAuthPolicy.allowBypass &&
!params.preserveInsecureLocalControlUiScopes &&
// trusted-proxy auth can bypass pairing for some clients, but those
// self-declared scopes are still unbound without device identity.
(params.authMethod === "token" ||
params.authMethod === "password" ||
params.authMethod === "trusted-proxy" ||
params.trustedProxyAuthOk === true))
);
}
export function evaluateMissingDeviceIdentity(params: {
hasDeviceIdentity: boolean;
role: GatewayRole;

View File

@@ -90,6 +90,7 @@ import {
evaluateMissingDeviceIdentity,
isTrustedProxyControlUiOperatorAuth,
resolveControlUiAuthPolicy,
shouldClearUnboundScopesForMissingDeviceIdentity,
shouldSkipControlUiPairing,
} from "./connect-policy.js";
import {
@@ -548,10 +549,13 @@ export function attachGatewayWsMessageHandler(params: {
// allow path, including trusted token-authenticated backend operators.
if (
!device &&
(decision.kind !== "allow" ||
(!controlUiAuthPolicy.allowBypass &&
!preserveInsecureLocalControlUiScopes &&
(authMethod === "token" || authMethod === "password" || trustedProxyAuthOk)))
shouldClearUnboundScopesForMissingDeviceIdentity({
decision,
controlUiAuthPolicy,
preserveInsecureLocalControlUiScopes,
authMethod,
trustedProxyAuthOk,
})
) {
clearUnboundScopes();
}