Skip to content

Commit 48b3c4a

Browse files
fix(auth): treat unconfigured-owner sessions as owner for ownerOnly tools (openclaw#26331)
Merged via squash. Prepared head SHA: 1fbe1c7 Co-authored-by: widingmarcus-cyber <[email protected]> Co-authored-by: jalehman <[email protected]> Reviewed-by: @jalehman
1 parent ae96a81 commit 48b3c4a

File tree

3 files changed

+164
-1
lines changed

3 files changed

+164
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,7 @@ Docs: https://docs.openclaw.ai
714714
- 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.
715715
- 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.
716716
- 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.
717+
- 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
717718

718719
## 2026.2.26
719720

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
2+
import type { OpenClawConfig } from "../config/config.js";
3+
import { setActivePluginRegistry } from "../plugins/runtime.js";
4+
import { createOutboundTestPlugin, createTestRegistry } from "../test-utils/channel-plugins.js";
5+
import { resolveCommandAuthorization } from "./command-auth.js";
6+
import type { MsgContext } from "./templating.js";
7+
8+
const createRegistry = () =>
9+
createTestRegistry([
10+
{
11+
pluginId: "discord",
12+
plugin: createOutboundTestPlugin({ id: "discord", outbound: { deliveryMode: "direct" } }),
13+
source: "test",
14+
},
15+
]);
16+
17+
beforeEach(() => {
18+
setActivePluginRegistry(createRegistry());
19+
});
20+
21+
afterEach(() => {
22+
setActivePluginRegistry(createRegistry());
23+
});
24+
25+
describe("senderIsOwner defaults to true when no owner allowlist configured (#26319)", () => {
26+
it("senderIsOwner is true when no ownerAllowFrom is configured (single-user default)", () => {
27+
const cfg = {
28+
channels: { discord: {} },
29+
} as OpenClawConfig;
30+
31+
const ctx = {
32+
Provider: "discord",
33+
Surface: "discord",
34+
ChatType: "direct",
35+
From: "discord:123",
36+
SenderId: "123",
37+
} as MsgContext;
38+
39+
const auth = resolveCommandAuthorization({
40+
ctx,
41+
cfg,
42+
commandAuthorized: true,
43+
});
44+
45+
// Without an explicit ownerAllowFrom list, the sole authorized user should
46+
// be treated as owner so ownerOnly tools (cron, gateway) are available.
47+
expect(auth.senderIsOwner).toBe(true);
48+
});
49+
50+
it("senderIsOwner is false when no ownerAllowFrom is configured in a group chat", () => {
51+
const cfg = {
52+
channels: { discord: {} },
53+
} as OpenClawConfig;
54+
55+
const ctx = {
56+
Provider: "discord",
57+
Surface: "discord",
58+
ChatType: "group",
59+
From: "discord:123",
60+
SenderId: "123",
61+
} as MsgContext;
62+
63+
const auth = resolveCommandAuthorization({
64+
ctx,
65+
cfg,
66+
commandAuthorized: true,
67+
});
68+
69+
expect(auth.senderIsOwner).toBe(false);
70+
});
71+
72+
it("senderIsOwner is false when ownerAllowFrom is configured and sender does not match", () => {
73+
const cfg = {
74+
channels: { discord: {} },
75+
commands: { ownerAllowFrom: ["456"] },
76+
} as OpenClawConfig;
77+
78+
const ctx = {
79+
Provider: "discord",
80+
Surface: "discord",
81+
From: "discord:789",
82+
SenderId: "789",
83+
} as MsgContext;
84+
85+
const auth = resolveCommandAuthorization({
86+
ctx,
87+
cfg,
88+
commandAuthorized: true,
89+
});
90+
91+
expect(auth.senderIsOwner).toBe(false);
92+
});
93+
94+
it("senderIsOwner is true when ownerAllowFrom matches sender", () => {
95+
const cfg = {
96+
channels: { discord: {} },
97+
commands: { ownerAllowFrom: ["456"] },
98+
} as OpenClawConfig;
99+
100+
const ctx = {
101+
Provider: "discord",
102+
Surface: "discord",
103+
From: "discord:456",
104+
SenderId: "456",
105+
} as MsgContext;
106+
107+
const auth = resolveCommandAuthorization({
108+
ctx,
109+
cfg,
110+
commandAuthorized: true,
111+
});
112+
113+
expect(auth.senderIsOwner).toBe(true);
114+
});
115+
116+
it("senderIsOwner is true when ownerAllowFrom is wildcard (*)", () => {
117+
const cfg = {
118+
channels: { discord: {} },
119+
commands: { ownerAllowFrom: ["*"] },
120+
} as OpenClawConfig;
121+
122+
const ctx = {
123+
Provider: "discord",
124+
Surface: "discord",
125+
From: "discord:anyone",
126+
SenderId: "anyone",
127+
} as MsgContext;
128+
129+
const auth = resolveCommandAuthorization({
130+
ctx,
131+
cfg,
132+
commandAuthorized: true,
133+
});
134+
135+
expect(auth.senderIsOwner).toBe(true);
136+
});
137+
138+
it("senderIsOwner is true for internal operator.admin sessions", () => {
139+
const cfg = {} as OpenClawConfig;
140+
141+
const ctx = {
142+
Provider: "webchat",
143+
Surface: "webchat",
144+
GatewayClientScopes: ["operator.admin"],
145+
} as MsgContext;
146+
147+
const auth = resolveCommandAuthorization({
148+
ctx,
149+
cfg,
150+
commandAuthorized: true,
151+
});
152+
153+
expect(auth.senderIsOwner).toBe(true);
154+
});
155+
});

src/auto-reply/command-auth.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,8 +350,15 @@ export function resolveCommandAuthorization(params: {
350350
isInternalMessageChannel(ctx.Provider) &&
351351
Array.isArray(ctx.GatewayClientScopes) &&
352352
ctx.GatewayClientScopes.includes("operator.admin");
353-
const senderIsOwner = senderIsOwnerByIdentity || senderIsOwnerByScope;
354353
const ownerAllowlistConfigured = ownerAllowAll || explicitOwners.length > 0;
354+
const isDirectChat = (ctx.ChatType ?? "").trim().toLowerCase() === "direct";
355+
// In the default single-user direct-chat setup, allow an identified sender to
356+
// keep ownerOnly tools even without an explicit owner allowlist.
357+
const senderIsOwner =
358+
senderIsOwnerByIdentity ||
359+
senderIsOwnerByScope ||
360+
ownerAllowAll ||
361+
(!ownerAllowlistConfigured && isDirectChat && Boolean(senderId));
355362
const requireOwner = enforceOwner || ownerAllowlistConfigured;
356363
const isOwnerForCommands = !requireOwner
357364
? true

0 commit comments

Comments
 (0)