From 48b3c4a043e3e0bbd24ccc01631ab4adc7db715d Mon Sep 17 00:00:00 2001 From: Marcus Widing Date: Sat, 7 Mar 2026 00:37:07 +0100 Subject: [PATCH] fix(auth): treat unconfigured-owner sessions as owner for ownerOnly tools (#26331) Merged via squash. Prepared head SHA: 1fbe1c765102c223b4e8d6f8e831db54c975430d Co-authored-by: widingmarcus-cyber <245375637+widingmarcus-cyber@users.noreply.github.com> Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com> Reviewed-by: @jalehman --- CHANGELOG.md | 1 + .../command-auth.owner-default.test.ts | 155 ++++++++++++++++++ src/auto-reply/command-auth.ts | 9 +- 3 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 src/auto-reply/command-auth.owner-default.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index dd664c4dadc..e1ebcf56b75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -714,6 +714,7 @@ Docs: https://docs.openclaw.ai - Slack/Disabled channel startup: skip Slack monitor socket startup entirely when `channels.slack.enabled=false` (including configs that still contain valid tokens), preventing disabled accounts from opening websocket connections. (#30586) Thanks @liuxiaopai-ai. - Onboarding/Custom providers: use Azure OpenAI-specific verification auth/payload shape (`api-key`, deployment-path chat completions payload) when probing Azure endpoints so valid Azure custom-provider setup no longer fails preflight. (#29421) Thanks @kunalk16. - Feishu/Docx editing tools: add `feishu_doc` positional insert, table row/column operations, table-cell merge, and color-text updates; switch markdown write/append/insert to Descendant API insertion with large-document batching; and harden image uploads for data URI/base64/local-path inputs with strict validation and routing-safe upload metadata. (#29411) Thanks @Elarwei001. +- Commands/Owner-only tools: treat identified direct-chat senders as owners when no owner allowlist is configured, while preserving internal `operator.admin` owner sessions. (#26331) thanks @widingmarcus-cyber ## 2026.2.26 diff --git a/src/auto-reply/command-auth.owner-default.test.ts b/src/auto-reply/command-auth.owner-default.test.ts new file mode 100644 index 00000000000..117370192d8 --- /dev/null +++ b/src/auto-reply/command-auth.owner-default.test.ts @@ -0,0 +1,155 @@ +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import type { OpenClawConfig } from "../config/config.js"; +import { setActivePluginRegistry } from "../plugins/runtime.js"; +import { createOutboundTestPlugin, createTestRegistry } from "../test-utils/channel-plugins.js"; +import { resolveCommandAuthorization } from "./command-auth.js"; +import type { MsgContext } from "./templating.js"; + +const createRegistry = () => + createTestRegistry([ + { + pluginId: "discord", + plugin: createOutboundTestPlugin({ id: "discord", outbound: { deliveryMode: "direct" } }), + source: "test", + }, + ]); + +beforeEach(() => { + setActivePluginRegistry(createRegistry()); +}); + +afterEach(() => { + setActivePluginRegistry(createRegistry()); +}); + +describe("senderIsOwner defaults to true when no owner allowlist configured (#26319)", () => { + it("senderIsOwner is true when no ownerAllowFrom is configured (single-user default)", () => { + const cfg = { + channels: { discord: {} }, + } as OpenClawConfig; + + const ctx = { + Provider: "discord", + Surface: "discord", + ChatType: "direct", + From: "discord:123", + SenderId: "123", + } as MsgContext; + + const auth = resolveCommandAuthorization({ + ctx, + cfg, + commandAuthorized: true, + }); + + // Without an explicit ownerAllowFrom list, the sole authorized user should + // be treated as owner so ownerOnly tools (cron, gateway) are available. + expect(auth.senderIsOwner).toBe(true); + }); + + it("senderIsOwner is false when no ownerAllowFrom is configured in a group chat", () => { + const cfg = { + channels: { discord: {} }, + } as OpenClawConfig; + + const ctx = { + Provider: "discord", + Surface: "discord", + ChatType: "group", + From: "discord:123", + SenderId: "123", + } as MsgContext; + + const auth = resolveCommandAuthorization({ + ctx, + cfg, + commandAuthorized: true, + }); + + expect(auth.senderIsOwner).toBe(false); + }); + + it("senderIsOwner is false when ownerAllowFrom is configured and sender does not match", () => { + const cfg = { + channels: { discord: {} }, + commands: { ownerAllowFrom: ["456"] }, + } as OpenClawConfig; + + const ctx = { + Provider: "discord", + Surface: "discord", + From: "discord:789", + SenderId: "789", + } as MsgContext; + + const auth = resolveCommandAuthorization({ + ctx, + cfg, + commandAuthorized: true, + }); + + expect(auth.senderIsOwner).toBe(false); + }); + + it("senderIsOwner is true when ownerAllowFrom matches sender", () => { + const cfg = { + channels: { discord: {} }, + commands: { ownerAllowFrom: ["456"] }, + } as OpenClawConfig; + + const ctx = { + Provider: "discord", + Surface: "discord", + From: "discord:456", + SenderId: "456", + } as MsgContext; + + const auth = resolveCommandAuthorization({ + ctx, + cfg, + commandAuthorized: true, + }); + + expect(auth.senderIsOwner).toBe(true); + }); + + it("senderIsOwner is true when ownerAllowFrom is wildcard (*)", () => { + const cfg = { + channels: { discord: {} }, + commands: { ownerAllowFrom: ["*"] }, + } as OpenClawConfig; + + const ctx = { + Provider: "discord", + Surface: "discord", + From: "discord:anyone", + SenderId: "anyone", + } as MsgContext; + + const auth = resolveCommandAuthorization({ + ctx, + cfg, + commandAuthorized: true, + }); + + expect(auth.senderIsOwner).toBe(true); + }); + + it("senderIsOwner is true for internal operator.admin sessions", () => { + const cfg = {} as OpenClawConfig; + + const ctx = { + Provider: "webchat", + Surface: "webchat", + GatewayClientScopes: ["operator.admin"], + } as MsgContext; + + const auth = resolveCommandAuthorization({ + ctx, + cfg, + commandAuthorized: true, + }); + + expect(auth.senderIsOwner).toBe(true); + }); +}); diff --git a/src/auto-reply/command-auth.ts b/src/auto-reply/command-auth.ts index ed37427d50b..87f64e8c8f1 100644 --- a/src/auto-reply/command-auth.ts +++ b/src/auto-reply/command-auth.ts @@ -350,8 +350,15 @@ export function resolveCommandAuthorization(params: { isInternalMessageChannel(ctx.Provider) && Array.isArray(ctx.GatewayClientScopes) && ctx.GatewayClientScopes.includes("operator.admin"); - const senderIsOwner = senderIsOwnerByIdentity || senderIsOwnerByScope; const ownerAllowlistConfigured = ownerAllowAll || explicitOwners.length > 0; + const isDirectChat = (ctx.ChatType ?? "").trim().toLowerCase() === "direct"; + // In the default single-user direct-chat setup, allow an identified sender to + // keep ownerOnly tools even without an explicit owner allowlist. + const senderIsOwner = + senderIsOwnerByIdentity || + senderIsOwnerByScope || + ownerAllowAll || + (!ownerAllowlistConfigured && isDirectChat && Boolean(senderId)); const requireOwner = enforceOwner || ownerAllowlistConfigured; const isOwnerForCommands = !requireOwner ? true