mirror of https://github.com/openclaw/openclaw.git
refactor: centralize gateway method policy helpers
This commit is contained in:
parent
d766465e38
commit
bbb0b574c4
|
|
@ -9,6 +9,16 @@ import {
|
|||
import { listGatewayMethods } from "./server-methods-list.js";
|
||||
import { coreGatewayHandlers } from "./server-methods.js";
|
||||
|
||||
const RESERVED_ADMIN_PLUGIN_METHOD = "config.plugin.inspect";
|
||||
|
||||
function setPluginGatewayMethodScope(method: string, scope: "operator.read" | "operator.write") {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.gatewayMethodScopes = {
|
||||
[method]: scope,
|
||||
};
|
||||
setActivePluginRegistry(registry);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
setActivePluginRegistry(createEmptyPluginRegistry());
|
||||
});
|
||||
|
|
@ -52,13 +62,9 @@ describe("method scope resolution", () => {
|
|||
});
|
||||
|
||||
it("keeps reserved admin namespaces admin-only even if a plugin scope is narrower", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.gatewayMethodScopes = {
|
||||
"config.plugin.inspect": "operator.read",
|
||||
};
|
||||
setActivePluginRegistry(registry);
|
||||
setPluginGatewayMethodScope(RESERVED_ADMIN_PLUGIN_METHOD, "operator.read");
|
||||
|
||||
expect(resolveLeastPrivilegeOperatorScopesForMethod("config.plugin.inspect")).toEqual([
|
||||
expect(resolveLeastPrivilegeOperatorScopesForMethod(RESERVED_ADMIN_PLUGIN_METHOD)).toEqual([
|
||||
"operator.admin",
|
||||
]);
|
||||
});
|
||||
|
|
@ -119,13 +125,11 @@ describe("operator scope authorization", () => {
|
|||
});
|
||||
|
||||
it("requires admin for reserved admin namespaces even if a plugin registered a narrower scope", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.gatewayMethodScopes = {
|
||||
"config.plugin.inspect": "operator.read",
|
||||
};
|
||||
setActivePluginRegistry(registry);
|
||||
setPluginGatewayMethodScope(RESERVED_ADMIN_PLUGIN_METHOD, "operator.read");
|
||||
|
||||
expect(authorizeOperatorScopesForMethod("config.plugin.inspect", ["operator.read"])).toEqual({
|
||||
expect(
|
||||
authorizeOperatorScopesForMethod(RESERVED_ADMIN_PLUGIN_METHOD, ["operator.read"]),
|
||||
).toEqual({
|
||||
allowed: false,
|
||||
missingScope: "operator.admin",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { getActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { isReservedAdminGatewayMethod } from "../shared/gateway-method-prefixes.js";
|
||||
import { resolveReservedGatewayMethodScope } from "../shared/gateway-method-policy.js";
|
||||
|
||||
export const ADMIN_SCOPE = "operator.admin" as const;
|
||||
export const READ_SCOPE = "operator.read" as const;
|
||||
|
|
@ -161,8 +161,9 @@ function resolveScopedMethod(method: string): OperatorScope | undefined {
|
|||
if (explicitScope) {
|
||||
return explicitScope;
|
||||
}
|
||||
if (isReservedAdminGatewayMethod(method)) {
|
||||
return ADMIN_SCOPE;
|
||||
const reservedScope = resolveReservedGatewayMethodScope(method);
|
||||
if (reservedScope) {
|
||||
return reservedScope;
|
||||
}
|
||||
const pluginScope = getActivePluginRegistry()?.gatewayMethodScopes?.[method];
|
||||
if (pluginScope) {
|
||||
|
|
|
|||
|
|
@ -74,6 +74,10 @@ function memoryPluginBody(id: string) {
|
|||
return `module.exports = { id: ${JSON.stringify(id)}, kind: "memory", register() {} };`;
|
||||
}
|
||||
|
||||
const RESERVED_ADMIN_PLUGIN_METHOD = "config.plugin.inspect";
|
||||
const RESERVED_ADMIN_SCOPE_WARNING =
|
||||
"gateway method scope coerced to operator.admin for reserved core namespace";
|
||||
|
||||
function writeBundledPlugin(params: {
|
||||
id: string;
|
||||
body?: string;
|
||||
|
|
@ -975,7 +979,7 @@ describe("loadOpenClawPlugins", () => {
|
|||
id: "reserved-gateway-scope",
|
||||
register(api) {
|
||||
api.registerGatewayMethod(
|
||||
"config.plugin.inspect",
|
||||
${JSON.stringify(RESERVED_ADMIN_PLUGIN_METHOD)},
|
||||
({ respond }) => respond(true, { ok: true }),
|
||||
{ scope: "operator.read" },
|
||||
);
|
||||
|
|
@ -994,12 +998,12 @@ describe("loadOpenClawPlugins", () => {
|
|||
},
|
||||
});
|
||||
|
||||
expect(Object.keys(registry.gatewayHandlers)).toContain("config.plugin.inspect");
|
||||
expect(registry.gatewayMethodScopes?.["config.plugin.inspect"]).toBe("operator.admin");
|
||||
expect(Object.keys(registry.gatewayHandlers)).toContain(RESERVED_ADMIN_PLUGIN_METHOD);
|
||||
expect(registry.gatewayMethodScopes?.[RESERVED_ADMIN_PLUGIN_METHOD]).toBe("operator.admin");
|
||||
expect(
|
||||
registry.diagnostics.some((diag) =>
|
||||
String(diag.message).includes(
|
||||
"gateway method scope coerced to operator.admin for reserved core namespace: config.plugin.inspect",
|
||||
`${RESERVED_ADMIN_SCOPE_WARNING}: ${RESERVED_ADMIN_PLUGIN_METHOD}`,
|
||||
),
|
||||
),
|
||||
).toBe(true);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import type {
|
|||
} from "../gateway/server-methods/types.js";
|
||||
import { registerInternalHook } from "../hooks/internal-hooks.js";
|
||||
import type { HookEntry } from "../hooks/types.js";
|
||||
import { isReservedAdminGatewayMethod } from "../shared/gateway-method-prefixes.js";
|
||||
import { normalizePluginGatewayMethodScope } from "../shared/gateway-method-policy.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { buildPluginApi } from "./api-builder.js";
|
||||
import { registerPluginCommand, validatePluginCommandDefinition } from "./command-registration.js";
|
||||
|
|
@ -434,20 +434,16 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
|||
return;
|
||||
}
|
||||
registry.gatewayHandlers[trimmed] = handler;
|
||||
let effectiveScope = opts?.scope;
|
||||
if (
|
||||
effectiveScope &&
|
||||
effectiveScope !== "operator.admin" &&
|
||||
isReservedAdminGatewayMethod(trimmed)
|
||||
) {
|
||||
const normalizedScope = normalizePluginGatewayMethodScope(trimmed, opts?.scope);
|
||||
if (normalizedScope.coercedToReservedAdmin) {
|
||||
pushDiagnostic({
|
||||
level: "warn",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: `gateway method scope coerced to operator.admin for reserved core namespace: ${trimmed}`,
|
||||
});
|
||||
effectiveScope = "operator.admin";
|
||||
}
|
||||
const effectiveScope = normalizedScope.scope;
|
||||
if (effectiveScope) {
|
||||
registry.gatewayMethodScopes ??= {};
|
||||
registry.gatewayMethodScopes[trimmed] = effectiveScope;
|
||||
|
|
|
|||
|
|
@ -1992,6 +1992,13 @@ export type OpenClawPluginApi = {
|
|||
registerHttpRoute: (params: OpenClawPluginHttpRouteParams) => void;
|
||||
/** Register a native messaging channel plugin (channel capability). */
|
||||
registerChannel: (registration: OpenClawPluginChannelRegistration | ChannelPlugin) => void;
|
||||
/**
|
||||
* Register a gateway RPC method for this plugin.
|
||||
*
|
||||
* Reserved core admin namespaces (`config.*`, `exec.approvals.*`,
|
||||
* `wizard.*`, `update.*`) always normalize to `operator.admin` even if a
|
||||
* narrower scope is requested.
|
||||
*/
|
||||
registerGatewayMethod: (
|
||||
method: string,
|
||||
handler: GatewayRequestHandler,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
export const RESERVED_ADMIN_GATEWAY_METHOD_PREFIXES = [
|
||||
"exec.approvals.",
|
||||
"config.",
|
||||
"wizard.",
|
||||
"update.",
|
||||
] as const;
|
||||
|
||||
export const RESERVED_ADMIN_GATEWAY_METHOD_SCOPE = "operator.admin" as const;
|
||||
|
||||
export function isReservedAdminGatewayMethod(method: string): boolean {
|
||||
return RESERVED_ADMIN_GATEWAY_METHOD_PREFIXES.some((prefix) => method.startsWith(prefix));
|
||||
}
|
||||
|
||||
export function resolveReservedGatewayMethodScope(
|
||||
method: string,
|
||||
): typeof RESERVED_ADMIN_GATEWAY_METHOD_SCOPE | undefined {
|
||||
if (!isReservedAdminGatewayMethod(method)) {
|
||||
return undefined;
|
||||
}
|
||||
return RESERVED_ADMIN_GATEWAY_METHOD_SCOPE;
|
||||
}
|
||||
|
||||
export function normalizePluginGatewayMethodScope<TScope extends string>(
|
||||
method: string,
|
||||
scope: TScope | undefined,
|
||||
): {
|
||||
scope: TScope | typeof RESERVED_ADMIN_GATEWAY_METHOD_SCOPE | undefined;
|
||||
coercedToReservedAdmin: boolean;
|
||||
} {
|
||||
const reservedScope = resolveReservedGatewayMethodScope(method);
|
||||
if (!reservedScope || !scope || scope === reservedScope) {
|
||||
return {
|
||||
scope,
|
||||
coercedToReservedAdmin: false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
scope: reservedScope,
|
||||
coercedToReservedAdmin: true,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
export const ADMIN_GATEWAY_METHOD_PREFIXES = [
|
||||
"exec.approvals.",
|
||||
"config.",
|
||||
"wizard.",
|
||||
"update.",
|
||||
] as const;
|
||||
|
||||
export function isReservedAdminGatewayMethod(method: string): boolean {
|
||||
return ADMIN_GATEWAY_METHOD_PREFIXES.some((prefix) => method.startsWith(prefix));
|
||||
}
|
||||
Loading…
Reference in New Issue