fix(gateway): grant operator.read for token-only auth to fix missing scope (#48167)
This commit is contained in:
parent
c2c7087fc8
commit
c99f92fb05
@ -54,6 +54,8 @@ Docs: https://docs.openclaw.ai
|
||||
- CLI/Ollama onboarding: keep the interactive model picker for explicit `openclaw onboard --auth-choice ollama` runs so setup still selects a default model without reintroducing pre-picker auto-pulls. (#49249) Thanks @BruceMacD.
|
||||
- Plugins/bundler TDZ: fix `RESERVED_COMMANDS` temporal dead zone error that prevented device-pair, phone-control, and talk-voice plugins from registering when the bundler placed the commands module after call sites in the same output chunk. Thanks @BunsDev.
|
||||
- Plugins/imports: fix stale googlechat runtime-api import paths and signal SDK circular re-exports broken by recent plugin-sdk refactors. Thanks @BunsDev.
|
||||
- Gateway/auth: grant operator.read for device-less token/password auth so CLI and Dashboard can run read RPCs (devices list, status, probe) instead of getting "missing scope: operator.read". Fixes #48167, #46117, #46716, #17095.
|
||||
- Gateway/handshake: restore handshake timeout to 10s (was reduced to 3s in #44089) so CLI commands like `openclaw devices list` no longer fail with "gateway closed (1000 normal closure)" on slower systems. (#47103)
|
||||
- Google auth/Node 25: patch `gaxios` to use native fetch without injecting `globalThis.window`, while translating proxy and mTLS transport settings so Google Vertex and Google Chat auth keep working on Node 25. (#47914) Thanks @pdd-cli.
|
||||
- Gateway/startup: load bundled channel plugins from compiled `dist/extensions` entries in built installs, so gateway boot no longer recompiles bundled extension TypeScript on every startup and WhatsApp-class cold starts drop back to seconds instead of tens of seconds or worse. (#47560) Thanks @ngutman.
|
||||
- Agents/openai-responses: strip `prompt_cache_key` and `prompt_cache_retention` for non-OpenAI-compatible Responses endpoints while keeping them on direct OpenAI and Azure OpenAI paths, so third-party OpenAI-compatible providers no longer reject those requests with HTTP 400. (#49877) Thanks @ShaunTsai.
|
||||
|
||||
@ -178,13 +178,14 @@ export function registerDefaultAuthTokenSuite(): void {
|
||||
expectConnectError?: string;
|
||||
expectStatusOk?: boolean;
|
||||
expectStatusError?: string;
|
||||
expectAdminRestricted?: boolean;
|
||||
}> = [
|
||||
{
|
||||
name: "operator + valid shared token => connected with cleared scopes",
|
||||
name: "operator + valid shared token => connected with operator.read (fixes #48167)",
|
||||
opts: { role: "operator", token, device: null },
|
||||
expectConnectOk: true,
|
||||
expectStatusOk: false,
|
||||
expectStatusError: "missing scope",
|
||||
expectStatusOk: true,
|
||||
expectAdminRestricted: true,
|
||||
},
|
||||
{
|
||||
name: "node + valid shared token => rejected without device",
|
||||
@ -220,6 +221,13 @@ export function registerDefaultAuthTokenSuite(): void {
|
||||
);
|
||||
}
|
||||
}
|
||||
if (scenario.expectAdminRestricted) {
|
||||
const adminRes = await rpcReq(ws, "set-heartbeats", { enabled: false });
|
||||
expect(adminRes.ok, scenario.name).toBe(false);
|
||||
expect(adminRes.error?.message ?? "", scenario.name).toContain(
|
||||
"missing scope: operator.admin",
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
ws.close();
|
||||
}
|
||||
|
||||
@ -33,6 +33,7 @@ import {
|
||||
mintCanvasCapabilityToken,
|
||||
} from "../../canvas-capability.js";
|
||||
import { normalizeDeviceMetadataForAuth } from "../../device-auth.js";
|
||||
import { READ_SCOPE } from "../../method-scopes.js";
|
||||
import {
|
||||
isLocalishHost,
|
||||
isLoopbackAddress,
|
||||
@ -506,9 +507,12 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
});
|
||||
close(1008, truncateCloseReason(authMessage));
|
||||
};
|
||||
const clearUnboundScopes = () => {
|
||||
if (scopes.length > 0) {
|
||||
scopes = [];
|
||||
const clearUnboundScopes = (grantDefaultForTokenAuth = false) => {
|
||||
if (scopes.length > 0 || grantDefaultForTokenAuth) {
|
||||
// When shared token/password auth succeeds for device-less operator, grant
|
||||
// operator.read instead of zero scopes so CLI/Dashboard can run read RPCs
|
||||
// (devices list, status, probe, etc.). Fixes #48167, #46117, #46716, #17095.
|
||||
scopes = grantDefaultForTokenAuth ? [READ_SCOPE] : [];
|
||||
connectParams.scopes = scopes;
|
||||
}
|
||||
};
|
||||
@ -531,11 +535,19 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
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.
|
||||
if (!device && decision.kind !== "allow") {
|
||||
clearUnboundScopes();
|
||||
// Shared token/password auth can bypass pairing for trusted operators, but
|
||||
// device-less backend clients must not self-declare scopes. Control UI
|
||||
// keeps its explicitly allowed device-less scopes on the allow path.
|
||||
// When allowing device-less token auth, grant operator.read so read RPCs work.
|
||||
// Restrict read-scope fallback to token/password auth only; trusted-proxy
|
||||
// sessions must not gain read scope without a bound device identity.
|
||||
const grantReadForTokenAuth =
|
||||
decision.kind === "allow" &&
|
||||
sharedAuthOk &&
|
||||
(authMethod === "token" || authMethod === "password");
|
||||
if (!device && (!isControlUi || decision.kind !== "allow" || trustedProxyAuthOk)) {
|
||||
clearUnboundScopes(grantReadForTokenAuth);
|
||||
}
|
||||
}
|
||||
if (decision.kind === "allow") {
|
||||
return true;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user