mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-24 00:11:31 +00:00
fix(gateway): clear trusted-proxy control ui scopes
This commit is contained in:
@@ -104,6 +104,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Tlon: honor explicit empty allowlists and defer cite expansion. (#46788) Thanks @zpbrent and @vincentkoc.
|
||||
- Tlon/DM auth: defer cited-message expansion until after DM authorization and owner command handling, so unauthorized DMs and owner approval/admin commands no longer trigger cross-channel cite fetches before the deny or command path.
|
||||
- Docs/security audit: spell out that `gateway.controlUi.allowedOrigins: ["*"]` is an explicit allow-all browser-origin policy and should be avoided outside tightly controlled local testing.
|
||||
- Gateway/auth: clear self-declared scopes for device-less trusted-proxy Control UI sessions so proxy-authenticated connects cannot claim admin or secrets scopes without a bound device identity.
|
||||
- Nodes/pending actions: re-check queued foreground actions against the current node command policy before returning them to the node. (#46815) Thanks @zpbrent and @vincentkoc.
|
||||
- Node/startup: remove leftover debug `console.log("node host PATH: ...")` that printed the resolved PATH on every `openclaw node run` invocation. (#46515) Fixes #46411. Thanks @ademczuk.
|
||||
- CLI/completion: reduce recursive completion-script string churn and fix nested PowerShell command-path matching so generated nested completions resolve on PowerShell too. (#45537) Thanks @yiShanXin and @vincentkoc.
|
||||
|
||||
@@ -36,14 +36,12 @@ export function registerControlUiAndPairingSuite(): void {
|
||||
expectedOk: boolean;
|
||||
expectedErrorSubstring?: string;
|
||||
expectedErrorCode?: string;
|
||||
expectStatusChecks: boolean;
|
||||
}> = [
|
||||
{
|
||||
name: "allows trusted-proxy control ui operator without device identity",
|
||||
role: "operator",
|
||||
withUnpairedNodeDevice: false,
|
||||
expectedOk: true,
|
||||
expectStatusChecks: true,
|
||||
},
|
||||
{
|
||||
name: "rejects trusted-proxy control ui node role without device identity",
|
||||
@@ -52,7 +50,6 @@ export function registerControlUiAndPairingSuite(): void {
|
||||
expectedOk: false,
|
||||
expectedErrorSubstring: "control ui requires device identity",
|
||||
expectedErrorCode: ConnectErrorDetailCodes.CONTROL_UI_DEVICE_IDENTITY_REQUIRED,
|
||||
expectStatusChecks: false,
|
||||
},
|
||||
{
|
||||
name: "requires pairing for trusted-proxy control ui node role with unpaired device",
|
||||
@@ -61,7 +58,6 @@ export function registerControlUiAndPairingSuite(): void {
|
||||
expectedOk: false,
|
||||
expectedErrorSubstring: "pairing required",
|
||||
expectedErrorCode: ConnectErrorDetailCodes.PAIRING_REQUIRED,
|
||||
expectStatusChecks: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -96,6 +92,26 @@ export function registerControlUiAndPairingSuite(): void {
|
||||
expect(admin.ok).toBe(true);
|
||||
};
|
||||
|
||||
const expectStatusMissingScopeButHealthOk = async (ws: WebSocket) => {
|
||||
const status = await rpcReq(ws, "status");
|
||||
expect(status.ok).toBe(false);
|
||||
expect(status.error?.message ?? "").toContain("missing scope");
|
||||
const health = await rpcReq(ws, "health");
|
||||
expect(health.ok).toBe(true);
|
||||
};
|
||||
|
||||
const expectAdminRpcDenied = async (ws: WebSocket) => {
|
||||
const admin = await rpcReq(ws, "set-heartbeats", { enabled: false });
|
||||
expect(admin.ok).toBe(false);
|
||||
expect(admin.error?.message).toBe("missing scope: operator.admin");
|
||||
};
|
||||
|
||||
const expectTalkSecretsDenied = async (ws: WebSocket) => {
|
||||
const talk = await rpcReq(ws, "talk.config", { includeSecrets: true });
|
||||
expect(talk.ok).toBe(false);
|
||||
expect(talk.error?.message).toBe("missing scope: operator.read");
|
||||
};
|
||||
|
||||
const connectControlUiWithoutDeviceAndExpectOk = async (params: {
|
||||
ws: WebSocket;
|
||||
token?: string;
|
||||
@@ -221,17 +237,34 @@ export function registerControlUiAndPairingSuite(): void {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
if (tc.expectStatusChecks) {
|
||||
await expectStatusAndHealthOk(ws);
|
||||
if (tc.role === "operator") {
|
||||
await expectAdminRpcOk(ws);
|
||||
}
|
||||
}
|
||||
ws.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test("clears self-declared scopes for trusted-proxy control ui without device identity", async () => {
|
||||
await configureTrustedProxyControlUiAuth();
|
||||
await withGatewayServer(async ({ port }) => {
|
||||
const ws = await openWs(port, TRUSTED_PROXY_CONTROL_UI_HEADERS);
|
||||
try {
|
||||
const res = await connectReq(ws, {
|
||||
skipDefaultAuth: true,
|
||||
scopes: ["operator.admin"],
|
||||
device: null,
|
||||
client: { ...CONTROL_UI_CLIENT },
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
expect((res.payload as { auth?: unknown } | undefined)?.auth).toBeUndefined();
|
||||
|
||||
await expectStatusMissingScopeButHealthOk(ws);
|
||||
await expectAdminRpcDenied(ws);
|
||||
await expectTalkSecretsDenied(ws);
|
||||
} finally {
|
||||
ws.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("allows localhost control ui without device identity when insecure auth is enabled", async () => {
|
||||
testState.gatewayControlUi = { allowInsecureAuth: true };
|
||||
const { server, ws, prevToken } = await startServerWithClient("secret", {
|
||||
|
||||
@@ -532,9 +532,9 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
isLocalClient,
|
||||
});
|
||||
// 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.
|
||||
if (!device && (!isControlUi || decision.kind !== "allow")) {
|
||||
// device-less clients must not keep self-declared scopes unless the
|
||||
// operator explicitly chose a local break-glass Control UI mode.
|
||||
if (!device && (!isControlUi || decision.kind !== "allow" || trustedProxyAuthOk)) {
|
||||
clearUnboundScopes();
|
||||
}
|
||||
if (decision.kind === "allow") {
|
||||
|
||||
Reference in New Issue
Block a user