fix(auth): treat unconfigured-owner sessions as owner for ownerOnly tools (#26331)

Merged via squash.

Prepared head SHA: 1fbe1c7651
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
This commit is contained in:
Marcus Widing
2026-03-07 00:37:07 +01:00
committed by GitHub
parent ae96a81916
commit 48b3c4a043
3 changed files with 164 additions and 1 deletions

View File

@@ -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

View File

@@ -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);
});
});

View File

@@ -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