harden talk-voice config persistence scope checks

This commit is contained in:
Robin Waslander 2026-03-30 15:38:05 +02:00
parent ee52f64226
commit a4e447a16e
No known key found for this signature in database
GPG Key ID: 712657D6EA17B7E5
2 changed files with 20 additions and 4 deletions

View File

@ -233,6 +233,14 @@ describe("talk-voice plugin", () => {
expect(runtime.config.writeConfigFile).not.toHaveBeenCalled();
});
it("rejects /voice set from non-webchat gateway callers missing operator.admin", async () => {
const { runtime, run } = createElevenlabsVoiceSetHarness("telegram", ["operator.write"]);
const result = await run();
expect(result.text).toContain("requires operator.admin");
expect(runtime.config.writeConfigFile).not.toHaveBeenCalled();
});
it("allows /voice set from gateway client with operator.admin scope", async () => {
const { runtime, run } = createElevenlabsVoiceSetHarness("webchat", ["operator.admin"]);
const result = await run();

View File

@ -99,6 +99,15 @@ function asProviderBaseUrl(value: unknown): string | undefined {
return trimmed || undefined;
}
const TALK_ADMIN_SCOPE = "operator.admin";
function requiresAdminToSetVoice(channel: string, gatewayClientScopes?: readonly string[]): boolean {
if (Array.isArray(gatewayClientScopes)) {
return !gatewayClientScopes.includes(TALK_ADMIN_SCOPE);
}
return channel === "webchat";
}
export default definePluginEntry({
id: "talk-voice",
name: "Talk Voice",
@ -164,10 +173,9 @@ export default definePluginEntry({
}
if (action === "set") {
// Internal gateway callers already expose operator scopes and should
// match the admin-only config.patch RPC. External channels rely on
// the plugin command's authorized-sender gate instead.
if (ctx.channel === "webchat" && !ctx.gatewayClientScopes?.includes("operator.admin")) {
// Gateway callers can override messageChannel, so scope presence is
// the reliable signal for internal admin-only mutations.
if (requiresAdminToSetVoice(ctx.channel, ctx.gatewayClientScopes)) {
return { text: `⚠️ ${commandLabel} set requires operator.admin.` };
}