mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-27 18:01:46 +00:00
fix(auth): treat unconfigured-owner sessions as owner for ownerOnly tools (#26319)
When no commands.ownerAllowFrom is configured (single-user default), senderIsOwner was always false because matchedSender requires a non-empty ownerList. This caused ownerOnly tools (cron, gateway) to be silently filtered from the agent's tool list even for the sole authorized user. Now senderIsOwner defaults to true when no owner allowlist is configured, matching the expected single-user behavior. # Conflicts: # src/auto-reply/command-auth.ts
This commit is contained in:
@@ -213,6 +213,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/probes: keep `/health`, `/healthz`, `/ready`, and `/readyz` reachable when the Control UI is mounted at `/`, preserve plugin-owned route precedence on those paths, and make `/ready` and `/readyz` report channel-backed readiness with startup grace plus `503` on disconnected managed channels, while `/health` and `/healthz` stay shallow liveness probes. (#18446) Thanks @vibecodooor, @mahsumaktas, and @vincentkoc.
|
||||
- Feishu/media downloads: drop invalid timeout fields from SDK method calls now that client-level `httpTimeoutMs` applies to requests. (#38267) Thanks @ant1eicher and @thewilloftheshadow.
|
||||
- PI embedded runner/Feishu docs: propagate sender identity into embedded attempts so Feishu doc auto-grant restores requester access for embedded-runner executions. (#32915) thanks @cszhouwei.
|
||||
- Auth/owner-only tool gating: preserve internal `operator.admin` owner authorization and limit the no-owner-allowlist fallback to direct chats so ownerOnly tools stay available for safe single-user setups without granting group chats implicit owner access. (#26331) thanks @widingmarcus-cyber.
|
||||
|
||||
## 2026.3.2
|
||||
|
||||
@@ -573,6 +574,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Logging/Subsystem console timestamps: route subsystem console timestamp rendering through `formatConsoleTimestamp(...)` so `pretty` and timestamp-prefix output use local timezone formatting consistently instead of inline UTC `toISOString()` paths. (#25970) Thanks @openperf.
|
||||
- Feishu/Multi-account + reply reliability: add `channels.feishu.defaultAccount` outbound routing support with schema validation, prevent inbound preview text from leaking into prompt system events, keep quoted-message extraction text-first (post/interactive/file placeholders instead of raw JSON), route Feishu video sends as `msg_type: "file"`, and avoid websocket event blocking by using non-blocking event handling in monitor dispatch. Landed from contributor PRs #31209, #29610, #30432, #30331, and #29501. Thanks @stakeswky, @hclsys, @bmendonca3, @patrick-yingxi-pan, and @zwffff.
|
||||
- Feishu/Target routing + replies + dedupe: normalize provider-prefixed targets (`feishu:`/`lark:`), prefer configured `channels.feishu.defaultAccount` for tool execution, honor Feishu outbound `renderMode` in adapter text/caption sends, fall back to normal send when reply targets are withdrawn/deleted, and add synchronous in-memory dedupe guard for concurrent duplicate inbound events. Landed from contributor PRs #30428, #30438, #29958, #30444, and #29463. Thanks @bmendonca3 and @Yaxuan42.
|
||||
- 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
|
||||
- Channels/Multi-account default routing: add optional `channels.<channel>.defaultAccount` default-selection support across message channels so omitted `accountId` routes to an explicit configured account instead of relying on implicit first-entry ordering (fallback behavior unchanged when unset).
|
||||
- Google Chat/Thread replies: set `messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD` on threaded sends so replies attach to existing threads instead of silently failing thread placement. Landed from contributor PR #30965 by @novan. Thanks @novan.
|
||||
- Mattermost/Private channel policy routing: map Mattermost private channel type `P` to group chat type so `groupPolicy`/`groupAllowFrom` gates apply correctly instead of being treated as open public channels. Landed from contributor PR #30891 by @BlueBirdBack. Thanks @BlueBirdBack.
|
||||
|
||||
155
src/auto-reply/command-auth.owner-default.test.ts
Normal file
155
src/auto-reply/command-auth.owner-default.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user