mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix(gateway): isolate method descriptor rows
This commit is contained in:
@@ -218,6 +218,29 @@ describe("method scope resolution", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("skips unreadable plugin gateway method descriptors during scope lookup", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.gatewayHandlers["browser.request"] = pluginHandler;
|
||||
registry.gatewayMethodDescriptors.push(
|
||||
Object.defineProperty({}, "name", {
|
||||
get() {
|
||||
throw new Error("gateway descriptor exploded");
|
||||
},
|
||||
}) as never,
|
||||
createPluginGatewayMethodDescriptor({
|
||||
pluginId: "browser",
|
||||
name: "browser.request",
|
||||
handler: pluginHandler,
|
||||
scope: "operator.read",
|
||||
}),
|
||||
);
|
||||
setActivePluginRegistry(registry);
|
||||
|
||||
expect(resolveLeastPrivilegeOperatorScopesForMethod("browser.request")).toEqual([
|
||||
"operator.read",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps reserved admin namespaces admin-only even if a plugin scope is narrower", () => {
|
||||
setPluginGatewayMethodScope(RESERVED_ADMIN_PLUGIN_METHOD, "operator.read");
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
isDynamicOperatorGatewayMethod,
|
||||
resolveCoreOperatorGatewayMethodScope,
|
||||
} from "./methods/core-descriptors.js";
|
||||
import { resolveGatewayMethodDescriptorScope } from "./methods/registry.js";
|
||||
import {
|
||||
ADMIN_SCOPE,
|
||||
APPROVALS_SCOPE,
|
||||
@@ -49,10 +50,10 @@ function resolveScopedMethod(method: string): OperatorScope | undefined {
|
||||
if (reservedScope) {
|
||||
return reservedScope;
|
||||
}
|
||||
const pluginDescriptor = getPluginRegistryState()?.activeRegistry?.gatewayMethodDescriptors?.find(
|
||||
(descriptor) => descriptor.name === method,
|
||||
const pluginScope = resolveGatewayMethodDescriptorScope(
|
||||
getPluginRegistryState()?.activeRegistry?.gatewayMethodDescriptors,
|
||||
method,
|
||||
);
|
||||
const pluginScope = pluginDescriptor?.scope;
|
||||
return pluginScope === "node" || pluginScope === "dynamic" ? undefined : pluginScope;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
createGatewayMethodRegistry,
|
||||
createPluginGatewayMethodDescriptors,
|
||||
createPluginGatewayMethodDescriptor,
|
||||
listGatewayMethodDescriptorNames,
|
||||
resolveGatewayMethodDescriptorScope,
|
||||
} from "./registry.js";
|
||||
|
||||
const handler: GatewayRequestHandler = ({ respond }) => respond(true, { ok: true });
|
||||
@@ -104,4 +106,70 @@ describe("gateway method registry", () => {
|
||||
expect(registry.getHandler("legacy.ping")).toBe(handler);
|
||||
expect(registry.getScope("legacy.ping")).toBe(ADMIN_SCOPE);
|
||||
});
|
||||
|
||||
it("skips unreadable plugin gateway method descriptor rows", () => {
|
||||
const poisonedDescriptor = Object.defineProperty({}, "name", {
|
||||
get() {
|
||||
throw new Error("gateway descriptor exploded");
|
||||
},
|
||||
});
|
||||
const healthyDescriptor = createPluginGatewayMethodDescriptor({
|
||||
pluginId: "healthy",
|
||||
name: "healthy.ping",
|
||||
handler,
|
||||
scope: READ_SCOPE,
|
||||
});
|
||||
|
||||
const descriptors = createPluginGatewayMethodDescriptors({
|
||||
gatewayHandlers: { "healthy.ping": handler },
|
||||
gatewayMethodDescriptors: [poisonedDescriptor as never, healthyDescriptor],
|
||||
});
|
||||
const registry = createGatewayMethodRegistry(descriptors);
|
||||
|
||||
expect(registry.listMethods()).toEqual(["healthy.ping"]);
|
||||
expect(registry.getScope("healthy.ping")).toBe(READ_SCOPE);
|
||||
});
|
||||
|
||||
it("reads gateway method descriptor names and scopes row-locally", () => {
|
||||
const poisonedDescriptor = Object.defineProperties(
|
||||
{},
|
||||
{
|
||||
name: {
|
||||
get() {
|
||||
throw new Error("gateway descriptor name exploded");
|
||||
},
|
||||
},
|
||||
scope: {
|
||||
get() {
|
||||
throw new Error("gateway descriptor scope exploded");
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
const unreadableScopeDescriptor = Object.defineProperty({ name: "healthy.ping" }, "scope", {
|
||||
get() {
|
||||
throw new Error("gateway descriptor scope exploded");
|
||||
},
|
||||
});
|
||||
const healthyDescriptor = createPluginGatewayMethodDescriptor({
|
||||
pluginId: "healthy",
|
||||
name: "healthy.ping",
|
||||
handler,
|
||||
scope: WRITE_SCOPE,
|
||||
});
|
||||
|
||||
expect(
|
||||
listGatewayMethodDescriptorNames([
|
||||
poisonedDescriptor,
|
||||
unreadableScopeDescriptor,
|
||||
healthyDescriptor,
|
||||
]),
|
||||
).toEqual(["healthy.ping", "healthy.ping"]);
|
||||
expect(
|
||||
resolveGatewayMethodDescriptorScope(
|
||||
[poisonedDescriptor, unreadableScopeDescriptor, healthyDescriptor],
|
||||
"healthy.ping",
|
||||
),
|
||||
).toBe(WRITE_SCOPE);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import {
|
||||
DYNAMIC_GATEWAY_METHOD_SCOPE,
|
||||
type GatewayMethodDescriptor,
|
||||
type GatewayMethodScope,
|
||||
type GatewayMethodHandler,
|
||||
type GatewayMethodDescriptorInput,
|
||||
type GatewayMethodOwner,
|
||||
@@ -22,6 +23,33 @@ function normalizeMethodName(name: string): string {
|
||||
return name.trim();
|
||||
}
|
||||
|
||||
function readGatewayMethodDescriptorName(descriptor: unknown): string | undefined {
|
||||
try {
|
||||
const name = (descriptor as { name?: unknown }).name;
|
||||
return typeof name === "string" ? normalizeMethodName(name) || undefined : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function readGatewayMethodDescriptorScope(descriptor: unknown): GatewayMethodScope | undefined {
|
||||
try {
|
||||
return (descriptor as { scope?: GatewayMethodScope }).scope;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeReadablePluginDescriptor(
|
||||
descriptor: unknown,
|
||||
): GatewayMethodDescriptorInput | undefined {
|
||||
try {
|
||||
return normalizeDescriptor(descriptor as GatewayMethodDescriptorInput);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeDescriptor(input: GatewayMethodDescriptorInput): GatewayMethodDescriptor {
|
||||
const name = normalizeMethodName(input.name);
|
||||
if (!name) {
|
||||
@@ -50,6 +78,37 @@ function normalizeDescriptor(input: GatewayMethodDescriptorInput): GatewayMethod
|
||||
};
|
||||
}
|
||||
|
||||
/** Lists readable method names from plugin/channel-owned descriptor rows. */
|
||||
export function listGatewayMethodDescriptorNames(
|
||||
descriptors: readonly unknown[] | undefined,
|
||||
): string[] {
|
||||
const names: string[] = [];
|
||||
for (const descriptor of descriptors ?? []) {
|
||||
const name = readGatewayMethodDescriptorName(descriptor);
|
||||
if (name) {
|
||||
names.push(name);
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/** Resolves the scope for a readable plugin gateway method descriptor. */
|
||||
export function resolveGatewayMethodDescriptorScope(
|
||||
descriptors: readonly unknown[] | undefined,
|
||||
method: string,
|
||||
): GatewayMethodScope | undefined {
|
||||
for (const descriptor of descriptors ?? []) {
|
||||
if (readGatewayMethodDescriptorName(descriptor) !== method) {
|
||||
continue;
|
||||
}
|
||||
const scope = readGatewayMethodDescriptorScope(descriptor);
|
||||
if (scope !== undefined) {
|
||||
return scope;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Creates a read-only registry for gateway method lookup, listing, and policy metadata. */
|
||||
export function createGatewayMethodRegistry(
|
||||
inputs: readonly GatewayMethodDescriptorInput[],
|
||||
@@ -123,7 +182,10 @@ export function createPluginGatewayMethodDescriptors(
|
||||
): GatewayMethodDescriptorInput[] {
|
||||
const descriptors = registry.gatewayMethodDescriptors ?? [];
|
||||
if (descriptors.length > 0) {
|
||||
return [...descriptors];
|
||||
return descriptors.flatMap((descriptor) => {
|
||||
const normalized = normalizeReadablePluginDescriptor(descriptor);
|
||||
return normalized ? [normalized] : [];
|
||||
});
|
||||
}
|
||||
// Older plugin registries only carried handlers, so keep them callable but assign admin scope
|
||||
// until the plugin can provide explicit descriptor metadata.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { listLoadedChannelPlugins } from "../channels/plugins/registry-loaded.js";
|
||||
import { GATEWAY_EVENT_UPDATE_AVAILABLE } from "./events.js";
|
||||
import { listCoreAdvertisedGatewayMethodNames } from "./methods/core-descriptors.js";
|
||||
import { listGatewayMethodDescriptorNames } from "./methods/registry.js";
|
||||
import { GATEWAY_AUX_METHODS } from "./server-aux-methods.js";
|
||||
|
||||
type GatewayMethodChannelPlugin = {
|
||||
@@ -19,9 +20,7 @@ function listChannelGatewayMethods(): string[] {
|
||||
// Plugins may still expose legacy names while newer plugins expose descriptors.
|
||||
// Merge both so method discovery stays compatible during descriptor adoption.
|
||||
methods.push(...(plugin.gatewayMethods ?? []));
|
||||
for (const descriptor of plugin.gatewayMethodDescriptors ?? []) {
|
||||
methods.push(descriptor.name);
|
||||
}
|
||||
methods.push(...listGatewayMethodDescriptorNames(plugin.gatewayMethodDescriptors));
|
||||
}
|
||||
return methods;
|
||||
}
|
||||
|
||||
@@ -180,6 +180,44 @@ describe("webHandlers web.login.start", () => {
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("skips unreadable gateway method descriptors while resolving the login provider", async () => {
|
||||
const poisonedDescriptor = Object.defineProperty({}, "name", {
|
||||
get() {
|
||||
throw new Error("gateway descriptor exploded");
|
||||
},
|
||||
});
|
||||
const loginWithQrStart = vi.fn().mockResolvedValue({ connected: true });
|
||||
mocks.listChannelPlugins.mockReturnValue([
|
||||
{
|
||||
id: "bad",
|
||||
gatewayMethodDescriptors: [poisonedDescriptor],
|
||||
},
|
||||
{
|
||||
id: "whatsapp",
|
||||
gatewayMethodDescriptors: [{ name: "web.login.start" }],
|
||||
gateway: { loginWithQrStart },
|
||||
},
|
||||
]);
|
||||
const respond = vi.fn();
|
||||
|
||||
await webHandlers["web.login.start"](
|
||||
createOptions(
|
||||
{ accountId: "default" },
|
||||
{
|
||||
respond,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
expect(loginWithQrStart).toHaveBeenCalledWith({
|
||||
accountId: "default",
|
||||
force: false,
|
||||
timeoutMs: undefined,
|
||||
verbose: false,
|
||||
});
|
||||
expect(respond).toHaveBeenCalledWith(true, { connected: true }, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("webHandlers web.login.wait", () => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from "../../../packages/gateway-protocol/src/index.js";
|
||||
import { listChannelPlugins } from "../../channels/plugins/index.js";
|
||||
import type { ChannelId } from "../../channels/plugins/types.public.js";
|
||||
import { listGatewayMethodDescriptorNames } from "../methods/registry.js";
|
||||
import { formatForLog } from "../ws-log.js";
|
||||
import type { GatewayRequestHandlers, RespondFn } from "./types.js";
|
||||
import { assertValidParams } from "./validation.js";
|
||||
@@ -17,7 +18,7 @@ const resolveWebLoginProvider = () =>
|
||||
listChannelPlugins().find((plugin) =>
|
||||
[
|
||||
...(plugin.gatewayMethods ?? []),
|
||||
...(plugin.gatewayMethodDescriptors ?? []).map((descriptor) => descriptor.name),
|
||||
...listGatewayMethodDescriptorNames(plugin.gatewayMethodDescriptors),
|
||||
].some((method) => WEB_LOGIN_METHODS.has(method)),
|
||||
) ?? null;
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ import {
|
||||
createGatewayMethodRegistry,
|
||||
createPluginGatewayMethodDescriptors,
|
||||
isCoreGatewayMethodClassified,
|
||||
listGatewayMethodDescriptorNames,
|
||||
type GatewayMethodRegistry,
|
||||
} from "./methods/registry.js";
|
||||
import { isLoopbackHost } from "./net.js";
|
||||
@@ -730,9 +731,7 @@ export async function startGatewayServer(
|
||||
const methods: string[] = [];
|
||||
for (const plugin of listGatewayStartupChannelPlugins()) {
|
||||
methods.push(...(plugin.gatewayMethods ?? []));
|
||||
for (const descriptor of plugin.gatewayMethodDescriptors ?? []) {
|
||||
methods.push(descriptor.name);
|
||||
}
|
||||
methods.push(...listGatewayMethodDescriptorNames(plugin.gatewayMethodDescriptors));
|
||||
}
|
||||
return methods;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user