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

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