Skip to content

Commit 9a4afec

Browse files
committed
fix(gateway): authorize attached plugin method registry
1 parent 9823cd4 commit 9a4afec

7 files changed

Lines changed: 93 additions & 385 deletions

scripts/repro/issue-92044-workboard-e2e.mjs

Lines changed: 0 additions & 65 deletions
This file was deleted.

scripts/repro/issue-92044-workboard-scope.mjs

Lines changed: 0 additions & 151 deletions
This file was deleted.

src/gateway/method-scopes.test.ts

Lines changed: 1 addition & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,11 @@
33
*/
44
import { afterEach, describe, expect, it } from "vitest";
55
import { createEmptyPluginRegistry } from "../plugins/registry-empty.js";
6-
import {
7-
pinActivePluginChannelRegistry,
8-
pinActivePluginHttpRouteRegistry,
9-
setActivePluginRegistry,
10-
} from "../plugins/runtime.js";
6+
import { setActivePluginRegistry } from "../plugins/runtime.js";
117
import {
128
authorizeOperatorScopesForMethod,
139
isGatewayMethodClassified,
1410
resolveLeastPrivilegeOperatorScopesForMethod,
15-
resolveRequiredOperatorScopeForMethod,
1611
} from "./method-scopes.js";
1712
import { createPluginGatewayMethodDescriptor } from "./methods/registry.js";
1813
import { listGatewayMethods } from "./server-methods-list.js";
@@ -230,144 +225,6 @@ describe("method scope resolution", () => {
230225
"operator.admin",
231226
]);
232227
});
233-
234-
// Regression coverage for #92044: plugin-registered gateway methods were
235-
// silently requiring operator.admin because resolveScopedMethod only looked at
236-
// activeRegistry.gatewayMethodDescriptors. The plugin descriptors can live in
237-
// the http-route or channel surface instead, and a request entering through
238-
// those surfaces must still see the plugin-declared scope.
239-
it("resolves a plugin method scope when its descriptor lives on the http route surface", () => {
240-
const registry = createEmptyPluginRegistry();
241-
registry.gatewayHandlers["workboard.cards.dispatch"] = pluginHandler;
242-
registry.gatewayMethodDescriptors.push(
243-
createPluginGatewayMethodDescriptor({
244-
pluginId: "workboard",
245-
name: "workboard.cards.dispatch",
246-
handler: pluginHandler,
247-
scope: "operator.write",
248-
}),
249-
);
250-
setActivePluginRegistry(createEmptyPluginRegistry());
251-
pinActivePluginHttpRouteRegistry(registry);
252-
253-
expect(resolveRequiredOperatorScopeForMethod("workboard.cards.dispatch")).toBe(
254-
"operator.write",
255-
);
256-
expect(
257-
authorizeOperatorScopesForMethod("workboard.cards.dispatch", ["operator.write"]),
258-
).toEqual({ allowed: true });
259-
expect(authorizeOperatorScopesForMethod("workboard.cards.dispatch", ["operator.read"])).toEqual(
260-
{ allowed: false, missingScope: "operator.write" },
261-
);
262-
});
263-
264-
it("resolves a plugin method scope when its descriptor lives on the channel surface", () => {
265-
const registry = createEmptyPluginRegistry();
266-
registry.gatewayHandlers["workboard.cards.dispatch"] = pluginHandler;
267-
registry.gatewayMethodDescriptors.push(
268-
createPluginGatewayMethodDescriptor({
269-
pluginId: "workboard",
270-
name: "workboard.cards.dispatch",
271-
handler: pluginHandler,
272-
scope: "operator.write",
273-
}),
274-
);
275-
setActivePluginRegistry(createEmptyPluginRegistry());
276-
pinActivePluginChannelRegistry(registry);
277-
278-
expect(resolveRequiredOperatorScopeForMethod("workboard.cards.dispatch")).toBe(
279-
"operator.write",
280-
);
281-
expect(
282-
authorizeOperatorScopesForMethod("workboard.cards.dispatch", ["operator.write"]),
283-
).toEqual({ allowed: true });
284-
});
285-
286-
it("prefers the active surface over http-route or channel surfaces for plugin scopes", () => {
287-
const httpRouteRegistry = createEmptyPluginRegistry();
288-
httpRouteRegistry.gatewayHandlers["workboard.cards.dispatch"] = pluginHandler;
289-
httpRouteRegistry.gatewayMethodDescriptors.push(
290-
createPluginGatewayMethodDescriptor({
291-
pluginId: "workboard",
292-
name: "workboard.cards.dispatch",
293-
handler: pluginHandler,
294-
scope: "operator.read",
295-
}),
296-
);
297-
const activeRegistry = createEmptyPluginRegistry();
298-
activeRegistry.gatewayHandlers["workboard.cards.dispatch"] = pluginHandler;
299-
activeRegistry.gatewayMethodDescriptors.push(
300-
createPluginGatewayMethodDescriptor({
301-
pluginId: "workboard",
302-
name: "workboard.cards.dispatch",
303-
handler: pluginHandler,
304-
scope: "operator.write",
305-
}),
306-
);
307-
setActivePluginRegistry(activeRegistry);
308-
pinActivePluginHttpRouteRegistry(httpRouteRegistry);
309-
310-
expect(resolveRequiredOperatorScopeForMethod("workboard.cards.dispatch")).toBe(
311-
"operator.write",
312-
);
313-
});
314-
315-
it("still returns the admin-scope default for an unknown method when surfaces are empty", () => {
316-
setActivePluginRegistry(createEmptyPluginRegistry());
317-
pinActivePluginHttpRouteRegistry(createEmptyPluginRegistry());
318-
pinActivePluginChannelRegistry(createEmptyPluginRegistry());
319-
320-
// Unknown method should fall through to admin via authorizeOperatorScopesForMethod
321-
// (resolveScopedMethod returns undefined, then the auth check defaults to admin).
322-
expect(authorizeOperatorScopesForMethod("totally.unknown.method", ["operator.write"])).toEqual({
323-
allowed: false,
324-
missingScope: "operator.admin",
325-
});
326-
});
327-
328-
// Sibling coverage for #78894 (memory-core dream-promotion cron, same root-cause
329-
// shape): a memory-core style plugin that registers its method via
330-
// api.registerGatewayMethod should resolve to its declared scope through the same
331-
// cross-surface path.
332-
it("resolves a memory-core style plugin descriptor from the active registry", () => {
333-
const registry = createEmptyPluginRegistry();
334-
registry.gatewayHandlers["memory.dream.promote"] = pluginHandler;
335-
registry.gatewayMethodDescriptors.push(
336-
createPluginGatewayMethodDescriptor({
337-
pluginId: "memory-core",
338-
name: "memory.dream.promote",
339-
handler: pluginHandler,
340-
scope: "operator.write",
341-
}),
342-
);
343-
setActivePluginRegistry(registry);
344-
345-
expect(resolveRequiredOperatorScopeForMethod("memory.dream.promote")).toBe("operator.write");
346-
expect(authorizeOperatorScopesForMethod("memory.dream.promote", ["operator.write"])).toEqual({
347-
allowed: true,
348-
});
349-
expect(authorizeOperatorScopesForMethod("memory.dream.promote", ["operator.read"])).toEqual({
350-
allowed: false,
351-
missingScope: "operator.write",
352-
});
353-
});
354-
355-
it("resolves a memory-core style plugin descriptor from the channel surface", () => {
356-
const registry = createEmptyPluginRegistry();
357-
registry.gatewayHandlers["memory.dream.promote"] = pluginHandler;
358-
registry.gatewayMethodDescriptors.push(
359-
createPluginGatewayMethodDescriptor({
360-
pluginId: "memory-core",
361-
name: "memory.dream.promote",
362-
handler: pluginHandler,
363-
scope: "operator.write",
364-
}),
365-
);
366-
setActivePluginRegistry(createEmptyPluginRegistry());
367-
pinActivePluginChannelRegistry(registry);
368-
369-
expect(resolveRequiredOperatorScopeForMethod("memory.dream.promote")).toBe("operator.write");
370-
});
371228
});
372229

373230
describe("operator scope authorization", () => {

0 commit comments

Comments
 (0)